SDK

AW86233 haptic driver

The chip and the motor that make a Kode Dot buzz: the 235 Hz resonant actuator, RTP playback, the amplitude limit, and the driver and owner over them.

What it is

A Kode Dot buzzes in your hand. The motor is a linear resonant actuator (LRA), a small mass on a spring, the same kind of part a phone vibrates with. It is the LD0832BC-0391F from Leader. The chip that drives it is the AW86233 from Awinic, a haptic controller.

An LRA moves only near its resonance, 235 Hz here (KDRV_AW_F0_HZ), and a steady voltage does not move it at all. So the signal that drives it is a sine wave at 235 Hz. The chip plays in RTP mode, real-time playback: it does not hold a stored effect. The ESP32-P4 streams the waveform to it as signed 8-bit samples, and the chip moves the motor as they say. Everything on this page follows from that mode and from that motor.

The motor is rated 1.8 Vrms (1.9 at most) and measures 21 Ω. The driver limits every amplitude to 80 % of full scale for that rating.

How it is connected

flowchart LR
ao["AO_Haptic"] -- "RTP samples<br/>10 ms feed" --> p4["ESP32-P4"]
subgraph ant["Antenna PCB"]
  aw["AW86233, U7"] -- "drive" --> lra["LRA, 235 Hz"]
end
p4 -- "I2C 0x5A<br/>through J10" --> aw
lra -. "felt by" .-> imu["QMI8658A"]
class ao ao
class p4 chip
class aw chip
class lra chip
class imu chip
The haptic chain on the Kode Dot

The chip is U7 on the Antenna PCB, a board that connects to the main board through connector J10, and the LRA plugs in beside it. The shared I2C bus reaches the chip through J10. The address is 0x5A (KBSP_I2C_ADDR_HAPTIC) and it is fixed in the chip.

Another part answers at 0x5A too. The CST916E touch controller of the panel module from supplier B uses the same address, on a different board. An acknowledge at 0x5A therefore says nothing about which board is there. kdrv_aw86233_create() reads the 16-bit CHIPID at 0x57 and 0x69, which reads 0x2330 on an AW86233, and refuses anything whose high byte is not 0x23, the AW8623X family. kodeOS probes the touch controller at 0x15 and never at 0x5A.

An acknowledge does not prove a motor. The chip answers on I2C with or without the LRA fitted, and acknowledges every register write whether or not anything moves. Only vibration at 235 Hz proves the motor, which is why the production row uses the accelerometer as its witness. See sdk/kdrv/qmi8658a.

Who feeds the chip

AO_Haptic owns the chip (KBSP_RES_HAPTIC) at priority 22. The chip drains its RTP FIFO in about five milliseconds, so the waveform exists only while someone keeps pushing it, and two pushers would interleave two waveforms into noise. One owner feeds it.

How the driver works

The driver is written by hand. Awinic's driver is for Linux, so there is nothing to wrap. The register map comes from the AW8623X datasheet (V1.4) and from the sequence that moves the motor on this board. Nine registers are used. The datasheet describes much more, such as the stored effect memory, continuous mode and trimming, and the driver leaves all of it out rather than implement it partly.

Five facts decide whether the motor moves:

kdrv_aw86233_create() runs five steps:

    1. Probe 0x5A.
    2. Read and check CHIPID.
    3. Write 0xAA to RSTCFG for a soft reset and wait 3 ms.
    4. Take the chip out of standby at 12 kHz.
    5. Put it back into standby.

The run write proves the part can leave standby at all, so the first buzz does not find a chip that cannot.

It calls khal_delay_ms(), so it belongs to a task's start-up and never to an AO's step. AO_Haptic calls it in its initial transition. On a board with no Antenna PCB it returns KODE_IO_ERROR, which is the expected result there.

The sine is generated. The FIFO is fed in bursts, and a stored table would have to be as long as the buzz. At 12 kHz, 12000 samples are exactly 235 whole cycles, so the phase is kept as a sample index modulo 12000 and two bursts join without a jump, which would be a click the motor plays faithfully. A float accumulator would drift.

The amplitude limit comes from the motor. KDRV_AW_GAIN_MAX_PCT is 80 %, where the LD0832BC reaches its rating. The driver applies it twice. kdrv_aw86233_rtp_begin() clamps the gain register, so 100 % goes out as 0xCC, and kdrv_aw86233_sine_fill() gives the samples a peak at the same fraction. Each alone keeps the motor within its rating. A request for more is not an error, and the extra is not granted.

Specifications

The board

ThingValue
ChipAW86233QNR, U7 on the Antenna PCB, I2C 0x5A (KBSP_I2C_ADDR_HAPTIC) fixed
IdentityCHIPID 0x2330 at 0x57 and 0x69 (KDRV_AW_CHIPID). The high byte 0x23 is the AW8623X family. AW86234 reads 0x2340, AW86235 0x2350
MotorLD0832BC-0391F, resonance 235 Hz, rated 1.8 Vrms (1.9 maximum), 21 Ω
Play modeRTP: signed 8-bit PCM, 12 kHz (KDRV_AW_RATE_HZ), 64-byte bursts (KDRV_AW_BURST)
Gain0 to 255 in PLAYCFG2. Clamped to 80 % (KDRV_AW_GAIN_MAX_PCT), which is 0xCC
One buzz1 to 2000 ms (KODE_HAPTIC_MAX_MS)
Standby start-up0.4 ms (Awinic). The owner busy-waits 1 ms
ResetWrite 0xAA to RSTCFG, wait 3 ms

What the witness measured

The production row buzzes the motor and the QMI8658A measures the board at 235 Hz in two windows, motor off and motor on. Measured on a Kode Dot with a validation example that drives the coil without the 80 % clamp:

GainEnergy offEnergy onOn over offSamples
0x402.6640.8246.1x256 of 256
0x80111.218340.7165.0x256 of 256
0xFF1141.841768.136.6x256 of 256

The motor moves 65 times more at 0xFF than at 0x40, exactly as asked. The ratio falls to a seventh because the motor-off window rises by 400 times with the gain. The cause is not settled: the bench may still be shaking from the earlier buzzes of the same run, or the chip's drive may reach the window measured with the motor off. TBD: a run with a settling pause between windows to tell the two apart. What the data does show is that the weakest gain gives the cleanest ratio.

So the row buzzes at 25 %, 50 % and 80 % (0x3F, 0x7F, 0xCC), weakest first, and stops at the first that passes. It reports an amplitude ratio, the square root of an energy ratio, and the threshold is 5.2 (KODE_HAPTIC_TEST_RATIO_X10 is 52), a third of what the board delivered at 0x40. The row also measures the motor-off window first and refuses to judge a bench above 1.2 mg at 235 Hz (KODE_HAPTIC_FLOOR_MAX_X10): a hand resting on the bench reads 123.7 mg. A quiet bench reads 0.1 to 0.6 mg.

The registers

The nine lines the driver uses (ten registers).

RegNameUsed for
0x00RSTCFGWrite 0xAA: soft reset
0x01SYSSTBit 4 FF_AES, bit 3 FF_AFS (the RTP FIFO is almost full), bit 0 DONES
0x07PLAYCFG2The gain, 0 to 255
0x08PLAYCFG3Bits [1:0] the play mode; 1 is RTP
0x09PLAYCFG4Bit 0 GO, bit 1 STOP
0x32RTPDATAThe FIFO input: signed 8-bit PCM, one address, no increment
0x3FGLBRD5Bits [3:0] GLB_STATE: 0 standby, 8 RTP, 11 brake
0x46SYSCTRL2Bit 6 standby; bits [1:0] WAVDAT_MODE, the sample rate. 0x02 is out of standby at 12 kHz
0x57, 0x69CHIPID_H, CHIPID_LThe 16-bit identity

The field table

Nine fields for the console's haptic. The table is small on purpose: the waveform comes from this board and not from the chip's memory. The driver writes the registers of all four writable fields itself, so a value written here lasts only until the next buzz overwrites it; the standby bit is the first to go.

FieldWhereWhat it is
presentdriverRO. create found the chip
chipiddriverRO. The 16 bits read at bring-up, served from a copy with no bus access
sysst0x01RO. The whole status byte
ff_afs0x01 [3]RO. The RTP FIFO is almost full
gain0x07The amplitude, 0 to 255
play_mode0x08 [1:0]1 is RTP
glb_state0x3F [3:0]RO. 0 standby, 8 RTP, 11 brake
sysctrl20x46Standby and the sample rate
standby0x46 [6]Nothing plays while set

The driver

CallWhat it does
kdrv_aw86233_create()The five steps above. KODE_IO_ERROR when nothing answers, the family byte is wrong or a write did not take
kdrv_aw86233_present()true when the last create returned KODE_OK
kdrv_aw86233_standby(on)A read-modify-write of SYSCTRL2 bit 6, so the sample rate stays as it was. No other field changes
kdrv_aw86233_rtp_begin(gain_pct, &reg_written)Clamps and writes the gain, sets RTP, sends GO before any sample exists (the chip idles on an empty FIFO), and confirms GLB_STATE is 8. reg_written is the PLAYCFG2 byte the chip got; pass NULL if you do not need it. The chip must be out of standby first
kdrv_aw86233_rtp_feed(pcm, n, &accepted)One status read and at most one 64-byte write, then returns. accepted is 0 and the result KODE_OK when the FIFO is nearly full. Never waits
kdrv_aw86233_rtp_end()STOP, unconditionally. Call it even after an error, or the motor keeps vibrating with what the FIFO holds
kdrv_aw86233_sine_fill(out, n, &phase)A 235 Hz sine at the 80 % peak, continuing from phase
kdrv_aw86233_playing_ms(samples)How long the samples take to play, rounded up. The owner waits this long before rtp_end()
kdrv_aw86233_get(), _set(), _field_info(), _field_by_name(), _dump()The field table

One buzz uses them in this order: create once at start; standby(false) then rtp_begin(); rtp_feed() again and again until the samples are queued; wait playing_ms(); rtp_end() then standby(true).

The files

FileWhat it does
motion/include/kdrv_aw86233.h, motion/src/kdrv_aw86233.cThe driver: bring-up, standby, RTP, the sine, the field table
kaos/haptic/src/haptic_ao.cAO_Haptic: the feed tick, the tail timer, the busy flag, the production row

Gotchas

Example code

Buzz the motor for 200 ms at half strength every two seconds. You feel a short tick in your hand, and the serial log prints a line each time the request was taken.

/*
 * buzz.c - 2026-10-28 - from Kode with love
 *
 * Makes the Kode Dot's vibration motor buzz for 200 ms at half strength every
 * two seconds. You feel a short tick in your hand, and the serial log prints
 * "buzz" each time the request was taken.
 */
#include "kode.h"
 
/* Bring the system up, then ask AO_Haptic for a buzz every two seconds. */
void app_main(void)
{
    kode_init();
 
    while (1) {
        if (kode_haptic_play(200, 50) == KODE_OK) {
            KLOG_I("buzz", "buzz");
        }
        kode_delay_ms(2000);
    }
}

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.

In the SDK