198г. Mini-project: gestures from an accelerometer
A button without a button: tap the case — one response, tap twice — another, shake it — a third, turn it on its side — a fourth. Any inexpensive three-axis I²C accelerometer can do all of this, and the whole project is about one observation: a gesture differs from a non-gesture by its shape in time, not by the magnitude of the acceleration. A threshold on the "raw" value does not work: one movement gives a burst of triggers, a weak shake never reaches the threshold at all, and moving the device from the desk to a shelf looks like anything at all.
That is why recognition here is done by a state machine over the stream of samples, and it is built
like the thermostat from section 196: the logic (gest.c) knows nothing about registers and
is run on a computer — on fifty synthesized recordings of each
gesture, with a "gesture × recognized" matrix as the output. The files are in _verify/gest198g/:
gest.c and gest.h (logic), plat_lsm6dsl.c (sensor setup),
host_main.c, synth.py, run_host.py (the run); the run log is
gest198g_host.txt. Checked by _verify/gest198g_check.py.
Terms used in the documentation
Term Meaning single tap, double tap one knock and two knocks in a row shake shaking the device output data rate (ODR) how often the sensor outputs samples vector magnitude the length of the acceleration vector shock time, quiet time the "impact" window and the "silence" window after it wake-up (activity) interrupt a "wake up" interrupt: motion above a threshold free-fall detection recognizing free fall 6D orientation detection recognizing which of six faces the device lies on
198г.1 What the device does
| Gesture | What it looks like in acceleration | Response |
|---|---|---|
| tap | a single spike of a few g lasting a few milliseconds | "click" |
| double tap | two spikes with a pause of 130–320 ms | "action" |
| shake | oscillations of ±1…2 g at a few hertz | "cancel" |
| turn | gravity has moved onto another face of the case | mode change |
| tilt, carrying | a slow change without spikes | nothing |
Why "raw" thresholds are not enough is clear from the run: a naive detector "|a| greater than 1.5 g" fires 1–2 times on a single tap (the ringing after the impact crosses the threshold again), 1–4 times on a double tap, and on a shake — from 0 to 14 times: a strong shake goes above the threshold a dozen times, a weak one never touches it. From triggers like these you can build neither "one event per gesture" nor telling gestures apart.
198г.2 The accelerometer on the bus
Any three-axis accelerometer with I²C will do — including the MPU-6050 and ADXL345 from
section 99. Here it is the LSM6DSL: its registers are checked against the header
lsm6dsl.h from the B-L475E-IOT01A_BSP package, and the sensor itself can wake the processor
on motion (198г.6). I²C mechanics are in section 17, module pull-ups and power are in
section 95.
| What | Where | Value |
|---|---|---|
| bus address | SA0 pin | 0x6A to ground, 0x6B to supply |
| device ID | register WHO_AM_I (0x0F) |
0x6A |
| rate and range | register CTRL1_XL (0x10) |
upper nibble — rate, bits 3:2 — range |
| data | six bytes from OUTX_L_XL (0x28) |
two per axis, low byte first |
After power-up the sensor is idle: the upper nibble of CTRL1_XL is zero —
power-down mode. Starting it takes two writes:
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;
}
⚠️ The range codes are not in order. In
CTRL1_XLof the LSM6DSL 00 is ±2 g, but 01 jumps straight to ±16 g, 10 is ±4 g and 11 is ±8 g: in the package header ±16 g is 0x04 and ±8 g is 0x0C. Copy the "neighboring" code and the sensor answers fine, but every code unit is off by a factor of two, and all the thresholds shift twofold.
198г.3 The data stream: rate, range, filter
Sample rate. An impact on the case is a spike lasting a few milliseconds, and it is easy to miss between samples. An experiment: a 4 g impact lasting 6 ms, over all sampling phases — what peak the sensor sees in the worst case:
| Sample rate | Worst visible peak |
|---|---|
| 52 Hz | 0.00 g — the whole impact fits between samples |
| 104 Hz | 0.00 g |
| 208 Hz | 1.24 g |
| 416 Hz | 3.24 g |
| 833 Hz | 3.80 g |
Up to and including 104 Hz a tap may not be seen at all; at 416 Hz at least two samples fall into the impact, and it is seen as no less than 3.24 g out of 4. Higher rates are just cosmetics. 416 Hz is chosen; by the way, the package header marks the LSM6DSL rates 13–52 Hz as Low Power, 104–208 as Normal, and 416 Hz and above as High Performance: we will save power not with the rate but with an interrupt (198г.6).
Range. An impact reaches 5 g, a vigorous shake about 2 g. At ±2 g both are clipped to 2 g and cannot be told apart (ratio 1.0); at ±4 g it is 4 g against 2 g; at ±8 g it is 5 g against 2 g, a margin of 2.5 times. The sensitivity at ±8 g is 0.244 thousandths of a g per code unit (0.061/0.122/0.244/0.488 for ±2/4/8/16 g from the same header), so 1 g ≈ 4096 units — a convenient power of two.
Filter. Both components are needed: the slow one — gravity (for turns)
and the fast one — spikes (for taps and shakes). Gravity is separated by
exponential smoothing (section 81) y += (x − y) / 64: at 416 Hz
the time constant K/f = 64/416 ≈ 154 ms, the cutoff is about 1 Hz — tilts
and turns pass, jitter does not. The fast part is two simple high-pass
filters: the difference of adjacent samples ("jump per sample") for the impact
and the difference from the smoothed value for the shake. In the code the smoothed value is stored
multiplied by K, so that division does not eat the fractional part:
s.gx += x - s.gx / EMA_K;
198г.4 The algorithm: vector magnitude and a state machine
People tap the case from any side, so the threshold must be compared with the vector magnitude, not with one axis. The run confirms this: the same detector watching only the Z axis loses 19 out of 50 taps and 34 out of 50 double taps — impacts along X and Y go unnoticed. The magnitude needs no square root: comparing the squared magnitude with the squared threshold gives the same answer (sections 64 and 69). One trap: the sum of three int16 squares does not fit in a signed 32 bits — 3·32767² ≈ 3.2·10⁹. Here the components are clamped first:
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);
}
The heart of recognition is the tap state machine driven by the "jump per sample" 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;
}
Requiring silence before the impact cuts off spikes in the middle of handling; requiring silence after it tells an impact from the start of a movement. A single tap is reported only after the waiting window expires — this is the price of being able to detect a double one: the response to a single tap is always delayed by the width of the window.
Shake is counted separately and on a different signal: the high-frequency component relative to the smoothed gravity. For each axis the sign of the last swing above the threshold is remembered:
int8_t sg = h[a] > SHAKE_A ? 1 : h[a] < -SHAKE_A ? -1 : 0;
and every real sign change is written into a small buffer of times; the event fires when four changes fit into the window, but not too densely:
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;
The lower bound "no denser than 180 ms" rejects the ringing after an impact: it also changes sign, but within tens of milliseconds. In addition, changes are not counted while the tap state machine is in the "impact" and "silence" states — otherwise a double tap would collect a "shake" from its own ringing.
Turn is detected from the smoothed gravity: the axis whose projection exceeds 0.75 g gives the face the device lies on; the event fires when the face has changed and the new one holds for 250 ms:
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;
}
The 0.75 g threshold means that a tilt of up to 41° (cos 41° ≈ 0.75) stays on the previous face — a "false tilt" gives no event.
198г.5 The run: the recognition matrix
synth.py synthesizes recordings of 2.5 s at 416 Hz: a tap is a half-sine of
2.5–5 g lasting 4–8 ms with ringing at 60–90 Hz; a double tap is two of these
with a pause of 130–320 ms; a shake is 0.9–2 g at 3.2–6.5 Hz for 0.9–1.2 s;
a turn moves gravity onto a side face over 0.25–0.6 s;
a false tilt goes to 12–22° and back. Everywhere there is sensor noise of 8 code units
(about 0.002 g) and a zero offset of up to 80 units per axis. 50 recordings
of each class, 300 runs through the same gest.c:
| Played ↓ | tap | double | shake | turn | nothing |
|---|---|---|---|---|---|
| tap | 50 | 0 | 0 | 0 | 0 |
| double tap | 0 | 50 | 0 | 0 | 0 |
| shake | 0 | 0 | 50 | 0 | 0 |
| turn | 0 | 0 | 0 | 50 | 0 |
| false tilt | 0 | 0 | 0 | 0 | 50 |
| silence | 0 | 0 | 0 | 0 | 50 |
Extra events (a second one for the same gesture) — 0. The thresholds and windows that achieved this, all in codes and samples, numbers from the same run:
| Parameter | Value | In codes / samples |
|---|---|---|
| quiet before the impact | 50 ms | 21 samples |
| impact threshold (jump per sample) | above 1.25 g | 5120 |
| impact phase | no longer than 12 ms | 5 samples |
| silence threshold | below 0.125 g | 512 |
| silence after the impact | 24 ms in a row | 10 samples |
| ringing before silence | no longer than 60 ms | 25 samples |
| window waiting for the second impact | 400 ms | 166 samples |
| shake swing | above 0.5 g | 2048 |
| sign changes | 4 within 180…700 ms | 75…291 samples |
| turn face | projection above 0.75 g | 3072 |
| stability of the new face | 250 ms | 104 samples |
| pause after an event / a shake | 300 / 600 ms | 125 / 250 samples |
Where the numbers come from. The weakest synthesized impact (2.5 g, 4–8 ms) in the worst sampling phase gives a jump of 1.32 g — it passes the 1.25 g threshold just barely, and a slightly stricter threshold would already lose taps. The ringing after an impact dies down no later than 22 ms — hence a silence window of 24 ms with ringing forbidden beyond 60 ms; the silence is collected at the latest by 46 ms after the impact, so the 400 ms waiting window covers a double-tap pause of up to 320 ms with a margin.
🔧 Thresholds for your own board — with the same run. The recording format is three integers per line. Output a minute of raw codes from the board over UART into the same CSV, put it in
rec/and runrun_host.py: the matrix will show what is caught and what gets confused, before you solder any response logic.
198г.6 Saving power: an interrupt instead of polling
Polling 416 times a second is not cheap: reading six bytes takes 84 bit clocks, which at 400 kHz is 210 µs (section 17.11), giving 8.7 % of the bus time, and the processor does not sleep meanwhile. Between gestures all of this is wasted work.
The way out is built into the sensor: the LSM6DSL has a "wake-up" interrupt — motion
above a threshold raises the INT1 pin. The threshold is the WAKE_UP_THS register (0x5B),
the routing of the event to INT1 is MD1_CFG (0x5E); the bit values are in the datasheet
of the specific chip, what matters here is the scheme:
i2c_write_reg(ACC_ADDR, 0x5B, ths_bits); /* WAKE_UP_THS: порог движения */
i2c_write_reg(ACC_ADDR, 0x5E, route_bits); /* MD1_CFG: событие — на INT1 */
INT1 goes to PA1: EXTI on an edge (section 7) wakes the processor from
Stop mode (section 10), which turns on 416 Hz polling and feeds the state machine;
half a second of silence — and back to sleep. In the same family the sensor also offers free
fall (FREE_FALL, 0x5D), orientation by faces (TAP_THS_6D, 0x59)
and even its own tap recognizer (TAP_CFG1 0x58, flags in
TAP_SRC 0x1C): it combines well with ours — a coarse tap in the sensor
wakes the processor, the precise state machine confirms. The logic in the sensor itself is fixed,
ours can be anything.
The cost of the logic on the BluePill is modest: gest.c for Cortex-M3 with -Os is
940 bytes of code and 100 bytes of RAM for the whole state; the sensor setup
(plat_lsm6dsl.c) adds another 120 bytes.
198г.7 What to improve
- Batches instead of single samples: the LSM6DSL has a FIFO — read twenty samples once every 50 ms, 20 times fewer transactions at the same rate.
- Adaptive thresholds: estimate the noise swing during silence and set
QUIET_Jfrom it — the device will stop getting confused on a vibrating table. - A low rate at rest: keep 52 Hz (Low Power) until wake-up, 416 Hz after it; wake-up does not suffer, and the average current drops.
- More gestures: "picked up from the desk" (lift-off: a spike + a change of tilt),
free fall (the magnitude near zero — the hardware
FREE_FALLhelps). - Real-life recordings: run
run_host.pyon CSV from a real board (the recipe is in 198г.5) and tune the thresholds to the mechanics of your case.
198г.8 Pitfalls
Taps are caught only every other time, even when hit hard. The sample rate is too low: at 52 Hz a 4 g / 6 ms impact in the worst phase is seen as 0 g — it passes entirely between samples. A tap needs hundreds of hertz; here it is 416.
A hard tap is lost, a weak one is caught. The impact phase of a strong blow shakes the board for longer: with a limit of "no longer than 7 ms", three hard double taps out of fifty were lost — 4 strong jumps in a row were taken for movement. The limit is 12 ms.
One shake — a burst of events. The tail of the gesture after the event collects sign changes again. After a shake the pause is longer than usual — 600 ms against 300.
A double tap is recognized as a shake. The ringing after the impacts also changes the sign of the high-frequency component. There are two cures: do not count the changes while the tap state machine is in "impact" and "silence", and require the four changes to stretch over no less than 180 ms.
All the thresholds are off by a factor of two. The range code in CTRL1_XL is not in order:
01 is ±16 g, 11 is ±8 g. The range was set with the "neighboring" code — a code unit
is off by a factor of two, while the sensor answers fine.
The squared magnitude is sometimes smaller than for an obviously weak movement. The sum of three int16 squares overflows a signed 32 bits: 3·32767² ≈ 3.2·10⁹. Compute in unsigned and clamp the components before summing.
A turn fires while the device is being carried. The face threshold is too low: at 0.5 g a tilt of 60° already changes the face. At 0.75 g the face holds up to 41°, and the 250 ms stability requirement rejects the intermediate positions in the hand.
198г.9 Summary
- A gesture differs by its shape in time, not by magnitude: a naive 1.5 g threshold gives from 0 to 14 triggers on a shake — no events can be built from that.
- The rate and range come from the physics of the impact: at 52 Hz a 4 g / 6 ms impact in the worst phase is invisible (0 g), at 416 Hz it is seen as 3.24 g; the ±8 g range keeps a 5 g impact apart from a 2 g shake with a margin of 2.5 times; 1 g ≈ 4096 units.
- The tap state machine: quiet 50 ms → impact no longer than 12 ms → silence 24 ms → a second-impact window of 400 ms; a shake is 4 sign changes of the high-frequency component above 0.5 g within 180…700 ms; a turn is a face by a projection of 0.75 g, stable for 250 ms.
- The matrix on 300 synthesized recordings — without a single error and without extra events; a detector on the Z axis alone lost 19 out of 50 taps and 34 out of 50 double taps.
- The vector magnitude is used squared and without a root, but with the components clamped: 3·32767² does not fit in int32.
- Polling took 210 µs per read and 8.7 % of the bus time; the sensor's "wake-up" interrupt (WAKE_UP_THS → INT1 → EXTI) lets the processor sleep between gestures. The whole logic is 940 bytes of code and 100 bytes of RAM.
Full source code
gest.c — gestures from an accelerometer: tap, double tap, shake, turn.
/* 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 — setting up the LSM6DSL over 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 */
}
Next: section 198д "Mini-project: a logic analyzer made from a BluePill".