Embedded HandbookПриложения
Все разделы › XI. Датчики: подключение, схемы, драйверы
STM32F103 · уровень средний · 41 мин чтения
Открыть в интерактивном справочнике

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б.

Вверху — APDS-9960 на STM32F103: PB6 — SCL, PB7 — SDA на I2C1 на 400 кГц, адрес 0x39, ID 0xAB; INT датчика — открытый сток, на PB8 с подтяжкой, EXTI8 по спаду; питание VCC 3,3 В и отдельный вывод VL светодиода; прерывание приближения снимается обращением к PICLEAR 0xE5 или AICLEAR 0xE7. Посередине — ИК-светодиод с током LDRIVE и надбавкой LED_BOOST, четыре фотодиода U, D, L, R вокруг окна, движок приближения даёт PDATA, пороги PILT и PIHT, персистентность PERS, прерывание PINT; движок жестов входит при PDATA больше 40 и выходит, когда все четыре канала меньше 30, наборы идут в FIFO на 32 набора по 4 байта, прерывание с 4 наборов, набор в модели каждые 4,28 мс, читать пачкой 4 · GFLVL байт с 0xFC. Внизу — отношение (U−D)/(U+D) · 100 при взмахе вверх над датчиком в 5 см по модели: от плюса к минусу, на краях ±8 см примерно ±51, сдвиг −102

Регистры — обычные: адрес регистра и данные, без командного байта, который нужен 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 Итоги


Полные исходники

Драйвер и распознаватель общие для прошивки и стенда; стенд 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» — шунт и калибровочный регистр, датчик Холла и его ноль посередине шкалы.

Открыть в интерактивном справочнике