100в. Датчик жестов, приближения и цвета APDS-9960 по I2C: ИК-светодиод и четыре фотодиода, FIFO жестов, распознавание взмаха, прерывание и пороги
В разделе 100 расстояние мерил звук, в 100а радар искал человека в комнате, в 100б VL53L0X мерил миллиметры временем пролёта света. APDS-9960 ничего не мерит в миллиметрах: он светит ИК-светодиодом и смотрит, сколько света вернулось, — в одно число «насколько близко» и в четыре числа от направленных фотодиодов. Из того, как эти четыре числа меняются во времени, получают взмах руки: вверх, вниз, влево, вправо, ближе, дальше. Цвет он тоже видит — каналы C, R, G и B, как у TCS34725 из 103б; здесь о цвете только коротко.
Здесь: что внутри и как устроены два движка — приближения и жестов; адрес, регистры и включение; ток и импульсы светодиода, усиление; пороги приближения, персистентность и прерывание, которое надо снимать; вход в жесты и выход из них, FIFO на 32 набора и как его читать, чтобы не переполнить; как из четырёх чисел получить направление; что делают солнце и стекло над датчиком и как вычесть стекло смещениями.
Опоры: драйвер apds9960 в Zephyr 4.4.2 (github.com/zephyrproject-rtos/zephyr,
коммит dccb095, Apache-2.0) — адреса регистров, биты, коды усиления, тока,
импульсов и пауз, адрес 0x39, ID 0xAB, пауза 5,7 мс после включения,
описание вывода INT; открытая библиотека apds9960 на Python (GPL-3, копия
лежит в наборе скриптов OpenMV, github.com/openmv/openmv) — настройки по
умолчанию с комментариями и разбор жестов по первому и последнему набору.
Документа производителя (Broadcom) в проекте нет: размер FIFO, порядок
работы движков, правила снятия прерываний, формат смещений и питание —
по документации производителя, не проверено. Модель датчика на Python и
синтетическая рука — свои, числа ниже говорят о модели, а не о датчике.
Стенд — драйвер apds100d.c и распознаватель на Си, собранные gcc и
прогнанные против модели; тот же код и прошивка fw100d.c собраны armclang
под STM32F103 и на плате не запускались. RM0008 (DocID13902 Rev 17):
AFIO_EXTICR3, с. 191; EXTI_FTSR, с. 211; таблица векторов 63, с. 204;
приём больше двух байт ведущим, способ 2, с. 762–763; приём одного и двух
байт, с. 764. DocID13587 Rev 17: PB6 — I2C1_SCL, PB7 — I2C1_SDA, оба FT,
с. 32; PB8 — FT, с. 33. Шина I²C — раздел 17, прерывания от вывода — 7.5,
подключение модулей — 95.
Как это называется в документации
Оригинал Перевод proximity engine / gesture engine движок приближения / движок жестов directional photodiodes U, D, L, R четыре фотодиода, каждый смотрит в свою сторону LED drive ( LDRIVE), LED boostток ИК-светодиода и его надбавка pulse count and length ( PPULSE,GPULSE)число и длина импульсов светодиода proximity gain ( PGAIN), gesture gain (GGAIN)усиление приближения и жестов proximity interrupt thresholds ( PILT,PIHT)нижний и верхний пороги приближения proximity persistence ( PPERS)сколько циклов подряд за порогом до прерывания приближения gesture enter / exit threshold ( GPENTH,GEXTH)порог входа в жесты и выхода из них gesture FIFO, dataset очередь жестов; набор — четыре байта U, D, L, R FIFO level ( GFLVL), FIFO overflow (GFOV)сколько наборов в очереди; очередь переполнилась proximity offset ( POFFSET_UR,POFFSET_DL)вычитаемое смещение пар U+R и D+L interrupt clear ( PICLEAR,AICLEAR)снятие прерывания обращением к адресу swipe взмах рукой над датчиком
100в.1 Что внутри: светодиод, четыре фотодиода и два движка
В корпусе — ИК-светодиод и фотодиоды за окном. Светодиод даёт пачку
импульсов, фотодиоды копят отражённый свет, из него вычитается фон, и
получается байт. Движок приближения складывает фотодиоды в одно число
PDATA (0…255); движок жестов пишет четыре числа — U, D, L и R — в очередь.
Пятый движок, цвета, — фотодиоды C, R, G и B за фильтрами, как у TCS34725
(103б): шестнадцатибитные CDATA…BDATA с 0x94, младший байт первым,
усиление AGAIN 1, 4, 16 и 64 (у TCS34725 — 1, 4, 16 и 60), шаг времени
2,78 мс вместо 2,4 мс. Подробно о цвете — в 103б.
Регистры — обычные: адрес регистра и данные, без командного байта, который нужен TCS34725. Главные:
| Адрес | Имя | Что в нём |
|---|---|---|
0x80 |
ENABLE |
PON 0, AEN 1, PEN 2, WEN 3, AIEN 4, PIEN 5, GEN 6 |
0x81, 0x83 |
ATIME, WTIME |
время цвета и пауза: (256 − код) × 2,78 мс |
0x89, 0x8B, 0x8C |
PILT, PIHT, PERS |
пороги приближения и персистентность (PPERS — биты 7:4) |
0x8E |
PPULSE |
длина импульса (биты 7:6: 4, 8, 16, 32 мкс) и число импульсов − 1 |
0x8F |
CONTROL |
LDRIVE 7:6 (100, 50, 25, 12,5 мА), 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 |
приближение и его смещения |
0xA0…0xA3 |
GPENTH, GEXTH, GCONF1, GCONF2 |
вход и выход; GFIFOTH 7:6, GEXMSK 5:2, GEXPERS 1:0; GGAIN 6:5, GLDRIVE 4:3, GWTIME 2:0 |
0xA6, 0xAB |
GPULSE, GCONF4 |
импульсы жестов; GFIFO_CLR 2, GIEN 1, GMODE 0 |
0xAE, 0xAF |
GFLVL, GSTATUS |
уровень FIFO; GFOV 1, GVALID 0 |
0xE5, 0xE7 |
PICLEAR, AICLEAR |
снять прерывание приближения / все, кроме жестов |
0xFC…0xFF |
GFIFO_U…GFIFO_R |
первый набор очереди |
Адреса, биты и коды — из заголовка драйвера Zephyr. Шаг 2,78 мс там же
(APDS9960_DEFAULT_WAIT_TIME), и с ним сходятся комментарии библиотеки:
ATIME 219 — «103ms» (37 × 2,78 = 102,86 мс), WTIME 246 — «27ms»
(10 × 2,78 = 27,8 мс). Множитель паузы ×12 (WLONG, бит 1 CONFIG1) — из
комментария библиотеки к CONFIG1 = 0x60. Паузы движка жестов GWTIME —
восемь кодов: 0; 2,8; 5,6; 8,4; 14,0; 22,4; 30,8 и 39,2 мс. Порядок работы
движков по циклу «приближение → жесты → пауза → цвет» — по документации
производителя, не проверено; модель работает так.
100в.2 Подключение к F103 и включение
| Вывод модуля | STM32F103 | Назначение |
|---|---|---|
| VCC | 3,3 В | питание датчика |
| VL | 3,3 В или своё питание | питание ИК-светодиода |
| GND | GND | земля |
| SCL, SDA | PB6, PB7 | I2C1, 400 кГц, адрес 0x39 |
| INT | PB8 | открытый сток, ноль активен; вход с подтяжкой, EXTI8 по спаду |
Адрес 0x39 — из дерева устройств Zephyr (apds9960@39) и из README
библиотеки («hard wired»: менять нечем, второй датчик на ту же шину не
поставить). Про INT описание привязки Zephyr говорит прямо: открытый сток,
активный ноль, при прямом подключении — подтяжка на выводе
микроконтроллера; у платы Arduino Nano 33 BLE Sense вывод так и описан,
GPIO_ACTIVE_LOW | GPIO_PULL_UP. Питание 3,3 В названо в README
библиотеки; отдельный вывод VL для светодиода и его допустимое напряжение —
по документации производителя, не проверено.
I2C1 на 400 кГц и приём ведущим — тот же код, что в 100б: CCR = 30,
TRISE = 11, приём больше двух байт способом 2 (RM0008, с. 762–763),
одного и двух — с. 764. PB8 — вход с подтяжкой; в AFIO_EXTICR3 (с. 191)
поле EXTI8 = 0001 — порт B; EXTI_FTSR (с. 211) — спад; обработчик
EXTI9_5 — позиция 23 в таблице 63 (с. 204). PB6, PB7 и PB8 — FT
(DocID13587, с. 32–33).
Включение — как у Zephyr: подождать 5,7 мс (в драйвере — комментарий
«Initialize time 5.7ms» и k_sleep(K_MSEC(6))), проверить ID (0xAB;
Zephyr принимает и 0x9C), всё выключить, записать настройки, снять
прерывания и только потом включить движки:
/* Включение: выждать 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},
};
Настройки прошивки — из констант библиотеки и Zephyr: приближение
PPULSE = 0x87 (в библиотеке — «16us, 8 pulses»), жесты GPULSE = 0xC9
(«32us, 10 pulses»), ток 100 мА, усиление 4x, LED_BOOST 150 % (так по
умолчанию в привязке Zephyr), GPENTH 40, GEXTH 30, GCONF1 = 0x40
(«4 gesture events for int., 1 for exit»), GWTIME 2,8 мс. На стенде
включение — 28 обменов и 56 байт: 2057,5 мкс шины на 400 кГц и 7757,5 мкс
вместе с паузой.
100в.3 Приближение: светодиод, усиление, пороги и прерывание
Сколько света. Сигнал растёт с током светодиода (LDRIVE), надбавкой
(LED_BOOST), усилением (PGAIN), числом и длиной импульсов. В модели
отражение пропорционально их произведению, рука — диск радиусом 3 см.
PDATA над центром, среднее 10 чтений:
| Настройка (модель) | 2 см | 3 см | 5 см | 8 см | 12 см | 20 см | пусто |
|---|---|---|---|---|---|---|---|
| 12,5 мА, 1x, 100 % | 4,3 | 4,0 | 2,6 | 1,7 | 0,4 | 0,2 | 0,1 |
| 100 мА, 4x, 150 % (прошивка) | 203,1 | 186,9 | 120,2 | 61,2 | 30,2 | 11,6 | 0,2 |
| 100 мА, 8x, 300 % | 255 | 255 | 255 | 245,0 | 121,7 | 47,0 | 0,7 |
Слабая настройка руку почти не видит, сильная упирается в 255 до 5 см включительно и не отличает 2 см от 5 см. Байт — это всё, что есть: при насыщении приближение перестаёт мерить, при слабом сигнале — шум.
Пороги и персистентность. PINT встаёт, когда PDATA меньше PILT
или больше PIHT столько циклов подряд, сколько в PPERS (0 — каждый
цикл; так в модели, по документации производителя, не проверено). Если
PIEN = 1, INT уходит в ноль и остаётся там, пока прерывание не снимут
обращением к PICLEAR (0xE5) или AICLEAR (0xE7). Zephyr снимает
записью нуля в AICLEAR, библиотека — чтением PICLEAR; в модели
снимает любое обращение.
/* Приближение: прочитать 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;
}
Опыт: цикл приближения 712 мкс плюс пауза WTIME = 0xF6 (10 × 2,78 мс),
PILT = 0, PIHT = 50, PPERS = 1. Рука 5 раз по секунде в 4 см и по
секунде вдали; потом рука висит 5 с в 9,3 см, где PDATA = 47,8, а шум
вшестеро больше обычного:
| Опыт (модель) | Спадов INT | Обслуживаний | INT в нуле |
|---|---|---|---|
| прерывание не снимаем | 1 | 4 | 10 285 мс |
| снимаем, пороги 0 и 50 | 175 | 175 | 47 мс |
| снимаем, гистерезис: рядом — ждать < 30 | 10 | 10 | 4 мс |
рука у порога, 0 и 50, PPERS = 1 |
19 | 19 | 5 мс |
рука у порога, 0 и 50, PPERS = 4 |
0 | 0 | 0 мс |
| рука у порога, гистерезис 30/50 | 1 | 1 | 0 мс |
Не сняли — INT встал в ноль на первом приближении и простоял до конца опыта:
EXTI ловит спад, а спада больше нет. Четыре обслуживания вместо одного —
это наш цикл: после обслуживания прошивка смотрит уровень INT и, пока он в
нуле, обслуживает ещё раз, но не больше четырёх. Сняли, но пороги не
трогали — прерывание каждый цикл, пока рука рядом: 175 за 5 с. Гистерезис —
по одному на приход и уход. Шум у порога: PPERS = 4 убирает 19 ложных
срабатываний, гистерезис тоже.
100в.4 Жесты: вход, выход и FIFO на 32 набора
Движок жестов включён битом GEN, но сам по себе не меряет: когда цикл
приближения дал PDATA больше GPENTH, автомат входит в жесты (GMODE в
GCONF4 становится 1 — в библиотеке этот бит назван «gesture state machine
is currently running») и пишет наборы U, D, L, R в очередь, пока все
четыре канала не станут меньше GEXTH (GEXPERS раз подряд). Наборы
идут один за другим: в модели набор — это GWTIME плюс импульсы плюс
200 мкс, 4,28 мс при настройках прошивки и 456 мкс при GWTIME 0 и
8 импульсах по 8 мкс. Очередь — 32 набора по 4 байта (по документации
производителя; в библиотеке разбор тоже ограничен 32 наборами). Прерывание
GINT — когда в очереди набралось GFIFOTH наборов: код 1 — четыре
(комментарий библиотеки к GCONF1 = 0x40), остальные коды — по
документации производителя.
Как читать. Zephyr и библиотека читают по набору: GFLVL, затем столько
раз по 4 байта с 0xFC. Чтение подряд с 0xFC идёт по кругу 0xFC…0xFF
и выдаёт следующий набор (по документации производителя, не проверено), так
что вся очередь — одно чтение 4 · GFLVL байт, до 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;
}
Набор по отдельности — 670 мкс шины на 100 кГц и 167,5 мкс на 400 кГц;
очередь целиком (128 байт) — 11 830 и 2957,5 мкс, то есть 369,69 и
92,42 мкс на набор. GFLVL и GSTATUS стоят рядом (0xAE, 0xAF) и
читаются одним обменом. Рука секунду над датчиком в 5 см, прерывание с
четырёх наборов:
| Набор (модель) | Шина, чтение | Сделано | Прочитано | Потеряно | GFOV |
Шина занята |
|---|---|---|---|---|---|---|
| 4,28 мс | 100 кГц, по набору | 232 | 230 | 0 | 0 | 31,3 % |
| 4,28 мс | 100 кГц, пачкой | 232 | 232 | 0 | 0 | 21,8 % |
| 4,28 мс | 400 кГц, пачкой | 232 | 232 | 0 | 0 | 5,8 % |
| 456 мкс | 100 кГц, по набору | 2323 | 1345 | 947 | 40 | 95,8 % |
| 456 мкс | 100 кГц, пачкой | 2206 | 1552 | 644 | 23 | 74,9 % |
| 456 мкс | 400 кГц, по набору | 2186 | 2184 | 0 | 0 | 63,0 % |
| 456 мкс | 400 кГц, пачкой | 2186 | 2184 | 0 | 0 | 48,8 % |
Набор раз в 456 мкс, а чтение одного набора на 100 кГц — 670 мкс: шина не
успевает, очередь переполняется 40 раз и теряет 947 наборов из 2323. Пачка
на 100 кГц уменьшает потери до 644, 400 кГц убирает их совсем. При обычных 4,28 мс
очередь заполняется за 32 × 4,28 = 137 мс; если не слушать INT, а
заглядывать раз в 100 мс, ничего не теряется, раз в 200 мс — 72 набора из
232 и 4 переполнения. В модели при полной очереди новый набор пропадает и
ставится GFOV, а снимается он, когда очередь опустела, — это допущения
модели.
Когда жест кончился. Автомат вышел — GMODE снова 0. Обслуживание
читает очередь, смотрит GCONF4 и, если автомат вышел, а очередь пуста,
отдаёт набранное распознавателю. Пока жест идёт, прошивка заглядывает в
очередь не реже раза в 10 мс и по 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 Распознавание взмаха
Каждый фотодиод сильнее видит руку со своей стороны. Если рука идёт снизу
вверх, сначала больше U, в конце — D; слева направо — сначала больше L, в
конце — R. Так считают и Zephyr (разность U − D каждого набора, «вверх» —
когда за плюсом пришёл минус), и библиотека (отношение
(u_first - d_first) * 100 / (u_first + d_first) в первом и последнем
наборе, порог 50, «вверх» — сдвиг вниз). Отношение лучше разности: оно не
зависит от того, насколько рука далеко. Наш разбор — как в библиотеке, без
её счётчиков:
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;
В разбор идут наборы, где все четыре канала больше 10 (как
APDS9960_GESTURE_THRESHOLD_OUT в библиотеке); годных должно быть не
меньше четырёх. Сдвиг отношения на 50 и больше — взмах, направление по
большей оси и знаку. Сдвиг меньше 20 по обеим осям — рука шла не вбок:
сумма четырёх в конце в полтора раза больше, чем в начале, — «ближе», в
полтора раза меньше — «дальше». «Ближе» не ждёт ухода руки: рука
опустилась и стоит, автомат не выходит, — и через 96 наборов окно
проверяется без выхода.
Набор взмахов. Шесть жестов × 4 скорости (0,25; 0,5; 1 и 2 м/с) × 4 высоты (3, 5, 8 и 12 см) × 3 руки: смещение пути вбок до ±2 см, отражение 0,7…1,0 — всего 288 опытов. Взмах — проход на 44 см, «ближе» — спуск с 25 см и 0,8 с на месте, «дальше» — 0,8 с на месте и подъём. Верным считается первый названный жест. Настройки прошивки, 400 кГц, пачкой:
| Жест (модель) | 3 см | 5 см | 8 см | 12 см | 0,25 м/с | 0,5 м/с | 1 м/с | 2 м/с |
|---|---|---|---|---|---|---|---|---|
| вверх | 12/12 | 12/12 | 12/12 | 0/12 | 9/12 | 9/12 | 9/12 | 9/12 |
| вниз | 12/12 | 12/12 | 12/12 | 0/12 | 9/12 | 9/12 | 9/12 | 9/12 |
| влево | 12/12 | 12/12 | 11/12 | 0/12 | 9/12 | 9/12 | 9/12 | 8/12 |
| вправо | 12/12 | 12/12 | 11/12 | 0/12 | 9/12 | 9/12 | 9/12 | 8/12 |
| ближе | 11/12 | 9/12 | 0/12 | 0/12 | 6/12 | 6/12 | 6/12 | 2/12 |
| дальше | 12/12 | 12/12 | 12/12 | 0/12 | 9/12 | 9/12 | 9/12 | 9/12 |
Всего 198 из 288: взмахи — 142 из 192, «ближе» и «дальше» — 56 из 96.
Неверных нет — 90 промахов, ни одного жеста не названо; 72 из них —
12 см: там PDATA около 30 (100в.3), меньше порога входа 40, и автомат
жестов не просыпается. «Ближе» до 8 см не доходит по той же причине: рука
входит в жесты между 12 и 8 см, и к 8 см сумма вырастает меньше чем в полтора
раза. «Ближе» на 2 м/с — 2 из 12: на такой скорости спуск в модели
почти не попадает в окно разбора.
100в.6 Солнце, стекло и смещения
Солнце. Свет, не связанный с импульсом, датчик вычитает, но вход
усилителя от него насыщается (бит PGSAT в STATUS). В модели это
происходит, когда засветка в клк × усиление × длина импульса / 8 мкс
доходит до 400: при 4x и 32 мкс — с 25 клк, при 8x и 32 мкс — с 12,5 клк,
при 2x и 8 мкс — с 200 клк. Порог и шум засветки — допущения модели.
Стекло. Крышка над датчиком отражает часть импульса прямо в фотодиоды.
В модели стекло добавляет 70, 40, 40 и 70 отсчётов к U, D, L, R (при
настройках жестов прошивки). Без руки PDATA = 22,0 вместо 0,2, каналы
жестов — 70, 40, 41, 71: больше GEXTH 30, и автомат, войдя однажды, не
выходит — после взмаха GMODE так и остался 1, жест не назван.
Смещения. POFFSET_UR и POFFSET_DL вычитаются из пар U+R и D+L
приближения, GOFFSET_U…GOFFSET_R — из каналов жестов. Формат «знак и
модуль» (бит 7 — минус) и то, что в модели смещение вычитается один к
одному в отсчётах, — по документации производителя и допущение модели.
Калибровка — без руки: 16 циклов приближения, затем 16 наборов в
принудительном режиме жестов (запись GMODE = 1, GEXTH = 0, чтобы
автомат не вышел):
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)); /* войти в жесты без руки */
На стенде калибровка — 108 обменов; смещения 22 и 22 у приближения, 70, 40,
40 и 70 у жестов. После неё без руки PDATA = 3,2, каналы — 0, 1, 0, 0, и
взмах вверх снова назван.
| Условия (модель), 288 опытов | Верно | Взмахи из 192 | Ближе/дальше из 96 | Неверно | Лишних |
|---|---|---|---|---|---|
| настройки прошивки | 198 | 142 | 56 | 0 | 0 |
| шум втрое больше | 191 | 138 | 53 | 0 | 0 |
| солнце 30 клк | 0 | 0 | 0 | 0 | 0 |
солнце 30 клк, GGAIN 2x, 64 импульса по 8 мкс |
194 | 139 | 55 | 0 | 0 |
| стекло | 48 | 0 | 48 | 90 | 10 |
| стекло, после калибровки | 198 | 143 | 55 | 0 | 0 |
| 100 мА, 300 %, усиление 8x | 288 | 192 | 96 | 0 | 0 |
| то же и стекло | 0 | 0 | 0 | 0 | 0 |
| то же, стекло после калибровки | 40 | 0 | 40 | 7 | 0 |
цвет включён, ATIME 219 (102,86 мс) |
138 | 99 | 39 | 6 | 0 |
GWTIME 39,2 мс |
115 | 81 | 34 | 0 | 0 |
| набор 456 мкс, 100 кГц, по набору | 101 | 57 | 44 | 43 | 52 |
| набор 456 мкс, 100 кГц, пачкой | 101 | 58 | 43 | 38 | 54 |
| набор 456 мкс, 400 кГц, пачкой | 92 | 48 | 44 | 48 | 70 |
Что видно. Солнце 30 клк при 4x и 32 мкс глушит всё; меньшее усиление и
короткие частые импульсы возвращают 194 из 288. Стекло без калибровки
ломает взмахи совсем, после калибровки — как без стекла. Сильный светодиод
в модели выигрывает — рука в 12 см становится видна, — но то же стекло
вчетверо ярче: каналы упираются в 255, смещение — не больше 127, и после
калибровки (все четыре смещения жестов — 127) верных 40, взмахов — ни одного. Цвет с ATIME 219 растягивает цикл на 102,86 мс: автомат
проверяет вход раз в цикл и пропускает быстрые взмахи. Длинная пауза
GWTIME даёт на взмах слишком мало наборов. Короткий набор в 456 мкс
плох и без потерь: на 400 кГц очередь не переполняется, а верных 92 и
лишних названий 70 — 8 импульсов по 8 мкс несут в 5 раз меньше света, чем
10 по 32 мкс.
100в.7 Грабли
Прерывание пришло один раз и больше не приходит. Не сняли PINT: INT
стоит в нуле, спада нет. Обращение к PICLEAR (0xE5) или AICLEAR
(0xE7) после каждого обслуживания; после — посмотреть уровень INT.
Прерывания идут каждые 28 мс, пока рука рядом. Пороги не меняются:
PDATA больше PIHT каждый цикл. Гистерезис — после «рядом» ждать
PDATA меньше нижнего порога.
Ложные «рядом» у границы. Шум у порога. PPERS побольше или гистерезис.
Наборы пропадают, GFOV = 1. Очередь — 32 набора: на 100 кГц по
набору не успеть за набором раз в 456 мкс. Читать пачкой 4 · GFLVL байт,
400 кГц, обслуживать по INT, а не раз в 200 мс.
После взмаха датчик «залип» в жестах. Стекло или корпус дают каналы
больше GEXTH — автомат не выходит. Калибровка смещений без руки.
Взмахи не узнаются вовсе, а приближение работает. Солнце насытило вход
(PGSAT): усиление меньше, импульсы короче и чаще, тень.
Слишком большой ток — и стало хуже. Светодиод 100 мА с надбавкой 300 % и усилением 8x упирает приближение в 255 до 5 см и вчетверо поднимает отражение стекла — в модели 0 верных из 288. Брать ток по месту, а не «на максимум».
Включили цвет — быстрые взмахи пропали. Длинное ATIME удлиняет цикл,
вход в жесты проверяется редко.
Настройки из привязки Zephyr не те, что в подписи. В Zephyr 4.4.2
значения в avago,apds9960.yaml перечислены как 0x00, 0x01, 0x10, 0x11 —
двоичные коды, записанные шестнадцатеричными. Драйвер сдвигает их так, что
в регистр уходит другое:
| Свойство | Значение и подпись | В регистр |
|---|---|---|
pgain |
0x01 «4x» (по умолчанию) | 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» (по умолчанию) | PPULSE = 0x07 — 4 мкс, 8 импульсов |
ppulse-length |
0x11 «32us» | PPULSE = 0x17 — 4 мкс, 24 импульса |
pgain сдвигается на 1 и маскируется битами 3:2 — остаётся ноль;
pled-boost сдвигается на 4 в поле uint8_t — у 0x10 старший бит теряется;
длина импульса складывается с числом импульсов в младших битах. Переносить
настройки из чужого драйвера — по битам регистра, а не по подписям.
Модуль на 5 В. Датчик питается от 3,3 В. Есть ли на модуле стабилизатор, к чему подтянуты SDA, SCL и INT и откуда питается VL, — смотреть схему своего модуля (не проверено). PB6, PB7 и PB8 — FT.
100в.8 Итоги
- Два движка: приближение даёт
PDATA(0…255), жесты — наборы U, D, L, R в очередь на 32 набора. Адрес0x39, ID0xAB, после включения 5,7 мс; включение — 28 обменов, 2057,5 мкс на 400 кГц. - Сигнал — ток, надбавка, усиление и импульсы. В модели при настройках
прошивки
PDATA= 120,2 в 5 см и 30,2 в 12 см — ниже порога входа 40. - INT — открытый сток, ноль активен, подтяжка на PB8. Прерывание
приближения снимать обращением к
0xE5/0xE7: не сняли — 1 спад за 10 с; пороги без гистерезиса — 175 прерываний; с гистерезисом — 10. - Очередь читать пачкой: 92,42 мкс на набор на 400 кГц против 670 мкс по набору на 100 кГц; при наборе раз в 456 мкс по набору на 100 кГц теряется 947 из 2323.
- Направление — по сдвигу (U−D)/(U+D) и (L−R)/(L+R) от первого годного набора к последнему: 50 и больше — взмах. На синтетических взмахах модели — 198 верных из 288 и ни одного неверного; всё ниже порога входа (12 см) — промах.
- Солнце 30 клк при 4x и 32 мкс — 0 из 288, при 2x и 8 мкс — 194. Стекло — 48, после калибровки смещений — 198.
Полные исходники
Драйвер и распознаватель общие для прошивки и стенда; стенд host100d.c,
модель model100d.py и выдержки из Zephyr и библиотеки — в
_verify/ges100d/.
apds100d.h — регистры, биты, константы распознавания, настройки и состояние драйвера.
/* Раздел 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 — драйвер: включение, калибровка смещений, обслуживание INT, FIFO, распознаватель взмаха.
/* Раздел 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: I2C1 на 400 кГц, INT на PB8 через EXTI8, SysTick, главный цикл.
/* 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");
Дальше: раздел 101 «Ток и напряжение: INA219 и ACS712» — шунт и калибровочный регистр, датчик Холла и его ноль посередине шкалы.