16в. Bluetooth-модуль HC-05/HC-06: прозрачный UART, AT-режим, сопряжение и протокол поверх канала
В 16, 16а и 16б UART шёл по проводу: кадр, скорость, RS-485, приём через DMA в кольцо. Провод тянуть не всегда удобно: прибор стоит в шкафу, а настраивать его хочется с телефона. Самый дешёвый способ убрать провод — модуль HC-05 или HC-06. Для микроконтроллера это всё тот же UART, для телефона и компьютера — последовательный порт по Bluetooth. Просто на вид, но радио — не провод: байты приходят пачками и с задержкой, связь рвётся, и при обрыве пропадает всё, что было в пути.
Что внутри: чем HC-05 отличается от HC-06, как подключить модуль к STM32F103 и зачем выводы STATE и KEY; что такое прозрачный режим и как ведут себя байты по воздуху; AT-режим и настройка модуля без ожиданий в цикле; сопряжение с телефоном и со вторым HC-05; кадр с номером, длиной и CRC-16, подтверждение и повтор поверх канала; признак связи по STATE, переподключение и потребление — почему HC-05 не для батарейки и чем здесь лучше BLE.
Опоры: DocID13587 Rev 17 (STM32F103x8/xB): PA9, PA10 — USART1_TX,
USART1_RX с допуском 5 В, с. 31; порог высокого уровня входа
0,65·VDD, таблица 35, с. 62. Кадр COBS с CRC-16/XMODEM — из 194.4,
ведущий ноль — из 189а.2, разбор AT-ответов без блокировки — из 106г.3.
Документов на HC-05 и HC-06 и спецификации Bluetooth в проекте нет: набор
AT-команд, вид ответов, значения по умолчанию (имя, PIN, скорость),
скорость AT-режима и смысл выводов STATE и KEY взяты по памяти о
документации производителя и помечены «не проверено». Всё, что в разделе
названо прогоном, — код _verify/bt16d/hc05.c и _verify/bt16d/btl16d.c
на компьютере (gcc 12.2.0) с моделью модуля, эфира и телефона на Python
3.11; прошивка собрана armclang 6.24 под Cortex-M3.
Как это называется в документации
Оригинал Перевод Bluetooth Classic (BR/EDR) классический Bluetooth Serial Port Profile (SPP), RFCOMM профиль «последовательный порт» и его протокол Bluetooth Low Energy (BLE) Bluetooth с низким потреблением pairing, PIN (passkey) сопряжение, код сопряжения master / slave, role ведущий / ведомый, роль bind address адрес, к которому подключаться transparent (data) mode прозрачный режим: байты как есть AT command mode режим AT-команд link supervision timeout срок, после которого связь считается потерянной COBS (Consistent Overhead Byte Stuffing) замена нулей в кадре с постоянной добавкой ARQ, stop-and-wait повтор по сроку; «отправил — жди подтверждения»
16в.1 Модуль: HC-05 и HC-06, питание, уровни, STATE и KEY
Два модуля. Оба — классический Bluetooth с профилем SPP: телефон или
компьютер видят их как последовательный порт. HC-05 бывает и ведомым (к
нему подключаются), и ведущим (подключается сам к другому модулю) — роль
задаёт AT+ROLE. HC-06 — только ведомый, и AT-команды у него проще и
другого вида. Это по памяти о документации производителя; документа в
проекте нет, не проверено.
Питание и уровни. Сам модуль живёт на 3,3 В, и его UART — тоже 3,3 В. Чаще всего его продают на плате-переходнике со стабилизатором и шестью выводами: STATE, RXD, TXD, GND, VCC, EN (он же KEY); плату питают от 5 В (по памяти, диапазон — по документу на плату, не проверено). С STM32F103 линии соединяются напрямую: выход PA9 на 3,3 В — во вход RXD модуля, выход TXD модуля — в PA10. У PA10 допуск 5 В (DocID13587, с. 31), а «единицу» он гарантированно видит от 0,65·VDD (таблица 35, с. 62) — при 3,3 В это 2,145 В; уровень выхода модуля надо сравнить с этим порогом по его документу. Опасна обратная сторона — плата на 5 В: её выход TX в RXD модуля без делителя (16.7).
STATE и KEY. На распространённых платах вывод STATE в единице, пока есть связь с телефоном, — это признак, по которому прошивка узнаёт об обрыве (16в.6). KEY в единице в момент включения переводит HC-05 в AT-режим на 38 400 бод, иначе модуль включается в прозрачном режиме на своей рабочей скорости. Оба утверждения — по памяти, не проверено. Отсюда схема: питание модуля — через ключ на PB0, чтобы прошивка сама перезапускала модуль в нужном режиме; KEY — на PB1; STATE — на вход PB5 с подтяжкой к земле, чтобы оторванный провод читался как «связи нет».
16в.2 Прозрачный режим: UART по воздуху
В прозрачном режиме модуль ничего не знает о сообщениях: всё, что
прошивка выдала в TX, появляется в порту на телефоне, и наоборот. Границ
нет: где кончилась одна посылка и началась другая, модуль не передаёт.
Скорость UART модуля — та, что записана командой AT+UART; у F103 на
72 МГц BRR для 115 200 бод — 625, для 38 400 — 1875, для 9600 — 7500,
все три делятся без остатка. Байт на 115 200 бод идёт 86,8 мкс.
Внутри модуль копит байты из UART и отправляет их в эфир пачками, а радиоканал добавляет свою задержку. Как именно копит настоящий модуль — не проверено; модель стенда отправляет пачку через 5 мс после первого байта или по 128 байтам, и пачка идёт по эфиру 8…30 мс. Числа ниже — следствие этих параметров, а не замер модуля; важна их форма:
| Опыт в модели | Результат |
|---|---|
| один байт до телефона, 50 раз | от 13,2 до 34,4 мс, в середине 22,8 мс |
| блок 200 байт одной записью | у телефона 4 пачки: 58, 58, 58 и 26 байт |
| первая и последняя пачка | через 30,8 и 34,9 мс после записи |
| эхо того же блока обратно | у прибора через 72 мс, 18 кусками по опросам |
Отсюда правило, то же, что в 189.2 для BLE: сообщение должно переживать разрезание и склейку в любом месте. Блок, записанный разом, приходит кусками; два коротких сообщения подряд могут прийти одной пачкой. Читать «по одному сообщению за чтение» нельзя.
Сам радиоканал внутри установленной связи искажённых байтов, как правило, не отдаёт: у Bluetooth свои проверки и повторы в эфире (по памяти о спецификации, не проверено). Байты теряются в другом месте: при обрыве — всё, что лежало в буфере модуля и летело по эфиру, и на проводах UART, если они длинные и рядом шумит силовая часть.
16в.3 AT-режим: вход по KEY и настройка без ожиданий
Команда — строка AT…, оконченная парой CR LF; ответ — строка с данными
и OK или ошибка вида ERROR:(0). Набор и вид команд — по памяти о
документации производителя на прошивку 2.0, не проверено; у других
прошивок вид отличается (например, PIN — в кавычках):
| Команда | Ответ модели | Что делает |
|---|---|---|
AT |
OK |
модуль жив и понял скорость |
AT+VERSION? |
+VERSION:2.0-20100601 |
версия прошивки |
AT+NAME?, AT+NAME=имя |
+NAME:HC-05 |
имя, которое видит телефон |
AT+PSWD?, AT+PSWD=4321 |
+PSWD:1234 |
код сопряжения |
AT+UART?, AT+UART=115200,0,0 |
+UART:9600,0,0 |
скорость, стоп-биты, чётность прозрачного режима |
AT+ROLE?, AT+ROLE=1 |
+ROLE:0 |
0 — ведомый, 1 — ведущий |
AT+CMODE=0, AT+BIND=… |
OK |
ведущему: подключаться только к адресу из BIND |
AT+ADDR? |
+ADDR:… |
свой адрес — его вписывают в BIND ведущего |
Значения по умолчанию в модели — имя HC-05, PIN 1234, 9600,0,0, роль 0; так их обычно называют, но документ не проверен.
Разбор ответов устроен так же, как в 106г.3: кольцо, сборка строки до
LF, одна команда за раз со своим сроком, — и здесь не повторяется. Он
проще: незапрошенных сообщений в AT-режиме нет, и итог у каждой команды
один — OK или ERROR. Главное другое: сначала спросить, потом
писать. Для каждой настройки драйвер задаёт запрос, при расхождении
пишет и спрашивает снова:
while (h->step < 6) {
uint8_t i = (uint8_t)(h->step - 2u);
int same = r == HC_RES_OK && !strcmp(h->got, h->want[i]);
if (h->phase == 0) { /* запрос */
h->phase = 1;
at_cmd(h, Q[i], 1000);
return;
}
if (h->phase == 1 && !same) { /* отличается — записать */
char c[HC_LINE];
if (r != HC_RES_OK) {
fail(h);
return;
}
cat(cat(c, S[i], c + HC_LINE), h->want[i], c + HC_LINE);
h->phase = 2;
h->writes++;
at_cmd(h, c, 1000);
return;
}
Так модуль не переписывается при каждом включении, а запись, которая не прошла, видна сразу: повторный запрос вернёт старое значение, и драйвер остановится в состоянии FAIL вместо работы на чужой скорости.
Прогон: модуль «с завода». Драйвер снимает питание на 200 мс, ставит
KEY = 1, включает модуль и шлёт AT раз в 0,3 с до 10 раз. Модель
загружается 500 мс, поэтому первые два AT ушли без ответа, и OK
пришёл через 612 мс после включения. Дальше — 14 команд, из них 3 записи
(имя BENCH-16, PIN 4321, скорость 115 200); роль уже 0, её только
спросили. Затем питание снято ещё на 200 мс, KEY = 0, модуль включён в
прозрачном режиме на 115 200 бод — на 1192 мс от старта. Повторный
запуск с теми же настройками: 8 команд, ни одной записи, 1117 мс.
16в.4 Сопряжение: телефон и второй HC-05
Здесь всё качественно — в модели сопряжение сведено к одному событию «телефон подключился», а настоящий модуль не проверялся.
Телефон на Android. В настройках Bluetooth модуль виден под своим
именем (BENCH-16); сопряжение — с кодом из AT+PSWD. Сопряжение ещё не
связь: канал открывает приложение, которое умеет SPP, и только тогда
STATE поднимается. В модели после подключения драйвер увидел связь через
21 мс — 20 мс фильтра STATE и шаг опроса.
Айфон. Насколько известно, приложениям iOS классический SPP с произвольными устройствами недоступен, и HC-05 с айфоном не работает — нужен BLE (16в.6). Не проверено.
Компьютер. После сопряжения Windows заводит виртуальные порты «Standard Serial over Bluetooth link» — на машине, где писался 115б.2, их нашлось 7 и ни одного кабеля. Порт ищут по имени устройства, а не берут первый попавшийся.
Два HC-05 друг с другом. Один остаётся ведомым как есть. Второму в
AT-режиме: AT+ROLE=1, AT+CMODE=0, тот же PIN и та же скорость, и
AT+BIND= с адресом первого из его AT+ADDR?, где двоеточия заменены
запятыми: 1234:56:abcdef → AT+BIND=1234,56,abcdef (адрес условный).
После включения ведущий сам ищет и подключается к своему ведомому и сам
переподключается после обрыва. Всё по памяти, не проверено.
16в.5 Протокол поверх канала: кадр, подтверждение, повтор
Прозрачный канал даёт поток байтов без границ, а при обрыве теряет хвост. Поверх него нужен свой протокол, и почти весь он уже есть в книге: кадр из 194.4 — данные, CRC-16/XMODEM старшим байтом вперёд, всё в COBS, в конце ноль; ведущий ноль перед кадром — из 189а.2: обрывок прежнего кадра, застрявший в приёмнике после обрыва, закончится на нём и будет выброшен. Внутри кадра здесь три байта: тип (данные или подтверждение), номер и длина — длина ещё раз сверяется с тем, что получилось после COBS.
uint16_t btl_frame(uint8_t type, uint8_t seq, const uint8_t *p, uint8_t n, uint8_t *out)
{
uint8_t raw[BTL_RAW];
raw[0] = type;
raw[1] = seq;
raw[2] = n;
if (n)
memcpy(raw + 3, p, n);
uint16_t c = btl_crc16(raw, (uint16_t)(n + 3u));
raw[n + 3] = (uint8_t)(c >> 8);
raw[n + 4] = (uint8_t)c;
out[0] = 0; /* ведущий ноль: обрывок прежнего кадра закончится здесь */
uint16_t m = (uint16_t)(1u + cobs_enc(raw, (uint16_t)(n + 5u), out + 1));
out[m++] = 0;
return m;
}
Прогон сверяет Си с независимой реализацией на Python (binascii.crc_hqx
и свой COBS): CRC строки 123456789 — 0x31C3, как в 194.4; кадры
совпали байт в байт:
| Данные | Кадр, байт | Добавка | На 115 200 бод |
|---|---|---|---|
MSG 0001, номер 1 |
16: 00 0E 01 01 08 4D 53 47 20 30 30 30 31 6C 1C 00 |
8 | 1389 мкс |
| 8 нулевых байтов | 16: 00 04 01 01 08 01 01 01 01 01 01 01 03 9E F3 00 |
8 | 1389 мкс |
| 64 байта | 72 | 8 | 6250 мкс |
Добавка — 8 байт на кадр при любых данных до 64 байт: два нуля, байт COBS, тип, номер, длина и CRC.
Подтверждение и повтор. Отправитель держит один кадр, пока не придёт подтверждение с его номером, и повторяет его раз в 250 мс. Получатель подтверждает каждый целый кадр — и повтор тоже: значит, пропало прежнее подтверждение. Повтор узнаётся по номеру — он равен номеру прошлого принятого кадра — и выбрасывается, так что данные приложению отдаются ровно один раз:
static void on_frame(hc_t *h, const uint8_t *f, uint16_t n)
{
btl_t *l = &h->l;
uint8_t type = f[0], seq = f[1];
if (type == BTL_T_ACK) {
if (l->tx_busy && seq == l->tx_seq) { /* подтверждён кадр в пути */
l->tx_busy = 0;
l->tx_seq++;
l->n_ack++;
if (h->on_event)
h->on_event(h, HC_EV_ACKED, l->tx, l->tx_len);
}
return; /* запоздалое подтверждение повтора — не наше */
}
if (type != BTL_T_DATA)
return;
tx_frame(BTL_T_ACK, seq, 0, 0); /* подтвердить и повтор: прежнее подтверждение могло пропасть */
if (l->rx_sync && seq == (uint8_t)(l->rx_next - 1u)) {
l->n_dup++; /* тот же кадр ещё раз — данные уже отданы */
return;
}
l->rx_sync = 1;
l->rx_next = (uint8_t)(seq + 1u);
l->n_rx++;
if (h->on_event)
h->on_event(h, HC_EV_RX, f + 3, (uint16_t)(n - 5u));
}
Первый кадр после запуска принимается с любым номером (rx_sync = 0), и
любой номер, кроме номера прошлого кадра, тоже: перезапуск прошивки или
приложения на телефоне не ломает обмен.
Опыт. Прибор шлёт 1000 сообщений MSG 0001…MSG 1000, телефон
возвращает их эхом. Связь рвётся каждые 5…9 с на 2…4 с; эфир умирает
сразу, STATE падает через 1 с; на проводах UART байт искажается с
вероятностью 0,0003 и теряется с вероятностью 0,0001 — всё это параметры
модели. Без протокола прибор шлёт строку раз в 50 мс, телефон возвращает
байты как пришли; с протоколом прибор шлёт следующее сообщение после
подтверждения прошлого, телефон — эхо своим кадром.
| без протокола | с протоколом | |
|---|---|---|
| сообщений | 1000 | 1000 |
| обрывов связи за опыт | 5 | 6 |
| вернулось целыми, по одному разу, по порядку | 706 | 1000 |
| потеряно | 294 | 0 |
| испорченных строк | 3 | — |
| повторов кадра: прибор / телефон | — | 46 / 33 |
| повторов отброшено: прибор / телефон | — | 4 / 4 |
| кадров с неверной CRC: прибор / телефон | — | 6 / 9 |
| обрывков не-кадров: прибор / телефон | — | 2 / 8 |
| время опыта | 50 500 мс | 71 211 мс |
Без протокола пропала почти треть сообщений — всё, что ушло за секунду до падения STATE и пока связи не было. С протоколом дошло всё, цена — лишнее время: подтверждение приходило в середине через 45 мс, а через обрыв — до 3779 мс.
16в.6 STATE, переподключение и потребление
Признак связи. Вывод STATE читается на каждом опросе; уровень считается новым, если продержался 20 мс:
static void link_watch(hc_t *h, uint8_t pin)
{
if (pin != h->state_in) {
h->state_in = pin;
h->state_since = h->now;
return;
}
if (pin == h->link || !after(h->now, h->state_since + HC_STATE_MS))
return;
h->link = pin;
h->l.al = 0; /* начатый кадр — мусор */
h->l.over = 0;
if (pin) {
h->n_up++;
h->l.tx_at = h->now; /* кадр в пути — повторить сразу */
emit(h, HC_EV_LINK_UP, 0, 0);
} else {
h->n_down++;
emit(h, HC_EV_LINK_DOWN, 0, 0);
}
}
Пока связи нет, повторы стоят, а кадр в пути не выбрасывается. В опыте 16в.5 драйвер замечал падение STATE через 20 мс все 6 раз, а после возврата связи первое подтверждение приходило через 40…54 мс. Но STATE — признак запоздалый: модуль сам узнаёт об обрыве только по истечении срока потери связи (в модели — 1 с; у настоящего модуля — не проверено), и всё, что ушло за это время, пропадает. Эту дыру закрывает не STATE, а подтверждения. Если STATE не подключён вовсе, протокол работает на одних повторах; после 8 повторов подряд драйвер сообщает «канал молчит» — в прогоне STATE ни разу не дал до этого дойти.
Переподключение. Ведомый HC-05 ждёт, пока телефон подключится снова;
ведущий с AT+CMODE=0 подключается к своему адресу сам (по памяти, не
проверено). Прошивке делать ничего не нужно: номера кадров живут дольше
связи, повтор после переподключения отбросит получатель.
Потребление. Классический Bluetooth держит связь постоянно, и радио модуля почти всё время работает; глубокого сна с сохранением связи у HC-05 нет. Поэтому от батарейки он живёт недолго — чисел тока в проекте нет, не проверено. Сэкономить можно одним: снимать питание ключом на PB0, когда связь не нужна. Для батарейки задуман BLE — «для коротких посылок и долгого сна» (89.5):
| HC-05, классический SPP | BLE (158, 189, 198э) | |
|---|---|---|
| что видит приложение | поток байтов, порт | посылки не длиннее MTU: 20 байт при MTU 23 |
| телефоны | Android, компьютеры | Android, айфон, компьютеры, веб с оговорками |
| микроконтроллер | любой с UART, модуль сам держит стек | стек на кристалле (ESP32, STM32WB) или модуль |
| сон | связь держит радио включённым | связь живёт между короткими сеансами |
| протокол поверх | кадр, подтверждение, повтор | тот же кадр, плюс резка по MTU |
16в.7 Грабли
Модуль греется или умер после подключения к плате на 5 В. Выход TX платы на 5 В подан на RXD модуля напрямую. Вход модуля — 3,3 В: делитель или преобразователь уровней (16.7). С STM32 на 3,3 В — напрямую.
В AT-режиме модуль не отвечает, хотя в прозрачном работал на 9600.
AT-режим — на 38 400 бод, независимо от рабочей скорости (по
документации, не проверено). И наоборот: после AT+UART=115200,0,0
прозрачный режим — на 115 200, а не на прежних 9600. Забыли, какая
скорость записана, — войти в AT-режим и спросить AT+UART?.
Модуль молчит в ответ на AT. Терминал отправляет только CR, а
модулю нужна пара CR LF; без неё он ждёт конца строки. Следующая команда
с CR LF приходит склеенной с прошлой и получает ERROR (так в модели).
AT-режим не включается. KEY подан после включения, а не до него. Снять питание, поставить KEY = 1, включить — ради этого питание модуля и идёт через ключ.
Ни одного байта ни в одну сторону. Перепутаны TX и RX: TX микроконтроллера — на RXD модуля, TXD модуля — на RX микроконтроллера.
Сообщения приходят разрезанными или склеенными. Это свойство канала, а не ошибка: пачки модуля режут поток где придётся. Нужен кадр с разделителем (16в.5), а не «одно чтение — одно сообщение».
После обрыва потерялось несколько последних сообщений. STATE падает позже, чем рвётся связь; всё, что ушло в этот промежуток, пропало. Подтверждение каждого кадра и повтор — STATE этого не заменяет.
Айфон не видит модуль. HC-05 — классический SPP; для айфона нужен BLE (по памяти, не проверено).
Два HC-05 не соединяются. Разные PIN или скорость, ведомый тоже
переведён в ведущего, или в AT+BIND адрес записан с двоеточиями, а не
с запятыми.
16в.8 Итоги
- HC-05 — ведущий или ведомый, HC-06 — только ведомый; оба — UART на 3,3 В для микроконтроллера и последовательный порт для телефона. У F103 PA10 с допуском 5 В, порог «1» — 0,65·VDD, 2,145 В при 3,3 В.
- KEY = 1 при включении — AT-режим на 38 400 бод; питание модуля — через ключ, чтобы прошивка сама переключала режимы (200 мс без питания). Команды — с CR LF; набор и значения по умолчанию — по документации, не проверено.
- Настройка: спросить → при расхождении записать → спросить снова. С завода — 14 команд и 3 записи за 1192 мс, повторно — 8 команд без записей за 1117 мс.
- По воздуху байты идут пачками: блок 200 байт пришёл 4 кусками, байт — за 13,2…34,4 мс (параметры модели). Сообщение должно переживать разрезание.
- Кадр: ведущий 00, тип, номер, длина, данные, CRC-16/XMODEM, COBS,
завершающий 00 — 8 байт добавки, до 72 байт целиком; CRC
123456789—0x31C3. - Подтверждение каждого кадра и повтор раз в 250 мс: при 6 обрывах дошли 1000 из 1000 сообщений, без протокола — 706 из 1000.
- STATE с фильтром 20 мс останавливает повторы на время обрыва и запускает их сразу после возврата связи, но падает с опозданием — потери в этот промежуток закрывают подтверждения.
- HC-05 не для батарейки: радио держит связь постоянно. Для сна — BLE.
- Драйвер с протоколом — 800 байт ОЗУ и около 2,9 КБ кода на Cortex-M3.
Полные исходники
hc05.h — драйвер и кадры: буферы, состояния, события и прототипы.
/* hc05.h — раздел 16в: модуль Bluetooth HC-05 по UART и протокол кадров поверх канала.
* hc05.c — настройка модуля AT-командами (вход по KEY при включении), переход в прозрачный режим,
* признак связи по выводу STATE; байты приёма кладёт прерывание USART (hc_rx_isr) в кольцо.
* btl16d.c — кадр поверх канала: тип, номер, длина, данные, CRC-16/XMODEM, всё в COBS, ведущий и
* завершающий ноль; подтверждение каждого кадра и повтор по сроку, отбрасывание повторов.
* Платформа даёт три функции: hc_hw_tx (байты в USART), hc_hw_uart (скорость USART), hc_hw_pin (питание
* модуля и KEY). Один и тот же код собирается в прошивку (fw16d.c) и в стенд на компьютере (host16d.c). */
#ifndef HC05_H
#define HC05_H
#include <stdint.h>
#define HC_RXQ 256u /* кольцо приёма */
#define HC_LINE 64u /* строка ответа на AT-команду */
#define HC_AT_BAUD 38400u /* скорость AT-режима при KEY = 1 на включении (по документации, не проверено) */
#define HC_AT_TRIES 10u /* столько «AT» раз в 0,3 с, пока модуль не ответит */
#define HC_PWR_OFF 200u /* питание модуля снято на столько мс при смене режима */
#define HC_STATE_MS 20u /* столько мс STATE держит уровень — и связь считается поднятой или упавшей */
#define BTL_MAX 64u /* данные одного кадра */
#define BTL_RAW (BTL_MAX + 5u) /* тип, номер, длина, данные, два байта CRC */
#define BTL_WIRE (BTL_RAW + 3u) /* COBS (+1 байт до 254), ведущий и завершающий ноль */
#define BTL_RTO 250u /* срок подтверждения, мс */
#define BTL_STALL 8u /* столько повторов подряд без подтверждения — событие «канал молчит» */
enum { HC_PIN_PWR, HC_PIN_KEY };
/* OFF — питание снято, KEY = 1 (дальше AT-режим); RESTART — питание снято, KEY = 0 (дальше прозрачный режим) */
enum { HC_ST_OFF, HC_ST_AT, HC_ST_RESTART, HC_ST_DATA, HC_ST_FAIL };
enum { HC_RES_NONE, HC_RES_OK, HC_RES_ERROR, HC_RES_TIMEOUT };
enum { HC_EV_STATE, HC_EV_AT, HC_EV_LINK_UP, HC_EV_LINK_DOWN, HC_EV_RX, HC_EV_ACKED, HC_EV_STALL, HC_EV_RAW };
enum { BTL_T_DATA = 1, BTL_T_ACK = 2 };
typedef struct hc hc_t;
typedef void (*hc_event_fn)(hc_t *h, uint8_t ev, const uint8_t *p, uint16_t n);
typedef struct {
uint8_t acc[BTL_WIRE]; /* байты до нуля */
uint16_t al;
uint8_t over; /* кадр длиннее допустимого — ждать следующего нуля */
uint8_t rx_next, rx_sync; /* какой номер ждём; 0 — ещё ни одного кадра (принять любой) */
uint8_t tx[BTL_MAX]; /* кадр в пути: до подтверждения не выбрасывается */
uint8_t tx_len, tx_seq, tx_busy, tries;
uint32_t tx_at; /* когда слать (повторять) */
uint32_t n_tx, n_retx, n_rx, n_dup, n_ack, n_crc, n_junk, n_stall;
} btl_t;
struct hc {
/* кольцо приёма: head пишет прерывание, tail — hc_poll */
uint8_t rx[HC_RXQ];
volatile uint16_t rx_head;
uint16_t rx_tail, rx_hiwater;
volatile uint16_t rx_lost;
/* AT-режим */
char line[HC_LINE];
uint8_t len;
char cmd[HC_LINE]; /* текущая команда — для журнала */
char got[HC_LINE]; /* строка «+NAME:…» ответа */
uint8_t busy, result, tries, step, phase, writes;
uint32_t deadline;
/* настройки, которые модуль должен иметь */
const char *name, *pin;
uint32_t baud;
char want[4][24]; /* ответы на запросы при верной настройке: имя, PIN, UART, роль */
/* автомат */
uint8_t st, raw; /* raw = 1 — без протокола: байты приложению как есть */
uint32_t now, wait;
uint8_t state_in, link; /* вывод STATE и признак связи после фильтра */
uint32_t state_since;
btl_t l;
hc_event_fn on_event;
uint32_t n_cmd, n_tmo, n_up, n_down;
};
/* платформа */
void hc_hw_tx(const uint8_t *p, uint16_t n);
void hc_hw_uart(uint32_t baud);
void hc_hw_pin(uint8_t pin, uint8_t level);
void hc_init(hc_t *h, const char *name, const char *pin, uint32_t baud, hc_event_fn ev);
void hc_restart(hc_t *h); /* заново: AT-режим, сверка настроек, прозрачный режим */
void hc_rx_isr(hc_t *h, uint8_t b);
void hc_poll(hc_t *h, uint32_t now_ms, uint8_t state_pin);
int hc_send_raw(hc_t *h, const uint8_t *p, uint16_t n); /* без протокола — как есть */
/* кадры */
int btl_send(hc_t *h, const uint8_t *p, uint8_t n); /* 0 — принят, −1 — предыдущий ещё в пути */
uint16_t btl_crc16(const uint8_t *p, uint16_t n);
uint16_t btl_frame(uint8_t type, uint8_t seq, const uint8_t *p, uint8_t n, uint8_t *out);
void btl_reset(btl_t *l);
void btl_byte(hc_t *h, uint8_t b);
void btl_poll(hc_t *h);
#endif
hc05.c — модуль HC-05: AT-режим, сверка настроек, прозрачный режим, STATE.
/* hc05.c — раздел 16в: модуль HC-05 по UART. Ни одного ожидания в цикле: hc_poll разбирает то, что уже
* пришло, проверяет срок текущей AT-команды, делает шаг автомата и следит за выводом STATE.
* Набор AT-команд и вид ответов — по документации производителя (в проекте её нет), не проверено. */
#include <string.h>
#include "hc05.h"
static const char *const ST_NAME[] = {"OFF", "AT", "RESTART", "DATA", "FAIL"};
/* что сверить: запрос, начало ответа, команда записи; ответ при верной настройке — в h->want */
static const char *const Q[4] = {"AT+NAME?", "AT+PSWD?", "AT+UART?", "AT+ROLE?"};
static const char *const S[4] = {"AT+NAME=", "AT+PSWD=", "AT+UART=", "AT+ROLE="};
static int starts(const char *s, const char *p)
{
while (*p)
if (*s++ != *p++)
return 0;
return 1;
}
static char *cat(char *d, const char *s, const char *end)
{
while (*s && d < end - 1)
*d++ = *s++;
*d = 0;
return d;
}
static char *catu(char *d, uint32_t v, const char *end)
{
char t[10];
int n = 0;
do
t[n++] = (char)('0' + v % 10u);
while (v /= 10u);
while (n && d < end - 1)
*d++ = t[--n];
*d = 0;
return d;
}
static int after(uint32_t now, uint32_t t)
{
return (int32_t)(now - t) >= 0;
}
static void emit(hc_t *h, uint8_t ev, const uint8_t *p, uint16_t n)
{
if (h->on_event)
h->on_event(h, ev, p, n);
}
static void set_st(hc_t *h, uint8_t st)
{
h->st = st;
emit(h, HC_EV_STATE, (const uint8_t *)ST_NAME[st], st);
}
/* ── AT-режим: одна команда за раз, конец ответа — OK или ERROR ──────── */
static void at_cmd(hc_t *h, const char *c, uint32_t tmo)
{
cat(h->cmd, c, h->cmd + HC_LINE);
h->got[0] = 0;
h->busy = 1;
h->result = HC_RES_NONE;
h->deadline = h->now + tmo;
h->n_cmd++;
hc_hw_tx((const uint8_t *)h->cmd, (uint16_t)strlen(h->cmd));
hc_hw_tx((const uint8_t *)"\r\n", 2); /* без CR LF модуль молчит */
}
static void at_done(hc_t *h, uint8_t res)
{
h->busy = 0;
h->result = res;
if (res == HC_RES_TIMEOUT)
h->n_tmo++;
emit(h, HC_EV_AT, (const uint8_t *)h->cmd, res);
}
static void at_line(hc_t *h)
{
const char *s = h->line;
if (!h->busy)
return; /* строка без команды — мусор после включения */
if (!strcmp(s, "OK"))
at_done(h, HC_RES_OK);
else if (starts(s, "ERROR") || starts(s, "FAIL"))
at_done(h, HC_RES_ERROR);
else if (s[0] == '+' && strchr(s, ':'))
cat(h->got, strchr(s, ':') + 1, h->got + HC_LINE); /* «+NAME:HC-05» → «HC-05» */
}
static void at_byte(hc_t *h, uint8_t b)
{
if (b == '\r')
return;
if (b == '\n') {
if (h->len) {
h->line[h->len] = 0;
at_line(h);
}
h->len = 0;
return;
}
if (h->len < HC_LINE - 1u)
h->line[h->len++] = (char)b;
}
static void to_data(hc_t *h)
{
hc_hw_pin(HC_PIN_KEY, 0); /* KEY = 0 на включении — прозрачный режим */
hc_hw_pin(HC_PIN_PWR, 0);
h->wait = h->now + HC_PWR_OFF;
set_st(h, HC_ST_RESTART);
}
static void fail(hc_t *h)
{
hc_hw_pin(HC_PIN_KEY, 0);
set_st(h, HC_ST_FAIL);
}
/* шаг AT-автомата: AT до ответа → версия → для каждой настройки запрос, при расхождении запись и
* повторный запрос → выключить, KEY = 0, включить в прозрачном режиме */
static void at_step(hc_t *h)
{
uint8_t r = h->result;
h->result = HC_RES_NONE;
if (h->step == 0) {
if (r != HC_RES_OK) {
if (h->tries >= HC_AT_TRIES) {
fail(h);
return;
}
h->tries++;
at_cmd(h, "AT", 300);
return;
}
h->step = 1;
h->phase = 0;
r = HC_RES_NONE;
}
if (h->step == 1) {
if (h->phase == 0) {
h->phase = 1;
at_cmd(h, "AT+VERSION?", 1000);
return;
}
h->step = 2;
h->phase = 0;
r = HC_RES_NONE;
}
while (h->step < 6) {
uint8_t i = (uint8_t)(h->step - 2u);
int same = r == HC_RES_OK && !strcmp(h->got, h->want[i]);
if (h->phase == 0) { /* запрос */
h->phase = 1;
at_cmd(h, Q[i], 1000);
return;
}
if (h->phase == 1 && !same) { /* отличается — записать */
char c[HC_LINE];
if (r != HC_RES_OK) {
fail(h);
return;
}
cat(cat(c, S[i], c + HC_LINE), h->want[i], c + HC_LINE);
h->phase = 2;
h->writes++;
at_cmd(h, c, 1000);
return;
}
if (h->phase == 2) { /* записано — проверить запросом */
if (r != HC_RES_OK) {
fail(h);
return;
}
h->phase = 3;
at_cmd(h, Q[i], 1000);
return;
}
if (!same) { /* после записи всё ещё не то */
fail(h);
return;
}
h->step++;
h->phase = 0;
r = HC_RES_NONE;
}
to_data(h);
}
/* ── общий вход ──────────────────────────────────────────────────────── */
void hc_restart(hc_t *h)
{
hc_hw_pin(HC_PIN_PWR, 0);
hc_hw_pin(HC_PIN_KEY, 1); /* KEY = 1 на включении — AT-режим */
h->wait = h->now + HC_PWR_OFF;
h->busy = 0;
h->step = h->phase = h->tries = h->writes = 0;
h->result = HC_RES_NONE;
if (h->link)
emit(h, HC_EV_LINK_DOWN, 0, 0);
h->link = 0;
set_st(h, HC_ST_OFF);
}
void hc_init(hc_t *h, const char *name, const char *pin, uint32_t baud, hc_event_fn ev)
{
memset(h, 0, sizeof *h);
h->name = name;
h->pin = pin;
h->baud = baud;
h->on_event = ev;
cat(h->want[0], name, h->want[0] + sizeof h->want[0]);
cat(h->want[1], pin, h->want[1] + sizeof h->want[1]);
cat(catu(h->want[2], baud, h->want[2] + sizeof h->want[2]), ",0,0", h->want[2] + sizeof h->want[2]);
cat(h->want[3], "0", h->want[3] + sizeof h->want[3]); /* роль 0 — ведомый */
btl_reset(&h->l);
hc_restart(h);
}
void hc_rx_isr(hc_t *h, uint8_t b)
{
uint16_t nx = (uint16_t)((h->rx_head + 1u) % HC_RXQ);
if (nx == h->rx_tail) {
h->rx_lost++;
return;
}
h->rx[h->rx_head] = b;
h->rx_head = nx;
}
int hc_send_raw(hc_t *h, const uint8_t *p, uint16_t n)
{
if (h->st != HC_ST_DATA)
return -1;
hc_hw_tx(p, n);
return 0;
}
static void link_watch(hc_t *h, uint8_t pin)
{
if (pin != h->state_in) {
h->state_in = pin;
h->state_since = h->now;
return;
}
if (pin == h->link || !after(h->now, h->state_since + HC_STATE_MS))
return;
h->link = pin;
h->l.al = 0; /* начатый кадр — мусор */
h->l.over = 0;
if (pin) {
h->n_up++;
h->l.tx_at = h->now; /* кадр в пути — повторить сразу */
emit(h, HC_EV_LINK_UP, 0, 0);
} else {
h->n_down++;
emit(h, HC_EV_LINK_DOWN, 0, 0);
}
}
void hc_poll(hc_t *h, uint32_t now, uint8_t state_pin)
{
uint8_t raw[64];
uint16_t nr = 0;
h->now = now;
uint16_t used = (uint16_t)((h->rx_head - h->rx_tail + HC_RXQ) % HC_RXQ);
if (used > h->rx_hiwater)
h->rx_hiwater = used;
while (h->rx_tail != h->rx_head) {
uint8_t b = h->rx[h->rx_tail];
h->rx_tail = (uint16_t)((h->rx_tail + 1u) % HC_RXQ);
if (h->st == HC_ST_AT) {
at_byte(h, b);
} else if (h->st == HC_ST_DATA) {
if (!h->raw) {
btl_byte(h, b);
} else {
raw[nr++] = b;
if (nr == sizeof raw) {
emit(h, HC_EV_RAW, raw, nr);
nr = 0;
}
}
} /* в OFF и RESTART — мусор включения */
}
if (nr)
emit(h, HC_EV_RAW, raw, nr);
if (h->busy && after(now, h->deadline))
at_done(h, HC_RES_TIMEOUT);
switch (h->st) {
case HC_ST_OFF:
if (after(now, h->wait)) {
h->len = 0;
hc_hw_uart(HC_AT_BAUD);
hc_hw_pin(HC_PIN_PWR, 1);
set_st(h, HC_ST_AT);
}
break;
case HC_ST_AT:
if (!h->busy)
at_step(h);
break;
case HC_ST_RESTART:
if (after(now, h->wait)) {
hc_hw_uart(h->baud);
hc_hw_pin(HC_PIN_PWR, 1);
h->state_in = 0;
h->state_since = now;
set_st(h, HC_ST_DATA);
}
break;
case HC_ST_DATA:
link_watch(h, state_pin);
if (!h->raw)
btl_poll(h);
break;
default:
break;
}
}
btl16d.c — кадр поверх канала: COBS, CRC-16, подтверждение и повтор.
/* btl16d.c — раздел 16в: кадры поверх прозрачного канала HC-05.
* Кадр до COBS: тип, номер, длина, данные, CRC-16/XMODEM старшим байтом вперёд (как в разделе 194).
* На линии: 00, COBS(кадр), 00. Отправитель держит один кадр, пока не придёт подтверждение с его номером,
* и повторяет по сроку BTL_RTO; получатель подтверждает каждый целый кадр и выбрасывает повтор. */
#include <string.h>
#include "hc05.h"
uint16_t btl_crc16(const uint8_t *p, uint16_t n)
{
uint16_t c = 0; /* XMODEM: полином 0x1021, начальное 0 */
while (n--) {
c ^= (uint16_t)(*p++ << 8);
for (int k = 0; k < 8; k++)
c = (uint16_t)(c & 0x8000u ? (unsigned)c << 1 ^ 0x1021u : (unsigned)c << 1);
}
return c;
}
static uint16_t cobs_enc(const uint8_t *in, uint16_t n, uint8_t *out)
{
uint16_t code_at = 0, o = 1;
uint8_t code = 1;
for (uint16_t i = 0; i < n; i++) {
if (in[i] == 0) {
out[code_at] = code;
code_at = o++;
code = 1;
} else {
out[o++] = in[i];
if (++code == 0xFFu) {
out[code_at] = code;
code_at = o++;
code = 1;
}
}
}
out[code_at] = code;
return o;
}
static int cobs_dec(const uint8_t *in, uint16_t n, uint8_t *out, uint16_t cap)
{
uint16_t i = 0, o = 0;
while (i < n) {
uint8_t code = in[i++];
if (code == 0 || i + code - 1u > n)
return -1; /* блок длиннее кадра — испорчен */
for (uint8_t k = 1; k < code; k++) {
if (o >= cap)
return -1;
out[o++] = in[i++];
}
if (code < 0xFFu && i < n) {
if (o >= cap)
return -1;
out[o++] = 0;
}
}
return o;
}
uint16_t btl_frame(uint8_t type, uint8_t seq, const uint8_t *p, uint8_t n, uint8_t *out)
{
uint8_t raw[BTL_RAW];
raw[0] = type;
raw[1] = seq;
raw[2] = n;
if (n)
memcpy(raw + 3, p, n);
uint16_t c = btl_crc16(raw, (uint16_t)(n + 3u));
raw[n + 3] = (uint8_t)(c >> 8);
raw[n + 4] = (uint8_t)c;
out[0] = 0; /* ведущий ноль: обрывок прежнего кадра закончится здесь */
uint16_t m = (uint16_t)(1u + cobs_enc(raw, (uint16_t)(n + 5u), out + 1));
out[m++] = 0;
return m;
}
void btl_reset(btl_t *l)
{
memset(l, 0, sizeof *l);
}
static void tx_frame(uint8_t type, uint8_t seq, const uint8_t *p, uint8_t n)
{
uint8_t w[BTL_WIRE];
hc_hw_tx(w, btl_frame(type, seq, p, n, w));
}
int btl_send(hc_t *h, const uint8_t *p, uint8_t n)
{
btl_t *l = &h->l;
if (l->tx_busy || n > BTL_MAX)
return -1;
memcpy(l->tx, p, n);
l->tx_len = n;
l->tx_busy = 1;
l->tries = 0;
l->tx_at = h->now;
return 0;
}
static void on_frame(hc_t *h, const uint8_t *f, uint16_t n)
{
btl_t *l = &h->l;
uint8_t type = f[0], seq = f[1];
if (type == BTL_T_ACK) {
if (l->tx_busy && seq == l->tx_seq) { /* подтверждён кадр в пути */
l->tx_busy = 0;
l->tx_seq++;
l->n_ack++;
if (h->on_event)
h->on_event(h, HC_EV_ACKED, l->tx, l->tx_len);
}
return; /* запоздалое подтверждение повтора — не наше */
}
if (type != BTL_T_DATA)
return;
tx_frame(BTL_T_ACK, seq, 0, 0); /* подтвердить и повтор: прежнее подтверждение могло пропасть */
if (l->rx_sync && seq == (uint8_t)(l->rx_next - 1u)) {
l->n_dup++; /* тот же кадр ещё раз — данные уже отданы */
return;
}
l->rx_sync = 1;
l->rx_next = (uint8_t)(seq + 1u);
l->n_rx++;
if (h->on_event)
h->on_event(h, HC_EV_RX, f + 3, (uint16_t)(n - 5u));
}
void btl_byte(hc_t *h, uint8_t b)
{
btl_t *l = &h->l;
uint8_t f[BTL_RAW];
if (b) {
if (l->al < BTL_WIRE)
l->acc[l->al++] = b;
else
l->over = 1; /* длиннее любого кадра — мусор до нуля */
return;
}
uint16_t al = l->al;
uint8_t over = l->over;
l->al = 0;
l->over = 0;
if (!al)
return; /* ведущий ноль или два нуля подряд */
int n = over ? -1 : cobs_dec(l->acc, al, f, sizeof f);
if (n < 5 || f[2] != n - 5) {
l->n_junk++; /* не кадр: обрывок, склейка, мусор */
return;
}
if (btl_crc16(f, (uint16_t)(n - 2)) != (uint16_t)(f[n - 2] << 8 | f[n - 1])) {
l->n_crc++;
return;
}
on_frame(h, f, (uint16_t)n);
}
void btl_poll(hc_t *h)
{
btl_t *l = &h->l;
if (!h->link || !l->tx_busy || (int32_t)(h->now - l->tx_at) < 0)
return;
if (l->tries) {
l->n_retx++;
if (l->tries == BTL_STALL) {
l->n_stall++;
if (h->on_event)
h->on_event(h, HC_EV_STALL, l->tx, l->tx_len);
}
}
if (l->tries < 0xFFu)
l->tries++;
l->n_tx++;
tx_frame(BTL_T_DATA, l->tx_seq, l->tx, l->tx_len);
l->tx_at = h->now + BTL_RTO;
}
fw16d.c — прошивка STM32F103: USART1 с прерываниями, ключ питания, KEY, STATE, главный цикл.
/* fw16d.c — раздел 16в: модуль HC-05 на STM32F103 (BluePill), 72 МГц.
* USART1: PA9 — TX к RXD модуля, PA10 — RX от TXD модуля; 38 400 бод в AT-режиме, 115 200 — в прозрачном.
* PB0 — ключ питания модуля (1 — включено), PB1 — KEY (EN) модуля, PB5 — вход STATE с подтяжкой к земле.
* PC13 — светодиод: горит, пока есть связь. Приём — прерывание RXNE кладёт байт в кольцо драйвера;
* передача — своё кольцо и прерывание TXE. SysTick раз в 1 мс — только счёт времени; hc_poll — в главном
* цикле. Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); USART1 на APB2 = 72 МГц. */
#include <string.h>
#include "stm32f10x.h"
#include "hc05.h"
#define CPU_HZ 72000000u
#define BAUD 115200u
#define TXQ 256u
_Static_assert(CPU_HZ / BAUD == 625u && CPU_HZ % BAUD == 0u, "USART1: BRR = 625 — 115 200 бод");
_Static_assert(CPU_HZ / HC_AT_BAUD == 1875u && CPU_HZ % HC_AT_BAUD == 0u, "USART1: BRR = 1875 — 38 400 бод");
static hc_t H;
static volatile uint32_t ms;
static uint8_t txq[TXQ];
static volatile uint16_t tx_head, tx_tail;
static uint32_t last_tm;
static uint16_t count;
void SysTick_Handler(void)
{
ms++;
}
void USART1_IRQHandler(void)
{
uint32_t sr = USART1->SR;
if (sr & (USART_SR_RXNE | USART_SR_ORE)) /* чтение DR снимает и RXNE, и ORE */
hc_rx_isr(&H, (uint8_t)USART1->DR);
if ((USART1->CR1 & USART_CR1_TXEIE) && (sr & USART_SR_TXE)) {
if (tx_tail != tx_head) {
USART1->DR = txq[tx_tail];
tx_tail = (uint16_t)((tx_tail + 1u) % TXQ);
} else {
USART1->CR1 &= ~USART_CR1_TXEIE; /* очередь пуста */
}
}
}
/* ── платформа драйвера ───────────────────────────────────────────────── */
void hc_hw_tx(const uint8_t *p, uint16_t n)
{
for (uint16_t i = 0; i < n; i++) {
uint16_t nx = (uint16_t)((tx_head + 1u) % TXQ);
if (nx == tx_tail)
break; /* очередь полна — кадр длиннее 72 байт драйвер не шлёт */
txq[tx_head] = p[i];
tx_head = nx;
}
USART1->CR1 |= USART_CR1_TXEIE;
}
void hc_hw_uart(uint32_t baud)
{
/* скорость меняется при снятом питании модуля (200 мс): очередь передачи к этому времени пуста */
USART1->CR1 &= ~USART_CR1_UE;
USART1->BRR = CPU_HZ / baud;
USART1->CR1 |= USART_CR1_UE;
}
void hc_hw_pin(uint8_t pin, uint8_t level)
{
uint32_t bit = pin == HC_PIN_PWR ? 1u << 0 : 1u << 1;
GPIOB->BSRR = level ? bit : bit << 16;
}
/* ── приложение ───────────────────────────────────────────────────────── */
static void on_event(hc_t *h, uint8_t ev, const uint8_t *p, uint16_t n)
{
(void)h;
if (ev == HC_EV_LINK_UP)
GPIOC->BSRR = 1u << 29; /* PC13 = 0 — светодиод горит */
else if (ev == HC_EV_LINK_DOWN)
GPIOC->BSRR = 1u << 13;
else if (ev == HC_EV_RX && n == 3 && !memcmp(p, "CNT", 3))
count = 0; /* команда с телефона: обнулить счёт */
}
static void board_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_USART1EN;
GPIOB->BSRR = (1u << 16) | (1u << 17) | (1u << 21); /* питание выключено, KEY = 0, PB5 — к земле */
GPIOB->CRL = (GPIOB->CRL & ~0xF000FFu)
| 0x22u /* PB0, PB1 — выходы 2 МГц */
| 0x8u << 20; /* PB5 — вход с подтяжкой (к земле: ODR5 = 0) */
GPIOA->CRH = (GPIOA->CRH & ~0xFF0u)
| 0xBu << 4 /* PA9 — USART1_TX, альтернативная функция */
| 0x4u << 8; /* PA10 — USART1_RX, вход без подтяжки */
GPIOC->BSRR = 1u << 13;
GPIOC->CRH = (GPIOC->CRH & ~(0xFu << 20)) | 0x2u << 20; /* PC13 — светодиод платы */
USART1->BRR = CPU_HZ / HC_AT_BAUD;
USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE;
NVIC_EnableIRQ(USART1_IRQn);
SysTick_Config(CPU_HZ / 1000u);
}
int main(void)
{
board_init();
hc_init(&H, "BENCH-16", "4321", BAUD, on_event);
for (;;) {
uint32_t now = ms;
hc_poll(&H, now, (uint8_t)(GPIOB->IDR >> 5 & 1u));
if (H.link && now - last_tm >= 1000u) { /* раз в секунду — счёт; не ушёл — попробуем позже */
uint8_t m[4] = {'C', 'N', (uint8_t)(count >> 8), (uint8_t)count};
if (btl_send(&H, m, sizeof m) == 0) {
last_tm = now;
count++;
}
}
__WFI(); /* до следующего прерывания: байта или тика */
}
}
Дальше: раздел 16г «MP3-модуль DFPlayer Mini по UART: кадр команды, контрольная сумма, очередь треков, громкость и ответы модуля» — тот же UART, но на другом конце модуль со своим кадром из 10 байт, суммой, подтверждениями и событиями «трек закончился».