16в. Bluetooth module HC-05/HC-06: transparent UART, AT mode, pairing and a protocol over the link
In 16, 16а and 16б the UART ran over a wire: the frame, the baud rate, RS-485, reception through DMA into a ring. Running a wire is not always convenient: the device sits in a cabinet, and you want to set it up from a phone. The cheapest way to drop the wire is an HC-05 or HC-06 module. For the microcontroller it is still the same UART, for a phone or a computer it is a serial port over Bluetooth. Simple to look at, but radio is not a wire: bytes arrive in bursts and late, the link breaks, and when it breaks everything that was on the way is lost.
What is inside: how the HC-05 differs from the HC-06, how to connect the module to an STM32F103 and why the STATE and KEY pins matter; what the transparent mode is and how bytes behave in the air; the AT mode and setting the module up without waiting in a loop; pairing with a phone and with a second HC-05; a frame with a number, a length and CRC-16, acknowledgement and retransmission over the link; the link indicator on STATE, reconnection and power draw — why the HC-05 is not for a battery and why BLE is better there.
Sources: DocID13587 Rev 17 (STM32F103x8/xB): PA9, PA10 are USART1_TX,
USART1_RX with 5 V tolerance, p. 31; the input high threshold is
0.65·VDD, table 35, p. 62. The COBS frame with CRC-16/XMODEM comes from
194.4, the leading zero from 189а.2, non-blocking parsing of AT replies
from 106г.3. The project has no documents on the HC-05 and HC-06 and no
Bluetooth specification: the AT command set, the form of replies, the
defaults (name, PIN, baud rate), the AT-mode baud rate and the meaning of
the STATE and KEY pins are recalled from the manufacturer's documentation
and marked "not verified". Everything the section calls the run is the code
_verify/bt16d/hc05.c and _verify/bt16d/btl16d.c on a computer
(gcc 12.2.0) with a model of the module, the air and the phone in Python
3.11; the firmware is built by armclang 6.24 for the Cortex-M3.
What it is called in the documentation
Original Meaning Bluetooth Classic (BR/EDR) classic Bluetooth Serial Port Profile (SPP), RFCOMM the "serial port" profile and its protocol Bluetooth Low Energy (BLE) low-power Bluetooth pairing, PIN (passkey) pairing, pairing code master / slave, role master / slave, the role bind address the address to connect to transparent (data) mode transparent mode: bytes as they are AT command mode AT command mode link supervision timeout time after which the link is considered lost COBS (Consistent Overhead Byte Stuffing) replacing zeros in a frame with a fixed overhead ARQ, stop-and-wait retransmission on timeout; "send, then wait for the acknowledgement"
16в.1 The module: HC-05 and HC-06, supply, levels, STATE and KEY
Two modules. Both are classic Bluetooth with the SPP profile: a phone
or a computer sees them as a serial port. The HC-05 can be a slave (others
connect to it) or a master (it connects to another module itself) — the
role is set by AT+ROLE. The HC-06 is a slave only, and its AT commands are
simpler and look different. This is recalled from the manufacturer's
documentation; the project does not have it, not verified.
Supply and levels. The module itself lives on 3.3 V, and its UART is 3.3 V too. Most often it is sold on a carrier board with a regulator and six pins: STATE, RXD, TXD, GND, VCC, EN (also called KEY); the board is powered from 5 V (from memory, the range is in the board's document, not verified). With an STM32F103 the lines connect directly: the 3.3 V output PA9 goes to the module's RXD input, the module's TXD output goes to PA10. PA10 is 5 V tolerant (DocID13587, p. 31) and is guaranteed to see a "one" from 0.65·VDD (table 35, p. 62) — at 3.3 V that is 2.145 V; compare the module's output level against this threshold using its document. The dangerous side is the other one — a 5 V board: its TX output into the module's RXD without a divider (16.7).
STATE and KEY. On common boards STATE is high while there is a link to the phone — this is how the firmware learns about a break (16в.6). KEY high at the moment of power-up puts the HC-05 into AT mode at 38 400 baud; otherwise the module starts in transparent mode at its working baud rate. Both statements are from memory, not verified. Hence the circuit: the module's supply goes through a switch on PB0, so the firmware itself restarts the module in the needed mode; KEY goes to PB1; STATE goes to the PB5 input with a pull-down, so a broken wire reads as "no link".
16в.2 Transparent mode: UART through the air
In transparent mode the module knows nothing about messages: whatever the
firmware puts out on TX appears in the port on the phone, and back. There
are no boundaries: where one message ended and another began, the module
does not pass on. The module's UART baud rate is the one written with
AT+UART; on an F103 at 72 MHz BRR for 115 200 baud is 625, for 38 400 it
is 1875, for 9600 it is 7500, all three divide without a remainder. A byte
at 115 200 baud takes 86.8 µs.
Inside, the module collects bytes from the UART and sends them into the air in bursts, and the radio link adds its own delay. How exactly a real module collects them is not verified; the bench model sends a burst 5 ms after the first byte or at 128 bytes, and a burst travels through the air for 8…30 ms. The numbers below follow from these parameters and are not a measurement of a module; what matters is their shape:
| Experiment in the model | Result |
|---|---|
| one byte to the phone, 50 times | from 13.2 to 34.4 ms, median 22.8 ms |
| a 200-byte block in one write | 4 bursts at the phone: 58, 58, 58 and 26 bytes |
| first and last burst | 30.8 and 34.9 ms after the write |
| echo of the same block back | at the device after 72 ms, in 18 pieces by polls |
Hence the rule, the same as in 189.2 for BLE: a message must survive being cut and glued at any point. A block written at once arrives in pieces; two short messages in a row may arrive as one burst. Reading "one message per read" does not work.
Within an established link the radio channel itself usually does not deliver corrupted bytes: Bluetooth has its own checks and retransmissions in the air (recalled from the specification, not verified). Bytes are lost elsewhere: on a break — everything that lay in the module's buffer and was flying through the air, and on the UART wires, if they are long and a power stage is noisy nearby.
16в.3 AT mode: entering by KEY and setup without waiting
A command is an AT… line ended by a CR LF pair; the reply is a data line
and OK, or an error like ERROR:(0). The set and form of commands are
recalled from the manufacturer's documentation for firmware 2.0, not
verified; other firmware versions look different (for example, the PIN is in
quotes):
| Command | Model reply | What it does |
|---|---|---|
AT |
OK |
the module is alive and caught the baud rate |
AT+VERSION? |
+VERSION:2.0-20100601 |
firmware version |
AT+NAME?, AT+NAME=имя |
+NAME:HC-05 |
the name the phone sees |
AT+PSWD?, AT+PSWD=4321 |
+PSWD:1234 |
pairing code |
AT+UART?, AT+UART=115200,0,0 |
+UART:9600,0,0 |
baud rate, stop bits, parity of transparent mode |
AT+ROLE?, AT+ROLE=1 |
+ROLE:0 |
0 — slave, 1 — master |
AT+CMODE=0, AT+BIND=… |
OK |
for a master: connect only to the address from BIND |
AT+ADDR? |
+ADDR:… |
own address — it goes into the master's BIND |
The defaults in the model are name HC-05, PIN 1234, 9600,0,0, role 0; this is how they are usually named, but the document is not verified.
Parsing of replies works as in 106г.3: a ring, line assembly up to LF, one
command at a time with its own deadline, — and is not repeated here. It is
simpler: there are no unsolicited messages in AT mode, and every command has
one final reply — OK or ERROR. The main point is different: ask first,
then write. For each setting the driver sends a query, writes on a
mismatch and asks again:
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;
}
This way the module is not rewritten at every power-up, and a write that did not take is seen at once: the repeated query returns the old value, and the driver stops in the FAIL state instead of working at a foreign baud rate.
Run: a module "from the factory". The driver removes the supply for
200 ms, sets KEY = 1, powers the module up and sends AT every 0.3 s up to
10 times. The model boots for 500 ms, so the first two AT went
unanswered, and OK came 612 ms after power-up. Then — 14 commands, of
them 3 writes (name BENCH-16, PIN 4321, baud 115 200); the role is already
0, it was only queried. Then the supply is removed for another 200 ms,
KEY = 0, the module is powered up in transparent mode at 115 200 baud — at
1192 ms from the start. A repeated start with the same settings: 8
commands, not a single write, 1117 ms.
16в.4 Pairing: a phone and a second HC-05
Everything here is qualitative — in the model pairing is reduced to one event "the phone connected", and a real module was not tested.
An Android phone. In the Bluetooth settings the module is visible under
its name (BENCH-16); pairing uses the code from AT+PSWD. Pairing is not
yet a link: the channel is opened by an app that speaks SPP, and only then
does STATE go high. In the model, after the connection the driver saw the
link after 21 ms — 20 ms of STATE filtering and one poll step.
An iPhone. As far as is known, iOS apps cannot use classic SPP with arbitrary devices, and the HC-05 does not work with an iPhone — BLE is needed (16в.6). Not verified.
A computer. After pairing, Windows creates virtual ports "Standard Serial over Bluetooth link" — on the machine where 115б.2 was written, 7 of them were found and not a single cable. Find the port by the device name; do not take the first one.
Two HC-05 with each other. One stays a slave as it is. The second one in
AT mode gets: AT+ROLE=1, AT+CMODE=0, the same PIN and the same baud rate,
and AT+BIND= with the address of the first one from its AT+ADDR?, with
colons replaced by commas: 1234:56:abcdef → AT+BIND=1234,56,abcdef (the
address is a placeholder). After power-up the master looks for its slave and
connects by itself, and reconnects by itself after a break. All from memory,
not verified.
16в.5 A protocol over the link: frame, acknowledgement, retransmission
The transparent link gives a byte stream without boundaries, and on a break it loses the tail. A protocol of its own is needed on top, and almost all of it is already in the book: the frame from 194.4 — the data, CRC-16/XMODEM high byte first, everything in COBS, a zero at the end; the leading zero before the frame comes from 189а.2: a fragment of an earlier frame stuck in the receiver after a break ends on it and is dropped. Inside the frame there are three bytes here: type (data or acknowledgement), number and length — the length is checked once more against what came out of 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;
}
The run checks the C code against an independent implementation in Python
(binascii.crc_hqx and its own COBS): the CRC of the string 123456789 is
0x31C3, as in 194.4; the frames matched byte for byte:
| Data | Frame, bytes | Overhead | At 115 200 baud |
|---|---|---|---|
MSG 0001, number 1 |
16: 00 0E 01 01 08 4D 53 47 20 30 30 30 31 6C 1C 00 |
8 | 1389 µs |
| 8 zero bytes | 16: 00 04 01 01 08 01 01 01 01 01 01 01 03 9E F3 00 |
8 | 1389 µs |
| 64 bytes | 72 | 8 | 6250 µs |
The overhead is 8 bytes per frame for any data up to 64 bytes: two zeros, the COBS byte, type, number, length and CRC.
Acknowledgement and retransmission. The sender holds one frame until an acknowledgement with its number arrives, and repeats it every 250 ms. The receiver acknowledges every intact frame — repeats too: a repeat means the earlier acknowledgement got lost. A repeat is recognised by its number — it equals the number of the last accepted frame — and is dropped, so the data reaches the application exactly once:
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));
}
The first frame after start is accepted with any number (rx_sync = 0), and
so is any number other than that of the last frame: restarting the firmware
or the app on the phone does not break the exchange.
The experiment. The device sends 1000 messages MSG 0001…MSG 1000,
the phone echoes them back. The link breaks every 5…9 s for 2…4 s; the air
dies at once, STATE drops after 1 s; on the UART wires a byte is corrupted
with probability 0.0003 and lost with probability 0.0001 — all of these are
model parameters. Without a protocol the device sends a line every 50 ms,
and the phone returns the bytes as they came; with the protocol the device
sends the next message after the previous one is acknowledged, and the phone
echoes in a frame of its own.
| no protocol | with protocol | |
|---|---|---|
| messages | 1000 | 1000 |
| link breaks during the run | 5 | 6 |
| came back intact, once each, in order | 706 | 1000 |
| lost | 294 | 0 |
| corrupted lines | 3 | — |
| frame retransmissions: device / phone | — | 46 / 33 |
| repeats dropped: device / phone | — | 4 / 4 |
| frames with a bad CRC: device / phone | — | 6 / 9 |
| non-frame fragments: device / phone | — | 2 / 8 |
| run time | 50 500 ms | 71 211 ms |
Without a protocol almost a third of the messages vanished — everything sent in the second before STATE dropped and while there was no link. With the protocol everything arrived; the price is extra time: the median acknowledgement came after 45 ms, and across a break — up to 3779 ms.
16в.6 STATE, reconnection and power draw
The link indicator. The STATE pin is read on every poll; a level counts as new once it has held for 20 ms:
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);
}
}
While there is no link, retransmissions pause, and the frame on the way is not thrown away. In the experiment of 16в.5 the driver noticed STATE dropping after 20 ms all 6 times, and after the link came back the first acknowledgement arrived after 40…54 ms. But STATE is a late indicator: the module itself learns about a break only when the link loss timeout expires (in the model — 1 s; for a real module — not verified), and everything sent during that time is lost. This gap is closed not by STATE but by acknowledgements. If STATE is not connected at all, the protocol works on retransmissions alone; after 8 retransmissions in a row the driver reports "the link is silent" — in the run STATE never let it get that far.
Reconnection. A slave HC-05 waits for the phone to connect again; a
master with AT+CMODE=0 connects to its address by itself (from memory, not
verified). The firmware needs to do nothing: frame numbers outlive the link,
and the receiver drops a repeat after reconnection.
Power draw. Classic Bluetooth keeps the link up all the time, and the module's radio works almost all the time; the HC-05 has no deep sleep that keeps the link. So it does not live long on a battery — the project has no current figures, not verified. There is one way to save: remove the supply with the switch on PB0 when the link is not needed. BLE is designed for the battery — "for short messages and long sleep" (89.5):
| HC-05, classic SPP | BLE (158, 189, 198э) | |
|---|---|---|
| what the app sees | a byte stream, a port | messages no longer than the MTU: 20 bytes at MTU 23 |
| phones | Android, computers | Android, iPhone, computers, the web with caveats |
| microcontroller | any with a UART, the module runs the stack | stack on the chip (ESP32, STM32WB) or a module |
| sleep | the link keeps the radio on | the link lives between short sessions |
| protocol on top | frame, acknowledgement, retransmission | the same frame, plus cutting by the MTU |
16в.7 Pitfalls
The module heats up or died after being connected to a 5 V board. The TX output of a 5 V board went straight to the module's RXD. The module's input is 3.3 V: a divider or a level translator (16.7). From a 3.3 V STM32 — directly.
In AT mode the module does not answer, though in transparent mode it
worked at 9600. AT mode runs at 38 400 baud regardless of the working
baud rate (per the documentation, not verified). And the other way round:
after AT+UART=115200,0,0 transparent mode runs at 115 200, not at the old
9600. Forgot which baud rate was written — enter AT mode and ask AT+UART?.
The module is silent in reply to AT. The terminal sends only CR, and
the module needs the CR LF pair; without it the module waits for the end of
the line. The next command with CR LF arrives glued to the previous one and
gets ERROR (so in the model).
AT mode does not engage. KEY was applied after power-up, not before it. Remove the supply, set KEY = 1, power up — this is why the module's supply goes through a switch.
Not a single byte in either direction. TX and RX are swapped: the microcontroller's TX goes to the module's RXD, the module's TXD to the microcontroller's RX.
Messages arrive cut or glued. This is a property of the link, not a bug: the module's bursts cut the stream wherever they happen to. A frame with a separator is needed (16в.5), not "one read — one message".
After a break the last few messages are lost. STATE drops later than the link breaks; everything sent in that gap is gone. Acknowledgement of every frame and retransmission — STATE does not replace them.
An iPhone does not see the module. The HC-05 is classic SPP; an iPhone needs BLE (from memory, not verified).
Two HC-05 do not connect. Different PINs or baud rates, the slave was
also turned into a master, or the address in AT+BIND was written with
colons instead of commas.
16в.8 Summary
- The HC-05 is a master or a slave, the HC-06 a slave only; both are a 3.3 V UART for the microcontroller and a serial port for the phone. On an F103 PA10 is 5 V tolerant, the "1" threshold is 0.65·VDD, 2.145 V at 3.3 V.
- KEY = 1 at power-up is AT mode at 38 400 baud; the module's supply goes through a switch so the firmware switches modes itself (200 ms without supply). Commands end with CR LF; the set and defaults are per the documentation, not verified.
- Setup: ask → on a mismatch write → ask again. From the factory — 14 commands and 3 writes in 1192 ms, a repeated start — 8 commands without writes in 1117 ms.
- Through the air bytes travel in bursts: a 200-byte block arrived in 4 pieces, a byte in 13.2…34.4 ms (model parameters). A message must survive being cut.
- The frame: leading 00, type, number, length, data, CRC-16/XMODEM, COBS,
trailing 00 — 8 bytes of overhead, up to 72 bytes in total; the CRC of
123456789is0x31C3. - Acknowledgement of every frame and retransmission every 250 ms: with 6 breaks 1000 of 1000 messages arrived, without the protocol — 706 of 1000.
- STATE with a 20 ms filter pauses retransmissions for a break and restarts them right after the link returns, but it drops late — acknowledgements cover the losses in that gap.
- The HC-05 is not for a battery: the radio keeps the link all the time. For sleep — BLE.
- The driver with the protocol is 800 bytes of RAM and about 2.9 KB of code on the Cortex-M3.
Full sources
hc05.h — driver and frames: buffers, states, events and prototypes.
/* 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 — the HC-05 module: AT mode, settings check, transparent mode, 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 — the frame over the link: COBS, CRC-16, acknowledgement and retransmission.
/* 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 firmware: USART1 with interrupts, supply switch, KEY, STATE, the main loop.
/* 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(); /* до следующего прерывания: байта или тика */
}
}
Next: section 16г "MP3 module DFPlayer Mini over UART: command frame, checksum, track queue, volume and module replies" — the same UART, but at the other end a module with its own 10-byte frame, checksum, acknowledgements and "track finished" events.