Embedded HandbookApps
All sections › XI. Sensors: connection, circuits, drivers
STM32F103 · level intermediate · 41 min read
Open in the interactive handbook

100в. APDS-9960 gesture, proximity and colour sensor over I2C: an IR LED and four photodiodes, the gesture FIFO, swipe recognition, the interrupt and thresholds

In section 100 distance was measured with sound, in 100а a radar looked for a person in the room, in 100б the VL53L0X measured millimetres by the time of flight of light. The APDS-9960 measures nothing in millimetres: it shines an IR LED and looks at how much light came back — as one number "how close" and as four numbers from directional photodiodes. From how these four numbers change in time you get a hand swipe: up, down, left, right, nearer, farther. It also sees colour — the C, R, G and B channels, as in the TCS34725 from 103б; colour is covered only briefly here.

Here: what is inside and how the two engines work — proximity and gesture; the address, registers and power-up; LED current and pulses, gain; proximity thresholds, persistence and the interrupt that must be cleared; entering and leaving gesture mode, the 32-dataset FIFO and how to read it without overflowing; how to get a direction from four numbers; what sunlight and a cover glass do and how to subtract the glass with offsets.

Prerequisites: the apds9960 driver in Zephyr 4.4.2 (github.com/zephyrproject-rtos/zephyr, commit dccb095, Apache-2.0) — register addresses, bits, gain, current, pulse and wait codes, the address 0x39, ID 0xAB, the 5.7 ms wait after power-up, the description of the INT pin; the open apds9960 Python library (GPL-3, a copy lives in the OpenMV script set, github.com/openmv/openmv) — default settings with comments and gesture decoding by the first and last dataset. The manufacturer's document (Broadcom) is not in the project: the FIFO size, the engine order, the interrupt clearing rules, the offset format and the supply are taken from the manufacturer's documentation and not verified. The Python sensor model and the synthetic hand are our own; the numbers below describe the model, not the sensor. The bench is the apds100d.c driver and the C recognizer, built with gcc and run against the model; the same code and the fw100d.c firmware were built with armclang for the STM32F103 and have not run on a board. RM0008 (DocID13902 Rev 17): AFIO_EXTICR3, p. 191; EXTI_FTSR, p. 211; vector table 63, p. 204; master reception of more than two bytes, method 2, pp. 762–763; one and two bytes, p. 764. DocID13587 Rev 17: PB6 — I2C1_SCL, PB7 — I2C1_SDA, both FT, p. 32; PB8 — FT, p. 33. The I²C bus — section 17, pin interrupts — 7.5, connecting modules — 95.

Terms used in the documentation

Term Meaning
proximity engine / gesture engine the two measuring engines of the chip
directional photodiodes U, D, L, R four photodiodes, each looking its own way
LED drive (LDRIVE), LED boost IR LED current and its boost
pulse count and length (PPULSE, GPULSE) number and length of LED pulses
proximity gain (PGAIN), gesture gain (GGAIN) proximity and gesture gain
proximity interrupt thresholds (PILT, PIHT) low and high proximity thresholds
proximity persistence (PPERS) how many cycles in a row beyond a threshold before a proximity interrupt
gesture enter / exit threshold (GPENTH, GEXTH) thresholds to enter and leave gesture mode
gesture FIFO, dataset the gesture queue; a dataset is four bytes U, D, L, R
FIFO level (GFLVL), FIFO overflow (GFOV) datasets in the queue; the queue overflowed
proximity offset (POFFSET_UR, POFFSET_DL) the subtracted offset of the U+R and D+L pairs
interrupt clear (PICLEAR, AICLEAR) clearing an interrupt by accessing an address
swipe a hand moving across the sensor

100в.1 Inside: the LED, four photodiodes and two engines

The package holds an IR LED and photodiodes behind a window. The LED fires a burst of pulses, the photodiodes collect the reflected light, the background is subtracted, and a byte comes out. The proximity engine combines the photodiodes into one number PDATA (0…255); the gesture engine writes four numbers — U, D, L and R — into a queue. A fifth engine, colour, is the C, R, G and B photodiodes behind filters, as in the TCS34725 (103б): sixteen-bit CDATA…BDATA from 0x94, low byte first, gain AGAIN 1, 4, 16 and 64 (the TCS34725 has 1, 4, 16 and 60), a time step of 2.78 ms instead of 2.4 ms. Colour in detail — in 103б.

Top — the APDS-9960 on an STM32F103: PB6 — SCL, PB7 — SDA on I2C1 at 400 kHz, address 0x39, ID 0xAB; the sensor INT is open drain, on PB8 with a pull-up, EXTI8 on the falling edge; VCC 3.3 V and a separate VL pin for the LED; the proximity interrupt is cleared by accessing PICLEAR 0xE5 or AICLEAR 0xE7. Middle — the IR LED with LDRIVE current and LED_BOOST, four photodiodes U, D, L, R around the window, the proximity engine gives PDATA, thresholds PILT and PIHT, persistence PERS, the PINT interrupt; the gesture engine enters when PDATA is above 40 and leaves when all four channels are below 30, datasets go to a FIFO of 32 datasets of 4 bytes, interrupt from 4 datasets, a dataset every 4.28 ms in the model, read as a burst of 4 · GFLVL bytes from 0xFC. Bottom — the ratio (U−D)/(U+D) · 100 for an upward swipe 5 cm above the sensor in the model: from plus to minus, about ±51 at the edges of ±8 cm, a shift of −102

The registers are plain: register address and data, no command byte, unlike the TCS34725. The main ones:

Address Name Contents
0x80 ENABLE PON 0, AEN 1, PEN 2, WEN 3, AIEN 4, PIEN 5, GEN 6
0x81, 0x83 ATIME, WTIME colour time and wait: (256 − code) × 2.78 ms
0x89, 0x8B, 0x8C PILT, PIHT, PERS proximity thresholds and persistence (PPERS — bits 7:4)
0x8E PPULSE pulse length (bits 7:6: 4, 8, 16, 32 µs) and pulse count − 1
0x8F CONTROL LDRIVE 7:6 (100, 50, 25, 12.5 mA), PGAIN 3:2 (1, 2, 4, 8), AGAIN 1:0
0x90 CONFIG2 LED_BOOST 5:4: 100, 150, 200, 300 %
0x92, 0x93 ID, STATUS 0xAB; PGSAT 6, PINT 5, AINT 4, GINT 2, PVALID 1, AVALID 0
0x9C…0x9E PDATA, POFFSET_UR, POFFSET_DL proximity and its offsets
0xA0…0xA3 GPENTH, GEXTH, GCONF1, GCONF2 enter and exit; GFIFOTH 7:6, GEXMSK 5:2, GEXPERS 1:0; GGAIN 6:5, GLDRIVE 4:3, GWTIME 2:0
0xA6, 0xAB GPULSE, GCONF4 gesture pulses; GFIFO_CLR 2, GIEN 1, GMODE 0
0xAE, 0xAF GFLVL, GSTATUS FIFO level; GFOV 1, GVALID 0
0xE5, 0xE7 PICLEAR, AICLEAR clear the proximity interrupt / all but gesture
0xFC…0xFF GFIFO_U…GFIFO_R the first dataset of the queue

Addresses, bits and codes come from the Zephyr driver header. The 2.78 ms step is there too (APDS9960_DEFAULT_WAIT_TIME), and the library comments agree with it: ATIME 219 — "103ms" (37 × 2.78 = 102.86 ms), WTIME 246 — "27ms" (10 × 2.78 = 27.8 ms). The ×12 wait factor (WLONG, bit 1 of CONFIG1) comes from the library comment on CONFIG1 = 0x60. The gesture engine waits GWTIME have eight codes: 0; 2.8; 5.6; 8.4; 14.0; 22.4; 30.8 and 39.2 ms. The engine order in the cycle "proximity → gesture → wait → colour" is from the manufacturer's documentation, not verified; the model works this way.


100в.2 Connecting to the F103 and powering up

Module pin STM32F103 Purpose
VCC 3.3 V sensor supply
VL 3.3 V or its own supply IR LED supply
GND GND ground
SCL, SDA PB6, PB7 I2C1, 400 kHz, address 0x39
INT PB8 open drain, active low; input with pull-up, EXTI8 on the falling edge

The address 0x39 comes from the Zephyr device tree (apds9960@39) and the library README ("hard wired": nothing to change it with, so a second sensor cannot share the bus). For INT the Zephyr binding says it plainly: open drain, active low, when connected directly — a pull-up on the microcontroller pin; on the Arduino Nano 33 BLE Sense board the pin is described exactly so, GPIO_ACTIVE_LOW | GPIO_PULL_UP. The 3.3 V supply is named in the library README; the separate VL pin for the LED and its allowed voltage are from the manufacturer's documentation, not verified.

I2C1 at 400 kHz and master reception are the same code as in 100б: CCR = 30, TRISE = 11, reception of more than two bytes by method 2 (RM0008, pp. 762–763), of one and two bytes — p. 764. PB8 is an input with a pull-up; in AFIO_EXTICR3 (p. 191) the EXTI8 field = 0001 — port B; EXTI_FTSR (p. 211) — falling edge; the EXTI9_5 handler is position 23 in table 63 (p. 204). PB6, PB7 and PB8 are FT (DocID13587, pp. 32–33).

Power-up as in Zephyr: wait 5.7 ms (the driver has the comment "Initialize time 5.7ms" and k_sleep(K_MSEC(6))), check the ID (0xAB; Zephyr also accepts 0x9C), switch everything off, write the settings, clear the interrupts and only then enable the engines:

/* Включение: выждать 5,7 мс, проверить ID, всё выключить, записать настройки, снять прерывания, включить. */
int apds_init(apds_t *d, const apds_cfg_t *c)
{
    const uint8_t regs[][2] = {
        {APDS_ATIME, c->atime}, {APDS_WTIME, c->wtime}, {APDS_CONFIG1, APDS_CONFIG1_DEF}, {APDS_PPULSE, c->ppulse},
        {APDS_CONTROL, c->control}, {APDS_CONFIG2, c->config2}, {APDS_CONFIG3, 0}, {APDS_PERS, c->pers},
        {APDS_PILT, c->pilt}, {APDS_PIHT, c->piht}, {APDS_POFFSET_UR, 0}, {APDS_POFFSET_DL, 0},
        {APDS_GPENTH, c->gpenth}, {APDS_GEXTH, c->gexth}, {APDS_GCONF1, c->gconf1}, {APDS_GCONF2, c->gconf2},
        {APDS_GPULSE, c->gpulse}, {APDS_GOFFSET_U, 0}, {APDS_GOFFSET_D, 0}, {APDS_GOFFSET_L, 0},
        {APDS_GOFFSET_R, 0}, {APDS_GCONF3, 0},
    };

The firmware settings come from library and Zephyr constants: proximity PPULSE = 0x87 (the library says "16us, 8 pulses"), gesture GPULSE = 0xC9 ("32us, 10 pulses"), 100 mA current, 4x gain, LED_BOOST 150 % (the Zephyr binding default), GPENTH 40, GEXTH 30, GCONF1 = 0x40 ("4 gesture events for int., 1 for exit"), GWTIME 2.8 ms. On the bench power-up takes 28 transfers and 56 bytes: 2057.5 µs of bus time at 400 kHz and 7757.5 µs together with the wait.


100в.3 Proximity: the LED, gain, thresholds and the interrupt

How much light. The signal grows with LED current (LDRIVE), boost (LED_BOOST), gain (PGAIN), pulse count and length. In the model the reflection is proportional to their product; the hand is a disc of 3 cm radius. PDATA above the centre, the mean of 10 reads:

Setting (model) 2 cm 3 cm 5 cm 8 cm 12 cm 20 cm empty
12.5 mA, 1x, 100 % 4.3 4.0 2.6 1.7 0.4 0.2 0.1
100 mA, 4x, 150 % (firmware) 203.1 186.9 120.2 61.2 30.2 11.6 0.2
100 mA, 8x, 300 % 255 255 255 245.0 121.7 47.0 0.7

The weak setting barely sees the hand; the strong one hits 255 up to and including 5 cm and cannot tell 2 cm from 5 cm. A byte is all there is: saturated, proximity stops measuring; weak, it is noise.

Thresholds and persistence. PINT is set when PDATA is below PILT or above PIHT for as many cycles in a row as PPERS says (0 — every cycle; so in the model, from the manufacturer's documentation, not verified). If PIEN = 1, INT goes low and stays there until the interrupt is cleared by accessing PICLEAR (0xE5) or AICLEAR (0xE7). Zephyr clears by writing zero to AICLEAR, the library by reading PICLEAR; in the model any access clears.

/* Приближение: прочитать PDATA, при гистерезисе поменять пороги местами, снять прерывание.
 * Пока прерывание не снято, INT стоит в нуле — нового спада не будет. */
static int prox_irq(apds_t *d)
{
    const apds_cfg_t *c = d->cfg;
    d->pint++;
    TRY(rd(d, APDS_PDATA, &d->pdata, 1));
    if (c->hyst_lo) {
        d->near = d->pdata > c->hyst_lo;
        TRY(wr(d, APDS_PILT, d->near ? c->hyst_lo : 0u));     /* рядом — ждать, когда станет меньше */
        TRY(wr(d, APDS_PIHT, d->near ? 255u : c->piht));      /* далеко — ждать, когда станет больше */
    }
    if (c->clear_int)
        TRY(wr(d, APDS_PICLEAR, 0));
    return APDS_OK;
}

The experiment: a 712 µs proximity cycle plus a WTIME wait of 0xF6 (10 × 2.78 ms), PILT = 0, PIHT = 50, PPERS = 1. The hand 5 times for a second at 4 cm and a second away; then the hand hovers for 5 s at 9.3 cm, where PDATA = 47.8, with six times the usual noise:

Experiment (model) INT falls Services INT low
interrupt not cleared 1 4 10285 ms
cleared, thresholds 0 and 50 175 175 47 ms
cleared, hysteresis: near — wait for < 30 10 10 4 ms
hand at the threshold, 0 and 50, PPERS = 1 19 19 5 ms
hand at the threshold, 0 and 50, PPERS = 4 0 0 0 ms
hand at the threshold, hysteresis 30/50 1 1 0 ms

Not cleared — INT went low at the first approach and stayed low until the end: EXTI catches a falling edge, and there are no more edges. Four services instead of one are our loop: after a service the firmware looks at the INT level and, while it is low, services again, but at most four times. Cleared but thresholds untouched — an interrupt every cycle while the hand is near: 175 in 5 s. Hysteresis — one for arriving and one for leaving. Noise at the threshold: PPERS = 4 removes 19 false triggers, and so does hysteresis.


100в.4 Gestures: entering, leaving and the 32-dataset FIFO

The gesture engine is enabled by GEN, but does not measure by itself: when a proximity cycle gives PDATA above GPENTH, the state machine enters gesture mode (GMODE in GCONF4 becomes 1 — the library calls this bit "gesture state machine is currently running") and writes U, D, L, R datasets into the queue until all four channels drop below GEXTH (GEXPERS times in a row). Datasets come back to back: in the model a dataset is GWTIME plus the pulses plus 200 µs, 4.28 ms with the firmware settings and 456 µs with GWTIME 0 and 8 pulses of 8 µs. The queue holds 32 datasets of 4 bytes (from the manufacturer's documentation; the library also limits decoding to 32 datasets). The GINT interrupt fires when GFIFOTH datasets have accumulated: code 1 means four (the library comment on GCONF1 = 0x40), the other codes are from the manufacturer's documentation.

How to read. Zephyr and the library read dataset by dataset: GFLVL, then that many times 4 bytes from 0xFC. A consecutive read from 0xFC wraps round 0xFC…0xFF and returns the next dataset (from the manufacturer's documentation, not verified), so the whole queue is one read of 4 · GFLVL bytes, up to 128:

/* FIFO: block = 1 — одним чтением 4 · n байт (после 0xFF адрес снова 0xFC, это уже следующий набор);
 * block = 0 — каждый набор своим чтением четырёх байт, как в драйвере Zephyr. */
static int fifo_read(apds_t *d, uint8_t n)
{
    uint8_t b[4u * APDS_FIFO_SETS];
    if (d->cfg->block) {
        TRY(rd(d, APDS_GFIFO_U, b, 4u * n));
    } else {
        for (uint8_t i = 0; i < n; i++)
            TRY(rd(d, APDS_GFIFO_U, b + 4u * i, 4));
    }
    for (uint8_t i = 0; i < n; i++)
        ges_feed(&d->g, b + 4u * i);
    d->sets += n;
    return APDS_OK;
}

One dataset alone takes 670 µs of bus time at 100 kHz and 167.5 µs at 400 kHz; the whole queue (128 bytes) takes 11830 and 2957.5 µs, i.e. 369.69 and 92.42 µs per dataset. GFLVL and GSTATUS are neighbours (0xAE, 0xAF) and are read in one transfer. The hand for a second 5 cm above the sensor, interrupt from four datasets:

Dataset (model) Bus, read Made Read Lost GFOV Bus busy
4.28 ms 100 kHz, per dataset 232 230 0 0 31.3 %
4.28 ms 100 kHz, burst 232 232 0 0 21.8 %
4.28 ms 400 kHz, burst 232 232 0 0 5.8 %
456 µs 100 kHz, per dataset 2323 1345 947 40 95.8 %
456 µs 100 kHz, burst 2206 1552 644 23 74.9 %
456 µs 400 kHz, per dataset 2186 2184 0 0 63.0 %
456 µs 400 kHz, burst 2186 2184 0 0 48.8 %

A dataset every 456 µs, while reading one dataset at 100 kHz takes 670 µs: the bus cannot keep up, the queue overflows 40 times and loses 947 datasets out of 2323. A burst at 100 kHz reduces the loss to 644, 400 kHz removes it entirely. At the usual 4.28 ms the queue fills in 32 × 4.28 = 137 ms; if you do not listen to INT but look every 100 ms, nothing is lost, every 200 ms — 72 datasets out of 232 and 4 overflows. In the model a full queue drops the new dataset and sets GFOV, which is cleared when the queue empties — these are model assumptions.

When the gesture is over. The state machine left — GMODE is 0 again. The service reads the queue, looks at GCONF4 and, if the machine has left and the queue is empty, hands what it collected to the recognizer. While a gesture is going on the firmware looks into the queue at least every 10 ms and on INT:

    for (;;) {
        TRY(rd(d, APDS_GFLVL, lv, 2));                  /* GFLVL и GSTATUS — соседи: одно чтение */
        if (lv[1] & APDS_GS_GFOV)
            d->ovf++;
        if (lv[0] > APDS_FIFO_SETS)
            lv[0] = APDS_FIFO_SETS;
        if (lv[0])
            TRY(fifo_read(d, lv[0]));
        TRY(rd(d, APDS_GCONF4, &g4, 1));
        if (g4 & APDS_G4_GMODE)
            break;                                      /* рука ещё здесь */
        TRY(rd(d, APDS_GFLVL, lv, 1));
        if (lv[0] == 0) {                               /* автомат вышел, FIFO пуст — жест окончен */
            d->active = 0;
            return ges_decide(&d->g, 1);
        }
    }
        for (uint32_t k = 0; k < RETRY; k++) { /* INT всё ещё прижат — обслужить ещё раз, иначе нового спада не будет */
            int ev = apds_service(&D);
            if (ev != GES_NONE) {
                gesture = (int8_t)ev;
                if (ev > 0)
                    n_gest[ev]++;
            }
            if (!apds_port_int_low())
                break;
        }

100в.5 Recognizing a swipe

Each photodiode sees the hand better on its own side. If the hand moves from bottom to top, first U is larger, at the end D; from left to right — first L, at the end R. Zephyr counts this way (the U − D difference of each dataset, "up" when minus follows plus), and so does the library (the ratio (u_first - d_first) * 100 / (u_first + d_first) in the first and last dataset, threshold 50, "up" is a downward shift). A ratio is better than a difference: it does not depend on how far the hand is. Our decoding follows the library, without its counters:

void ges_feed(ges_t *g, const uint8_t s[4])
{
    uint32_t sum = (uint32_t)s[0] + s[1] + s[2] + s[3];
    g->n_all++;
    if (s[0] <= GES_MIN_IN || s[1] <= GES_MIN_IN || s[2] <= GES_MIN_IN || s[3] <= GES_MIN_IN)
        return;                                         /* рука на краю или её нет — в разбор не идёт */
    g->ud1 = ratio(s[0], s[1]);
    g->lr1 = ratio(s[2], s[3]);
    if (g->n_in == 0) {
        g->ud0 = g->ud1;
        g->lr0 = g->lr1;
    }
    if (g->n_in < GES_AVG)
        g->s0 += sum;
    g->s1[g->n_in % GES_AVG] = sum;
    g->n_in++;
}
    if (final && (aud >= GES_SENS || alr >= GES_SENS)) {
        if (aud >= alr)
            return dud < 0 ? GES_UP : GES_DOWN;         /* вверх: сначала U больше D, в конце меньше */
        return dlr < 0 ? GES_LEFT : GES_RIGHT;
    }
    if (aud < GES_STILL && alr < GES_STILL) {
        if (2u * s1 >= 3u * g->s0)
            return GES_NEAR;                            /* сумма выросла в полтора раза — рука ближе */
        if (final && 3u * s1 <= 2u * g->s0)
            return GES_FAR;
    }
    return GES_NONE;

Only datasets where all four channels are above 10 are decoded (as APDS9960_GESTURE_THRESHOLD_OUT in the library); at least four valid datasets are needed. A ratio shift of 50 or more is a swipe, the direction by the larger axis and the sign. A shift below 20 on both axes means the hand did not move sideways: if the sum of the four at the end is one and a half times the sum at the start — "nearer", one and a half times smaller — "farther". "Nearer" does not wait for the hand to leave: the hand went down and stays, the machine does not exit, — so after 96 datasets the window is checked without an exit.

The swipe set. Six gestures × 4 speeds (0.25; 0.5; 1 and 2 m/s) × 4 heights (3, 5, 8 and 12 cm) × 3 hands: the path shifted sideways by up to ±2 cm, reflectance 0.7…1.0 — 288 trials in total. A swipe is a 44 cm pass, "nearer" is a descent from 25 cm and 0.8 s still, "farther" is 0.8 s still and a rise. A trial counts as correct when the first named gesture is right. Firmware settings, 400 kHz, burst:

Gesture (model) 3 cm 5 cm 8 cm 12 cm 0.25 m/s 0.5 m/s 1 m/s 2 m/s
up 12/12 12/12 12/12 0/12 9/12 9/12 9/12 9/12
down 12/12 12/12 12/12 0/12 9/12 9/12 9/12 9/12
left 12/12 12/12 11/12 0/12 9/12 9/12 9/12 8/12
right 12/12 12/12 11/12 0/12 9/12 9/12 9/12 8/12
nearer 11/12 9/12 0/12 0/12 6/12 6/12 6/12 2/12
farther 12/12 12/12 12/12 0/12 9/12 9/12 9/12 9/12

In total 198 of 288: swipes 142 of 192, "nearer" and "farther" 56 of 96. No wrong answers — 90 misses where no gesture was named; 72 of them at 12 cm: there PDATA is about 30 (100в.3), below the entry threshold of 40, and the gesture engine never wakes up. "Nearer" down to 8 cm fails for the same reason: the hand enters gesture mode between 12 and 8 cm, and by 8 cm the sum grows less than one and a half times. "Nearer" at 2 m/s — 2 of 12: at that speed the descent hardly falls into the decoding window in the model.


100в.6 Sunlight, glass and offsets

Sunlight. The sensor subtracts light not tied to the pulse, but the amplifier input saturates from it (the PGSAT bit in STATUS). In the model this happens when ambient light in klx × gain × pulse length / 8 µs reaches 400: at 4x and 32 µs — from 25 klx, at 8x and 32 µs — from 12.5 klx, at 2x and 8 µs — from 200 klx. The threshold and the ambient noise are model assumptions.

Glass. A cover over the sensor reflects part of the pulse straight into the photodiodes. In the model the glass adds 70, 40, 40 and 70 counts to U, D, L, R (at the firmware gesture settings). Without a hand PDATA = 22.0 instead of 0.2, the gesture channels are 70, 40, 41, 71: above GEXTH 30, so once the machine enters it never leaves — after a swipe GMODE stayed 1 and no gesture was named.

Offsets. POFFSET_UR and POFFSET_DL are subtracted from the U+R and D+L proximity pairs, GOFFSET_U…GOFFSET_R from the gesture channels. The sign-magnitude format (bit 7 — minus) is from the manufacturer's documentation, and subtracting one to one in counts is a model assumption. Calibration without a hand: 16 proximity cycles, then 16 datasets in forced gesture mode (writing GMODE = 1, GEXTH = 0 so the machine does not leave):

    TRY(wr(d, APDS_GEXTH, 0));                          /* «меньше нуля» не бывает — автомат не выйдет */
    TRY(wr(d, APDS_ENABLE, APDS_EN_PON | APDS_EN_PEN | APDS_EN_GEN));
    TRY(wr(d, APDS_GCONF4, APDS_G4_GMODE));             /* войти в жесты без руки */

On the bench calibration takes 108 transfers; the offsets are 22 and 22 for proximity, 70, 40, 40 and 70 for gestures. After it, without a hand, PDATA = 3.2, the channels are 0, 1, 0, 0, and the upward swipe is named again.

Conditions (model), 288 trials Correct Swipes of 192 Nearer/farther of 96 Wrong Extra
firmware settings 198 142 56 0 0
three times the noise 191 138 53 0 0
sunlight 30 klx 0 0 0 0 0
sunlight 30 klx, GGAIN 2x, 64 pulses of 8 µs 194 139 55 0 0
glass 48 0 48 90 10
glass, after calibration 198 143 55 0 0
100 mA, 300 %, 8x gain 288 192 96 0 0
the same with glass 0 0 0 0 0
the same, glass after calibration 40 0 40 7 0
colour on, ATIME 219 (102.86 ms) 138 99 39 6 0
GWTIME 39.2 ms 115 81 34 0 0
456 µs dataset, 100 kHz, per dataset 101 57 44 43 52
456 µs dataset, 100 kHz, burst 101 58 43 38 54
456 µs dataset, 400 kHz, burst 92 48 44 48 70

What it shows. Sunlight of 30 klx at 4x and 32 µs kills everything; lower gain and short frequent pulses bring back 194 of 288. Glass without calibration breaks swipes completely, after calibration it is as if there were no glass. A strong LED wins in the model — a hand at 12 cm becomes visible — but the same glass is four times brighter: the channels hit 255, an offset is at most 127, and after calibration (all four gesture offsets 127) there are 40 correct, no swipes at all. Colour with ATIME 219 stretches the cycle by 102.86 ms: the machine checks for entry once a cycle and misses fast swipes. A long GWTIME gives a swipe too few datasets. A short 456 µs dataset is bad even without losses: at 400 kHz the queue does not overflow, yet there are 92 correct and 70 extra names — 8 pulses of 8 µs carry 5 times less light than 10 of 32 µs.


100в.7 Pitfalls

The interrupt came once and never again. PINT was not cleared: INT stays low, there is no falling edge. Access PICLEAR (0xE5) or AICLEAR (0xE7) after every service; then check the INT level.

Interrupts every 28 ms while the hand is near. The thresholds do not change: PDATA is above PIHT every cycle. Hysteresis — after "near" wait for PDATA below the low threshold.

False "near" at the border. Noise at the threshold. A larger PPERS or hysteresis.

Datasets are lost, GFOV = 1. The queue is 32 datasets: at 100 kHz per dataset you cannot keep up with a dataset every 456 µs. Read a burst of 4 · GFLVL bytes, use 400 kHz, service on INT rather than every 200 ms.

After a swipe the sensor is "stuck" in gesture mode. Glass or the housing push the channels above GEXTH — the machine does not leave. Calibrate the offsets without a hand.

Swipes are not recognized at all, while proximity works. Sunlight saturated the input (PGSAT): lower gain, shorter and more frequent pulses, shade.

Too much current — and it got worse. A 100 mA LED with a 300 % boost and 8x gain pins proximity at 255 up to 5 cm and makes the glass reflection four times brighter — 0 correct of 288 in the model. Choose the current for the place, not "at maximum".

Colour switched on — fast swipes disappeared. A long ATIME stretches the cycle; entry into gesture mode is checked rarely.

The Zephyr binding settings are not what the labels say. In Zephyr 4.4.2 the values in avago,apds9960.yaml are listed as 0x00, 0x01, 0x10, 0x11 — binary codes written in hex. The driver shifts them so that something else ends up in the register:

Property Value and label In the register
pgain 0x01 "4x" (default) PGAIN = 0 — 1x
pgain 0x11 "8x" PGAIN = 0 — 1x
again 0x10 "16x" AGAIN = 0 — 1x
again 0x11 "64x" AGAIN = 1 — 4x
pled-boost 0x10 "200" LED_BOOST = 0 — 100 %
pled-boost 0x11 "300" LED_BOOST = 1 — 150 %
ppulse-length 0x01 "8us" (default) PPULSE = 0x07 — 4 µs, 8 pulses
ppulse-length 0x11 "32us" PPULSE = 0x17 — 4 µs, 24 pulses

pgain is shifted by 1 and masked with bits 3:2 — zero remains; pled-boost is shifted by 4 into a uint8_t field — 0x10 loses its top bit; the pulse length is OR-ed with the pulse count in the low bits. Carry settings over from someone else's driver by register bits, not by labels.

A 5 V module. The sensor runs from 3.3 V. Whether the module has a regulator, what SDA, SCL and INT are pulled up to and where VL is fed from — check the schematic of your module (not verified). PB6, PB7 and PB8 are FT.


100в.8 Summary


Full sources

The driver and the recognizer are shared by the firmware and the bench; the host100d.c bench, the model100d.py model and the excerpts from Zephyr and the library are in _verify/ges100d/.

apds100d.h — registers, bits, recognition constants, the settings and the driver state.

/* Раздел 100в: APDS-9960 по I2C — приближение по порогу с прерыванием, движок жестов с FIFO, распознавание взмаха.
 * Адреса регистров, биты и коды — как в драйвере apds9960 Zephyr 4.4.2 (include/zephyr/drivers/sensor/apds9960.h);
 * шаг 2,78 мс, коды GWTIME, PPULSE 0x87 и GPULSE 0xC9 — там же и в открытой библиотеке apds9960 (Python, GPL-3).
 * Помечено «док.» — по документации производителя, не проверено. Шину, паузу и ожидание INT даёт прошивка
 * (fw100d.c — I2C1 STM32F103) или стенд (host100d.c — строки к модели model100d.py). */
#ifndef APDS100D_H
#define APDS100D_H
#include <stdint.h>

#define APDS_ADDR        0x39u      /* apds9960@39 в дереве устройств Zephyr */
#define APDS_ID_A        0xABu      /* APDS9960_ID_1 */
#define APDS_ID_B        0x9Cu      /* APDS9960_ID_2 — Zephyr принимает и его */
#define APDS_BOOT_US     5700u      /* «Initialize time 5.7ms» — комментарий в apds9960_init */

/* ── регистры ── */
#define APDS_ENABLE      0x80u
#define APDS_ATIME       0x81u
#define APDS_WTIME       0x83u
#define APDS_PILT        0x89u
#define APDS_PIHT        0x8Bu
#define APDS_PERS        0x8Cu      /* PPERS [7:4], APERS [3:0] */
#define APDS_CONFIG1     0x8Du      /* WLONG [1]: пауза × 12 */
#define APDS_PPULSE      0x8Eu      /* длина [7:6]: 4, 8, 16, 32 мкс; число импульсов − 1 [5:0] */
#define APDS_CONTROL     0x8Fu      /* LDRIVE [7:6], PGAIN [3:2], AGAIN [1:0] */
#define APDS_CONFIG2     0x90u      /* LED_BOOST [5:4]: 100, 150, 200, 300 % */
#define APDS_ID          0x92u
#define APDS_STATUS      0x93u
#define APDS_CDATAL      0x94u      /* 0x94…0x9B — C, R, G, B по два байта, младший первым */
#define APDS_PDATA       0x9Cu
#define APDS_POFFSET_UR  0x9Du      /* смещения приближения для пар U+R и D+L — док. */
#define APDS_POFFSET_DL  0x9Eu
#define APDS_CONFIG3     0x9Fu
#define APDS_GPENTH      0xA0u      /* порог входа в жесты (по PDATA) */
#define APDS_GEXTH       0xA1u      /* порог выхода из жестов */
#define APDS_GCONF1      0xA2u      /* GFIFOTH [7:6], GEXMSK [5:2], GEXPERS [1:0] */
#define APDS_GCONF2      0xA3u      /* GGAIN [6:5], GLDRIVE [4:3], GWTIME [2:0] */
#define APDS_GOFFSET_U   0xA4u
#define APDS_GOFFSET_D   0xA5u
#define APDS_GPULSE      0xA6u
#define APDS_GOFFSET_L   0xA7u
#define APDS_GOFFSET_R   0xA9u
#define APDS_GCONF3      0xAAu
#define APDS_GCONF4      0xABu      /* GFIFO_CLR [2], GIEN [1], GMODE [0] */
#define APDS_GFLVL       0xAEu      /* сколько наборов в FIFO */
#define APDS_GSTATUS     0xAFu      /* GFOV [1], GVALID [0] */
#define APDS_PICLEAR     0xE5u      /* обращение снимает прерывание приближения */
#define APDS_AICLEAR     0xE7u      /* обращение снимает все, кроме жестов */
#define APDS_GFIFO_U     0xFCu      /* 0xFC…0xFF — U, D, L, R первого набора FIFO */

/* ── биты ── */
#define APDS_EN_PON      0x01u
#define APDS_EN_AEN      0x02u
#define APDS_EN_PEN      0x04u
#define APDS_EN_WEN      0x08u
#define APDS_EN_AIEN     0x10u
#define APDS_EN_PIEN     0x20u
#define APDS_EN_GEN      0x40u
#define APDS_ST_PGSAT    0x40u      /* насыщение приближения и жестов */
#define APDS_ST_PINT     0x20u
#define APDS_ST_AINT     0x10u
#define APDS_ST_GINT     0x04u
#define APDS_ST_PVALID   0x02u
#define APDS_G4_CLR      0x04u
#define APDS_G4_GIEN     0x02u
#define APDS_G4_GMODE    0x01u
#define APDS_GS_GFOV     0x02u
#define APDS_GS_GVALID   0x01u

#define APDS_STEP_US     2780u      /* шаг ATIME и WTIME: 2,78 мс (APDS9960_DEFAULT_WAIT_TIME) */
#define APDS_FIFO_SETS   32u        /* FIFO — 32 набора по 4 байта (док.) */
#define APDS_CONFIG1_DEF 0x60u      /* APDS9960_DEFAULT_CONFIG1 */
#define APDS_CONFIG2_B0  0x01u      /* бит 0 CONFIG2 оба драйвера держат единицей */

/* ── распознавание ── */
#define GES_MIN_IN       10u        /* в разбор идут наборы, где все четыре больше 10 */
#define GES_SENS         50         /* сдвиг отношения на 50 из 100 — это взмах */
#define GES_STILL        20         /* сдвиг меньше 20 по обеим осям — рука не шла вбок */
#define GES_MIN_SETS     4u         /* меньше четырёх годных наборов — не жест */
#define GES_HOLD         96u        /* рука не уходит 96 наборов — решить «ближе» не дожидаясь выхода */
#define GES_AVG          4u         /* сумма U+D+L+R в начале и в конце — среднее по 4 наборам */

enum { GES_NONE = 0, GES_UP, GES_DOWN, GES_LEFT, GES_RIGHT, GES_NEAR, GES_FAR };
enum { APDS_OK = 0, APDS_E_NACK = -1, APDS_E_ID = -2, APDS_E_ARG = -3 };

/* ── шина, время и вывод INT: реализует прошивка или стенд ── */
int  apds_port_write(uint8_t addr7, const uint8_t *buf, uint32_t n);          /* 0 — ведомый подтвердил всё */
int  apds_port_read(uint8_t addr7, uint8_t reg, uint8_t *buf, uint32_t n);    /* адрес регистра, повторный старт, n байт */
void apds_port_delay_us(uint32_t us);
int  apds_port_wait(uint32_t max_us);   /* ждать спада INT: 1 — был спад, 0 — время вышло, 2 — конец опыта (стенд) */
int  apds_port_int_low(void);           /* уровень INT сейчас: 1 — линия прижата */

typedef struct {
    uint8_t atime, wtime, ppulse, control, config2, pers, pilt, piht;
    uint8_t gpenth, gexth, gconf1, gconf2, gpulse;
    uint8_t enable;                     /* PON добавится сам */
    uint8_t gien;                       /* прерывание по FIFO жестов */
    uint8_t block;                      /* 1 — FIFO одним чтением 4 · GFLVL байт, 0 — по набору (как Zephyr) */
    uint8_t clear_int;                  /* 0 — не снимать прерывание приближения (опыт «INT залип») */
    uint8_t hyst_lo;                    /* ≠ 0 — менять пороги местами: ближе → ждать PDATA < hyst_lo */
} apds_cfg_t;

typedef struct {
    int16_t ud0, lr0, ud1, lr1;         /* отношения (U−D)/(U+D) и (L−R)/(L+R) · 100: первое и последнее */
    uint32_t s0, s1[GES_AVG];           /* сумма четырёх: начало (накоплено) и последние GES_AVG */
    uint16_t n_in, n_all;               /* годных наборов и всех с начала окна */
} ges_t;

typedef struct {
    uint8_t addr, enable, gconf4, pdata, status, near;
    const apds_cfg_t *cfg;
    uint8_t active;                     /* автомат жестов работал при прошлом обслуживании */
    ges_t g;
    uint32_t xfers, bytes, sets, ovf, pint, sessions;
} apds_t;

int  apds_init(apds_t *d, const apds_cfg_t *c);
int  apds_calibrate(apds_t *d, uint8_t n);
int  apds_service(apds_t *d);
int  apds_read_crgb(apds_t *d, uint16_t crgb[4]);
uint8_t apds_sm(int v);
void ges_reset(ges_t *g);
void ges_feed(ges_t *g, const uint8_t s[4]);
int  ges_decide(const ges_t *g, int final);
#endif

apds100d.c — the driver: power-up, offset calibration, INT service, FIFO, the swipe recognizer.

/* Раздел 100в: драйвер APDS-9960 и распознаватель взмаха. Общий для прошивки и стенда. */
#include <string.h>
#include "apds100d.h"

#define TRY(x) do { int rc_ = (x); if (rc_ != APDS_OK) return rc_; } while (0)

/* ── шина: каждая запись — адрес регистра и байт; чтение — адрес, повторный старт, n байт ── */
static int wr(apds_t *d, uint8_t reg, uint8_t v)
{
    uint8_t b[2] = {reg, v};
    d->xfers++;
    d->bytes += 2u;
    return apds_port_write(d->addr, b, 2) ? APDS_E_NACK : APDS_OK;
}

static int rd(apds_t *d, uint8_t reg, uint8_t *p, uint32_t n)
{
    d->xfers++;
    d->bytes += 1u + n;
    return apds_port_read(d->addr, reg, p, n) ? APDS_E_NACK : APDS_OK;
}

/* смещение в формате «знак и модуль»: бит 7 — минус, 6:0 — величина (док.) */
uint8_t apds_sm(int v)
{
    if (v > 127)
        v = 127;
    if (v < -127)
        v = -127;
    return (uint8_t)(v < 0 ? 0x80 | -v : v);
}

/* Включение: выждать 5,7 мс, проверить ID, всё выключить, записать настройки, снять прерывания, включить. */
int apds_init(apds_t *d, const apds_cfg_t *c)
{
    const uint8_t regs[][2] = {
        {APDS_ATIME, c->atime}, {APDS_WTIME, c->wtime}, {APDS_CONFIG1, APDS_CONFIG1_DEF}, {APDS_PPULSE, c->ppulse},
        {APDS_CONTROL, c->control}, {APDS_CONFIG2, c->config2}, {APDS_CONFIG3, 0}, {APDS_PERS, c->pers},
        {APDS_PILT, c->pilt}, {APDS_PIHT, c->piht}, {APDS_POFFSET_UR, 0}, {APDS_POFFSET_DL, 0},
        {APDS_GPENTH, c->gpenth}, {APDS_GEXTH, c->gexth}, {APDS_GCONF1, c->gconf1}, {APDS_GCONF2, c->gconf2},
        {APDS_GPULSE, c->gpulse}, {APDS_GOFFSET_U, 0}, {APDS_GOFFSET_D, 0}, {APDS_GOFFSET_L, 0},
        {APDS_GOFFSET_R, 0}, {APDS_GCONF3, 0},
    };
    uint8_t id = 0;
    memset(d, 0, sizeof *d);
    d->addr = APDS_ADDR;
    d->cfg = c;
    apds_port_delay_us(APDS_BOOT_US);
    TRY(rd(d, APDS_ID, &id, 1));
    if (id != APDS_ID_A && id != APDS_ID_B)
        return APDS_E_ID;
    TRY(wr(d, APDS_ENABLE, 0));                         /* всё выключить, как Zephyr */
    TRY(wr(d, APDS_GCONF4, APDS_G4_CLR));               /* выйти из жестов, очистить FIFO */
    for (uint32_t i = 0; i < sizeof regs / sizeof regs[0]; i++)
        TRY(wr(d, regs[i][0], regs[i][1]));
    TRY(wr(d, APDS_AICLEAR, 0));                        /* снять всё, что накопилось */
    d->gconf4 = c->gien ? APDS_G4_GIEN : 0u;
    TRY(wr(d, APDS_GCONF4, d->gconf4));
    d->enable = (uint8_t)(APDS_EN_PON | c->enable);
    return wr(d, APDS_ENABLE, d->enable);
}

/* Над датчиком пусто: сколько дают стекло и корпус. PDATA усреднить по n циклам и вычесть из обеих пар
 * (POFFSET_UR и POFFSET_DL); затем n наборов в принудительном режиме жестов — каждый канал в свой GOFFSET. */
int apds_calibrate(apds_t *d, uint8_t n)
{
    const apds_cfg_t *c = d->cfg;
    const uint8_t goff[4] = {APDS_GOFFSET_U, APDS_GOFFSET_D, APDS_GOFFSET_L, APDS_GOFFSET_R};
    uint32_t cycle = (256u - c->wtime) * APDS_STEP_US + 1000u, ps = 0, sum[4] = {0, 0, 0, 0};
    uint8_t p, lvl, s[4], got = 0;
    if (n == 0 || n > APDS_FIFO_SETS)
        return APDS_E_ARG;
    TRY(wr(d, APDS_ENABLE, APDS_EN_PON | APDS_EN_PEN | (c->enable & APDS_EN_WEN)));
    for (uint8_t i = 0; i < n; i++) {
        apds_port_delay_us(cycle);
        TRY(rd(d, APDS_PDATA, &p, 1));
        ps += p;
    }
    TRY(wr(d, APDS_POFFSET_UR, apds_sm((int)((ps + n / 2u) / n))));
    TRY(wr(d, APDS_POFFSET_DL, apds_sm((int)((ps + n / 2u) / n))));
    TRY(wr(d, APDS_GEXTH, 0));                          /* «меньше нуля» не бывает — автомат не выйдет */
    TRY(wr(d, APDS_ENABLE, APDS_EN_PON | APDS_EN_PEN | APDS_EN_GEN));
    TRY(wr(d, APDS_GCONF4, APDS_G4_GMODE));             /* войти в жесты без руки */
    for (uint32_t tries = 0; got < n && tries < 1000u; tries++) {
        apds_port_delay_us(1000u);
        TRY(rd(d, APDS_GFLVL, &lvl, 1));
        for (; lvl > 0 && got < n; lvl--, got++) {
            TRY(rd(d, APDS_GFIFO_U, s, 4));
            for (uint8_t k = 0; k < 4u; k++)
                sum[k] += s[k];
        }
    }
    TRY(wr(d, APDS_GCONF4, APDS_G4_CLR));               /* выйти и очистить FIFO */
    if (got < n)
        return APDS_E_ARG;
    for (uint8_t k = 0; k < 4u; k++)
        TRY(wr(d, goff[k], apds_sm((int)((sum[k] + n / 2u) / n))));
    TRY(wr(d, APDS_GEXTH, c->gexth));
    TRY(wr(d, APDS_AICLEAR, 0));
    TRY(wr(d, APDS_GCONF4, d->gconf4));
    return wr(d, APDS_ENABLE, d->enable);
}

/* ── распознавание: отношения (U−D)/(U+D) и (L−R)/(L+R) в первом и последнем годном наборе ── */
void ges_reset(ges_t *g)
{
    memset(g, 0, sizeof *g);
}

static int16_t ratio(uint8_t a, uint8_t b)
{
    return (int16_t)(((int)a - (int)b) * 100 / ((int)a + (int)b));
}

void ges_feed(ges_t *g, const uint8_t s[4])
{
    uint32_t sum = (uint32_t)s[0] + s[1] + s[2] + s[3];
    g->n_all++;
    if (s[0] <= GES_MIN_IN || s[1] <= GES_MIN_IN || s[2] <= GES_MIN_IN || s[3] <= GES_MIN_IN)
        return;                                         /* рука на краю или её нет — в разбор не идёт */
    g->ud1 = ratio(s[0], s[1]);
    g->lr1 = ratio(s[2], s[3]);
    if (g->n_in == 0) {
        g->ud0 = g->ud1;
        g->lr0 = g->lr1;
    }
    if (g->n_in < GES_AVG)
        g->s0 += sum;
    g->s1[g->n_in % GES_AVG] = sum;
    g->n_in++;
}

/* final = 1 — автомат жестов вышел (рука ушла): взмах, «ближе» или «дальше»;
 * final = 0 — рука ещё над датчиком: только «ближе» (опустилась и стоит). */
int ges_decide(const ges_t *g, int final)
{
    int dud = g->ud1 - g->ud0, dlr = g->lr1 - g->lr0;
    int aud = dud < 0 ? -dud : dud, alr = dlr < 0 ? -dlr : dlr;
    uint32_t s1 = 0;
    if (g->n_in < GES_MIN_SETS)
        return GES_NONE;
    for (uint8_t i = 0; i < GES_AVG; i++)
        s1 += g->s1[i];
    if (final && (aud >= GES_SENS || alr >= GES_SENS)) {
        if (aud >= alr)
            return dud < 0 ? GES_UP : GES_DOWN;         /* вверх: сначала U больше D, в конце меньше */
        return dlr < 0 ? GES_LEFT : GES_RIGHT;
    }
    if (aud < GES_STILL && alr < GES_STILL) {
        if (2u * s1 >= 3u * g->s0)
            return GES_NEAR;                            /* сумма выросла в полтора раза — рука ближе */
        if (final && 3u * s1 <= 2u * g->s0)
            return GES_FAR;
    }
    return GES_NONE;
}

/* FIFO: block = 1 — одним чтением 4 · n байт (после 0xFF адрес снова 0xFC, это уже следующий набор);
 * block = 0 — каждый набор своим чтением четырёх байт, как в драйвере Zephyr. */
static int fifo_read(apds_t *d, uint8_t n)
{
    uint8_t b[4u * APDS_FIFO_SETS];
    if (d->cfg->block) {
        TRY(rd(d, APDS_GFIFO_U, b, 4u * n));
    } else {
        for (uint8_t i = 0; i < n; i++)
            TRY(rd(d, APDS_GFIFO_U, b + 4u * i, 4));
    }
    for (uint8_t i = 0; i < n; i++)
        ges_feed(&d->g, b + 4u * i);
    d->sets += n;
    return APDS_OK;
}

/* Приближение: прочитать PDATA, при гистерезисе поменять пороги местами, снять прерывание.
 * Пока прерывание не снято, INT стоит в нуле — нового спада не будет. */
static int prox_irq(apds_t *d)
{
    const apds_cfg_t *c = d->cfg;
    d->pint++;
    TRY(rd(d, APDS_PDATA, &d->pdata, 1));
    if (c->hyst_lo) {
        d->near = d->pdata > c->hyst_lo;
        TRY(wr(d, APDS_PILT, d->near ? c->hyst_lo : 0u));     /* рядом — ждать, когда станет меньше */
        TRY(wr(d, APDS_PIHT, d->near ? 255u : c->piht));      /* далеко — ждать, когда станет больше */
    }
    if (c->clear_int)
        TRY(wr(d, APDS_PICLEAR, 0));
    return APDS_OK;
}

/* Обслужить INT (или тик, пока идёт жест). Возвращает GES_…, если жест распознан, или код ошибки. */
int apds_service(apds_t *d)
{
    uint8_t st, lv[2], g4;
    int ev = GES_NONE;
    TRY(rd(d, APDS_STATUS, &st, 1));
    d->status = st;
    if (st & APDS_ST_PINT)
        TRY(prox_irq(d));
    if (!(st & APDS_ST_GINT) && !d->active)
        return GES_NONE;
    if (!d->active) {
        d->active = 1;
        d->sessions++;
        ges_reset(&d->g);
    }
    for (;;) {
        TRY(rd(d, APDS_GFLVL, lv, 2));                  /* GFLVL и GSTATUS — соседи: одно чтение */
        if (lv[1] & APDS_GS_GFOV)
            d->ovf++;
        if (lv[0] > APDS_FIFO_SETS)
            lv[0] = APDS_FIFO_SETS;
        if (lv[0])
            TRY(fifo_read(d, lv[0]));
        TRY(rd(d, APDS_GCONF4, &g4, 1));
        if (g4 & APDS_G4_GMODE)
            break;                                      /* рука ещё здесь */
        TRY(rd(d, APDS_GFLVL, lv, 1));
        if (lv[0] == 0) {                               /* автомат вышел, FIFO пуст — жест окончен */
            d->active = 0;
            return ges_decide(&d->g, 1);
        }
    }
    if (d->g.n_all >= GES_HOLD) {
        ev = ges_decide(&d->g, 0);
        if (ev != GES_NONE)
            ges_reset(&d->g);                           /* «ближе» сказано — дальше считать заново */
    }
    return ev;
}

/* Цвет и освещённость: C, R, G, B — восемь байт с 0x94, младший первым (подробно — TCS34725, раздел 103б). */
int apds_read_crgb(apds_t *d, uint16_t crgb[4])
{
    uint8_t b[8];
    TRY(rd(d, APDS_CDATAL, b, 8));
    for (uint8_t i = 0; i < 4u; i++)
        crgb[i] = (uint16_t)(b[2 * i] | b[2 * i + 1] << 8);
    return APDS_OK;
}

fw100d.c — F103 firmware: I2C1 at 400 kHz, INT on PB8 via EXTI8, SysTick, the main loop.

/* fw100d.c — раздел 100в: APDS-9960 на I2C1 STM32F103 (BluePill), 72 МГц.
 * I2C1: PB6 — SCL, PB7 — SDA, 400 кГц (как в 100б). INT датчика — PB8: вход с подтяжкой, EXTI8 по спаду
 * (INT — открытый сток, активный ноль). SysTick — 1 мс: тик для ожидания и для конца жеста.
 * Жесты — с прерыванием по FIFO (4 набора), FIFO читается пачкой; итог — в gesture и счётчиках n_gest.
 * Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); APB1 = 36 МГц. */
#include "stm32f10x.h"
#include "apds100d.h"

#define CPU_HZ 72000000u
#define APB1_MHZ 36u
#define I2C_FAST_CCR (APB1_MHZ * 1000000u / (3u * 400000u))   /* F/S = 1, DUTY = 0: Tlow = 2 Thigh */
#define I2C_FAST_TRISE (APB1_MHZ * 300u / 1000u + 1u)         /* фронт до 300 нс */
#define I2C_SPIN 20000u
#define TICK_US 10000u                                         /* пока идёт жест — смотреть FIFO раз в 10 мс */
#define RETRY 4u

_Static_assert(I2C_FAST_CCR == 30u, "CCR = 30: 36 МГц / (3 · 30) = 400 кГц");
_Static_assert(I2C_FAST_TRISE == 11u, "TRISE = 11");

/* Настройки: приближение 8 × 16 мкс, 100 мА, 4x; цвет 4x; жесты 10 × 32 мкс, 100 мА, 4x, пауза 2,8 мс;
 * LED_BOOST 150 %; вход в жесты при PDATA > 40, выход — все четыре меньше 30; прерывание с 4 наборов. */
static const apds_cfg_t CFG = {
    .atime = 0xDBu, .wtime = 0xFFu, .ppulse = 0x87u, .control = 0x09u, .config2 = 0x11u, .pers = 0x10u,
    .pilt = 0u, .piht = 0xFFu, .gpenth = 40u, .gexth = 30u, .gconf1 = 0x40u, .gconf2 = 0x41u, .gpulse = 0xC9u,
    .enable = APDS_EN_PEN | APDS_EN_WEN | APDS_EN_GEN, .gien = 1u, .block = 1u, .clear_int = 1u, .hyst_lo = 0u,
};

static apds_t D;
static volatile uint8_t int_flag;
static volatile uint32_t ms;
volatile int8_t gesture;                       /* последний жест GES_…, отрицательное — ошибка */
volatile uint32_t n_gest[7];

/* ── I2C1, ведущий, опросом; на каждом ожидании — предел ── */
static int wait1(uint32_t mask)
{
    for (uint32_t n = 0; n < I2C_SPIN; n++) {
        uint32_t sr = I2C1->SR1;
        if (sr & I2C_SR1_AF) {
            I2C1->SR1 = (uint16_t)~I2C_SR1_AF;
            return 1;
        }
        if (sr & mask)
            return 0;
    }
    return 2;
}

static void stop(void)
{
    I2C1->CR1 |= I2C_CR1_STOP;
    while (I2C1->CR1 & I2C_CR1_STOP) {
    }
}

static int start(uint8_t a8)
{
    I2C1->CR1 |= I2C_CR1_START;
    if (wait1(I2C_SR1_SB))
        return 1;
    I2C1->DR = a8;
    if (wait1(I2C_SR1_ADDR)) {
        stop();
        return 1;
    }
    return 0;
}

int apds_port_write(uint8_t addr7, const uint8_t *buf, uint32_t n)
{
    if (start((uint8_t)(addr7 << 1)))
        return 1;
    (void)I2C1->SR1;
    (void)I2C1->SR2;
    for (uint32_t i = 0; i < n; i++) {
        if (wait1(I2C_SR1_TXE)) {
            stop();
            return 1;
        }
        I2C1->DR = buf[i];
    }
    int e = n ? wait1(I2C_SR1_BTF) : 0;
    stop();
    return e;
}

/* Приём по RM0008: один байт — ACK снять до снятия ADDR; два — POS; больше двух — способ 2 (последние три через BTF).
 * FIFO жестов пачкой — до 128 байт одним приёмом. */
int apds_port_read(uint8_t addr7, uint8_t reg, uint8_t *buf, uint32_t n)
{
    if (start((uint8_t)(addr7 << 1)))
        return 1;
    (void)I2C1->SR1;
    (void)I2C1->SR2;
    if (wait1(I2C_SR1_TXE)) {
        stop();
        return 1;
    }
    I2C1->DR = reg;
    if (wait1(I2C_SR1_BTF) || start((uint8_t)((addr7 << 1) | 1u))) {
        stop();
        return 1;
    }
    int e = 0;
    if (n == 1) {
        I2C1->CR1 &= ~I2C_CR1_ACK;
        (void)I2C1->SR1;
        (void)I2C1->SR2;
        I2C1->CR1 |= I2C_CR1_STOP;
        e = wait1(I2C_SR1_RXNE);
        buf[0] = (uint8_t)I2C1->DR;
    } else if (n == 2) {
        I2C1->CR1 |= I2C_CR1_POS;
        I2C1->CR1 &= ~I2C_CR1_ACK;
        (void)I2C1->SR1;
        (void)I2C1->SR2;
        e = wait1(I2C_SR1_BTF);
        I2C1->CR1 |= I2C_CR1_STOP;
        buf[0] = (uint8_t)I2C1->DR;
        buf[1] = (uint8_t)I2C1->DR;
    } else {
        (void)I2C1->SR1;
        (void)I2C1->SR2;
        uint32_t i = 0;
        for (; i < n - 3u && !e; i++) {
            e = wait1(I2C_SR1_RXNE);
            buf[i] = (uint8_t)I2C1->DR;
        }
        e |= wait1(I2C_SR1_BTF);
        I2C1->CR1 &= ~I2C_CR1_ACK;
        buf[i++] = (uint8_t)I2C1->DR;
        I2C1->CR1 |= I2C_CR1_STOP;
        buf[i++] = (uint8_t)I2C1->DR;
        e |= wait1(I2C_SR1_RXNE);
        buf[i] = (uint8_t)I2C1->DR;
    }
    while (I2C1->CR1 & I2C_CR1_STOP) {
    }
    I2C1->CR1 = (I2C1->CR1 & ~I2C_CR1_POS) | I2C_CR1_ACK;
    return e;
}

/* ── время, INT, сон ── */
void apds_port_delay_us(uint32_t us)
{
    uint32_t t0 = DWT->CYCCNT, n = us * (CPU_HZ / 1000000u);
    while (DWT->CYCCNT - t0 < n) {
    }
}

/* Ждать спада INT не дольше max_us. Флаг и сон — под запретом прерываний: спад между проверкой и WFI всё равно разбудит. */
int apds_port_wait(uint32_t max_us)
{
    uint32_t t0 = ms;
    for (;;) {
        __disable_irq();
        if (int_flag) {
            int_flag = 0;
            __enable_irq();
            return 1;
        }
        if ((ms - t0) * 1000u >= max_us) {
            __enable_irq();
            return 0;
        }
        __WFI();
        __enable_irq();
    }
}

int apds_port_int_low(void)
{
    return !(GPIOB->IDR & (1u << 8));
}

void SysTick_Handler(void)
{
    ms++;
}

void EXTI9_5_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR8) {
        EXTI->PR = EXTI_PR_PR8;
        int_flag = 1;
    }
}

static void board_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    SysTick_Config(CPU_HZ / 1000u);

    /* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (11 11) */
    GPIOB->CRL = (GPIOB->CRL & ~0xFF000000u) | 0xFF000000u;
    /* PB8 — вход с подтяжкой (10 00), ODR = 1 — к питанию */
    GPIOB->CRH = (GPIOB->CRH & ~0x0000000Fu) | 0x00000008u;
    GPIOB->BSRR = 1u << 8;

    I2C1->CR1 = I2C_CR1_SWRST;
    I2C1->CR1 = 0;
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = I2C_CCR_FS | I2C_FAST_CCR;
    I2C1->TRISE = I2C_FAST_TRISE;
    I2C1->CR1 = I2C_CR1_PE | I2C_CR1_ACK;

    /* EXTI8 от порта B: AFIO_EXTICR3, поле EXTI8 = 0001; по спаду */
    AFIO->EXTICR[2] = (AFIO->EXTICR[2] & ~AFIO_EXTICR3_EXTI8) | AFIO_EXTICR3_EXTI8_PB;
    EXTI->FTSR |= EXTI_FTSR_TR8;
    EXTI->PR = EXTI_PR_PR8;
    EXTI->IMR |= EXTI_IMR_MR8;
    NVIC_EnableIRQ(EXTI9_5_IRQn);
}

int main(void)
{
    int rc;
    board_init();
    rc = apds_init(&D, &CFG);
    if (rc == APDS_OK)
        rc = apds_calibrate(&D, 16u);          /* над датчиком пусто: вычесть стекло и корпус */
    gesture = (int8_t)rc;
    for (;;) {
        int w = apds_port_wait(D.active ? TICK_US : 1000000u);
        if (w == 0 && !D.active)
            continue;
        for (uint32_t k = 0; k < RETRY; k++) { /* INT всё ещё прижат — обслужить ещё раз, иначе нового спада не будет */
            int ev = apds_service(&D);
            if (ev != GES_NONE) {
                gesture = (int8_t)ev;
                if (ev > 0)
                    n_gest[ev]++;
            }
            if (!apds_port_int_low())
                break;
        }
    }
}

/* ── вектора и запуск (armclang + microlib): __main заполняет .data и .bss и вызывает main ── */
#define STACK_TOP 0x20005000u                  /* конец 20 КБ ОЗУ F103C8 */
__asm(".global __initial_sp\n.set __initial_sp, 0x20005000");
extern void __main(void);

void Reset_Handler(void)
{
    __main();
}

static void Default_Handler(void)
{
    for (;;) {
    }
}

__attribute__((section("RESET"), used)) static void (*const vectors[16 + 43])(void) = {
    [0] = (void (*)(void))STACK_TOP, [1] = Reset_Handler,
    [2 ... 14] = Default_Handler, [15] = SysTick_Handler,
    [16 ... 16 + 22] = Default_Handler,
    [16 + 23] = EXTI9_5_IRQHandler,
    [16 + 24 ... 16 + 42] = Default_Handler,
};
_Static_assert(EXTI9_5_IRQn == 23, "EXTI9_5 — прерывание 23");

Next: section 101 "Current and voltage: INA219 and ACS712" — a shunt and the calibration register, a Hall sensor and its zero in the middle of the scale.

Open in the interactive handbook