Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/45-haptics-cs40l20.md
2026-08-05 19:16:41 -04:00

6.1 KiB
Raw Permalink Blame History

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:

  • feedbackd is 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. Profile quiet is live (Profile property reads "quiet"), and the udev rule types spmi_haptics as FEEDBACKD_TYPE=vibra with TAG+="uaccess"casey holds rw ACL on /dev/input/event5.
  • The shell's exact call (TriggerFeedback button-pressed/button-released/ bell-terminal as shown in Haptics.qml) all succeeded (feedback ids 5965 in a row), and feedbackd holds fd 5 open on /dev/input/event5.
  • Correction to the old acceptance: /sys/class/leds/vibrator/brightness does 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/vibrator must 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) and cs40l20.bin (6,352 b, wavetable). Extracted from LineageOS vendor blobs. They install to /lib/firmware/ under those exact names — the driver request_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

  1. 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.
  2. 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.dts and dtbo_9.dts are already decompiled — before trusting the Kirisakura overlay's pin story for this variant.
  3. cs40l50-vibra.c in 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's drivers/misc/cs40l2x.c and 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 TriggerFeedback path the shell already uses) results in the LRA firing — the physical half of Question 1. The old /sys/class/leds/vibrator/brightness check 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).