100а. Присутствие человека радаром 24 ГГц: HLK-LD2420 — дальность по воротам, протокол UART и настройка порогов
В разделе 100.6 пироэлектрический датчик HC-SR501 честно признался: он видит движение, а не присутствие. Человек сел читать — и вскоре свет погас. Радар миллиметрового диапазона устроен иначе: он сам излучает и ловит отражение, поэтому замечает и неподвижного человека — по дыханию, по мелким движениям головы и рук. HLK-LD2420 — маленький модуль такого радара на 24 ГГц: 20 × 20 мм, питание 3,3 В, на выходе логический уровень «есть человек» и порт UART для настройки.
Здесь: чем радар отличается от пироэлемента и как его подключить; как он делит пространство на ворота дальности по 0,7 м и зачем у каждых ворот два своих порога; кадр команды и кадр ответа — байт в байт по описанию протокола, вместе с местами, где описание противоречит само себе; драйвер на C — кольцо приёма, автомат разбора, который переживает мусор и обрывки, команды с тайм-аутом и повтором, настройка порогов с обратным чтением. И маленький прибор: светильник, который гаснет, только когда человек ушёл.
Опоры: руководство пользователя и описание протокола производителя (Hi-Link) — все числа о модуле взяты оттуда. Драйвер прогнан на компьютере против своей модели модуля, которая отвечает по тому же описанию: на всех шестнадцати примерах протокола, на потоке с мусором, потерянными и испорченными байтами и на сценарии «человек вошёл, сел, ушёл». Тот же прогон повторён на Cortex-M4 в QEMU, прошивка светильника собрана для F103 и F407. С живым модулем драйвер не проверялся — об этом в подписи в конце. UART — разделы 16 и 16б, подключение модулей — раздел 95.
Как это называется в документации
Оригинал Перевод millimeter wave radar радар миллиметрового диапазона FMCW (frequency modulated continuous wave) непрерывное излучение с частотной модуляцией: дальность — по разности частот presence, micro-motion присутствие; мелкие движения (дыхание, жесты) distance gate ворота дальности: отрезок расстояния, который радар оценивает отдельно trigger threshold порог срабатывания: «никого» → «есть человек» maintain (hold) threshold порог удержания: человек всё ещё здесь target disappearance delay задержка исчезновения цели bottom noise scanning сканирование шума пустого помещения command mode режим команд packet header, end of packet заголовок кадра, хвост кадра upper computer tool программа настройки на компьютере
100а.1 Радар вместо пироэлемента: что он видит и как подключается
Пироэлемент пассивен: он ловит перемену инфракрасного излучения, когда тёплое тело переходит из одной зоны линзы в другую. Радар активен: он излучает на 24 ГГц и сравнивает отражение с излучённым. В LD2420 стоит радар с частотной модуляцией: частота излучения ходит в полосе 0,25 ГГц, и разница частот излучённого и отражённого сигнала говорит о расстоянии до отражателя. Изменение отражения от раза к разу — о движении. Грудная клетка дышащего человека двигается на миллиметры, и при длине волны 12,5 мм этого хватает.
| HC-SR501 (раздел 100) | HLK-LD2420 | |
|---|---|---|
| принцип | пассивный, инфракрасный | активный, радиоволны 24–24,25 ГГц |
| что видит | движение тёплого тела | движение и неподвижного человека по мелким движениям |
| расстояние | нет | да, у движущейся цели; точность ±0,35 м |
| дальность (на стене) | — | движущийся человек 8 м, мелкие движения 6 м |
| дальность (на потолке) | — | 5 м и 4 м |
| угол | — | ±60° |
| питание | 5 В | 3,0–3,6 В, обычно 3,3 В, в среднем 50 мА |
| выход | уровень 3,3 В | OT1 — уровень 3,3 В, плюс UART |
| настройка | подстроечный резистор и перемычка | ворота, пороги, задержка — командами по UART |
Все числа о LD2420 здесь и дальше — из руководства производителя. Там же: данные обновляются 10 раз в секунду, ближняя граница — 0,2 м, рабочая температура от −40 до 85 °C, эквивалентная излучаемая мощность — 11 dBm.
У модуля два ряда отверстий с шагом 2,54 мм, штыри не впаяны. J1 — это SWD самого модуля, для его прошивки; к микроконтроллеру он не нужен. J2 — рабочий разъём:
| J2 | Имя | Что это | Куда |
|---|---|---|---|
| 1 | 3V3 | питание 3,0–3,6 В | 3,3 В платы |
| 2 | GND | общий | общий |
| 3 | OT1 | «есть человек»: единица — есть, ноль — нет | вход GPIO (PA0) |
| 4 | RX | приёмник UART модуля | TX микроконтроллера (PA9 у F103) |
| 5 | OT2 | передатчик UART модуля | RX микроконтроллера (PA10 у F103) |
Имя OT2 обманчиво: это не второй логический выход, а TX радара. В таблице подключения к переходнику USB–UART руководство так и пишет: O_T2 — на RXD переходника. Порт работает на 256 000 бод — не 115 200 и не 230 400, и делитель надо посчитать (100а.8, раздел 16.2).
Питание — отдельный разговор. Модуль ест в среднем 50 мА, а его приёмник слушает слабое отражение. Руководство требует, чтобы в пульсациях питания не было заметных пиков спектра до 100 кГц. Импульсный преобразователь рядом без фильтра — первый кандидат на ложные срабатывания; развязка и земля — раздел 95.8.
100а.2 Ворота дальности, два порога и задержка исчезновения
Радар не говорит «человек в 2,34 м». Он делит расстояние на ворота — отрезки по 0,7 м — и в каждом воротах считает энергию отражения, которое меняется. Ворота пронумерованы от 0 до 15: шестнадцать ворот по 0,7 м — это 11,2 м, хотя обещанная дальность — 8 м. Дальние ворота есть в протоколе, но человека там радар уже не различит.
Для каждых ворот задаются два порога, и это главное в настройке:
| Параметр | Имя в протоколе | Что делает | Совет руководства |
|---|---|---|---|
| ближние ворота | 0x0000 |
с каких ворот смотреть, 0…15 | — |
| дальние ворота | 0x0001 |
до каких ворот смотреть, 0…15, не меньше ближних | — |
| задержка | 0x0004 |
сколько ждать, прежде чем сказать «никого» | — |
| порог срабатывания ворот g | 0x0010 + g |
энергия выше — «никого» сменяется на «есть человек» | больше 5 шумов ворот |
| порог удержания ворот g | 0x0020 + g |
энергия выше — человек всё ещё здесь | 2–5 шумов ворот |
Два порога — это гистерезис. Чтобы включиться, нужно движение: человек вошёл, идёт, машет рукой — энергия высокая. Чтобы оставаться включённым, хватит дыхания — энергия ниже, но выше шума. Если порог удержания поставить равным порогу срабатывания, сидящий человек «пропадёт» — это видно на модели в 100а.8.
Шум — энергия ворот в пустой комнате. Её снимает программа производителя на компьютере: сканирование идёт с заданным шагом и длительностью, в комнате в это время никого быть не должно, и для каждых ворот получается график шума во времени. Своего чтения энергий ворот в описании протокола нет, поэтому таблица шума попадает в прошивку числами — из этого сканирования.
Задержка исчезновения работает так: когда радар перестал видеть человека, он не говорит «никого» сразу, а запускает таймер. Увидел снова — таймер начинается заново. Только когда за всё время задержки никого не было, OT1 падает в ноль. Единица задержки в документах не названа; в примере чтения из описания протокола — 30.
Зона — ворота от ближних до дальних. Всё за ней радар не замечает, и это лучшее средство от помех: вентилятор в 7,5 м попадает в ворота 10 (7,5 / 0,7 = 10,7), и если дальние ворота — 8 (до 6,3 м), его качание никто не увидит. Руководство перечисляет главных нарушителей: животные, занавески на сквозняке, растения под кондиционером, кондиционеры и вентиляторы на потолке. И напоминает, что у антенны есть задний лепесток: то, что качается за радаром, он тоже видит; помогает металлический экран сзади.
Ставят модуль на потолок (2,7–3 м; зона — конус с радиусом основания 5 м) или на стену (1,5–2 м; сектор радиусом 8 м и ±45° по горизонтали и вертикали). Корпус — только из материала, прозрачного для 24 ГГц, без металла.
100а.3 Кадр команды и кадр ответа
Команды и ответы ходят кадрами одного вида:
| Поле | Байты | Команда | Ответ |
|---|---|---|---|
| заголовок | 4 | FD FC FB FA |
FD FC FB FA |
| длина данных | 2 | код + параметры | код + состояние + значения |
| код | 2 | код команды | код команды | 0x0100 |
| состояние | 2 | — | 00 00 — успех, иначе отказ |
| параметры, значения | N | имена (по 2 байта) или пары имя + значение (2 + 4) | значения по 4 байта |
| хвост | 4 | 04 03 02 01 |
04 03 02 01 |
Все многобайтовые поля — младшим байтом вперёд: код 0x0068 идёт как
68 00, длина 4 — как 04 00, порог 60 000 (0xEA60) — как
60 EA 00 00. Длина считает только данные — без заголовка, самой длины и
хвоста, поэтому кадр всегда на 10 байт длиннее неё. Кадр без параметров
(«закрыть режим», «версия») — 12 байт.
Команды по описанию протокола:
| Команда | Код | Параметры | Ответ в примере |
|---|---|---|---|
| открыть режим команд | 0x00FF |
01 00 |
02 00 20 00 после состояния |
| закрыть режим команд | 0x00FE |
— | только состояние |
| версия | 0x0000 |
— | длина строки 06 00 и v1.5.4 |
| перезапуск | 0x0068 |
— | в описании нет |
| прочитать параметры | 0x0008 |
имена по 2 байта | значения по 4 байта, в том же порядке |
| записать параметры | 0x0007 |
N пар: имя (2) + значение (4) | только состояние |
Чтение и запись — в режиме команд: открыть, сделать своё, закрыть. Сборка кадра в драйвере:
uint16_t ld2420_build(uint8_t *out, uint16_t cmd, const uint8_t *par, uint16_t n)
{
uint16_t len = (uint16_t)(2u + n);
if (len > LD2420_MAX_DATA)
return 0;
memcpy(out, HDR, 4);
out[4] = (uint8_t)len;
out[5] = (uint8_t)(len >> 8);
out[6] = (uint8_t)cmd;
out[7] = (uint8_t)(cmd >> 8);
if (n)
memcpy(&out[8], par, n);
memcpy(&out[8 + n], TAIL, 4);
return (uint16_t)(len + 10u);
}
Пример из описания — чтение порога срабатывания ворот 0 и ответ:
→ FD FC FB FA 04 00 08 00 10 00 04 03 02 01
← FD FC FB FA 08 00 08 01 00 00 40 9C 00 00 04 03 02 01
Длина 04 00: код 08 00 и имя 10 00. В ответе длина 08 00: код
08 01, состояние 00 00 и значение 40 9C 00 00. Что это за значение,
разобрано в следующем подразделе — тут описание ошибается.
100а.4 Где описание расходится само с собой
Все 16 примеров из описания протокола записаны в прогон байт в байт. Для каждого драйвер собрал свой кадр из кода и параметров, а модель модуля — свой ответ. Итог:
doc_examples=16 doc_send_same=15 model_resp_same=15
Кадров совпало 15 из 16, ответов модели — 15 из 16. Оба расхождения — в самом описании, и вот все места, где документ неоднозначен:
- Запись порогов 00…05 одним кадром. В примере длина
24 00(36), а последнее значение —90 01без старших00 00. Шесть пар по 6 байт и код — это 38 байт,26 00. Кадр примера на 2 байта короче правильного:
doc_send_differs=set_trig00_05 doc_len=46 our_len=48
Модуль, получив кадр примера, либо прочтёт последнее значение не так, либо отвергнет кадр — как именно, без живого модуля не узнать. Драйвер собирает полный кадр.
- Чтение порога срабатывания 00. Текст примера говорит: младший байт
впереди,
9C 40, значит 60 000. Но0x9C40— это 40 000; 60 000 — это0xEA60. И в примере чтения порогов 00…05 первым идёт как раз60 EA 00 00. Те же байты40 9C 00 00стоят в примере чтения порога удержания 00 — похоже, ответ скопирован оттуда. Модель отвечает 60 000, отсюда второе расхождение:
model_differs=read_trig00
doc_resp=FD FC FB FA 08 00 08 01 00 00 40 9C 00 00 04 03 02 01
model_resp=FD FC FB FA 08 00 08 01 00 00 60 EA 00 00 04 03 02 01
- Диапазон задержки. В таблице параметров протокола —
0x00…0x0F, а примеры читают 30 (1E) и пишут 26 (1A); в руководстве — 0…65 535. Модель и драйвер принимают 0…65 535. Единица — нигде не названа. - Диапазон порогов. В протоколе — «0–65536», в руководстве —
0…65 535. Поле — 4 байта, но 65 536 и больше драйвер не пишет:
ld2420_thr_from_noiseограничивает порог числом 65 535. - Порядок полей ответа. Словами описание ставит состояние перед кодом
ответа, а во всех примерах код идёт первым (
08 01 00 00). Драйвер следует примерам. - Ответ на открытие режима —
02 00 20 00после состояния. Что значат 2 и 32, описание не говорит; драйвер отдаёт их наружу как есть. - Перезапуск — для него в описании нет ответа. Драйвер его и не ждёт.
- Отчёт в обычном режиме. Руководство предлагает открыть порт и
смотреть «текущий результат» в терминале, но формат этого вывода ни в
одном из двух документов не описан. Поэтому драйвер не разбирает
отчёты: всё, что не сложилось в кадр ответа, он отдаёт обратному
вызову
on_otherкак есть, а светильник смотрит на OT1.
Ответы из описания, пропущенные через разбор драйвера, дают такие поля:
resp open frames=1 len=8 code=0x01FF status=0 a=2 b=32
resp version frames=1 len=12 code=0x0100 status=0 n=6 s=v1.5.4
resp read_min frames=1 len=8 code=0x0108 status=0 0
resp read_max frames=1 len=8 code=0x0108 status=0 12
resp read_delay frames=1 len=8 code=0x0108 status=0 30
resp read_hold00 frames=1 len=8 code=0x0108 status=0 40000
resp read_trig00 frames=1 len=8 code=0x0108 status=0 40000
resp read_trig00_05 frames=1 len=28 code=0x0108 status=0 60000 30000 3000 2000 500 400
100а.5 Приём: кольцо и автомат разбора
На 256 000 бод байт идёт 39 мкс. Принимать его можно прерыванием на каждый байт — так здесь — или DMA в кольцо (раздел 16б). Прерывание кладёт байт в кольцо, основной цикл забирает:
void ld2420_ring_put(ld2420_ring_t *r, uint8_t b)
{
uint16_t h = r->head;
if ((uint16_t)(h - r->tail) >= LD2420_RING) { /* полно: байт теряется, счётчик это помнит */
r->lost++;
return;
}
r->buf[h & (LD2420_RING - 1u)] = b;
r->head = (uint16_t)(h + 1u); /* сначала байт, потом индекс */
}
int ld2420_ring_get(ld2420_ring_t *r, uint8_t *b)
{
uint16_t t = r->tail;
if (t == r->head)
return 0;
*b = r->buf[t & (LD2420_RING - 1u)];
r->tail = (uint16_t)(t + 1u);
return 1;
}
Индексы — 16-битные счётчики, растущие без конца; разность head − tail
даёт занятость даже после перехода через ноль, а номер ячейки — маска
& 255. head пишет только прерывание, tail — только цикл, поэтому
запрещать прерывания не нужно. Кольцо в 256 байт вмещает больше трёх
самых длинных ответов (68 байт данных — 78 байт кадра) и заполняется за
10 мс непрерывного потока. Если цикл не успел, байт теряется, а счётчик
lost это помнит:
ring size=256 put=300 got=256 lost=44
Разбор — автомат по одному байту. Его состояние — сколько байт кадра уже
собрано (rawn): 0–3 — заголовок, 4–5 — длина, дальше данные, потом
хвост. Сам шаг:
/* Один байт. 0 — принят (в кадр или мимо), 1 — кадр не сложился: в raw лежит всё от его первого FD
* до этого байта включительно, и это надо разобрать заново со второго байта. */
static int step(ld2420_parser_t *p, uint8_t c)
{
uint16_t i = p->rawn;
if (i == 0 && c != HDR[0]) {
other(p, c);
return 0;
}
p->raw[p->rawn++] = c;
if (i < 4)
return c != HDR[i];
if (i == 4)
return 0;
if (i == 5) {
p->len = (uint16_t)(p->raw[4] | p->raw[5] << 8);
if (p->len < 2u || p->len > LD2420_MAX_DATA) {
p->bad_len++;
return 1;
}
return 0;
}
if (i < 6u + p->len)
return 0;
uint16_t k = (uint16_t)(i - 6u - p->len);
if (c != TAIL[k]) {
p->bad_tail++;
return 1;
}
if (k < 3u)
return 0;
if (p->on_frame && !p->on_frame(p->ctx, &p->raw[6], p->len)) {
p->rejected++; /* хвост совпал случайно: «кадр» начался с ложного заголовка */
return 1;
}
p->frames++;
p->rawn = 0;
return 0;
}
Тонкость в том, что делать при сбое. Самое простое — бросить собранное и
ждать следующий FD. Но так теряются настоящие кадры. Пусть в мусоре
случайно оказался заголовок с длиной 10, а сразу за ним пришёл настоящий
ответ: разбор примет начало ответа за данные ложного кадра, на месте
хвоста хвоста не найдёт — и если бросить всё, ответ пропал. Поэтому при
сбое первый FD объявляется мусором, а все остальные байты разбираются
заново:
/* Сбой: первый FD — мусор, остальное — в начало очереди. В заголовке нет повторяющихся байтов,
* но внутри «кадра» может начинаться настоящий — его и находит повторный разбор. */
void ld2420_parse(ld2420_parser_t *p, uint8_t b)
{
uint8_t q[LD2420_RAW_MAX]; /* непрочитанное + байты брошенного кадра: всего не больше RAW_MAX */
uint16_t qi = 0, qn = 1;
q[0] = b;
while (qi < qn) {
if (!step(p, q[qi++]))
continue;
uint16_t n = (uint16_t)(p->rawn - 1u), rest = (uint16_t)(qn - qi);
memmove(&q[n], &q[qi], rest);
memcpy(q, &p->raw[1], n);
qi = 0;
qn = (uint16_t)(n + rest);
other(p, p->raw[0]);
p->rawn = 0;
}
}
Байты заголовка все разные, поэтому сдвиг на один байт ничего не
пропускает. Очередь не растёт: каждый повтор на байт короче прежнего
(первый FD уходит в мусор), и RAW_MAX байт хватает всегда.
Третий случай — кадр оборвался, и больше ничего не идёт: ложный заголовок
с длиной 100, а дальше тишина. Автомат ждал бы вечно. Для этого
ld2420_poll смотрит на паузу: посреди кадра больше 10 мс без байтов —
кадр бросается и его хвост разбирается заново (ld2420_parser_flush). На
256 000 бод самый длинный кадр, какой примет драйвер, — 138 байт — идёт 5,4 мс, так что 10 мс
паузы посреди кадра настоящий модуль не делает. Это допущение: о паузах
внутри кадра документы молчат.
Последний и самый коварный случай: длина ложного заголовка случайно
привела точно на хвост настоящего ответа. Хвост 04 03 02 01 совпал —
кадр «сложился», но код у него — случайные байты, а настоящий ответ
съеден. В опыте с мусором (100а.7) драйвер без этой проверки получил так
2 тайм-аута на 2000 операций (junk_lax). Лечится это проверкой смысла: обратный вызов
может сказать «это не кадр», и тогда разбор поступает как при любом сбое —
разбирает заново со второго байта. Драйвер принимает только ответы на
известные команды:
/* Ответ — это код известной команды с 0x01 в старшем байте и не меньше 4 байт. Всё прочее — не кадр:
* ложный заголовок из мусора, длина которого случайно привела на хвост настоящего ответа. */
static int is_ack(uint16_t code, uint16_t len)
{
uint16_t c = (uint16_t)(code & 0x00FFu);
return (code & 0xFF00u) == LD2420_ACK && len >= 4u &&
(c == LD2420_CMD_VERSION || c == LD2420_CMD_WRITE || c == LD2420_CMD_READ || c == LD2420_CMD_REBOOT ||
c == LD2420_CMD_CLOSE || c == LD2420_CMD_OPEN);
}
Проверки разбора — короткие потоки, после которых должен остаться ровно один правильный ответ (значение 12, «дальние ворота»):
unit whole frames=1 value=12 other=0 bad_len=0 bad_tail=0 rejected=0
unit garbage_before frames=1 value=12 other=7 bad_len=0 bad_tail=0 rejected=0
unit double_fd frames=1 value=12 other=1 bad_len=0 bad_tail=0 rejected=0
unit bad_len frames=1 value=12 other=6 bad_len=1 bad_tail=0 rejected=0
unit short_len frames=1 value=12 other=6 bad_len=1 bad_tail=0 rejected=0
unit fake_header frames=1 value=12 other=7 bad_len=0 bad_tail=1 rejected=0
unit cut_frame frames=1 value=12 other=9 bad_len=0 bad_tail=1 rejected=0
unit bad_tail frames=0 value=0 other=18 bad_len=0 bad_tail=1 rejected=0
unit long_fake_then_quiet frames=1 before_flush=0 value=12 other=6 bad_len=0 bad_tail=0 rejected=0
unit tail_alias_any frames=1 value=12 other=0 bad_len=0 bad_tail=0 rejected=0
unit tail_alias_strict frames=1 value=12 other=10 bad_len=0 bad_tail=0 rejected=1
| Поток | Что в нём | Что вышло |
|---|---|---|
whole |
один кадр | кадр |
garbage_before |
7 байт мусора с обрывками заголовка, потом кадр | кадр, 7 байт мусора |
double_fd |
лишний FD перед кадром |
кадр, 1 байт мусора |
bad_len, short_len |
заголовок с длиной 32 767 или 1, потом кадр | кадр |
fake_header |
ложный заголовок с длиной 10, сразу кадр | кадр — нашёлся повторным разбором |
cut_frame |
начало кадра без конца, потом целый кадр | кадр |
bad_tail |
кадр с испорченным хвостом | ничего, 18 байт мусора |
long_fake_then_quiet |
заголовок с длиной 100, кадр и тишина | до паузы — ничего, после — кадр |
tail_alias_any |
длина ложного заголовка ведёт на хвост кадра; принимать всё | «кадр» из мусора, настоящий потерян |
tail_alias_strict |
то же; принимать только ответы | настоящий кадр, ложный отвергнут |
100а.6 Команды: тайм-аут, повтор и обратное чтение
Команда — это «собрать кадр, отправить, ждать ответ с кодом команды
| 0x0100». Ответ ищет обратный вызов разбора:
static int on_frame(void *ctx, const uint8_t *data, uint16_t len)
{
ld2420_t *d = ctx;
uint16_t code = (uint16_t)(data[0] | data[1] << 8);
if (!is_ack(code, len))
return 0;
if (d->got || code != (d->want | LD2420_ACK)) {
d->stray++; /* опоздавший ответ на брошенную команду */
return 1;
}
memcpy(d->ack, data, len);
d->ack_len = len;
d->got = 1;
return 1;
}
Ответ, пришедший после тайм-аута, — опоздавший, на прошлую попытку: он
считается в stray и не путается с новым. Сама команда — три попытки по
100 мс:
int ld2420_cmd(ld2420_t *d, uint16_t cmd, const uint8_t *par, uint16_t n, uint32_t tmo_ms)
{
uint8_t f[LD2420_RAW_MAX];
uint16_t fl = ld2420_build(f, cmd, par, n);
if (!fl)
return LD2420_EARG;
for (unsigned t = 0; t < LD2420_TRIES; t++) {
if (t)
d->retries++;
ld2420_poll(d); /* старое — до отправки, чтобы не принять его за ответ */
d->want = cmd;
d->got = 0;
d->tx(f, fl);
uint32_t t0 = d->now_ms();
for (;;) {
ld2420_poll(d);
if (d->got || d->now_ms() - t0 >= tmo_ms)
break;
d->idle();
}
if (d->got) {
d->status = (uint16_t)(d->ack[2] | d->ack[3] << 8);
return d->status ? LD2420_ESTATUS : LD2420_OK;
}
d->timeouts++;
}
return LD2420_ETIMEOUT;
}
Время ответа модуля в документах не названо; 100 мс — выбор с запасом, а
не замер. Ожидание не крутит процессор вхолостую: idle на плате — это
__WFI(), на компьютере — шаг модели. Если модуль молчит, команда
сдаётся через 300 мс:
silent rc=-1 timeouts=3 retries=2 ms=300
Чтение и запись поверх ld2420_cmd — это раскладка имён и значений по
байтам и проверка длины ответа: на n имён ответ должен быть ровно
4 + 4·n байт. Сеанс с моделью на чистой линии:
session open=0 a=2 b=32
session version=0 s=v1.5.4
session read_general=0 min=0 max=12 delay=30
session read_trig16=0 ack_len=68 t0=60000 t5=400 t15=400
session close=0
session read_closed=-2 status=1
Вне режима команд модель отказывает (состояние 1). Как ведёт себя настоящий модуль, описание не говорит; драйвер в любом случае открывает режим перед чтением и записью.
В кадре нет контрольной суммы. Заголовок и хвост ловят обрывки, но не перевёрнутый бит в значении: модуль примет испорченный порог и ответит «успех». Поэтому настройка читает обратно всё, что записала, и при расхождении или отказе пишет заново — до трёх раз:
/* Контрольной суммы в кадре нет: испорченное значение модуль примет и ответит «успех».
* Поэтому всё записанное читается обратно; не сошлось или модуль отказал (в запросе мог испортиться
* байт имени) — записать всё ещё раз, до трёх проходов. Молчание модуля повтором не лечится. */
int ld2420_configure(ld2420_t *d, const ld2420_cfg_t *c)
{
uint16_t names[NPAR];
uint32_t vals[NPAR];
uint16_t a, b;
if (c->min_gate > c->max_gate || c->max_gate >= LD2420_GATES)
return LD2420_EARG;
cfg_list(c, names, vals);
int r = ld2420_open(d, &a, &b);
if (r)
return r;
for (unsigned pass = 0; pass < 3u; pass++) {
r = write_all(d, names, vals);
if (r == LD2420_OK)
r = verify_all(d, names, vals);
if (r == LD2420_OK || r == LD2420_ETIMEOUT)
break;
}
int rc = ld2420_close(d);
return r ? r : rc;
}
Параметров настройки 35: ближние и дальние ворота, задержка и по 16 порогов двух видов. Записываются они по 6 в кадре — как в самом длинном примере описания (какой предел у модуля, неизвестно), читаются по 16 — ответ на 16 имён занимает 68 байт. Это 11 команд: открыть, 6 записей, 3 чтения, закрыть. Пороги — из таблицы шума:
/* Руководство: порог срабатывания — больше 5 шумов ворот, порог удержания — от 2 до 5 шумов. */
void ld2420_thr_from_noise(const uint32_t *noise, uint32_t *trig, uint32_t *hold, uint32_t kt, uint32_t kh)
{
for (uint16_t g = 0; g < LD2420_GATES; g++) {
uint32_t t = noise[g] * kt, h = noise[g] * kh;
trig[g] = t > 65535u ? 65535u : t;
hold[g] = h > 65535u ? 65535u : h;
}
}
configure=0 cmds=11 ms=18 trig0=2400 trig2=900 hold2=450 model_max=8 model_delay=5 cmd_mode=0
configure_bad_gate=-5
Шум ворот 0 — 400, ворот 2 — 150 (условные единицы модели): срабатывание 2400 и 900, удержание — 450. Вся настройка на модели заняла 18 мс; на живом модуле время зависит от того, как быстро он отвечает.
100а.7 Поток с мусором, потерями и сбоями
Как драйвер переносит плохую линию, проверено на модели. Линия переносит 25 байт за 1 мс (256 000 бод); каждая операция — чтение 1–16 случайных параметров или запись 1–6, всего 2000 операций на опыт. Модель знает, что у неё на самом деле записано, и каждое прочитанное значение сверяется с ней. Мусор — это случайные байты, обрывки заголовка, «кадры» без хвоста и с длиной больше допустимой; с вероятностью 30 % он стоит перед ответом.
fuzz clean ops=2000 ok=2000 timeout=0 status=0 short=0 wrong_read=0 wrong_write=0
fuzz clean retries=0 timeouts=0 stray=0 frames=2001 bad_len=0 bad_tail=0 rejected=0 flushed=0 other=0 junk=0 drops=0 flips=0 ms=3403 lost=0
fuzz junk ops=2000 ok=2000 timeout=0 status=0 short=0 wrong_read=0 wrong_write=0
fuzz junk retries=0 timeouts=0 stray=0 frames=2001 bad_len=157 bad_tail=46 rejected=4 flushed=114 other=11315 junk=11315 drops=0 flips=0 ms=5010 lost=0
fuzz junk_lax ops=2000 ok=2000 timeout=0 status=0 short=0 wrong_read=0 wrong_write=0
fuzz junk_lax retries=2 timeouts=2 stray=0 frames=2003 bad_len=158 bad_tail=44 rejected=0 flushed=118 other=11303 junk=11333 drops=0 flips=0 ms=5252 lost=0
fuzz drops ops=2000 ok=1985 timeout=15 status=0 short=0 wrong_read=0 wrong_write=0
fuzz drops retries=367 timeouts=382 stray=0 frames=1986 bad_len=179 bad_tail=156 rejected=0 flushed=122 other=18466 junk=12025 drops=325 flips=0 ms=43159 lost=0
fuzz flips ops=2000 ok=1969 timeout=1 status=30 short=0 wrong_read=41 wrong_write=8
fuzz flips retries=40 timeouts=41 stray=0 frames=2000 bad_len=1 bad_tail=6 rejected=6 flushed=1 other=548 junk=0 drops=0 flips=117 ms=7517 lost=0
| Опыт | Удачно | Отказ | Тайм-аут | Неверных значений, принятых за верные |
|---|---|---|---|---|
| чистая линия | 2000 | 0 | 0 | 0 |
| мусор перед 30 % ответов | 2000 | 0 | 0 | 0 |
| мусор + 0,2 % байтов пропадает | 1985 | 0 | 15 | 0 |
| 0,1 % байтов с перевёрнутым битом | 1969 | 30 | 1 | 41 при чтении, 8 при записи |
Мусор драйвер отсеивает полностью: 11 315 байт мусора — ровно 11 315 байт
в other, ни одного тайм-аута. Повторный разбор нашёл четыре кадра, хвост
которых совпал случайно (rejected=4); без проверки is_ack
(junk_lax) те же 2000 операций дали 2 тайм-аута. Пропавшие байты
лечатся повтором: 367 повторов, и только 15 операций не уложились в три
попытки.
Пропажа байта в команде дороже, чем в ответе: модель ждёт недостающий
байт и съедает им начало следующей попытки.
Сбои битов показывают цену протокола без контрольной суммы. Испорченное имя параметра модуль отвергает (30 отказов), а испорченное значение проходит: 41 неверное значение при чтении и 8 записей, после которых в модуле не то, что просили, — и всё это с состоянием «успех». Та же линия и настройка целиком, с обратным чтением, 300 раз:
cfg_flips runs=300 ok=280 verify_fail=6 status_fail=12 timeout=2 bad_after_ok=0 flips=282 retries=103 cmds=4136
280 настроек прошли, 20 закончились честной ошибкой, и ни одной «успешной» настройки, после которой в модуле что-то не так. Обратное чтение не делает линию надёжной — оно делает ошибку видимой.
100а.8 Светильник: модель, прошивка и размеры
Логика светильника проще некуда: OT1 в единице — свет горит и запоминается время; OT1 в нуле дольше своей выдержки — свет гаснет.
int lamp_step(lamp_t *l, uint32_t now_ms, int occupied)
{
if (occupied) {
l->last_seen = now_ms;
if (!l->on) {
l->on = 1;
l->switches++;
}
} else if (l->on && now_ms - l->last_seen >= l->hold_ms) {
l->on = 0; /* разность беззнаковая: переход счётчика через ноль не страшен */
l->switches++;
}
return l->on;
}
Вся тонкость — в настройке радара. Модель присутствия устроена так: 10 раз в секунду в каждых воротах шум ±50 % от своего среднего; цель добавляет энергию в своих воротах (расстояние / 0,7 м) и по четверти — в соседних. OT1 поднимается, когда хоть в одних воротах зоны энергия выше порога срабатывания, и падает, когда ни в одних она не выше порога удержания всю задержку подряд:
/* 100 мс работы радара. Модель: шум ворот ±50 %, цель — в воротах cm/70 и по четверти в соседних.
* Нет человека → есть: энергия выше порога срабатывания хоть в одних воротах от min до max.
* Есть → нет: ни в одних не выше порога удержания delay секунд подряд. */
int model_tick(model_t *m, const m_target_t *t, int n)
{
for (int g = 0; g < 16; g++)
m->energy[g] = m->noise[g] * (50u + model_rand(m) % 101u) / 100u;
for (int i = 0; i < n; i++) {
int g = t[i].cm / 70;
if (g > 15)
continue;
m->energy[g] += t[i].amp * (80u + model_rand(m) % 41u) / 100u;
if (g > 0)
m->energy[g - 1] += t[i].amp / 4u;
if (g < 15)
m->energy[g + 1] += t[i].amp / 4u;
}
int trig = 0, hold = 0;
for (uint32_t g = m->min_gate; g <= m->max_gate && g < 16; g++) {
trig |= m->energy[g] > m->trig[g];
hold |= m->energy[g] > m->hold[g];
}
if (!m->ot1) {
if (trig)
m->ot1 = 1, m->quiet = 0;
} else if (hold) {
m->quiet = 0;
} else if (++m->quiet >= m->delay * (1000u / M_TICK_MS)) {
m->ot1 = 0;
}
return m->ot1;
}
Энергии в модели условные: идущий человек — 6000, дышащий — 500, вентилятор — 2000, шум — 150 (у ворот 0 и 1 — 400 и 300). Это не замер настоящего модуля, а сценарий, на котором видна разница между настройками. Комната — 300 с: вентилятор в 7,5 м качается всё время; человек входит на 30-й секунде и идёт от 6 м к 2 м, со 40-й по 160-ю сидит неподвижно, потом уходит и на 165-й исчезает. Задержка радара — 5 (в модели — секунды), выдержка светильника — 10 с.
lamp gates0-8 configure=0 max_gate=8 trig=900 hold=450
lamp gates0-8 t=30000 on=1
lamp gates0-8 t=179800 on=0
lamp gates0-8 switches=2 on_present_s=135 on_absent_s=14 ot1_s=139 present_s=135
lamp gates0-12 configure=0 max_gate=12 trig=900 hold=450
lamp gates0-12 t=0 on=1
lamp gates0-12 switches=1 on_present_s=135 on_absent_s=165 ot1_s=300 present_s=135
lamp no_hysteresis configure=0 max_gate=8 trig=900 hold=900
lamp no_hysteresis t=30000 on=1
lamp no_hysteresis t=54800 on=0
lamp no_hysteresis t=160000 on=1
lamp no_hysteresis t=179800 on=0
lamp no_hysteresis switches=4 on_present_s=29 on_absent_s=14 ot1_s=24 present_s=135
| Настройка | Свет включился | Свет погас | Горел при человеке | Горел без человека |
|---|---|---|---|---|
| ворота 0…8, срабатывание 6 шумов, удержание 3 | 30,0 с | 179,8 с | 135 из 135 с | 14 с |
| ворота 0…12 (заводские дальние ворота) | 0 с | не погас | 135 из 135 с | 165 с |
| ворота 0…8, удержание = срабатыванию | 30,0 с | 54,8 с и снова 179,8 с | 29 из 135 с | 14 с |
Первая строка — как задумано: свет зажёгся, едва человек вошёл, горел всё время, пока он сидел, и погас через 5 + 10 с после ухода. Во второй в зону попал вентилятор, и свет горит в пустой комнате всегда. В третьей нет гистерезиса: дыхание не дотягивает до порога срабатывания, OT1 падает через 5 с после того, как человек сел, а свет — ещё через 10 с; человек сидит в темноте, пока не встанет.
Прошивка — та же логика на плате. USART принимает в кольцо прерыванием:
void UART_IRQHandler(void)
{
uint32_t sr = UART->SR;
if (sr & (USART_SR_RXNE | USART_SR_ORE)) { /* чтение SR, затем DR снимает и RXNE, и ORE */
uint8_t b = (uint8_t)UART->DR;
if (sr & USART_SR_ORE)
uart_ore++;
ld2420_ring_put(&rx, b);
}
}
Делитель скорости — частота шины на 256 000, с округлением. У F103 USART1
на шине 72 МГц: 281, это 256 228 бод (+0,09 %). У F407 USART2 на шине
42 МГц: 164, это 256 098 бод (+0,04 %). Оба числа проверены
_Static_assert при сборке. Главный цикл:
int main(void)
{
clock_init();
SysTick->LOAD = HCLK / 1000u - 1u;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
pins_init();
uart_init();
ld2420_init(&radar, &rx, uart_tx, now_ms, idle);
radar.on_other = count_other;
while (ms < 1000u) /* сколько модуль стартует, в документах нет: ждём секунду */
__WFI();
static ld2420_cfg_t c; /* static: 132 байта не на стеке; поля — здесь, а не в .data */
c.min_gate = 0;
c.max_gate = 8; /* ворота 0…8: до 6,3 м */
c.delay = 5;
ld2420_thr_from_noise(NOISE, c.trig, c.hold, 6, 3); /* срабатывание — 6 шумов, удержание — 3 */
cfg_rc = ld2420_configure(&radar, &c); /* не вышло — модуль работает со своими настройками */
lamp_init(&lamp, 10000u);
uint32_t next = ms;
for (;;) {
ld2420_poll(&radar); /* отчёты обычного режима уходят в count_other */
if ((int32_t)(ms - next) >= 0) {
next += 100u;
light(lamp_step(&lamp, ms, ot1()));
}
__WFI();
}
}
Секунда ожидания перед настройкой — выбор, а не число из документа: сколько модуль стартует, там не сказано. Если настройка не удалась, модуль работает с тем, что у него записано, а свет всё равно идёт за OT1.
Сборка arm-none-eabi-gcc 12.2.1, -Os:
text data bss dec hex filename
1820 0 0 1820 71c ld2420_F103.o
52 0 0 52 34 lamp_F103.o
882 4 780 1666 682 fw100a_F103.o
3368 4 780 4152 1038 fw_F103.elf
| F103 | F407 | |
|---|---|---|
драйвер ld2420.o |
1820 байт | 1812 байт |
светильник lamp.o |
52 байта | 52 байта |
| вся прошивка: код и константы | 3368 байт | 3456 байт |
ОЗУ: .data + .bss |
4 + 780 байт | 4 + 780 байт |
| из них драйвер / кольцо / светильник | 356 / 264 / 16 байт | 356 / 264 / 16 байт |
| стек самой глубокой цепочки | 960 байт | 960 байт |
Глубже всего стек уходит в настройке: ld2420_configure (320 байт по
-fstack-usage) → запись (72) → команда (176) → ld2420_poll (24) →
сброс оборванного кадра (168) → разбор (184) → обратный вызов (16). Больше
всего занимают копии кадра на стеке — это цена повторного разбора.
Тот же прогон — test100a.c с моделью — собран для Cortex-M4 и запущен в
QEMU (машина netduinoplus2, вывод через полухостинг): все 74 строки вывода
совпали с прогоном на компьютере. Прошивка светильника в QEMU не
запускалась: модуля на другом конце его UART нет.
100а.9 Грабли
Модуль молчит. Перепутаны линии: OT2 — это TX радара, его на RX микроконтроллера. Или скорость не 256 000.
Чтение и запись не работают, а версия читается. Не открыт режим команд. У модели вне режима — отказ; что делает настоящий модуль, описание не говорит, поэтому драйвер всегда открывает режим перед настройкой и закрывает после.
Пример записи из описания не проходит. Кадр записи порогов 00…05 в
описании на 2 байта короче правильного (длина 24 00 вместо 26 00).
Собирать кадр функцией, а не копировать из документа.
Порог прочитан как 40 000, а в тексте — 60 000. Ошибка примера:
40 9C 00 00 — это 40 000. Младший байт впереди.
Свет горит в пустой комнате. В зоне качается что-то постороннее: вентилятор, занавеска, растение под кондиционером. Сузить дальние ворота; в модели с воротами 0…12 вентилятор в 7,5 м держал свет 165 с из 165.
Свет гаснет, когда человек сидит. Порог удержания слишком высок — дыханию его не перешагнуть. Удержание — 2–5 шумов, срабатывание — больше 5; без этого зазора в модели свет горел 29 с из 135.
Ложные срабатывания от того, что за радаром. Задний лепесток антенны: радар видит и сзади. Металлический экран за модулем.
Шум от питания. Пики в спектре питания до 100 кГц — помеха приёмнику. Фильтр, отдельный стабилизатор, короткий общий провод.
Значение «успешно» записалось, а в модуле другое. Контрольной суммы в кадре нет: перевёрнутый бит в значении проходит. Читать обратно; в опыте со сбоями без обратного чтения 8 записей из 2000 оставили в модуле не то, что просили.
Ответ «потерялся» в мусоре. Ложный заголовок съел начало настоящего кадра. Не бросать собранное при сбое, а разбирать заново со второго байта; принимать только ответы на известные команды.
Разбор завис посреди кадра. Байт пропал, и автомат ждёт недостающие. Пауза дольше 10 мс посреди кадра — бросить кадр и разобрать его хвост.
Отчёт модуля в обычном режиме не разбирается. Его формат в этих
документах не описан. Присутствие — по OT1; байты отчёта драйвер отдаёт
on_other как есть.
Неподвижному человеку радар не мерит расстояние. Руководство прямо говорит: расстояние выдаётся только для движущейся цели в пределах 8 м.
100а.10 Итоги
- HLK-LD2420 — радар 24 ГГц: видит и неподвижного человека — по дыханию. Питание 3,0–3,6 В, в среднем 50 мА; дальность 8 м для движущегося человека и 6 м для мелких движений на стене, 5 и 4 м — на потолке; ±60°, обновление 10 раз в секунду.
- Выход OT1 — уровень «есть человек»; OT2 — это TX радара, порт 256 000 бод. Делитель: 281 на 72 МГц (+0,09 %), 164 на 42 МГц (+0,04 %).
- Ворота 0…15 по 0,7 м; у каждых два порога: срабатывание (больше 5 шумов) и удержание (2–5 шумов). Это гистерезис: без него сидящий человек «пропадает» — в модели свет горел 29 с из 135.
- Кадр:
FD FC FB FA, длина, данные,04 03 02 01, младший байт впереди, контрольной суммы нет. Из 16 примеров описания 15 совпали байт в байт; в одном кадр на 2 байта короче, в другом40 9Cвыдано за 60 000. - Разбор с повторным разбором при сбое и паузой 10 мс: 2000 из 2000 операций на линии с мусором, 11 315 байт мусора отсеяно; при пропаже 0,2 % байтов — 1985 из 2000.
- Без контрольной суммы сбои битов проходят: 41 неверное чтение и 8 неверных записей на 2000. С обратным чтением — 280 из 300 настроек, ни одной «успешной» с ошибкой внутри.
- Прошивка светильника: 3368 байт кода у F103, 3456 у F407, 784 байта ОЗУ; тот же прогон в QEMU совпал с компьютером строка в строку.
Полные исходники
Драйвер, логика светильника, модель модуля, прогон, прошивка для F103 и F407 и обвязка для QEMU — всё, что нужно, чтобы повторить опыты раздела.
ld2420.h — драйвер: константы протокола, кольцо, разбор, команды, настройка.
/* Раздел 100а: драйвер радара HLK-LD2420 — кадры команд по описанию протокола Hi-Link.
* Кадр: FD FC FB FA | длина (2 байта, младший первым) | данные | 04 03 02 01.
* Данные команды: код (2 байта) + параметры; данные ответа: код | 0x0100, состояние (0 — успех), значения.
* Контрольной суммы в кадре нет. Формат отчётов в обычном режиме в документах не описан: всё, что не
* сложилось в кадр, драйвер отдаёт как есть обратному вызову on_other. */
#ifndef LD2420_H
#define LD2420_H
#include <stdint.h>
#define LD2420_GATES 16u /* ворота 0…15, шаг 0,7 м (руководство) */
#define LD2420_GATE_CM 70u
#define LD2420_MAX_DATA 128u /* своё ограничение: самый длинный ответ здесь — 68 байт */
#define LD2420_RAW_MAX (4u + 2u + LD2420_MAX_DATA + 4u)
#define LD2420_WRITE_CHUNK 6u /* параметров в одной записи — как в примере производителя */
#define LD2420_GAP_MS 10u /* пауза посреди кадра дольше этой — кадр брошен */
#define LD2420_TMO_MS 100u /* ждать ответ; время ответа модуля в документах не названо */
#define LD2420_TRIES 3u
/* коды команд */
#define LD2420_CMD_VERSION 0x0000u
#define LD2420_CMD_WRITE 0x0007u
#define LD2420_CMD_READ 0x0008u
#define LD2420_CMD_REBOOT 0x0068u
#define LD2420_CMD_CLOSE 0x00FEu
#define LD2420_CMD_OPEN 0x00FFu
#define LD2420_ACK 0x0100u /* ответ = код команды | 0x0100: 08 00 → 08 01 */
/* имена параметров */
#define LD2420_P_MIN_GATE 0x0000u
#define LD2420_P_MAX_GATE 0x0001u
#define LD2420_P_DELAY 0x0004u
#define LD2420_P_TRIG(g) (0x0010u + (g))
#define LD2420_P_HOLD(g) (0x0020u + (g))
enum { LD2420_OK = 0, LD2420_ETIMEOUT = -1, LD2420_ESTATUS = -2, LD2420_ESHORT = -3, LD2420_EVERIFY = -4, LD2420_EARG = -5 };
/* ── кольцо приёма: пишет прерывание UART, читает основной цикл ── */
#define LD2420_RING 256u /* степень двойки: индексы по маске */
typedef struct {
uint8_t buf[LD2420_RING];
volatile uint16_t head, tail; /* head пишет только прерывание, tail — только цикл */
volatile uint32_t lost; /* байты, которым не нашлось места */
} ld2420_ring_t;
void ld2420_ring_put(ld2420_ring_t *r, uint8_t b);
int ld2420_ring_get(ld2420_ring_t *r, uint8_t *b);
/* ── разбор потока ── */
typedef struct {
uint8_t raw[LD2420_RAW_MAX]; /* байты текущего кадра от первого FD: при сбое их разбирают заново */
uint16_t rawn, len;
int (*on_frame)(void *ctx, const uint8_t *data, uint16_t len); /* 0 — «не кадр»: разобрать заново */
void (*on_other)(void *ctx, uint8_t b);
void *ctx;
uint32_t frames, bad_len, bad_tail, rejected, other, flushed;
} ld2420_parser_t;
void ld2420_parser_init(ld2420_parser_t *p, int (*on_frame)(void *, const uint8_t *, uint16_t),
void (*on_other)(void *, uint8_t), void *ctx);
void ld2420_parse(ld2420_parser_t *p, uint8_t b);
void ld2420_parser_flush(ld2420_parser_t *p); /* долгая пауза посреди кадра: бросить его и разобрать хвост */
uint16_t ld2420_build(uint8_t *out, uint16_t cmd, const uint8_t *par, uint16_t n);
/* ── команды ── */
typedef struct {
ld2420_ring_t *rx;
ld2420_parser_t p;
void (*tx)(const uint8_t *b, uint16_t n);
uint32_t (*now_ms)(void);
void (*idle)(void); /* ждём байты: __WFI() на плате, шаг модели на компьютере */
void (*on_other)(void *ctx, uint8_t b);
void *other_ctx;
uint32_t last_ms; /* когда пришёл последний байт */
uint16_t want; /* какой ответ ждём */
uint8_t got;
uint16_t ack_len;
uint8_t ack[LD2420_MAX_DATA];
uint16_t status; /* состояние из последнего ответа */
uint32_t timeouts, retries, stray; /* чужие ответы — от прошлой команды, которую не дождались */
} ld2420_t;
void ld2420_init(ld2420_t *d, ld2420_ring_t *rx, void (*tx)(const uint8_t *, uint16_t), uint32_t (*now_ms)(void),
void (*idle)(void));
void ld2420_poll(ld2420_t *d); /* разобрать всё, что накопилось в кольце */
int ld2420_cmd(ld2420_t *d, uint16_t cmd, const uint8_t *par, uint16_t n, uint32_t tmo_ms);
int ld2420_open(ld2420_t *d, uint16_t *a, uint16_t *b);
int ld2420_close(ld2420_t *d);
int ld2420_version(ld2420_t *d, char *s, uint16_t size);
int ld2420_reboot(ld2420_t *d);
int ld2420_read(ld2420_t *d, const uint16_t *names, uint16_t n, uint32_t *vals);
int ld2420_write(ld2420_t *d, const uint16_t *names, const uint32_t *vals, uint16_t n);
typedef struct {
uint8_t min_gate, max_gate;
uint16_t delay; /* единица в документах не названа */
uint32_t trig[LD2420_GATES], hold[LD2420_GATES];
} ld2420_cfg_t;
int ld2420_configure(ld2420_t *d, const ld2420_cfg_t *c); /* открыть, записать, прочитать обратно, закрыть */
void ld2420_thr_from_noise(const uint32_t *noise, uint32_t *trig, uint32_t *hold, uint32_t kt, uint32_t kh);
#endif
ld2420.c — драйвер: кольцо приёма, автомат разбора с повторным разбором, команды с тайм-аутом и повтором, настройка с обратным чтением.
/* Раздел 100а: драйвер HLK-LD2420 — кольцо приёма, разбор кадров с повторным разбором при сбое,
* команды с тайм-аутом и повтором, настройка порогов с проверкой обратным чтением. */
#include <string.h>
#include "ld2420.h"
static const uint8_t HDR[4] = {0xFD, 0xFC, 0xFB, 0xFA};
static const uint8_t TAIL[4] = {0x04, 0x03, 0x02, 0x01};
/* ── кольцо ── */
void ld2420_ring_put(ld2420_ring_t *r, uint8_t b)
{
uint16_t h = r->head;
if ((uint16_t)(h - r->tail) >= LD2420_RING) { /* полно: байт теряется, счётчик это помнит */
r->lost++;
return;
}
r->buf[h & (LD2420_RING - 1u)] = b;
r->head = (uint16_t)(h + 1u); /* сначала байт, потом индекс */
}
int ld2420_ring_get(ld2420_ring_t *r, uint8_t *b)
{
uint16_t t = r->tail;
if (t == r->head)
return 0;
*b = r->buf[t & (LD2420_RING - 1u)];
r->tail = (uint16_t)(t + 1u);
return 1;
}
/* ── разбор ── */
void ld2420_parser_init(ld2420_parser_t *p, int (*on_frame)(void *, const uint8_t *, uint16_t),
void (*on_other)(void *, uint8_t), void *ctx)
{
memset(p, 0, sizeof *p);
p->on_frame = on_frame;
p->on_other = on_other;
p->ctx = ctx;
}
static void other(ld2420_parser_t *p, uint8_t b)
{
p->other++;
if (p->on_other)
p->on_other(p->ctx, b);
}
/* Один байт. 0 — принят (в кадр или мимо), 1 — кадр не сложился: в raw лежит всё от его первого FD
* до этого байта включительно, и это надо разобрать заново со второго байта. */
static int step(ld2420_parser_t *p, uint8_t c)
{
uint16_t i = p->rawn;
if (i == 0 && c != HDR[0]) {
other(p, c);
return 0;
}
p->raw[p->rawn++] = c;
if (i < 4)
return c != HDR[i];
if (i == 4)
return 0;
if (i == 5) {
p->len = (uint16_t)(p->raw[4] | p->raw[5] << 8);
if (p->len < 2u || p->len > LD2420_MAX_DATA) {
p->bad_len++;
return 1;
}
return 0;
}
if (i < 6u + p->len)
return 0;
uint16_t k = (uint16_t)(i - 6u - p->len);
if (c != TAIL[k]) {
p->bad_tail++;
return 1;
}
if (k < 3u)
return 0;
if (p->on_frame && !p->on_frame(p->ctx, &p->raw[6], p->len)) {
p->rejected++; /* хвост совпал случайно: «кадр» начался с ложного заголовка */
return 1;
}
p->frames++;
p->rawn = 0;
return 0;
}
/* Сбой: первый FD — мусор, остальное — в начало очереди. В заголовке нет повторяющихся байтов,
* но внутри «кадра» может начинаться настоящий — его и находит повторный разбор. */
void ld2420_parse(ld2420_parser_t *p, uint8_t b)
{
uint8_t q[LD2420_RAW_MAX]; /* непрочитанное + байты брошенного кадра: всего не больше RAW_MAX */
uint16_t qi = 0, qn = 1;
q[0] = b;
while (qi < qn) {
if (!step(p, q[qi++]))
continue;
uint16_t n = (uint16_t)(p->rawn - 1u), rest = (uint16_t)(qn - qi);
memmove(&q[n], &q[qi], rest);
memcpy(q, &p->raw[1], n);
qi = 0;
qn = (uint16_t)(n + rest);
other(p, p->raw[0]);
p->rawn = 0;
}
}
void ld2420_parser_flush(ld2420_parser_t *p)
{
uint8_t t[LD2420_RAW_MAX];
uint16_t n = p->rawn;
if (!n)
return;
memcpy(t, p->raw, n);
p->rawn = 0;
p->flushed++;
other(p, t[0]);
for (uint16_t i = 1; i < n; i++)
ld2420_parse(p, t[i]);
}
uint16_t ld2420_build(uint8_t *out, uint16_t cmd, const uint8_t *par, uint16_t n)
{
uint16_t len = (uint16_t)(2u + n);
if (len > LD2420_MAX_DATA)
return 0;
memcpy(out, HDR, 4);
out[4] = (uint8_t)len;
out[5] = (uint8_t)(len >> 8);
out[6] = (uint8_t)cmd;
out[7] = (uint8_t)(cmd >> 8);
if (n)
memcpy(&out[8], par, n);
memcpy(&out[8 + n], TAIL, 4);
return (uint16_t)(len + 10u);
}
/* ── команды ── */
/* Ответ — это код известной команды с 0x01 в старшем байте и не меньше 4 байт. Всё прочее — не кадр:
* ложный заголовок из мусора, длина которого случайно привела на хвост настоящего ответа. */
static int is_ack(uint16_t code, uint16_t len)
{
uint16_t c = (uint16_t)(code & 0x00FFu);
return (code & 0xFF00u) == LD2420_ACK && len >= 4u &&
(c == LD2420_CMD_VERSION || c == LD2420_CMD_WRITE || c == LD2420_CMD_READ || c == LD2420_CMD_REBOOT ||
c == LD2420_CMD_CLOSE || c == LD2420_CMD_OPEN);
}
static int on_frame(void *ctx, const uint8_t *data, uint16_t len)
{
ld2420_t *d = ctx;
uint16_t code = (uint16_t)(data[0] | data[1] << 8);
if (!is_ack(code, len))
return 0;
if (d->got || code != (d->want | LD2420_ACK)) {
d->stray++; /* опоздавший ответ на брошенную команду */
return 1;
}
memcpy(d->ack, data, len);
d->ack_len = len;
d->got = 1;
return 1;
}
static void on_other(void *ctx, uint8_t b)
{
ld2420_t *d = ctx;
if (d->on_other)
d->on_other(d->other_ctx, b);
}
void ld2420_init(ld2420_t *d, ld2420_ring_t *rx, void (*tx)(const uint8_t *, uint16_t), uint32_t (*now_ms)(void),
void (*idle)(void))
{
memset(d, 0, sizeof *d);
d->rx = rx;
d->tx = tx;
d->now_ms = now_ms;
d->idle = idle;
ld2420_parser_init(&d->p, on_frame, on_other, d);
}
void ld2420_poll(ld2420_t *d)
{
uint8_t b;
uint32_t now = d->now_ms();
while (ld2420_ring_get(d->rx, &b)) {
d->last_ms = now;
ld2420_parse(&d->p, b);
}
if (d->p.rawn && now - d->last_ms >= LD2420_GAP_MS)
ld2420_parser_flush(&d->p); /* кадр оборвался: байты потеряны, ждать его нечего */
}
int ld2420_cmd(ld2420_t *d, uint16_t cmd, const uint8_t *par, uint16_t n, uint32_t tmo_ms)
{
uint8_t f[LD2420_RAW_MAX];
uint16_t fl = ld2420_build(f, cmd, par, n);
if (!fl)
return LD2420_EARG;
for (unsigned t = 0; t < LD2420_TRIES; t++) {
if (t)
d->retries++;
ld2420_poll(d); /* старое — до отправки, чтобы не принять его за ответ */
d->want = cmd;
d->got = 0;
d->tx(f, fl);
uint32_t t0 = d->now_ms();
for (;;) {
ld2420_poll(d);
if (d->got || d->now_ms() - t0 >= tmo_ms)
break;
d->idle();
}
if (d->got) {
d->status = (uint16_t)(d->ack[2] | d->ack[3] << 8);
return d->status ? LD2420_ESTATUS : LD2420_OK;
}
d->timeouts++;
}
return LD2420_ETIMEOUT;
}
static uint16_t rd16(const uint8_t *p) { return (uint16_t)(p[0] | p[1] << 8); }
static uint32_t rd32(const uint8_t *p) { return p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24; }
int ld2420_open(ld2420_t *d, uint16_t *a, uint16_t *b)
{
static const uint8_t par[2] = {0x01, 0x00}; /* «2-byte upper version, default 01» */
int r = ld2420_cmd(d, LD2420_CMD_OPEN, par, 2, LD2420_TMO_MS);
if (r)
return r;
if (d->ack_len < 8u)
return LD2420_ESHORT;
*a = rd16(&d->ack[4]); /* в примере 02 00 и 20 00; что это, документ не говорит */
*b = rd16(&d->ack[6]);
return LD2420_OK;
}
int ld2420_close(ld2420_t *d)
{
return ld2420_cmd(d, LD2420_CMD_CLOSE, 0, 0, LD2420_TMO_MS);
}
int ld2420_version(ld2420_t *d, char *s, uint16_t size)
{
int r = ld2420_cmd(d, LD2420_CMD_VERSION, 0, 0, LD2420_TMO_MS);
if (r)
return r;
if (d->ack_len < 6u || d->ack_len < 6u + rd16(&d->ack[4]))
return LD2420_ESHORT;
uint16_t n = rd16(&d->ack[4]);
if (n > size - 1u)
n = (uint16_t)(size - 1u);
memcpy(s, &d->ack[6], n);
s[n] = 0;
return LD2420_OK;
}
int ld2420_reboot(ld2420_t *d)
{
uint8_t f[12]; /* кадр без параметров — 12 байт */
d->tx(f, ld2420_build(f, LD2420_CMD_REBOOT, 0, 0)); /* ответа в описании нет — не ждём */
return LD2420_OK;
}
int ld2420_read(ld2420_t *d, const uint16_t *names, uint16_t n, uint32_t *vals)
{
uint8_t par[2 * LD2420_GATES];
if (n == 0 || n > LD2420_GATES)
return LD2420_EARG;
for (uint16_t i = 0; i < n; i++) {
par[2 * i] = (uint8_t)names[i];
par[2 * i + 1] = (uint8_t)(names[i] >> 8);
}
int r = ld2420_cmd(d, LD2420_CMD_READ, par, (uint16_t)(2u * n), LD2420_TMO_MS);
if (r)
return r;
if (d->ack_len != 4u + 4u * n)
return LD2420_ESHORT;
for (uint16_t i = 0; i < n; i++)
vals[i] = rd32(&d->ack[4 + 4 * i]);
return LD2420_OK;
}
int ld2420_write(ld2420_t *d, const uint16_t *names, const uint32_t *vals, uint16_t n)
{
uint8_t par[6 * LD2420_WRITE_CHUNK];
if (n == 0 || n > LD2420_WRITE_CHUNK)
return LD2420_EARG;
for (uint16_t i = 0; i < n; i++) {
uint8_t *q = &par[6 * i];
q[0] = (uint8_t)names[i];
q[1] = (uint8_t)(names[i] >> 8);
q[2] = (uint8_t)vals[i];
q[3] = (uint8_t)(vals[i] >> 8);
q[4] = (uint8_t)(vals[i] >> 16);
q[5] = (uint8_t)(vals[i] >> 24);
}
return ld2420_cmd(d, LD2420_CMD_WRITE, par, (uint16_t)(6u * n), LD2420_TMO_MS);
}
/* все 35 параметров настройки: 3 общих, 16 порогов срабатывания, 16 порогов удержания */
#define NPAR (3u + 2u * LD2420_GATES)
static void cfg_list(const ld2420_cfg_t *c, uint16_t *names, uint32_t *vals)
{
names[0] = LD2420_P_MIN_GATE, vals[0] = c->min_gate;
names[1] = LD2420_P_MAX_GATE, vals[1] = c->max_gate;
names[2] = LD2420_P_DELAY, vals[2] = c->delay;
for (uint16_t g = 0; g < LD2420_GATES; g++) {
names[3 + g] = (uint16_t)LD2420_P_TRIG(g), vals[3 + g] = c->trig[g];
names[3 + LD2420_GATES + g] = (uint16_t)LD2420_P_HOLD(g), vals[3 + LD2420_GATES + g] = c->hold[g];
}
}
static int write_all(ld2420_t *d, const uint16_t *names, const uint32_t *vals)
{
for (uint16_t i = 0; i < NPAR; i = (uint16_t)(i + LD2420_WRITE_CHUNK)) {
uint16_t k = (uint16_t)(NPAR - i < LD2420_WRITE_CHUNK ? NPAR - i : LD2420_WRITE_CHUNK);
int r = ld2420_write(d, &names[i], &vals[i], k);
if (r)
return r;
}
return LD2420_OK;
}
static int verify_all(ld2420_t *d, const uint16_t *names, const uint32_t *vals)
{
uint32_t got[LD2420_GATES];
for (uint16_t i = 0; i < NPAR; i = (uint16_t)(i + LD2420_GATES)) {
uint16_t k = (uint16_t)(NPAR - i < LD2420_GATES ? NPAR - i : LD2420_GATES);
int r = ld2420_read(d, &names[i], k, got);
if (r)
return r;
if (memcmp(got, &vals[i], 4u * k))
return LD2420_EVERIFY;
}
return LD2420_OK;
}
/* Контрольной суммы в кадре нет: испорченное значение модуль примет и ответит «успех».
* Поэтому всё записанное читается обратно; не сошлось или модуль отказал (в запросе мог испортиться
* байт имени) — записать всё ещё раз, до трёх проходов. Молчание модуля повтором не лечится. */
int ld2420_configure(ld2420_t *d, const ld2420_cfg_t *c)
{
uint16_t names[NPAR];
uint32_t vals[NPAR];
uint16_t a, b;
if (c->min_gate > c->max_gate || c->max_gate >= LD2420_GATES)
return LD2420_EARG;
cfg_list(c, names, vals);
int r = ld2420_open(d, &a, &b);
if (r)
return r;
for (unsigned pass = 0; pass < 3u; pass++) {
r = write_all(d, names, vals);
if (r == LD2420_OK)
r = verify_all(d, names, vals);
if (r == LD2420_OK || r == LD2420_ETIMEOUT)
break;
}
int rc = ld2420_close(d);
return r ? r : rc;
}
/* Руководство: порог срабатывания — больше 5 шумов ворот, порог удержания — от 2 до 5 шумов. */
void ld2420_thr_from_noise(const uint32_t *noise, uint32_t *trig, uint32_t *hold, uint32_t kt, uint32_t kh)
{
for (uint16_t g = 0; g < LD2420_GATES; g++) {
uint32_t t = noise[g] * kt, h = noise[g] * kh;
trig[g] = t > 65535u ? 65535u : t;
hold[g] = h > 65535u ? 65535u : h;
}
}
lamp.h — светильник по OT1: структура и вызовы.
/* Раздел 100а: светильник по присутствию. Вход — уровень OT1 радара (единица — «есть человек»),
* выход — свет. Включить сразу, выключить, когда OT1 в нуле дольше своей выдержки hold_ms. */
#ifndef LAMP_H
#define LAMP_H
#include <stdint.h>
typedef struct {
uint32_t hold_ms; /* своя выдержка поверх задержки радара */
uint32_t last_seen; /* когда OT1 последний раз был в единице */
uint8_t on;
uint32_t switches; /* сколько раз переключался */
} lamp_t;
void lamp_init(lamp_t *l, uint32_t hold_ms);
int lamp_step(lamp_t *l, uint32_t now_ms, int occupied);
#endif
lamp.c — светильник: включить сразу, выключить после выдержки.
/* Раздел 100а: светильник по присутствию — логика без железа, её гоняет модель. */
#include "lamp.h"
void lamp_init(lamp_t *l, uint32_t hold_ms)
{
l->hold_ms = hold_ms;
l->last_seen = 0;
l->on = 0;
l->switches = 0;
}
int lamp_step(lamp_t *l, uint32_t now_ms, int occupied)
{
if (occupied) {
l->last_seen = now_ms;
if (!l->on) {
l->on = 1;
l->switches++;
}
} else if (l->on && now_ms - l->last_seen >= l->hold_ms) {
l->on = 0; /* разность беззнаковая: переход счётчика через ноль не страшен */
l->switches++;
}
return l->on;
}
fw100a.c — прошивка светильника для F103 и F407: часы, выводы, USART на 256 000 бод с приёмом в кольцо, настройка радара, главный цикл, таблица векторов.
/* Раздел 100а: светильник с радаром HLK-LD2420 — прошивка для STM32F103 (-DCHIP_F103) и STM32F407 (-DCHIP_F407).
* F103: USART1, TX PA9 → RX радара, RX PA10 ← OT2 (TX радара); OT1 → PA0; свет — PC13 (светодиод BluePill, ноль — горит).
* F407: USART2, TX PA2, RX PA3 (AF7); OT1 → PA0; свет — PD12.
* 256 000 бод, 8N1. После старта прошивка задаёт ворота и пороги по таблице шума и дальше
* смотрит только на OT1: свет включается сразу, гаснет через 10 с после того, как OT1 упал. */
#if defined(CHIP_F103)
#include "stm32f1xx.h"
#define UART USART1
#define UART_IRQn USART1_IRQn
#define UART_IRQHandler USART1_IRQHandler
#define HCLK 72000000u
#define UART_CLK 72000000u /* USART1 — на APB2 */
#elif defined(CHIP_F407)
#include "stm32f4xx.h"
#define UART USART2
#define UART_IRQn USART2_IRQn
#define UART_IRQHandler USART2_IRQHandler
#define HCLK 168000000u
#define UART_CLK 42000000u /* USART2 — на APB1 = HCLK/4 */
#else
#error "CHIP_F103 или CHIP_F407"
#endif
#include "ld2420.h"
#include "lamp.h"
#define BAUD 256000u /* руководство: скорость порта 256000 */
#define BRR_VAL ((UART_CLK + BAUD / 2u) / BAUD)
_Static_assert(USART_SR_RXNE == 0x20u && USART_SR_ORE == 0x08u && USART_SR_TXE == 0x80u, "биты SR");
_Static_assert(BRR_VAL == (UART_CLK == 72000000u ? 281u : 164u), "делитель: 72 МГц/281, 42 МГц/164");
/* Шум ворот — из сканирования шума программой производителя в пустой комнате (здесь — для примера). */
static const uint32_t NOISE[LD2420_GATES] = {400, 300, 150, 150, 150, 150, 150, 150,
150, 150, 150, 150, 150, 150, 150, 150};
static volatile uint32_t ms;
static ld2420_ring_t rx;
static ld2420_t radar;
static lamp_t lamp;
volatile uint32_t uart_ore, other_bytes;
volatile int cfg_rc = 1;
void SysTick_Handler(void) { ms++; }
void UART_IRQHandler(void)
{
uint32_t sr = UART->SR;
if (sr & (USART_SR_RXNE | USART_SR_ORE)) { /* чтение SR, затем DR снимает и RXNE, и ORE */
uint8_t b = (uint8_t)UART->DR;
if (sr & USART_SR_ORE)
uart_ore++;
ld2420_ring_put(&rx, b);
}
}
static void uart_tx(const uint8_t *p, uint16_t n)
{
for (uint16_t i = 0; i < n; i++) {
while (!(UART->SR & USART_SR_TXE)) {
}
UART->DR = p[i];
}
}
static uint32_t now_ms(void) { return ms; }
static void idle(void) { __WFI(); }
static void count_other(void *ctx, uint8_t b) { (void)ctx; (void)b; other_bytes++; }
#if defined(CHIP_F103)
static void clock_init(void) /* HSE 8 МГц × 9 = 72 МГц, APB1 36 МГц */
{
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY)) {
}
FLASH->ACR = FLASH_ACR_PRFTBE | (2u << FLASH_ACR_LATENCY_Pos);
RCC->CFGR = RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9 | RCC_CFGR_PPRE1_DIV2;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY)) {
}
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
}
}
static void pins_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_USART1EN;
GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x8u; /* PA0: вход с подтяжкой… */
GPIOA->BRR = 1u << 0; /* …вниз: радара нет — «никого» */
GPIOA->CRH = (GPIOA->CRH & ~(0xFFu << 4)) | (0xBu << 4) | (0x8u << 8); /* PA9 — AF, PA10 — вход */
GPIOA->BSRR = 1u << 10; /* подтяжка RX вверх: линия в покое — 1 */
GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | (0x2u << 20); /* PC13 — выход */
GPIOC->BSRR = 1u << 13; /* свет выключен */
}
static int ot1(void) { return (int)(GPIOA->IDR & 1u); }
static void light(int on) { GPIOC->BSRR = on ? 1u << (13 + 16) : 1u << 13; }
#else
static void clock_init(void) /* HSE 8 МГц: /8 × 336 / 2 = 168 МГц, APB1 42, APB2 84 */
{
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY)) {
}
FLASH->ACR = FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_PRFTEN | FLASH_ACR_LATENCY_5WS;
RCC->PLLCFGR = 8u | (336u << RCC_PLLCFGR_PLLN_Pos) | (0u << RCC_PLLCFGR_PLLP_Pos) | RCC_PLLCFGR_PLLSRC_HSE |
(7u << RCC_PLLCFGR_PLLQ_Pos);
RCC->CFGR = RCC_CFGR_PPRE1_DIV4 | RCC_CFGR_PPRE2_DIV2;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY)) {
}
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
}
}
static void pins_init(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIODEN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
GPIOA->MODER = (GPIOA->MODER & ~(3u << 0 | 3u << 4 | 3u << 6)) | (2u << 4) | (2u << 6); /* PA0 вход, PA2/PA3 AF */
GPIOA->PUPDR = (GPIOA->PUPDR & ~(3u << 0 | 3u << 6)) | (2u << 0) | (1u << 6); /* PA0 вниз, PA3 вверх */
GPIOA->AFR[0] = (GPIOA->AFR[0] & ~(0xFFu << 8)) | (7u << 8) | (7u << 12); /* AF7 — USART2 */
GPIOD->MODER = (GPIOD->MODER & ~(3u << 24)) | (1u << 24); /* PD12 — выход */
}
static int ot1(void) { return (int)(GPIOA->IDR & 1u); }
static void light(int on) { GPIOD->BSRR = on ? 1u << 12 : 1u << (12 + 16); }
#endif
static void uart_init(void)
{
UART->BRR = BRR_VAL;
UART->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE;
NVIC_SetPriority(UART_IRQn, 5);
NVIC_EnableIRQ(UART_IRQn);
}
int main(void)
{
clock_init();
SysTick->LOAD = HCLK / 1000u - 1u;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
pins_init();
uart_init();
ld2420_init(&radar, &rx, uart_tx, now_ms, idle);
radar.on_other = count_other;
while (ms < 1000u) /* сколько модуль стартует, в документах нет: ждём секунду */
__WFI();
static ld2420_cfg_t c; /* static: 132 байта не на стеке; поля — здесь, а не в .data */
c.min_gate = 0;
c.max_gate = 8; /* ворота 0…8: до 6,3 м */
c.delay = 5;
ld2420_thr_from_noise(NOISE, c.trig, c.hold, 6, 3); /* срабатывание — 6 шумов, удержание — 3 */
cfg_rc = ld2420_configure(&radar, &c); /* не вышло — модуль работает со своими настройками */
lamp_init(&lamp, 10000u);
uint32_t next = ms;
for (;;) {
ld2420_poll(&radar); /* отчёты обычного режима уходят в count_other */
if ((int32_t)(ms - next) >= 0) {
next += 100u;
light(lamp_step(&lamp, ms, ot1()));
}
__WFI();
}
}
/* ── запуск и таблица векторов ── */
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
void Reset_Handler(void)
{
uint32_t *s = &_sidata, *d = &_sdata;
while (d < &_edata)
*d++ = *s++;
for (d = &_sbss; d < &_ebss;)
*d++ = 0;
main();
for (;;) {
}
}
static void Default_Handler(void)
{
for (;;) {
}
}
__attribute__((section(".isr_vector"), used)) void (*const vectors[16 + UART_IRQn + 1])(void) = {
[0] = (void (*)(void))&_estack, [1] = Reset_Handler, [2] = Default_Handler, [3] = Default_Handler,
[15] = SysTick_Handler, [16 + UART_IRQn] = UART_IRQHandler,
};
fw100a.ld — скрипт компоновщика прошивки и прогона в QEMU; размеры памяти — из командной строки.
/* Раздел 100а: прошивка светильника. Размеры памяти — из командной строки:
F103C8: --defsym=FLASH_LEN=64K --defsym=RAM_LEN=20K; F407VG: 1024K и 128K. */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = FLASH_LEN
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = RAM_LEN
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text .text.*) *(.rodata .rodata.*) KEEP(*(.init)) KEEP(*(.fini)) . = ALIGN(4); } > FLASH
.ARM.exidx : { *(.ARM.exidx*) } > FLASH
.data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
_sidata = LOADADDR(.data);
.bss (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(4); _ebss = .; } > RAM
end = .;
}
model100a.h — модель модуля: состояние протокола, мусор, энергии ворот.
/* Раздел 100а: модель HLK-LD2420 для прогона драйвера на компьютере.
* Протокол — по описанию производителя: ответы на открытие и закрытие режима команд, версию,
* чтение и запись параметров — байт в байт как в примерах. Чего в описании нет, модель решает сама
* (помечено «модель»): ответ на чтение и запись вне режима команд, значения по умолчанию для ворот,
* которых нет в примерах, единица задержки (секунды), поведение OT1 и энергии в воротах. */
#ifndef MODEL100A_H
#define MODEL100A_H
#include <stdint.h>
#define M_TICK_MS 100u /* обновление 10 раз в секунду (руководство) */
#define M_OUT 8192u
typedef struct {
uint16_t cm; /* расстояние до радара */
uint32_t amp; /* энергия, которую цель даёт в своих воротах */
} m_target_t;
typedef struct {
/* протокол */
uint8_t in[200];
uint16_t inn;
uint8_t out[M_OUT];
uint32_t oh, ot;
int cmd_mode;
uint32_t min_gate, max_gate, delay, trig[16], hold[16];
uint32_t cmds, rejected, ignored;
/* мусор между ответами: вероятность в тысячных */
uint32_t junk_pm, junk_bytes;
/* присутствие */
uint32_t noise[16]; /* средний шум ворот, условные единицы модели */
uint32_t energy[16];
int ot1;
uint32_t quiet; /* тиков подряд без удержания */
uint32_t rng;
} model_t;
void model_init(model_t *m, uint32_t seed);
void model_rx(model_t *m, uint8_t b); /* байт от микроконтроллера */
int model_tx(model_t *m, uint8_t *b); /* байт к микроконтроллеру, 0 — нечего */
uint32_t model_rand(model_t *m);
int model_tick(model_t *m, const m_target_t *t, int n); /* 100 мс: энергии, OT1 */
#endif
model100a.c — модель модуля: ответы по описанию протокола, мусор между ответами, OT1 по порогам ворот.
/* Раздел 100а: модель HLK-LD2420 — ответы по описанию протокола, OT1 по порогам ворот. */
#include <string.h>
#include "model100a.h"
static const uint8_t HDR[4] = {0xFD, 0xFC, 0xFB, 0xFA};
static const uint8_t TAIL[4] = {0x04, 0x03, 0x02, 0x01};
uint32_t model_rand(model_t *m)
{
m->rng ^= m->rng << 13; /* xorshift32: один и тот же ряд на компьютере и в QEMU */
m->rng ^= m->rng >> 17;
m->rng ^= m->rng << 5;
return m->rng;
}
void model_init(model_t *m, uint32_t seed)
{
memset(m, 0, sizeof *m);
m->rng = seed ? seed : 1u;
/* значения из примеров описания протокола */
m->min_gate = 0; /* «Read module parameters (minimum distance)» → 00 00 00 00 */
m->max_gate = 12; /* → 0C 00 00 00 */
m->delay = 30; /* → 1E 00 00 00 */
static const uint32_t t0[6] = {60000, 30000, 3000, 2000, 500, 400}; /* чтение порогов 00…05 */
for (int g = 0; g < 16; g++) {
m->trig[g] = g < 6 ? t0[g] : 400u; /* модель: ворот 6…15 в примерах нет */
m->hold[g] = g == 0 ? 40000u : 200u; /* удержание 00 → 40 9C 00 00; остальное — модель */
m->noise[g] = g == 0 ? 400u : g == 1 ? 300u : 150u;
}
}
static void put(model_t *m, uint8_t b)
{
if (m->oh - m->ot < M_OUT)
m->out[m->oh++ % M_OUT] = b;
}
/* мусор между ответами: случайные байты, обрывки заголовка, «кадры» без хвоста и с огромной длиной */
static void junk(model_t *m)
{
uint32_t kind = model_rand(m) % 4u, n = 1u + model_rand(m) % 40u;
if (kind == 1) {
put(m, 0xFD), put(m, 0xFC), m->junk_bytes += 2;
if (model_rand(m) & 1u)
put(m, 0xFB), m->junk_bytes++;
} else if (kind >= 2) {
uint32_t len = kind == 2 ? 2u + model_rand(m) % 127u : 129u + model_rand(m) % 60000u;
for (int i = 0; i < 4; i++)
put(m, HDR[i]);
put(m, (uint8_t)len), put(m, (uint8_t)(len >> 8));
m->junk_bytes += 6;
n = model_rand(m) % 21u;
}
for (uint32_t i = 0; i < n; i++)
put(m, (uint8_t)model_rand(m)), m->junk_bytes++;
}
static void reply(model_t *m, uint16_t code, uint16_t status, const uint8_t *p, uint16_t n)
{
if (m->junk_pm && model_rand(m) % 1000u < m->junk_pm)
junk(m);
uint16_t len = (uint16_t)(4u + n);
for (int i = 0; i < 4; i++)
put(m, HDR[i]);
put(m, (uint8_t)len), put(m, (uint8_t)(len >> 8));
put(m, (uint8_t)code), put(m, (uint8_t)(code >> 8));
put(m, (uint8_t)status), put(m, (uint8_t)(status >> 8));
for (uint16_t i = 0; i < n; i++)
put(m, p[i]);
for (int i = 0; i < 4; i++)
put(m, TAIL[i]);
}
static uint32_t *param(model_t *m, uint16_t name)
{
if (name == 0x0000)
return &m->min_gate;
if (name == 0x0001)
return &m->max_gate;
if (name == 0x0004)
return &m->delay;
if (name >= 0x0010 && name <= 0x001F)
return &m->trig[name - 0x10];
if (name >= 0x0020 && name <= 0x002F)
return &m->hold[name - 0x20];
return 0;
}
static uint32_t limit(uint16_t name)
{
return name <= 0x0001 ? 15u : 65535u; /* ворота 0…15; задержка и пороги — 0…65535 */
}
static void command(model_t *m, const uint8_t *d, uint16_t len)
{
uint16_t code = (uint16_t)(d[0] | d[1] << 8), ack = (uint16_t)(code | 0x0100u);
uint8_t p[4 * 16];
m->cmds++;
if (code == 0x00FF) { /* открыть: ответ как в примере, 02 00 20 00 */
static const uint8_t r[4] = {0x02, 0x00, 0x20, 0x00};
m->cmd_mode = 1;
reply(m, ack, 0, r, 4);
} else if (code == 0x00FE) {
m->cmd_mode = 0;
reply(m, ack, 0, 0, 0);
} else if (code == 0x0000) {
static const uint8_t r[8] = {0x06, 0x00, 'v', '1', '.', '5', '.', '4'};
reply(m, ack, 0, r, 8);
} else if (code == 0x0068) { /* перезапуск: ответа в описании нет */
m->cmd_mode = 0;
m->ot1 = 0;
m->quiet = 0;
} else if (code == 0x0008) {
uint16_t n = (uint16_t)((len - 2u) / 2u);
int bad = !m->cmd_mode || (len - 2u) % 2u || n == 0 || n > 16; /* модель: вне режима — отказ */
for (uint16_t i = 0; !bad && i < n; i++) {
uint32_t *v = param(m, (uint16_t)(d[2 + 2 * i] | d[3 + 2 * i] << 8));
if (!v)
bad = 1;
else
p[4 * i] = (uint8_t)*v, p[4 * i + 1] = (uint8_t)(*v >> 8), p[4 * i + 2] = (uint8_t)(*v >> 16),
p[4 * i + 3] = (uint8_t)(*v >> 24);
}
if (bad)
m->rejected++, reply(m, ack, 1, 0, 0);
else
reply(m, ack, 0, p, (uint16_t)(4u * n));
} else if (code == 0x0007) {
uint16_t n = (uint16_t)((len - 2u) / 6u);
int bad = !m->cmd_mode || (len - 2u) % 6u || n == 0;
for (uint16_t i = 0; !bad && i < n; i++) { /* сначала проверить всё, потом записать */
const uint8_t *q = &d[2 + 6 * i];
uint16_t name = (uint16_t)(q[0] | q[1] << 8);
uint32_t v = q[2] | (uint32_t)q[3] << 8 | (uint32_t)q[4] << 16 | (uint32_t)q[5] << 24;
bad = !param(m, name) || v > limit(name);
}
for (uint16_t i = 0; !bad && i < n; i++) {
const uint8_t *q = &d[2 + 6 * i];
*param(m, (uint16_t)(q[0] | q[1] << 8)) = q[2] | (uint32_t)q[3] << 8 | (uint32_t)q[4] << 16 | (uint32_t)q[5] << 24;
}
if (bad)
m->rejected++;
reply(m, ack, bad ? 1u : 0u, 0, 0);
} else {
m->rejected++;
reply(m, ack, 1, 0, 0);
}
}
void model_rx(model_t *m, uint8_t b)
{
if (m->inn < 4) {
if (b != HDR[m->inn]) {
m->inn = 0;
if (b == HDR[0])
m->in[m->inn++] = b;
return;
}
m->in[m->inn++] = b;
return;
}
m->in[m->inn++] = b;
if (m->inn < 6)
return;
uint16_t len = (uint16_t)(m->in[4] | m->in[5] << 8);
if (len < 2 || len + 10u > sizeof m->in) {
m->ignored++;
m->inn = 0;
return;
}
if (m->inn < len + 10u)
return;
m->inn = 0;
if (memcmp(&m->in[6 + len], TAIL, 4)) {
m->ignored++;
return;
}
command(m, &m->in[6], len);
}
int model_tx(model_t *m, uint8_t *b)
{
if (m->ot == m->oh)
return 0;
*b = m->out[m->ot++ % M_OUT];
return 1;
}
/* 100 мс работы радара. Модель: шум ворот ±50 %, цель — в воротах cm/70 и по четверти в соседних.
* Нет человека → есть: энергия выше порога срабатывания хоть в одних воротах от min до max.
* Есть → нет: ни в одних не выше порога удержания delay секунд подряд. */
int model_tick(model_t *m, const m_target_t *t, int n)
{
for (int g = 0; g < 16; g++)
m->energy[g] = m->noise[g] * (50u + model_rand(m) % 101u) / 100u;
for (int i = 0; i < n; i++) {
int g = t[i].cm / 70;
if (g > 15)
continue;
m->energy[g] += t[i].amp * (80u + model_rand(m) % 41u) / 100u;
if (g > 0)
m->energy[g - 1] += t[i].amp / 4u;
if (g < 15)
m->energy[g + 1] += t[i].amp / 4u;
}
int trig = 0, hold = 0;
for (uint32_t g = m->min_gate; g <= m->max_gate && g < 16; g++) {
trig |= m->energy[g] > m->trig[g];
hold |= m->energy[g] > m->hold[g];
}
if (!m->ot1) {
if (trig)
m->ot1 = 1, m->quiet = 0;
} else if (hold) {
m->quiet = 0;
} else if (++m->quiet >= m->delay * (1000u / M_TICK_MS)) {
m->ot1 = 0;
}
return m->ot1;
}
test100a.c — прогон: 16 примеров описания, проверки разбора, сеанс настройки, опыты с мусором, пропажей и сбоями, сценарии светильника.
/* Раздел 100а: прогон драйвера HLK-LD2420 против модели. Печатает строки «ключ=значение».
* Часы — свои: один вызов idle() = 1 мс, за 1 мс по линии 256 000 бод проходит 25 байт.
* Тот же файл собирается gcc на компьютере и arm-none-eabi-gcc для QEMU — вывод должен совпасть. */
#include <stdio.h>
#include <string.h>
#include "ld2420.h"
#include "lamp.h"
#include "model100a.h"
#define BYTES_PER_MS 25u /* 256 000 бод, 10 бит на байт: 25,6 байта за 1 мс */
static model_t M;
static ld2420_ring_t RX;
static ld2420_t D;
static uint32_t now;
static uint32_t drop_ppm, flip_ppm, wire_rng = 7u, wire_drops, wire_flips;
static uint32_t wrand(void)
{
wire_rng ^= wire_rng << 13;
wire_rng ^= wire_rng >> 17;
wire_rng ^= wire_rng << 5;
return wire_rng;
}
/* линия: байт может пропасть или прийти с одним перевёрнутым битом */
static int wire(uint8_t *b)
{
if (drop_ppm && wrand() % 1000000u < drop_ppm)
return wire_drops++, 0;
if (flip_ppm && wrand() % 1000000u < flip_ppm)
*b ^= (uint8_t)(1u << (wrand() % 8u)), wire_flips++;
return 1;
}
static void tx(const uint8_t *p, uint16_t n)
{
for (uint16_t i = 0; i < n; i++) {
uint8_t b = p[i];
if (wire(&b))
model_rx(&M, b);
}
}
static uint32_t now_ms(void) { return now; }
static void idle(void)
{
uint8_t b;
now++;
for (unsigned i = 0; i < BYTES_PER_MS && model_tx(&M, &b); i++)
if (wire(&b))
ld2420_ring_put(&RX, b);
}
static void reset_all(uint32_t seed)
{
model_init(&M, seed);
memset(&RX, 0, sizeof RX);
ld2420_init(&D, &RX, tx, now_ms, idle);
drop_ppm = flip_ppm = wire_drops = wire_flips = 0;
wire_rng = seed; /* у каждого опыта свой ряд случайных чисел линии */
now = 0;
}
static int hex(const char *s, uint8_t *out)
{
int n = 0;
unsigned v;
while (sscanf(s, "%2x", &v) == 1) {
out[n++] = (uint8_t)v;
s += 2;
while (*s == ' ')
s++;
}
return n;
}
static void put_hex(const char *key, const uint8_t *p, int n)
{
printf("%s=", key);
for (int i = 0; i < n; i++)
printf("%s%02X", i ? " " : "", p[i]);
printf("\n");
}
/* ══ 1. Кадры из описания протокола ══ */
struct ex { const char *name; uint16_t cmd; const char *par, *send, *resp; };
static const struct ex EX[] = {
{"open", 0x00FF, "01 00", "FD FC FB FA 04 00 FF 00 01 00 04 03 02 01", "FD FC FB FA 08 00 FF 01 00 00 02 00 20 00 04 03 02 01"},
{"close", 0x00FE, "", "FD FC FB FA 02 00 FE 00 04 03 02 01", "FD FC FB FA 04 00 FE 01 00 00 04 03 02 01"},
{"version", 0x0000, "", "FD FC FB FA 02 00 00 00 04 03 02 01",
"FD FC FB FA 0C 00 00 01 00 00 06 00 76 31 2E 35 2E 34 04 03 02 01"},
{"reboot", 0x0068, "", "FD FC FB FA 02 00 68 00 04 03 02 01", ""},
{"read_min", 0x0008, "00 00", "FD FC FB FA 04 00 08 00 00 00 04 03 02 01",
"FD FC FB FA 08 00 08 01 00 00 00 00 00 00 04 03 02 01"},
{"set_min_0", 0x0007, "00 00 00 00 00 00", "FD FC FB FA 08 00 07 00 00 00 00 00 00 00 04 03 02 01",
"FD FC FB FA 04 00 07 01 00 00 04 03 02 01"},
{"read_max", 0x0008, "01 00", "FD FC FB FA 04 00 08 00 01 00 04 03 02 01",
"FD FC FB FA 08 00 08 01 00 00 0C 00 00 00 04 03 02 01"},
{"set_max_12", 0x0007, "01 00 0C 00 00 00", "FD FC FB FA 08 00 07 00 01 00 0C 00 00 00 04 03 02 01",
"FD FC FB FA 04 00 07 01 00 00 04 03 02 01"},
{"read_delay", 0x0008, "04 00", "FD FC FB FA 04 00 08 00 04 00 04 03 02 01",
"FD FC FB FA 08 00 08 01 00 00 1E 00 00 00 04 03 02 01"},
{"set_delay", 0x0007, "04 00 1A 00 00 00", "FD FC FB FA 08 00 07 00 04 00 1A 00 00 00 04 03 02 01",
"FD FC FB FA 04 00 07 01 00 00 04 03 02 01"},
{"read_hold00", 0x0008, "20 00", "FD FC FB FA 04 00 08 00 20 00 04 03 02 01",
"FD FC FB FA 08 00 08 01 00 00 40 9C 00 00 04 03 02 01"},
{"set_hold00", 0x0007, "20 00 50 C3 00 00", "FD FC FB FA 08 00 07 00 20 00 50 C3 00 00 04 03 02 01",
"FD FC FB FA 04 00 07 01 00 00 04 03 02 01"},
{"read_trig00", 0x0008, "10 00", "FD FC FB FA 04 00 08 00 10 00 04 03 02 01",
"FD FC FB FA 08 00 08 01 00 00 40 9C 00 00 04 03 02 01"},
{"set_trig00", 0x0007, "10 00 50 9C 00 00", "FD FC FB FA 08 00 07 00 10 00 50 9C 00 00 04 03 02 01",
"FD FC FB FA 04 00 07 01 00 00 04 03 02 01"},
{"read_trig00_05", 0x0008, "10 00 11 00 12 00 13 00 14 00 15 00",
"FD FC FB FA 0E 00 08 00 10 00 11 00 12 00 13 00 14 00 15 00 04 03 02 01",
"FD FC FB FA 1C 00 08 01 00 00 60 EA 00 00 30 75 00 00 B8 0B 00 00 D0 07 00 00 F4 01 00 00 90 01 00 00 04 03 02 01"},
{"set_trig00_05", 0x0007,
"10 00 60 EA 00 00 11 00 30 75 00 00 12 00 B8 0B 00 00 13 00 D0 07 00 00 14 00 F4 01 00 00 15 00 90 01 00 00",
"FD FC FB FA 24 00 07 00 10 00 60 EA 00 00 11 00 30 75 00 00 12 00 B8 0B 00 00 13 00 D0 07 00 00 14 00 F4 01 00 00 15 00 90 01 04 03 02 01",
"FD FC FB FA 04 00 07 01 00 00 04 03 02 01"},
};
#define NEX (int)(sizeof EX / sizeof EX[0])
static uint8_t last[LD2420_MAX_DATA];
static uint16_t last_len;
static int nframes;
static int strict; /* 1 — как драйвер: только ответы с известным кодом */
static int grab(void *c, const uint8_t *d, uint16_t n)
{
(void)c;
if (strict && (d[1] != 0x01 || n < 4))
return 0;
memcpy(last, d, n);
last_len = n;
nframes++;
return 1;
}
static void doc_frames(void)
{
int same = 0, model_same = 0;
uint8_t want[160], got[160], par[64], resp[160], mo[160];
for (int i = 0; i < NEX; i++) {
int np = hex(EX[i].par, par), nw = hex(EX[i].send, want);
int ng = ld2420_build(got, EX[i].cmd, par, (uint16_t)np);
int eq = ng == nw && !memcmp(got, want, (size_t)nw);
same += eq;
if (!eq) {
printf("doc_send_differs=%s doc_len=%d our_len=%d\n", EX[i].name, nw, ng);
put_hex("doc_send", want, nw);
put_hex("our_send", got, ng);
}
/* модель в режиме команд: тот же ли ответ, что в описании */
int nr = hex(EX[i].resp, resp), nm = 0;
model_init(&M, 1);
M.cmd_mode = 1;
for (int k = 0; k < ng; k++)
model_rx(&M, got[k]);
while (model_tx(&M, &mo[nm]))
nm++;
if (nr == nm && !memcmp(resp, mo, (size_t)nr))
model_same++;
else {
printf("model_differs=%s\n", EX[i].name);
put_hex("doc_resp", resp, nr);
put_hex("model_resp", mo, nm);
}
}
printf("doc_examples=%d doc_send_same=%d model_resp_same=%d\n", NEX, same, model_same);
/* ответы из описания — через разбор: что в них */
ld2420_parser_t p;
ld2420_parser_init(&p, grab, 0, 0);
for (int i = 0; i < NEX; i++) {
int nr = hex(EX[i].resp, resp);
if (!nr)
continue;
nframes = 0;
for (int k = 0; k < nr; k++)
ld2420_parse(&p, resp[k]);
uint16_t code = (uint16_t)(last[0] | last[1] << 8), st = (uint16_t)(last[2] | last[3] << 8);
printf("resp %s frames=%d len=%u code=0x%04X status=%u", EX[i].name, nframes, last_len, code, st);
if (EX[i].cmd == 0x0008)
for (int k = 4; k + 3 < last_len; k += 4)
printf(" %lu", (unsigned long)(last[k] | (uint32_t)last[k + 1] << 8 | (uint32_t)last[k + 2] << 16 |
(uint32_t)last[k + 3] << 24));
if (EX[i].cmd == 0x00FF)
printf(" a=%u b=%u", last[4] | last[5] << 8, last[6] | last[7] << 8);
if (EX[i].cmd == 0x0000)
printf(" n=%u s=%.*s", last[4] | last[5] << 8, last[4], (const char *)&last[6]);
printf("\n");
}
printf("doc_parse other=%lu bad_len=%lu bad_tail=%lu\n", (unsigned long)p.other, (unsigned long)p.bad_len,
(unsigned long)p.bad_tail);
}
/* ══ 2. Разбор: обрывки, лишние байты, ложные заголовки ══ */
static void feed(ld2420_parser_t *p, const char *s)
{
uint8_t b[300];
int n = hex(s, b);
for (int i = 0; i < n; i++)
ld2420_parse(p, b[i]);
}
#define GOOD "FD FC FB FA 08 00 08 01 00 00 0C 00 00 00 04 03 02 01"
static void unit(const char *name, const char *a, const char *b, int flush)
{
ld2420_parser_t p;
ld2420_parser_init(&p, grab, 0, 0);
nframes = 0;
last_len = 0;
feed(&p, a);
if (b)
feed(&p, b);
int before = nframes;
if (flush)
ld2420_parser_flush(&p);
printf("unit %s frames=%d", name, nframes);
if (flush)
printf(" before_flush=%d", before);
printf(" value=%u other=%lu bad_len=%lu bad_tail=%lu rejected=%lu\n", nframes ? last[last_len - 4] : 0u,
(unsigned long)p.other, (unsigned long)p.bad_len, (unsigned long)p.bad_tail, (unsigned long)p.rejected);
}
static void units(void)
{
unit("whole", GOOD, 0, 0);
unit("garbage_before", "00 11 FD 22 FD FC 33", GOOD, 0);
unit("double_fd", "FD", GOOD, 0);
unit("bad_len", "FD FC FB FA FF 7F", GOOD, 0);
unit("short_len", "FD FC FB FA 01 00", GOOD, 0);
unit("fake_header", "FD FC FB FA 0A 00 99", GOOD, 0);
unit("cut_frame", "FD FC FB FA 08 00 08 01 00", GOOD, 0);
unit("bad_tail", "FD FC FB FA 08 00 08 01 00 00 0C 00 00 00 04 03 02 00", 0, 0);
unit("long_fake_then_quiet", "FD FC FB FA 64 00", GOOD, 1);
/* ложный заголовок с длиной 18: его «хвост» — это хвост настоящего ответа */
unit("tail_alias_any", "FD FC FB FA 12 00 B1 65 EC B2", GOOD, 0);
strict = 1;
unit("tail_alias_strict", "FD FC FB FA 12 00 B1 65 EC B2", GOOD, 0);
strict = 0;
ld2420_ring_t r;
uint8_t b;
int got = 0;
memset(&r, 0, sizeof r);
for (int i = 0; i < 300; i++)
ld2420_ring_put(&r, (uint8_t)i);
while (ld2420_ring_get(&r, &b))
got++;
printf("ring size=%u put=300 got=%d lost=%lu\n", LD2420_RING, got, (unsigned long)r.lost);
}
/* ══ 3. Сеанс настройки на чистой линии ══ */
static const uint32_t NOISE[16] = {400, 300, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150};
static void make_cfg(ld2420_cfg_t *c, uint8_t max_gate, uint32_t kt, uint32_t kh)
{
c->min_gate = 0;
c->max_gate = max_gate;
c->delay = 5;
ld2420_thr_from_noise(NOISE, c->trig, c->hold, kt, kh);
}
static void session(void)
{
uint16_t a = 0, b = 0;
char ver[16];
uint32_t v[16];
uint16_t names[16];
reset_all(1);
int r = ld2420_open(&D, &a, &b);
printf("session open=%d a=%u b=%u\n", r, a, b);
r = ld2420_version(&D, ver, sizeof ver);
printf("session version=%d s=%s\n", r, ver);
names[0] = LD2420_P_MIN_GATE, names[1] = LD2420_P_MAX_GATE, names[2] = LD2420_P_DELAY;
r = ld2420_read(&D, names, 3, v);
printf("session read_general=%d min=%lu max=%lu delay=%lu\n", r, (unsigned long)v[0], (unsigned long)v[1],
(unsigned long)v[2]);
for (int g = 0; g < 16; g++)
names[g] = (uint16_t)LD2420_P_TRIG(g);
r = ld2420_read(&D, names, 16, v);
printf("session read_trig16=%d ack_len=%u t0=%lu t5=%lu t15=%lu\n", r, D.ack_len, (unsigned long)v[0],
(unsigned long)v[5], (unsigned long)v[15]);
r = ld2420_close(&D);
printf("session close=%d\n", r);
names[0] = LD2420_P_MIN_GATE;
r = ld2420_read(&D, names, 1, v);
printf("session read_closed=%d status=%u\n", r, D.status);
ld2420_cfg_t c;
make_cfg(&c, 8, 6, 3);
uint32_t t0 = now, cmds0 = M.cmds;
r = ld2420_configure(&D, &c);
printf("configure=%d cmds=%lu ms=%lu trig0=%lu trig2=%lu hold2=%lu model_max=%lu model_delay=%lu cmd_mode=%d\n", r,
(unsigned long)(M.cmds - cmds0), (unsigned long)(now - t0), (unsigned long)M.trig[0],
(unsigned long)M.trig[2], (unsigned long)M.hold[2], (unsigned long)M.max_gate, (unsigned long)M.delay,
M.cmd_mode);
c.max_gate = 16;
r = ld2420_configure(&D, &c);
printf("configure_bad_gate=%d\n", r);
/* модуль молчит: три попытки по 100 мс */
reset_all(1);
M.cmd_mode = 1;
drop_ppm = 1000000u;
t0 = now;
r = ld2420_close(&D);
printf("silent rc=%d timeouts=%lu retries=%lu ms=%lu\n", r, (unsigned long)D.timeouts, (unsigned long)D.retries,
(unsigned long)(now - t0));
}
/* ══ 4. Поток с мусором, потерями и сбоями битов ══ */
static uint32_t *mparam(uint16_t name)
{
if (name == 0)
return &M.min_gate;
if (name == 1)
return &M.max_gate;
if (name == 4)
return &M.delay;
return name < 0x20 ? &M.trig[name - 0x10] : &M.hold[name - 0x20];
}
static uint16_t rand_name(void)
{
uint32_t k = wrand() % 35u;
return (uint16_t)(k == 0 ? 0 : k == 1 ? 1 : k == 2 ? 4 : k < 19 ? 0x10 + (k - 3) : 0x20 + (k - 19));
}
/* «Принимать всё»: обёртка над обратным вызовом драйвера, которая не даёт отвергнуть кадр с чужим кодом —
* так разбор вёл себя без проверки is_ack */
static int (*drv_frame)(void *, const uint8_t *, uint16_t);
static int lax_frame(void *c, const uint8_t *d, uint16_t n)
{
drv_frame(c, d, n);
return 1;
}
static void fuzz(const char *name, uint32_t junk_pm, uint32_t drop, uint32_t flip, int ops, int lax)
{
uint16_t a, b, names[16];
uint32_t vals[16], got[16];
int ok = 0, e_to = 0, e_st = 0, e_sh = 0, wrong_read = 0, wrong_write = 0;
reset_all(12345);
if (lax) {
drv_frame = D.p.on_frame;
D.p.on_frame = lax_frame;
}
ld2420_open(&D, &a, &b);
M.junk_pm = junk_pm;
drop_ppm = drop;
flip_ppm = flip;
for (int i = 0; i < ops; i++) {
M.cmd_mode = 1; /* сбой мог «закрыть» режим чужой командой — тест не про это */
int n = 1 + (int)(wrand() % 16u), r;
for (int k = 0; k < n; k++)
names[k] = rand_name();
if (wrand() & 1u) {
r = ld2420_read(&D, names, (uint16_t)n, got);
if (r == 0)
for (int k = 0; k < n; k++)
wrong_read += got[k] != *mparam(names[k]);
} else {
n = 1 + n % 6;
for (int k = 0; k < n; k++) {
for (int j = 0; j < k; j++)
if (names[j] == names[k])
names[k] = rand_name(), j = -1; /* в одной записи имена без повторов */
vals[k] = names[k] <= 1 ? wrand() % 16u : wrand() % 65536u;
}
r = ld2420_write(&D, names, vals, (uint16_t)n);
if (r == 0)
for (int k = 0; k < n; k++)
wrong_write += vals[k] != *mparam(names[k]);
}
ok += r == 0;
e_to += r == LD2420_ETIMEOUT;
e_st += r == LD2420_ESTATUS;
e_sh += r == LD2420_ESHORT;
}
printf("fuzz %s ops=%d ok=%d timeout=%d status=%d short=%d wrong_read=%d wrong_write=%d\n", name, ops, ok, e_to,
e_st, e_sh, wrong_read, wrong_write);
printf("fuzz %s retries=%lu timeouts=%lu stray=%lu frames=%lu bad_len=%lu bad_tail=%lu rejected=%lu flushed=%lu "
"other=%lu junk=%lu drops=%lu flips=%lu ms=%lu lost=%lu\n",
name, (unsigned long)D.retries, (unsigned long)D.timeouts, (unsigned long)D.stray,
(unsigned long)D.p.frames, (unsigned long)D.p.bad_len, (unsigned long)D.p.bad_tail, (unsigned long)D.p.rejected,
(unsigned long)D.p.flushed, (unsigned long)D.p.other, (unsigned long)M.junk_bytes,
(unsigned long)wire_drops, (unsigned long)wire_flips, (unsigned long)now, (unsigned long)RX.lost);
}
/* настройка целиком на линии со сбоями битов: обратное чтение ловит испорченное */
static void cfg_flips(int runs)
{
int ok = 0, verify = 0, status = 0, tmo = 0, bad_after_ok = 0;
reset_all(777);
flip_ppm = 1000;
for (int i = 0; i < runs; i++) {
ld2420_cfg_t c;
uint8_t gmax = (uint8_t)(4 + wrand() % 12u); /* по одному вызову в строке: порядок вычисления */
uint32_t kt = 5 + wrand() % 4u; /* аргументов у gcc для x86 и для ARM разный */
uint32_t kh = 2 + wrand() % 4u;
make_cfg(&c, gmax, kt, kh);
int r = ld2420_configure(&D, &c);
ok += r == 0;
verify += r == LD2420_EVERIFY;
status += r == LD2420_ESTATUS;
tmo += r == LD2420_ETIMEOUT;
if (r == 0) {
int bad = M.min_gate != c.min_gate || M.max_gate != c.max_gate || M.delay != c.delay;
for (int g = 0; g < 16; g++)
bad |= M.trig[g] != c.trig[g] || M.hold[g] != c.hold[g];
bad_after_ok += bad;
}
}
printf("cfg_flips runs=%d ok=%d verify_fail=%d status_fail=%d timeout=%d bad_after_ok=%d flips=%lu retries=%lu "
"cmds=%lu\n", runs, ok, verify, status, tmo, bad_after_ok, (unsigned long)wire_flips, (unsigned long)D.retries,
(unsigned long)M.cmds);
}
/* ══ 5. Светильник ══
* Комната 300 с. Вентилятор в 7,5 м качается всё время. Человек входит на 30-й секунде (идёт от 6 м
* к 2 м 10 с), сидит неподвижно до 160-й, уходит к 6 м за 5 с и исчезает на 165-й. */
#define AMP_WALK 6000u
#define AMP_BREATH 500u
#define AMP_FAN 2000u
static int scene(uint32_t t, m_target_t *o)
{
int n = 0;
o[n].cm = 750, o[n++].amp = AMP_FAN;
if (t >= 30000 && t < 40000)
o[n].cm = (uint16_t)(600 - (t - 30000) * 400 / 10000), o[n++].amp = AMP_WALK;
else if (t >= 40000 && t < 160000)
o[n].cm = 200, o[n++].amp = AMP_BREATH;
else if (t >= 160000 && t < 165000)
o[n].cm = (uint16_t)(200 + (t - 160000) * 400 / 5000), o[n++].amp = AMP_WALK;
return n;
}
static void lamp_run(const char *name, uint8_t max_gate, uint32_t kt, uint32_t kh)
{
ld2420_cfg_t c;
lamp_t l;
m_target_t tg[4];
reset_all(4242);
make_cfg(&c, max_gate, kt, kh);
int r = ld2420_configure(&D, &c);
lamp_init(&l, 10000);
uint32_t t0 = now, on_present = 0, on_absent = 0, ot1_on = 0, prev = 0;
printf("lamp %s configure=%d max_gate=%u trig=%lu hold=%lu\n", name, r, max_gate, (unsigned long)c.trig[5],
(unsigned long)c.hold[5]);
for (uint32_t t = 0; t < 300000; t += M_TICK_MS) {
int n = scene(t, tg);
int ot1 = model_tick(&M, tg, n);
int on = lamp_step(&l, t0 + t, ot1);
int present = t >= 30000 && t < 165000;
ot1_on += (uint32_t)ot1;
if (on)
present ? on_present++ : on_absent++;
if ((uint32_t)on != prev)
printf("lamp %s t=%lu on=%d\n", name, (unsigned long)t, on);
prev = (uint32_t)on;
}
printf("lamp %s switches=%lu on_present_s=%lu on_absent_s=%lu ot1_s=%lu present_s=135\n", name,
(unsigned long)l.switches, (unsigned long)(on_present / 10), (unsigned long)(on_absent / 10),
(unsigned long)(ot1_on / 10));
}
int main(void)
{
printf("== doc\n");
doc_frames();
printf("== unit\n");
units();
printf("== session\n");
session();
printf("== fuzz\n");
fuzz("clean", 0, 0, 0, 2000, 0);
fuzz("junk", 300, 0, 0, 2000, 0);
fuzz("junk_lax", 300, 0, 0, 2000, 1);
fuzz("drops", 300, 2000, 0, 2000, 0);
fuzz("flips", 0, 0, 1000, 2000, 0);
cfg_flips(300);
printf("== lamp\n");
lamp_run("gates0-8", 8, 6, 3);
lamp_run("gates0-12", 12, 6, 3);
lamp_run("no_hysteresis", 8, 6, 6);
printf("done=1\n");
return 0;
}
qemu100a.c — обвязка прогона для QEMU: запуск, вывод через полухостинг, выход.
/* Раздел 100а: тот же прогон test100a.c на Cortex-M4 в QEMU netduinoplus2 (STM32F405).
printf уходит через полухостинг (SYS_WRITE0), в конце — SYS_EXIT. */
#include <stdint.h>
#include <string.h>
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);
static int semi(int op, const void *arg)
{
register int r0 __asm__("r0") = op;
register const void *r1 __asm__("r1") = arg;
__asm__ volatile("bkpt 0xAB" : "+r"(r0) : "r"(r1) : "memory");
return r0;
}
int _write(int fd, const char *p, int n)
{
char buf[129];
(void)fd;
for (int done = 0; done < n;) {
int k = n - done > 128 ? 128 : n - done;
memcpy(buf, p + done, (size_t)k);
buf[k] = 0;
semi(0x04, buf);
done += k;
}
return n;
}
void Reset_Handler(void)
{
uint32_t *s = &_sidata, *d = &_sdata;
while (d < &_edata)
*d++ = *s++;
for (d = &_sbss; d < &_ebss;)
*d++ = 0;
main();
static const uint32_t ex[2] = {0x20026u, 0}; /* ADP_Stopped_ApplicationExit */
semi(0x18, ex);
for (;;) {
}
}
static void Default_Handler(void)
{
for (;;) {
}
}
__attribute__((section(".isr_vector"), used)) void (*const vectors[16])(void) = {
(void (*)(void))&_estack, Reset_Handler, Default_Handler, Default_Handler,
Default_Handler, Default_Handler, Default_Handler,
};
run100a.sh — сборка и прогон в контейнерах: host — gcc, arm — прошивки F103 и F407, размеры, стек и прогон в QEMU.
#!/bin/sh
# Раздел 100а: прогон. /v — этот каталог (только чтение).
# sh /v/run100a.sh host в stm-sys:1 — драйвер против модели (gcc)
# sh /v/run100a.sh arm в stm-arm:1 — прошивки F103 и F407 (-Os, размеры) и тот же прогон в QEMU
# заголовки: /f1 — CMSIS F1 (stm32f103xb.h), /f4 — CMSIS F4, /ci — ядро CMSIS
set -u
export LC_ALL=C.UTF-8
B=$(mktemp -d)
cd $B
SRC="/v/ld2420.c /v/lamp.c"
case "$1" in
host)
echo "== gcc $(gcc -dumpfullversion)"
gcc -std=c11 -O2 -Wall -Wextra -Werror -o t100a /v/test100a.c /v/model100a.c $SRC 2>&1
echo "build_host=$?"
./t100a
echo "run_host=$?"
;;
arm)
echo "== $(arm-none-eabi-gcc --version | head -1)"
W="-mthumb -Os -std=c11 -Wall -Wextra -Werror -ffunction-sections -fdata-sections -I/v -I/ci"
for chip in F103 F407; do
if [ $chip = F103 ]; then CPU="-mcpu=cortex-m3"; DEF="-DSTM32F103xB -I/f1"; MEM="-Wl,--defsym=FLASH_LEN=64K -Wl,--defsym=RAM_LEN=20K"
else CPU="-mcpu=cortex-m4"; DEF="-DSTM32F407xx -I/f4"; MEM="-Wl,--defsym=FLASH_LEN=1024K -Wl,--defsym=RAM_LEN=128K"; fi
for f in ld2420 lamp fw100a; do
arm-none-eabi-gcc $CPU $W -DCHIP_$chip $DEF -fstack-usage -c /v/$f.c -o ${f}_$chip.o 2>&1
echo "build_${f}_$chip=$?"
done
sed "s/^/su $chip /" ld2420_$chip.su
arm-none-eabi-gcc $CPU -mthumb -nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections \
-T /v/fw100a.ld $MEM -o fw_$chip.elf ld2420_$chip.o lamp_$chip.o fw100a_$chip.o 2>&1
echo "build_fw_$chip=$?"
echo "-- size $chip"
arm-none-eabi-size ld2420_$chip.o lamp_$chip.o fw100a_$chip.o fw_$chip.elf
arm-none-eabi-nm -S -t d ld2420_$chip.o | awk '$3 ~ /^[Tt]$/ {print "fn", $4, $2 + 0}' | sort
arm-none-eabi-nm -S -t d fw_$chip.elf | awk '$4 == "rx" || $4 == "radar" || $4 == "lamp" {print "obj", $4, $2 + 0}' | sort
done
echo "== QEMU netduinoplus2"
qemu-system-arm --version | head -1
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O2 -std=c11 -Wall -Wextra -Werror -I/v -nostartfiles --specs=nosys.specs \
-T /v/fw100a.ld -Wl,--defsym=FLASH_LEN=1024K -Wl,--defsym=RAM_LEN=128K -o q100a.elf \
/v/test100a.c /v/model100a.c /v/qemu100a.c $SRC 2>&1
echo "build_qemu=$?"
timeout 300 qemu-system-arm -M netduinoplus2 -nographic -semihosting -kernel q100a.elf -monitor none -serial none 2>&1 \
| tr -d '\r' > q.txt
echo "qemu_exit=$?"
sed 's/^/q| /' q.txt
;;
esac
cd /
rm -rf $B
Дальше: раздел 100б «Лазерный дальномер VL53L0X: время пролёта по I2C, режимы точности и скорости, несколько датчиков на одной шине» — свет вместо радиоволн: миллиметры по I2C, бюджет времени против шума и три датчика на одной шине.