6.1 KiB
TASK 45 — Haptics: the CS40L20, and which chip actually drives the motor
Status: open, raised 2026-07-28 (Casey). Size: one session for the
question below, then a driver port. Repo: Pixel3Arch (kernel + DT),
souveraine (the feedback vocabulary).
Something already vibrates, and that is the first thing to be precise about
The dial ships haptic detents today (RadialDial.qml, Haptics.tick() on
every selection change) against pmi8998_haptics — the PMIC's own SPMI
haptics block, node inherited from pmi8998.dtsi, CONFIG_INPUT_QCOM_SPMI_HAPTICS=m,
driven through feedbackd's quiet profile with a udev rule in the overlay.
But Google shipped a Cirrus CS40L20 on this board as the haptic amplifier, and both cannot be driving the same LRA. So before any port:
Measured 2026-08-02, narrowing this considerably. The PMIC path is live and
functional in software: spmi_haptics is input5/event5 under
qcom_spmi_haptics bound to ff_memless, feedbackd is running, and an
EVIOCSFF rumble upload succeeded (effect id 0) with the play event
accepted. The CS40L20 is absent from this kernel entirely — zero dmesg
hits, no i2c node. So this is no longer "which of two drivers"; it is one live
driver and one unbuilt one.
Measured again 2026-08-05, at the device, and the chain is hot end-to-end. A live check from the laptop repeated the 2026-08-02 result and added the two things the O2 did not know:
feedbackdis D-Bus activatable (/usr/share/dbus-1/services/ org.sigxcpu.Feedback.service), not a systemd unit — the unit will never "exist", it activates on first D-Bus caller. Profilequietis live (Profileproperty reads"quiet"), and the udev rule typesspmi_hapticsasFEEDBACKD_TYPE=vibrawithTAG+="uaccess"—caseyholdsrwACL on/dev/input/event5.- The shell's exact call (
TriggerFeedbackbutton-pressed/button-released/bell-terminalas shown inHaptics.qml) all succeeded (feedback ids 59–65 in a row), and feedbackd holds fd 5 open on/dev/input/event5. - Correction to the old acceptance:
/sys/class/leds/vibrator/brightnessdoes not exist on this kernel. The only output path is the input FF interface (EVIOCSFF/play), and that path is confirmed working. The acceptance line forgiving/sys/class/leds/vibratormust change.
Question 1, now down to one thing a hand can answer: does it buzz? If yes, the PMIC reaches the motor and the Cirrus part is an upgrade. If no, the LRA is wired to the CS40L20, nothing has ever vibrated, and this task is the enabler for TASK-52's tactile premonition rather than an improvement to it.
Original framing of Question 1: what is the motor physically wired to? If the detents are
genuinely felt, the PMIC path reaches the motor and the CS40L20 is an upgrade.
If they are not — if Haptics.tick() has been a no-op nobody checked — then
the CS40L20 is the enabler and the current haptics are a success-shaped
control. That is a five-minute check at the device and it decides the whole
task's value. Do it before writing a line of driver code.
What the port would buy, if the answer is "upgrade"
Open-loop PMIC buzz versus a closed-loop LRA amp with a wavetable: real
waveform vocabulary, f0 and ReDC tracking, sequenced playback (PBQ), and the
click/tick/thud distinction that makes a dial feel like hardware instead of a
phone. The elmyra_haptics_control the grip nanoapp exposes (TASK-36) is the
CS40L20's, not the PMIC's.
What already exists in-tree
- Firmware, both files, committed:
blobs/los-vendor/firmware/cs40l20.wmfw(34,416 b, DSP firmware) andcs40l20.bin(6,352 b, wavetable). Extracted from LineageOS vendor blobs. They install to/lib/firmware/under those exact names — the driverrequest_firmware()s them. - A full port plan, written 2026-07 and never executed:
Pixel3Arch/archive/tasks/docs-tasks/haptics-cs40l20-task.md. It has the DT node with this unit's real calibration values (f0 2514000, redc 327728, the boost converter constants), the Kconfig/Makefile entries, the file list from Kirisakura, and a mainline-style porting checklist. Read it rather than re-deriving any of that — this file exists to give it an owner and a number, not to replace it.
The three things that plan flags as risk, restated because they are the task
- I2C5 is not enabled in our blueline DTS. The CS40L20 sits at 0x43 on QUPv3 SE5. Enabling it is the prerequisite, and SE5 can be I2C or SPI.
- GPIO 85/86 are reserved for SPI in our DTS (Pixel Visual Core), and
those are SE5's I2C pins. Downstream's overlay explicitly disables
qupv3_se5_spi. Verify against our own extracts —~/pixel3-work/dtb_0.dtsanddtbo_9.dtsare already decompiled — before trusting the Kirisakura overlay's pin story for this variant. cs40l50-vibra.cin mainline is not this chip. Different architecture (MFD child vs standalone I2C), different register map, different device ID, different firmware format. It cannot be adapted. The driver comes from Kirisakura'sdrivers/misc/cs40l2x.cand has never been in mainline.
Acceptance
- Question 1 answered in writing, at the device, before the port starts.
- If the port proceeds: probe + firmware load visible in
dmesg, and f0 tuned against this unit's factory values rather than the defaults. - A trigger through feedbackd (the
TriggerFeedbackpath the shell already uses) results in the LRA firing — the physical half of Question 1. The old/sys/class/leds/vibrator/brightnesscheck is retired: that node does not exist on this kernel; the input FF interface is the only output path. - feedbackd drives it through the same profile the dial already uses — the
shell must not learn a second haptics API. One owner per input
(
SESSION-AUTHORITY-DOCTRINE§10) applies to outputs too.
Connects to
TASK-31 (the dial — the first real consumer of a haptic vocabulary), TASK-36
(elmyra_haptics_control lives in the nanoapp), TASK-38 and TASK-32 (the pill
and the OSK both want detents), Pixel3Arch/archive/tasks/docs-tasks/haptics-cs40l20-task.md
(the port plan).