gondolin · ThinkPad T14 Gen 2a AMD · Synaptics TM3471 over RMI4/SMBus

The 300 Millisecond Touchpad

Ten days of measuring what a Synaptics touchpad does after the laptop wakes up. Three symptoms, two of which turned out to be one fault, one fix that was quietly causing the next problem, and two kernel patches at the end of it.

kernel 7.1.9-arch1 pad LEN2073, TM3471-030, firmware 3942087 transport i2c-piix4 ASF Host Notify (own patch, DKMS) period 2026-09-04 → 2026-09-13
Symptom 1
80 Hz clock after resume

Half of all resumes came back with 12.5 ms reports and merged 24 ms frames instead of a flat 15 ms.

Symptom 2
Dropped taps, phantom pinches

Quick taps vanished; two-finger scrolls occasionally became a zoom.

Symptom 3
Wake jump

After a sleep, the cursor sat still on touch-down and then leapt. Appeared on the 8th, once symptom 1 was "fixed".

Where it landed

Symptoms 1 and 3 are one fault. On resume, the PS/2 driver deactivates the pad, and on this pad that command is a firmware reset. The SMBus driver then reconfigures the pad within a few milliseconds, while it is still coming up. Configured that early, it misbehaves until its next reset, as one symptom or the other. A 300 ms wait before reconfiguring cures both.

Symptom 2 is the firmware's power-saving doze. With the default doze interval the pad never reports most short touches and garbles the first frame of two-finger touches. Lenovo's Windows driver sets a longer interval for this pad and eleven others; the same value on Linux gives 20/20 taps and 0 pinches in 145 scrolls.

1. Where this started

This machine's touchpad had been stuck in PS/2 fallback since the laptop was new, because AMD's SMBus controller does not advertise Host Notify and the RMI4 driver will not attach without it. An earlier write-up covers the fix: an out-of-tree i2c-piix4 that drives the chipset's ASF block for Host Notify, the same single-driver shape Intel uses. With that installed the pad runs natively over SMBus at a clean 15 ms per report, and three residual problems came into focus.

What follows is the order the facts were found in, because the order is the point. Each fix was measured, each one cleared its symptom, and two of them were wrong.

2. Measuring a touch-down

Every conclusion here rests on one number, so it is worth being precise about it. A capture is thirty seconds of raw evtest output while six slow single-finger drags are made, lifting fully between them. For each drag the analyser takes the distance the reported position moves between the first and second frame. On a healthy pad that is zero or near it: the finger has just landed and is barely moving. On a pad in the wake-jump state it is 60 to 140 units, 5 to 12 mm, in a single 15 ms frame, a speed no slow drag ever reaches.

Healthyframes after touch-down
005141922
Wake jumpsame drag, same pad
091433936
Each bar is one 15 ms report; the number under it is the distance moved since the previous report, in pad units (12 per mm). In the wake-jump state the first report is the landing position, held stale while the finger moves, and the second is wherever the finger is now. To the user the cursor sticks, then jumps. The report clock itself stays a flat 15 ms throughout, which is why timing histograms never showed this.

Two things made the rig trustworthy. The interrupt counters along the pipeline, ASF hard interrupt, Host Notify into the RMI driver, F12 finger frames, and evtest reports, were snapshotted around every capture and always agreed, which ruled out host-side loss early. And a capture that needs hands is only accepted once it contains real drags; the script re-cues and retries, and a multi-step sequence aborts rather than advancing. That rule was added the hard way, after a night where a four-step bisect ran to completion on four empty captures.

3. Dropped taps and phantom pinches: doze

This was the clean thread. With the counters agreeing, the missing taps were missing at the source: the pad simply reported six of twenty quick taps, one frame each. Two-finger landings were reported with the objects in the wrong order, so libinput saw finger separation collapse from 165 to 78 units in one frame and called it a pinch, scale 2.45.

The pad's F01 doze registers explained it. Doze is the firmware's low-power scan mode between touches: interval 3, wakeup threshold 45, holdoff 0 on this pad, so it drops into doze the instant a finger lifts and samples too slowly to catch a short touch. Lenovo's Windows driver for this family sets RMIDozeInterval=6 for twelve pad IDs, LEN2073 among them, and does not disable doze. Measured with each configuration:

doze configurationquick taps seen of 20pinches / scroll episodes
firmware default (interval 3)6, one frame each1 / 10
doze disabled (NoSleep)200 / 232
Windows value (interval 6), doze kept20, each 60–190 ms0 / 145

The Windows value keeps the power saving and fixes both. It goes upstream as a small table in the PS/2 Synaptics driver, passing the interval through platform data for the same twelve IDs. Two red herrings on the way, both cheap to rule out and worth naming: libinput's thumb pressure threshold (fingertips here report 50 to 70, well under it) and ASF bus collisions between the pad and the TrackPoint pass-through (the counters matched).

4. The resume fault, and the fix that caused the next bug

What the 80 Hz clock looked like from the driver

The SMBus transport's resume path does three things in order: reset its own register-mapping cache, run the RMI configuration pass, then wake the sensor. The configuration pass rewrites every function's control registers: the F01 device control byte, the three doze bytes, and the interrupt mask. Reading that order, the story wrote itself: configuring a sensor that is still asleep half-applies, wake it afterwards and you get the degraded clock. Moving the pass after the wake gave 0 degraded resumes in 8, where the stock order gave 8 in 8. A patch was drafted.

It was tested with a knob that ran the pass both before and after the wake, because that was the smallest change. That knob stayed loaded. Two days later the cursor started sticking after every sleep.

Chasing the wake jump

The jump looked like a firmware sleep state. The register dump was byte-identical to a healthy pad; the report clock was perfect; and it seemed to need hours of suspend, since 90-second probe cycles had never shown it. Every one of those impressions was wrong, and the sequence of experiments that dismantled them is the useful part of this document.

  1. Nothing over SMBus touched it. The config pass alone, and a full unbind and re-probe of the SMBus driver, both left it in place. A reload of the PS/2 driver stack cleared it every time. So the cure was on the PS/2 side.
  2. Bisecting PS/2 commands with a trigger added to the experimental driver: the disable that stock resume sends did nothing; the identify-and-capability query sequence cured it; then a single Get ID cured it. The minimal cure was one PS/2 command.
  3. Get ID put into the resume path did not work. It ran, the log showed it, the pad still came back jumping. Minutes later, the same command in place cured it.
  4. Every PS/2 command is a firmware reset. This was the turn. After Get ID the pad's interrupt mask read 00 00 and its doze interval was back to 3. Get ID was not waking anything; it was wiping the host's configuration. The disable at resume does exactly the same, which the SMBus driver copes with only because it reconfigures afterwards, and because its resume happens to run after the PS/2 one.
  5. The "hours" threshold was false. A four-minute timed suspend produced the jump. Nobody had captured positions after a short cycle before; the earlier probes recorded timing only.
  6. Stock order, no jump. With the config pass back in its original place before the wake, a four-minute suspend gave drags from rest, and the 80 Hz clock. The after-wake fix was creating the jump.
  7. Per-register bisect, twice. Every write of the config pass, applied alone to a freshly reset pad, was clean. So was every write applied after the control byte. A second full pass after a first one jumped, and so did a pass whose only predecessor was the doze write. For an evening that looked like "writing the doze registers twice".
  8. The suspend log killed that theory. A build that skipped unchanged doze writes still jumped after a real resume, with the log proving the doze bytes had been written exactly once.
  9. What was left was time. Every clean in-place sequence had slept two seconds after the reset before writing anything. At resume, the pass followed the reset within milliseconds.
wait between the pad's reset and the config passfirst-frame jump, mediandrags from restclock
0 ms910 / 715 ms
30 ms970 / 615 ms
100 ms910 / 715 ms
300 ms07 / 715 ms
2000 ms07 / 715 ms
300 ms, across a real 4-minute suspend, stock order07 / 7flat 15 ms
0 ms across a real suspend, stock order (the original bug)08 / 812.5 ms, merged frames

The fault, stated once

On resume the PS/2 driver's deactivate resets the pad. Configuring it within roughly 100 ms of that reset leaves it broken until the next reset, as either the 80 Hz clock or the stale landing frame. Waiting 300 ms first gives a pad that is right in both respects. The reorder fix worked on the clock only because its second pass came late enough; the Get ID fix worked in place only because the script slept first.

5. What the pad taught us on the way

6. Harness notes

7. Timeline

8. Patches and what is left

patchchangestatus
0001 Input: synaptics-rmi4300 ms settle in rmi_smb_resume() before reconfiguring, with the measurement table in the messagemeasured across S3, checkpatch clean
0002 Input: synapticsWindows doze interval (6) via platform data for the twelve LEN pad IDsmeasured, checkpatch clean
retired: reorder the resume passcured the clock, caused the wake jumpwithdrawn
retired: Get ID on resumea reset presented as a cure; worked only with a sleep it did not havewithdrawn

Still to do before sending: a few more natural resumes including an overnight one under the settle delay; narrowing the threshold between 100 and 300 ms if a reviewer asks; real names in the sign-offs. The upstream RFC for the ASF transport itself, which all of this depends on, is a separate piece of work. One unexplained observation remains and is moot for the fix: in place, a second config pass issued a second or more after a clean first one also produced the jump, as if a doze rewrite on an awake sensor is itself a small reset.