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 boostIR 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б.
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
- Two engines: proximity gives
PDATA(0…255), gestures put U, D, L, R datasets into a 32-dataset queue. Address0x39, ID0xAB, 5.7 ms after power-up; power-up is 28 transfers, 2057.5 µs at 400 kHz. - The signal is current, boost, gain and pulses. In the model with the firmware settings
PDATA= 120.2 at 5 cm and 30.2 at 12 cm — below the entry threshold of 40. - INT is open drain, active low, pulled up on PB8. Clear the proximity interrupt by accessing
0xE5/0xE7: not cleared — 1 falling edge in 10 s; thresholds without hysteresis — 175 interrupts; with hysteresis — 10. - Read the queue as a burst: 92.42 µs per dataset at 400 kHz against 670 µs per dataset at 100 kHz; with a dataset every 456 µs, per-dataset reading at 100 kHz loses 947 of 2323.
- The direction is the shift of (U−D)/(U+D) and (L−R)/(L+R) from the first valid dataset to the last: 50 or more is a swipe. On the model's synthetic swipes — 198 correct of 288 and no wrong ones; everything below the entry threshold (12 cm) is a miss.
- Sunlight of 30 klx at 4x and 32 µs — 0 of 288, at 2x and 8 µs — 194. Glass — 48, after offset calibration — 198.
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.