Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32F103 · уровень средний · 21 мин чтения
Открыть в интерактивном справочнике

198г. Мини-проект: жесты по акселерометру

Кнопка без кнопки: стукнуть по корпусу — отклик, стукнуть дважды — другой, встряхнуть — третий, перевернуть на бок — четвёртый. Всё это умеет любой недорогой трёхосный акселерометр на I²C, и весь проект — про одно наблюдение: жест отличается от не-жеста формой во времени, а не величиной ускорения. Порог по «сырому» значению не работает: одно движение даёт очередь срабатываний, слабое встряхивание не дотягивает до порога вовсе, а перенос прибора со стола на полку выглядит как что угодно.

Поэтому распознаёт здесь конечный автомат над потоком выборок, и собран он, как термостат из раздела 196: логика (gest.c) не знает про регистры и прогоняется на компьютере — на пятидесяти синтезированных записях каждого жеста, с матрицей «жест × распознано» на выходе. Файлы — _verify/gest198g/: gest.c и gest.h (логика), plat_lsm6dsl.c (включение датчика), host_main.c, synth.py, run_host.py (прогон); журнал прогона — gest198g_host.txt. Сверяет _verify/gest198g_check.py.

Как это называется в документации

Оригинал Перевод
single tap, double tap одиночный и двойной стук
shake встряхивание
output data rate (ODR) частота выдачи выборок
vector magnitude модуль вектора ускорения
shock time, quiet time окна «удар» и «тишина» после него
wake-up (activity) interrupt прерывание «проснись»: движение выше порога
free-fall detection распознавание свободного падения
6D orientation detection распознавание ориентации по шести граням

198г.1 Что делает прибор

Жест Как выглядит в ускорении Отклик
стук одиночный всплеск в единицы g на несколько миллисекунд «щелчок»
двойной стук два всплеска с паузой 130–320 мс «действие»
встряхивание колебания ±1…2 g на частоте единиц герц «отмена»
поворот тяжесть перелегла на другую грань корпуса смена режима
наклон, перенос медленное изменение без всплесков ничего

Почему «сырых» порогов мало, видно по прогону: наивный детектор «|a| больше 1,5 g» срабатывает на одном стуке 1–2 раза (звон после удара пересекает порог повторно), на двойном — 1–4 раза, а на встряхивании — от 0 до 14: сильное трясётся выше порога десяток раз, слабое не задевает его ни разу. Из таких срабатываний не собрать ни «одно событие на один жест», ни различение жестов между собой.


198г.2 Акселерометр на шине

Подойдёт любой трёхосный акселерометр с I²C — MPU-6050 и ADXL345 из раздела 99 в том числе. Здесь — LSM6DSL: его регистры сверены по заголовку lsm6dsl.h из пакета B-L475E-IOT01A_BSP, а сам датчик умеет будить процессор по движению (198г.6). Механика I²C — раздел 17, подтяжки и питание модулей — раздел 95.

Схема подключения: модуль LSM6DSL на I²C1 BluePill, подтяжки SCL и SDA, SA0 на землю, INT1 на PA1

Что Где Значение
адрес на шине вывод SA0 0x6A к земле, 0x6B к питанию
код модели регистр WHO_AM_I (0x0F) 0x6A
частота и диапазон регистр CTRL1_XL (0x10) старший полубайт — частота, биты 3:2 — диапазон
данные шесть байт с OUTX_L_XL (0x28) по два на ось, младший байт первым

После включения датчик стоит: в старшем полубайте CTRL1_XL нули — режим выключения. Запуск — две записи:

int accel_init(void)
{
    uint8_t id = 0;
    if (!i2c_read(ACC_ADDR, 0x0F, &id, 1) || id != 0x6Au)
        return 0;                       /* WHO_AM_I: датчика нет или это не LSM6DSL */
    /* CTRL1_XL (0x10): старший полубайт — частота выборок (0x60 = 416 Гц),
       биты 3:2 — диапазон (0x0C = ±8 g) */
    i2c_write_reg(ACC_ADDR, 0x10, 0x60 | 0x0C);
    return 1;
}

int accel_read(int16_t a[3])
{
    uint8_t b[6];
    if (!i2c_read(ACC_ADDR, 0x28, b, 6))    /* OUTX_L_XL: шесть байт, младший первым */
        return 0;
    a[0] = (int16_t)(b[0] | (b[1] << 8));
    a[1] = (int16_t)(b[2] | (b[3] << 8));
    a[2] = (int16_t)(b[4] | (b[5] << 8));
    return 1;
}

⚠️ Коды диапазона идут не по порядку. В CTRL1_XL у LSM6DSL 00 — это ±2 g, но 01 — сразу ±16 g, 10 — ±4 g и 11 — ±8 g: в заголовке пакета ±16 g — это 0x04, а ±8 g — 0x0C. Скопировать «соседний» код — датчик исправно отвечает, только каждая единица кода вдвое другая, и все пороги едут вдвое.


198г.3 Поток данных: частота, диапазон, фильтр

Частота выборок. Удар о корпус — всплеск на единицы миллисекунд, и его легко проспать между выборками. Опыт: удар 4 g длительностью 6 мс, по всем фазам выборки — какой пик увидит датчик в худшем случае:

Частота выборок Худший видимый пик
52 Гц 0,00 g — удар целиком помещается между выборками
104 Гц 0,00 g
208 Гц 1,24 g
416 Гц 3,24 g
833 Гц 3,80 g

До 104 Гц включительно стук можно не увидеть вовсе; на 416 Гц в удар попадают минимум две выборки, и виден он не хуже 3,24 g из 4. Выше — уже косметика. Выбрано 416 Гц; кстати, в заголовке пакета частоты LSM6DSL 13–52 Гц помечены как Low Power, 104–208 — Normal, а 416 Гц и выше — High Performance: экономить будем не частотой, а прерыванием (198г.6).

Диапазон. Удар — до 5 g, энергичное встряхивание — около 2 g. При ±2 g оба обрезаются до 2 g и неразличимы (отношение 1,0); при ±4 g — 4 g против 2 g; при ±8 g — 5 g против 2 g, запас 2,5 раза. Чувствительность при ±8 g — 0,244 тысячной g на единицу кода (0,061/0,122/0,244/0,488 для ±2/4/8/16 g из того же заголовка), то есть 1 g ≈ 4096 единиц — удобная степень двойки.

Фильтр. Нужны обе составляющие: медленная — тяжесть (для поворота) и быстрая — всплески (для стука и встряхивания). Тяжесть отделяет экспоненциальное сглаживание (раздел 81) y += (x − y) / 64: при 416 Гц постоянная времени K/f = 64/416 ≈ 154 мс, срез около 1 Гц — наклоны и повороты проходят, дрожь нет. Быстрая часть — два простых фильтра верхних частот: разность соседних отсчётов («скачок за отсчёт») для удара и разность с сглаженным для встряхивания. В коде сглаженное хранится умноженным на K, чтобы деление не съедало дробную часть:

    s.gx += x - s.gx / EMA_K;

198г.4 Алгоритм: модуль вектора и конечный автомат

Стучат по корпусу с любой стороны, значит сравнивать с порогом надо модуль вектора, а не одну ось. Прогон это подтверждает: тот же детектор, следящий только за осью Z, теряет 19 из 50 стуков и 34 из 50 двойных — удары вдоль X и Y проходят мимо. Корень для модуля не нужен: сравнение квадрата модуля с квадратом порога даёт тот же ответ (разделы 64 и 69). Одна ловушка: сумма трёх квадратов int16 не влезает в знаковые 32 бита — 3·32767² ≈ 3,2·10⁹. Здесь проекции сперва ограничиваются:

static uint32_t mag2_clip(int32_t dx, int32_t dy, int32_t dz)
{
    const int32_t lim = 4 * (int32_t)G1;                /* 16384: 3 * lim^2 < 2^31 */
    if (dx > lim) dx = lim; else if (dx < -lim) dx = -lim;
    if (dy > lim) dy = lim; else if (dy < -lim) dy = -lim;
    if (dz > lim) dz = lim; else if (dz < -lim) dz = -lim;
    return (uint32_t)(dx * dx) + (uint32_t)(dy * dy) + (uint32_t)(dz * dz);
}

Сердце распознавания — конечный автомат стука по «скачку за отсчёт» j2:

Конечный автомат стука: тихо, удар, тишина, окно ожидания второго удара, отдельно считаются встряхивание и поворот

    switch (s.st) {
    case ST_IDLE:
        if (j2 < (uint32_t)QUIET_J * QUIET_J) {
            if (s.q < 0xFFFFu) s.q++;
        } else if (j2 >= (uint32_t)TAP_J * TAP_J && s.q >= PRE_QUIET) {
            s.st = ST_SHOCK; s.t = 1;   /* удар из тишины — кандидат в стук */
        } else
            s.q = 0;                    /* возня: стук не начинается посреди движения */
        break;
    case ST_SHOCK:
        if (j2 >= (uint32_t)TAP_J * TAP_J) {
            if (++s.t > SHOCK_MAX) {    /* сильные скачки дольше 12 мс — не стук */
                s.st = ST_IDLE; s.ntap = 0; s.q = 0;
            }
        } else {
            s.st = ST_QUIET; s.t = 0; s.ring = 0;
        }
        break;
    case ST_QUIET:                      /* после удара должен затихнуть звон */
        s.ring++;
        if (j2 < (uint32_t)QUIET_J * QUIET_J) s.t++;
        else s.t = 0;
        if (s.t >= QUIET_N) {           /* тишина состоялась: удар засчитан */
            s.ntap++;
            if (s.ntap == 2) { ev = GEST_DOUBLE; }
            else { s.st = ST_WAIT2; s.t = 0; }
        } else if (s.ring > RING_MAX) { /* звенит дольше 60 мс — это движение */
            s.st = ST_IDLE; s.ntap = 0; s.q = 0;
        }
        break;
    default:                            /* ST_WAIT2 */
        if (j2 >= (uint32_t)TAP_J * TAP_J) {
            s.st = ST_SHOCK; s.t = 1;   /* второй удар в окне */
        } else if (++s.t >= WAIT2_N) {  /* окно вышло: стук был одиночным */
            ev = GEST_TAP;
        }
        break;
    }

Требование тишины до удара отсекает всплески посреди возни, требование тишины после — отличает удар от начала движения. Одиночный стук объявляется только по истечении окна ожидания — это цена возможности двойного: отклик на одиночный всегда запаздывает на ширину окна.

Встряхивание считается отдельно и на другом сигнале: ВЧ-составляющая относительно сглаженной тяжести. По каждой оси запоминается знак последнего размаха больше порога:

            int8_t sg = h[a] > SHAKE_A ? 1 : h[a] < -SHAKE_A ? -1 : 0;

и каждая настоящая перемена знака записывается в маленький буфер времён; событие — когда четыре перемены уложились в окно, но не слишком плотно:

                    s.sw[a][s.nsw[a]++] = s.n;
                    if (s.nsw[a] == SHAKE_SW && s.n - s.sw[a][0] >= SHAKE_SPAN)
                        ev = GEST_SHAKE;

Нижняя граница «не плотнее 180 мс» отсекает звон после удара: он тоже меняет знак, но за десятки миллисекунд. Вдобавок перемены не считаются, пока автомат стука в состояниях «удар» и «тишина», — иначе двойной стук набирал бы «встряхивание» из собственного звона.

Поворот — по сглаженной тяжести: на какой оси лежит проекция больше 0,75 g, на той грани прибор и лежит; событие — грань сменилась и новая держится 250 мс:

        int8_t f = face_of(fx, fy, fz);
        if (f && f != s.face) {
            if (f == s.pend) {
                if (++s.stab >= TURN_STAB) { s.face = f; ev = GEST_TURN; }
            } else {
                s.pend = f; s.stab = 1;
            }
        } else {
            s.pend = 0; s.stab = 0;
        }

Порог 0,75 g означает, что наклон до 41° (cos 41° ≈ 0,75) остаётся на прежней грани — «ложный наклон» события не даёт.


198г.5 Прогон: матрица распознавания

synth.py синтезирует записи по 2,5 с при 416 Гц: стук — полусинус 2,5–5 g длительностью 4–8 мс со звоном 60–90 Гц; двойной — два таких с паузой 130–320 мс; встряхивание — 0,9–2 g на 3,2–6,5 Гц по 0,9–1,2 с; поворот — тяжесть перекладывается на боковую грань за 0,25–0,6 с; ложный наклон — на 12–22° и обратно. Всюду шум датчика 8 единиц кода (около 0,002 g) и смещение нуля до 80 единиц на ось. По 50 записей каждого класса, 300 прогонов через один и тот же gest.c:

Сыграно ↓ стук двойной встряхивание поворот ничего
стук 50 0 0 0 0
двойной стук 0 50 0 0 0
встряхивание 0 0 50 0 0
поворот 0 0 0 50 0
ложный наклон 0 0 0 0 50
тишина 0 0 0 0 50

Лишних событий (второе на тот же жест) — 0. Пороги и окна, на которых это получилось, — все в кодах и отсчётах, числа из того же прогона:

Параметр Значение В кодах / отсчётах
тихо перед ударом 50 мс 21 отсчёт
порог удара (скачок за отсчёт) больше 1,25 g 5120
ударная фаза не дольше 12 мс 5 отсчётов
порог тишины меньше 0,125 g 512
тишина после удара 24 мс подряд 10 отсчётов
звон до тишины не дольше 60 мс 25 отсчётов
окно ожидания второго удара 400 мс 166 отсчётов
размах встряхивания больше 0,5 g 2048
перемены знака 4 за 180…700 мс 75…291 отсчёт
грань поворота проекция больше 0,75 g 3072
стабильность новой грани 250 мс 104 отсчёта
пауза после события / встряхивания 300 / 600 мс 125 / 250 отсчётов

Откуда числа. Самый слабый удар синтеза (2,5 g, 4–8 мс) в худшей фазе выборки даёт скачок 1,32 g — порог 1,25 g он проходит впритык, а чуть более жёсткий порог уже терял бы стуки. Звон после удара затихает не позже 22 мс — окно тишины 24 мс с запретом звона дольше 60 мс; тишина набирается самое позднее к 46 мс от удара, так что окно ожидания 400 мс накрывает паузу двойного стука до 320 мс с запасом.

🔧 Пороги под свою плату — тем же прогоном. Формат записей — три целых в строке. Выведите с платы минуту сырых кодов по UART в тот же CSV, положите в rec/ и прогоните run_host.py: матрица покажет, что ловится и что путается, до всякой пайки логики отклика.


198г.6 Экономия: прерывание вместо опроса

Опрос 416 раз в секунду недёшев: чтение шести байт — это 84 бит-такта, на 400 кГц — 210 мкс (раздел 17.11), итого 8,7 % времени шины, и процессор при этом не спит. Между жестами всё это — работа впустую.

Выход встроен в датчик: у LSM6DSL есть прерывание «проснись» — движение выше порога поднимает вывод INT1. Порог — регистр WAKE_UP_THS (0x5B), маршрут события на INT1 — MD1_CFG (0x5E); чему равны биты — в даташите конкретной микросхемы, здесь важна схема:

    i2c_write_reg(ACC_ADDR, 0x5B, ths_bits);    /* WAKE_UP_THS: порог движения */
    i2c_write_reg(ACC_ADDR, 0x5E, route_bits);  /* MD1_CFG: событие — на INT1 */

INT1 заведён на PA1: EXTI по фронту (раздел 7) будит процессор из режима останова (раздел 10), тот включает опрос 416 Гц и кормит автомат; полсекунды тишины — и обратно в сон. В том же ряду у датчика — свободное падение (FREE_FALL, 0x5D), ориентация по граням (TAP_THS_6D, 0x59) и даже собственный распознаватель стука (TAP_CFG1 0x58, флаги в TAP_SRC 0x1C): его удобно комбинировать с нашим — грубый стук в датчике будит, точный автомат подтверждает. Логика в самом датчике фиксированная, наша — какая угодно.

Цена логики на BluePill скромная: gest.c для Cortex-M3 с -Os — 940 байт кода и 100 байт ОЗУ на всё состояние; включение датчика (plat_lsm6dsl.c) — ещё 120 байт.


198г.7 Что улучшить


198г.8 Грабли

Стук ловится через раз, хотя бьют сильно. Частота выборок мала: при 52 Гц удар 4 g / 6 мс в худшей фазе виден как 0 g — целиком проходит между выборками. Для стука нужны сотни герц; здесь — 416.

Крепкий стук теряется, слабый ловится. Ударная фаза сильного удара качает плату дольше: при пределе «не дольше 7 мс» три крепких двойных из пятидесяти пропали — 4 сильных скачка подряд принимались за движение. Предел 12 мс.

Одно встряхивание — очередь событий. Хвост жеста после события снова набирает перемены знака. После встряхивания пауза длиннее обычной — 600 мс против 300.

Двойной стук распознаётся как встряхивание. Звон после ударов тоже меняет знак ВЧ-составляющей. Лекарства два: не считать перемены, пока автомат стука в «ударе» и «тишине», и требовать, чтобы четыре перемены растянулись не меньше чем на 180 мс.

Все пороги «поехали» вдвое. Код диапазона в CTRL1_XL не по порядку: 01 — это ±16 g, 11 — ±8 g. Диапазон задан «соседним» кодом — единица кода вдвое другая, датчик при этом исправно отвечает.

Квадрат модуля иногда меньше, чем у заведомо слабого движения. Сумма трёх квадратов int16 переполняет знаковые 32 бита: 3·32767² ≈ 3,2·10⁹. Считать в беззнаковом и ограничивать проекции до суммирования.

Поворот срабатывает при переноске. Порог грани слишком мал: при 0,5 g уже наклон на 60° меняет грань. При 0,75 g грань держится до 41°, а стабильность 250 мс отсекает промежуточные положения в руках.


198г.9 Итоги

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

gest.c — жесты по акселерометру: стук, двойной стук, встряхивание, поворот.

/* gest.c — жесты по трёхосному акселерометру: стук, двойной стук,
   встряхивание, поворот. Вход — сырые коды осей: 4096 единиц кода на g
   (диапазон ±8 g), 416 выборок в секунду. Только целые числа: модули
   векторов сравниваются в квадратах, без корня. */
#include "gest.h"

#define G1        4096u                 /* 1 g в кодах */

/* стук: тихо -> удар -> тишина -> окно ожидания второго удара */
#define PRE_QUIET 21                    /* перед ударом тихо не меньше 50 мс */
#define TAP_J     5120                  /* удар: скачок за отсчёт больше 1,25 g */
#define SHOCK_MAX 5                     /* ударная фаза не длиннее 5 отсчётов (12 мс) */
#define QUIET_J   512                   /* «тихо»: скачок меньше 0,125 g */
#define QUIET_N   10                    /* тишина после удара: 10 отсчётов (24 мс) */
#define RING_MAX  25                    /* звон затих за 60 мс, иначе это не стук */
#define WAIT2_N   166                   /* окно ожидания второго удара: 400 мс */

/* встряхивание: перемены знака ВЧ-составляющей */
#define SHAKE_A   2048                  /* размах больше 0,5 g */
#define SHAKE_SW  4                     /* перемен знака подряд */
#define SHAKE_WIN 291                   /* все — внутри 700 мс */
#define SHAKE_SPAN 75                   /* и растянуты не меньше чем на 180 мс */

/* поворот: сменилась грань, на которой лежит тяжесть */
#define TURN_AX   3072                  /* грань: проекция тяжести больше 0,75 g */
#define TURN_STAB 104                   /* новая грань держится 250 мс */

#define COOL_N    125                   /* пауза после события: 300 мс */
#define COOL_SHAKE 250                  /* после встряхивания — 600 мс: хвост жеста */
#define EMA_K     64                    /* фильтр тяжести: y += (x - y) / K */

enum { ST_IDLE, ST_SHOCK, ST_QUIET, ST_WAIT2 };

static struct {
    uint32_t n;                         /* номер отсчёта */
    int16_t px, py, pz;                 /* прошлый отсчёт */
    int32_t gx, gy, gz;                 /* тяжесть: ЭС-фильтр, хранится умноженной на EMA_K */
    uint8_t st, ntap;                   /* автомат стука и число засчитанных ударов */
    uint16_t t, ring, q;                /* счётчики: в состоянии, после удара, тишины */
    uint16_t cool;                      /* пауза после события */
    int8_t sgn[3];                      /* знак последнего размаха по оси */
    uint32_t sw[3][SHAKE_SW];           /* времена последних перемен знака */
    uint8_t nsw[3];
    int8_t face, pend;                  /* грань: +-1 по X, +-2 по Y, +-3 по Z, 0 — нет */
    uint16_t stab;
    uint8_t started;
} s;

void gest_init(void)
{
    uint8_t *p = (uint8_t *)&s;
    unsigned i;
    for (i = 0; i < sizeof s; i++)
        p[i] = 0;
}

/* квадрат модуля вектора без корня; чтобы сумма трёх квадратов
   гарантированно влезла в 32 бита, каждая проекция сперва ограничивается */
static uint32_t mag2_clip(int32_t dx, int32_t dy, int32_t dz)
{
    const int32_t lim = 4 * (int32_t)G1;                /* 16384: 3 * lim^2 < 2^31 */
    if (dx > lim) dx = lim; else if (dx < -lim) dx = -lim;
    if (dy > lim) dy = lim; else if (dy < -lim) dy = -lim;
    if (dz > lim) dz = lim; else if (dz < -lim) dz = -lim;
    return (uint32_t)(dx * dx) + (uint32_t)(dy * dy) + (uint32_t)(dz * dz);
}

static int8_t face_of(int32_t fx, int32_t fy, int32_t fz)
{
    if (fx > TURN_AX) return 1;
    if (fx < -TURN_AX) return -1;
    if (fy > TURN_AX) return 2;
    if (fy < -TURN_AX) return -2;
    if (fz > TURN_AX) return 3;
    if (fz < -TURN_AX) return -3;
    return 0;                                           /* лежит на ребре — грани нет */
}

static void reset_after_event(void)
{
    unsigned a;
    s.st = ST_IDLE; s.ntap = 0; s.t = 0; s.ring = 0; s.q = 0;
    for (a = 0; a < 3; a++) { s.sgn[a] = 0; s.nsw[a] = 0; }
    s.pend = 0; s.stab = 0;
    s.cool = COOL_N;
}

enum gest_event gest_feed(int16_t x, int16_t y, int16_t z)
{
    enum gest_event ev = GEST_NONE;
    int32_t fx, fy, fz, h[3];
    uint32_t j2;
    unsigned a;

    if (!s.started) {                   /* первый отсчёт: фильтру не с чего стартовать */
        s.started = 1;
        s.gx = (int32_t)x * EMA_K; s.gy = (int32_t)y * EMA_K; s.gz = (int32_t)z * EMA_K;
        s.px = x; s.py = y; s.pz = z;
        s.face = face_of(x, y, z);
        return GEST_NONE;
    }
    s.n++;

    /* скачок за отсчёт — самый простой фильтр верхних частот */
#ifdef AXIS_ONLY                        /* для опыта «а если следить за одной осью» */
    j2 = mag2_clip(0, 0, z - s.pz);
#else
    j2 = mag2_clip(x - s.px, y - s.py, z - s.pz);
#endif
    s.px = x; s.py = y; s.pz = z;

    /* тяжесть: ЭС-фильтр хранится умноженным на K, чтобы дробь не терялась */
    s.gx += x - s.gx / EMA_K;
    s.gy += y - s.gy / EMA_K;
    s.gz += z - s.gz / EMA_K;
    fx = s.gx / EMA_K; fy = s.gy / EMA_K; fz = s.gz / EMA_K;
    h[0] = x - fx; h[1] = y - fy; h[2] = z - fz;        /* ВЧ-составляющая */

    if (s.cool) { s.cool--; return GEST_NONE; }

    /* --- стук: тихо -> удар -> тишина -> окно ожидания второго удара --- */
    switch (s.st) {
    case ST_IDLE:
        if (j2 < (uint32_t)QUIET_J * QUIET_J) {
            if (s.q < 0xFFFFu) s.q++;
        } else if (j2 >= (uint32_t)TAP_J * TAP_J && s.q >= PRE_QUIET) {
            s.st = ST_SHOCK; s.t = 1;   /* удар из тишины — кандидат в стук */
        } else
            s.q = 0;                    /* возня: стук не начинается посреди движения */
        break;
    case ST_SHOCK:
        if (j2 >= (uint32_t)TAP_J * TAP_J) {
            if (++s.t > SHOCK_MAX) {    /* сильные скачки дольше 12 мс — не стук */
                s.st = ST_IDLE; s.ntap = 0; s.q = 0;
            }
        } else {
            s.st = ST_QUIET; s.t = 0; s.ring = 0;
        }
        break;
    case ST_QUIET:                      /* после удара должен затихнуть звон */
        s.ring++;
        if (j2 < (uint32_t)QUIET_J * QUIET_J) s.t++;
        else s.t = 0;
        if (s.t >= QUIET_N) {           /* тишина состоялась: удар засчитан */
            s.ntap++;
            if (s.ntap == 2) { ev = GEST_DOUBLE; }
            else { s.st = ST_WAIT2; s.t = 0; }
        } else if (s.ring > RING_MAX) { /* звенит дольше 60 мс — это движение */
            s.st = ST_IDLE; s.ntap = 0; s.q = 0;
        }
        break;
    default:                            /* ST_WAIT2 */
        if (j2 >= (uint32_t)TAP_J * TAP_J) {
            s.st = ST_SHOCK; s.t = 1;   /* второй удар в окне */
        } else if (++s.t >= WAIT2_N) {  /* окно вышло: стук был одиночным */
            ev = GEST_TAP;
        }
        break;
    }

    /* --- встряхивание: перемены знака ВЧ-составляющей на одной оси ---
       во время ударной фазы и звона не считается: там качает сам стук */
    if (ev == GEST_NONE && (s.st == ST_IDLE || s.st == ST_WAIT2)) {
        for (a = 0; a < 3 && ev == GEST_NONE; a++) {
            int8_t sg = h[a] > SHAKE_A ? 1 : h[a] < -SHAKE_A ? -1 : 0;
            while (s.nsw[a] && s.n - s.sw[a][0] > SHAKE_WIN) {  /* старые — забыть */
                unsigned i;
                for (i = 1; i < s.nsw[a]; i++) s.sw[a][i - 1] = s.sw[a][i];
                s.nsw[a]--;
            }
            if (sg && sg != s.sgn[a]) {
                if (s.sgn[a]) {         /* настоящая перемена, а не первый выход за порог */
                    if (s.nsw[a] == SHAKE_SW) {
                        unsigned i;
                        for (i = 1; i < SHAKE_SW; i++) s.sw[a][i - 1] = s.sw[a][i];
                        s.nsw[a]--;
                    }
                    s.sw[a][s.nsw[a]++] = s.n;
                    if (s.nsw[a] == SHAKE_SW && s.n - s.sw[a][0] >= SHAKE_SPAN)
                        ev = GEST_SHAKE;
                }
                s.sgn[a] = sg;
            }
        }
    }

    /* --- поворот: тяжесть перелегла на другую грань и держится там --- */
    if (ev == GEST_NONE) {
        int8_t f = face_of(fx, fy, fz);
        if (f && f != s.face) {
            if (f == s.pend) {
                if (++s.stab >= TURN_STAB) { s.face = f; ev = GEST_TURN; }
            } else {
                s.pend = f; s.stab = 1;
            }
        } else {
            s.pend = 0; s.stab = 0;
        }
    } else if (ev != GEST_TURN) {
        int8_t f = face_of(fx, fy, fz); /* жест мог заодно перевернуть прибор */
        if (f) s.face = f;
    }

    if (ev != GEST_NONE) {
        reset_after_event();
        if (ev == GEST_SHAKE)
            s.cool = COOL_SHAKE;
    }
    return ev;
}

plat_lsm6dsl.c — включение LSM6DSL по I²C.

/* plat_lsm6dsl.c — включение LSM6DSL по I²C: те же i2c_read / i2c_write_reg,
   что в разделе 17. Адреса регистров сверены с заголовком lsm6dsl.h
   из пакета B-L475E-IOT01A_BSP. */
#include <stdint.h>

#define ACC_ADDR   0x6Au                /* SA0 к земле; SA0 к питанию — 0x6B */

extern int i2c_read(uint8_t addr, uint8_t reg, uint8_t *buf, uint8_t n);
extern int i2c_write_reg(uint8_t addr, uint8_t reg, uint8_t val);

int accel_init(void)
{
    uint8_t id = 0;
    if (!i2c_read(ACC_ADDR, 0x0F, &id, 1) || id != 0x6Au)
        return 0;                       /* WHO_AM_I: датчика нет или это не LSM6DSL */
    /* CTRL1_XL (0x10): старший полубайт — частота выборок (0x60 = 416 Гц),
       биты 3:2 — диапазон (0x0C = ±8 g) */
    i2c_write_reg(ACC_ADDR, 0x10, 0x60 | 0x0C);
    return 1;
}

int accel_read(int16_t a[3])
{
    uint8_t b[6];
    if (!i2c_read(ACC_ADDR, 0x28, b, 6))    /* OUTX_L_XL: шесть байт, младший первым */
        return 0;
    a[0] = (int16_t)(b[0] | (b[1] << 8));
    a[1] = (int16_t)(b[2] | (b[3] << 8));
    a[2] = (int16_t)(b[4] | (b[5] << 8));
    return 1;
}

/* датчик сам будит процессор: порог движения — и событие на вывод INT1.
   Чему равны биты порога и маршрута, скажет даташит датчика: здесь важна схема */
void accel_wakeup(uint8_t ths_bits, uint8_t route_bits)
{
    i2c_write_reg(ACC_ADDR, 0x5B, ths_bits);    /* WAKE_UP_THS: порог движения */
    i2c_write_reg(ACC_ADDR, 0x5E, route_bits);  /* MD1_CFG: событие — на INT1 */
}

Дальше: раздел 198д «Мини-проект: логический анализатор из BluePill».

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