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.
| Что | Где | Значение |
|---|---|---|
| адрес на шине | вывод 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 Что улучшить
- Пачки вместо отсчётов: у LSM6DSL есть FIFO — читать раз в 50 мс по двадцать выборок, транзакций в 20 раз меньше при той же частоте.
- Адаптивные пороги: оценивать размах шума в тишине и ставить
QUIET_Jот него — прибор перестанет путаться на вибрирующем столе. - Низкая частота в покое: до пробуждения держать 52 Гц (Low Power), после — 416 Гц; wake-up от этого не страдает, а средний ток падает.
- Больше жестов: «поднял со стола» (отрыв: всплеск + смена наклона),
свободное падение (модуль около нуля — аппаратный
FREE_FALLв помощь). - Записи с натуры: прогнать
run_host.pyна CSV с реальной платы (рецепт в 198г.5) и подкрутить пороги по своей механике корпуса.
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 Итоги
- Жест отличается формой во времени, а не величиной: наивный порог 1,5 g даёт на встряхивании от 0 до 14 срабатываний — события из этого не собрать.
- Частота и диапазон — из физики удара: при 52 Гц удар 4 g / 6 мс в худшей фазе невидим (0 g), при 416 Гц виден как 3,24 g; диапазон ±8 g держит удар 5 g против встряхивания 2 g с запасом 2,5 раза; 1 g ≈ 4096 единиц.
- Автомат стука: тихо 50 мс → удар не дольше 12 мс → тишина 24 мс → окно второго удара 400 мс; встряхивание — 4 перемены знака ВЧ больше 0,5 g за 180…700 мс; поворот — грань по проекции 0,75 g, стабильная 250 мс.
- Матрица на 300 синтезированных записях — без единой ошибки и без лишних событий; детектор по одной оси Z терял 19 из 50 стуков и 34 из 50 двойных.
- Модуль вектора — в квадратах и без корня, но с ограничением проекций: 3·32767² не влезает в int32.
- Опрос занимал 210 мкс на чтение и 8,7 % времени шины; прерывание «проснись» от датчика (WAKE_UP_THS → INT1 → EXTI) даёт спать между жестами. Вся логика — 940 байт кода и 100 байт ОЗУ.
Полные исходники
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».