194. Связь с микроконтроллером: порт со стороны Linux, кадры COBS и CRC
В разделе 191.1 прибор разделён надвое: микроконтроллер держит реальное время, процессор на Linux — сеть и хранение. Разговаривают они чаще всего по UART. Со стороны микроконтроллера порт устроен просто (раздел 16), а со стороны Linux между проводом и программой стоит драйвер терминала — и по умолчанию он портит двоичные данные. Этот раздел о том, как это выключить и как нарезать поток байтов на кадры, которые переживают помеху.
Опыт — пара псевдотерминалов (openpty) в образе stm-linux:1: у них тот же
драйвер tty, что у /dev/ttyS0 и /dev/ttyUSB0, поэтому поведение порта
одно и то же. Программа _verify/linux194/link194.c, сверяет
_verify/linux194_check.py.
Как это называется в документации
Оригинал Перевод canonical mode канонический режим: драйвер отдаёт данные строками raw mode сырой режим: байты как есть termios структура настроек терминала line discipline обработка потока драйвером терминала software flow control (XON/XOFF) программное управление потоком байтами 0x11 и 0x13 consistent overhead byte stuffing (COBS) кодирование, убирающее нули из данных frame delimiter разделитель кадров resynchronization восстановление после сбоя в потоке
194.1 Порт по умолчанию портит данные
Открыли /dev/ttyS0, записали в него с микроконтроллера четыре байта
11 0D 22 33 — и прочитали один байт 0A. В опыте так и вышло:
| Шаг | Что получила программа |
|---|---|
первое read |
0A |
| дальше, пока нет перевода строки | ничего: poll отвечает «данных нет» |
после ещё одного \n |
22 33 0A |
Виноваты три настройки, которые у порта включены по умолчанию:
| Флаг | Что делает | Что сделал с нашими байтами |
|---|---|---|
IXON |
байты 0x11 и 0x13 — это «продолжай» и «стоп» передатчику |
0x11 съеден без следа |
ICRNL |
переводит 0x0D в 0x0A |
0x0D стал 0x0A |
ICANON |
отдаёт данные только целыми строками | 0x22 0x33 ждут конца строки |
Плюс ECHO — драйвер отправляет принятое обратно в линию, и микроконтроллер
получает собственные байты. Для терминала с человеком всё это удобно; для
обмена с прибором — катастрофа, которая к тому же проявляется не всегда,
а только на пакетах, где случайно встретились 0x0D, 0x11 или 0x13.
194.2 Сырой режим
Почти всё лишнее выключает одна функция:
#include <termios.h>
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY);
struct termios t;
tcgetattr(fd, &t);
cfmakeraw(&t); /* ICANON, ECHO, ICRNL, IXON, OPOST — выключены */
cfsetispeed(&t, B115200);
cfsetospeed(&t, B115200);
t.c_cc[VMIN] = 0;
t.c_cc[VTIME] = 5; /* ждать до 0,5 с, в десятых долях секунды */
t.c_cflag |= CLOCAL | CREAD; /* не ждать сигнала DCD, включить приём */
t.c_cflag &= ~(CSTOPB | CRTSCTS); /* один стоп-бит, без RTS/CTS */
t.c_iflag &= ~(IXOFF | IXANY);
tcsetattr(fd, TCSANOW, &t);
tcflush(fd, TCIFLUSH); /* выбросить то, что пришло до настройки */
После неё в опыте ICANON, ECHO, ICRNL и OPOST сброшены, и те же
четыре байта приходят как есть: 11 0D 22 33.
Трогает cfmakeraw не всё: в c_cflag он ставит восемь бит и снимает
чётность, но оставляет как было два стоп-бита CSTOPB, аппаратное
управление потоком CRTSCTS, CLOCAL и CREAD, а в c_iflag — IXOFF
и IXANY. tcgetattr отдаёт то, что оставила на порту предыдущая
программа, поэтому эти флаги задают явно — строки после VTIME.
tcflush выбрасывает байты, накопленные до настройки.
Скорость задаётся не числом, а константой: B115200 — это 0x1002,
а не 115 200 (так в Linux с glibc; в BSD и macOS константа совпадает
с числом). Передать в cfsetispeed число 115200 — частая ошибка: компилятор
её не заметит, а вызов вернёт −1. Константа работает везде, поэтому пишут её.
Сколько ждёт read — задают два поля:
VMIN |
VTIME |
Поведение read |
В опыте |
|---|---|---|---|
| 0 | 5 | ждёт до 0,5 с первого байта; нет — вернёт 0 | вернул 0 примерно через полсекунды |
| 0 | 0 | не ждёт вовсе, отдаёт что есть | вернул 0 сразу |
| N | 0 | ждёт, пока наберётся N байт, хоть вечно | при N = 3 два байта не отдал, пока не пришёл третий |
| N | T | ждёт первый байт сколько угодно, потом возвращается, когда набралось N байт или пауза между байтами дольше T | при N = 3, T = 5 два байта отдал после 0,5 с тишины |
Для прибора обычно берут VMIN = 0, VTIME в несколько десятых секунды
или poll с таймаутом: программа не виснет навсегда, если микроконтроллер
замолчал, и может заметить это сама.
194.3 Кадры: зачем разделитель
UART передаёт поток байтов, а не пакеты. Если приёмник включился посреди передачи или помеха испортила байт, нужно найти начало следующего пакета. Отсюда правило: в потоке должен быть байт, который встречается только на границе кадра. Удобнее всего — ноль.
Но в данных нули есть почти всегда: номер пакета, температура, счётчики. Их убирает кодирование COBS. Данные режутся на куски до ближайшего нуля, и перед каждым куском ставится его длина плюс один, а сам ноль выбрасывается:
данные 11 00 22
COBS 02 11 02 22 02 — «один байт, потом был ноль», 02 — «один байт до конца»
кадр 02 11 02 22 00 00 — конец кадра, внутри нулей больше нет
Цена — предсказуемая, в этом название (consistent overhead):
| Данные | После COBS |
|---|---|
11 00 22 |
4 байта: 02 11 02 22 |
| 10 нулей | 11 байт |
| 254 байта без нулей | 256 байт |
| 508 байт без нулей | 511 байт |
Часть библиотек после полного блока из 254 байт последний код 01 не пишет и даёт 255 и 510 байт; декодер принимает оба варианта, но длины кадров у двух сторон могут разойтись. Не больше одного байта на каждые 254 байта данных и ещё один в начале — в отличие от «экранирования» спецсимволов, где пакет из одних спецсимволов раздувается вдвое.
194.4 Кадр целиком: данные, CRC, COBS, ноль
Порядок на передающей стороне — на микроконтроллере или на Linux, код один и тот же:
- к данным дописать CRC-16 (раздел 43.1; здесь CRC-16/XMODEM: полином
0x1021, начальное 0 — для строки123456789получается0x31C3, как во вкладке «CRC» калькуляторов); - закодировать всё это в COBS;
- дописать разделитель
00.
size_t frame(const uint8_t *data, size_t n, uint8_t *out)
{
uint8_t buf[256];
if (n > sizeof buf - 2) return 0; /* данные + CRC должны поместиться */
memcpy(buf, data, n);
uint16_t c = crc16(data, n);
buf[n] = c >> 8;
buf[n + 1] = c & 0xFF;
size_t m = cobs_encode(buf, n + 2, out);
out[m++] = 0; /* конец кадра */
return m;
}
Пакет телеметрии в опыте — восемь байт с тремя нулями внутри:
01 00 09 C4 0C E4 00 00 (номер, температура в сотых долях градуса,
напряжение в милливольтах). Кадр получился 12 байт:
02 01 05 09 C4 0C E4 01 03 86 4C 00
Нулей внутри кадра нет, последний байт — разделитель. На 115 200 бод при 10 битах на байт такой кадр идёт по линии 1042 мкс.
Приём — зеркально: копить байты до нуля, раскодировать COBS, сверить CRC. Не сошлось — кадр выбросить, и со следующего нуля всё снова в порядке. В опыте три кадра подряд, во втором испорчен один байт: приёмник принял первый и третий, второй отбросил по CRC — одна потеря, без сбоя всего потока.
for (;;) {
int n = read(fd, rx, sizeof rx); /* VMIN = 0, VTIME = 5 */
for (int i = 0; i < n; i++) {
if (rx[i] != 0) {
if (al < sizeof acc) acc[al++] = rx[i];
else overflow = 1; /* длиннее допустимого — кадр испорчен */
continue;
}
if (overflow) { overflow = 0; al = 0; errors++; continue; }
long dl = cobs_decode(acc, al, dec);
al = 0;
if (dl >= 3 && crc16(dec, dl - 2) == (dec[dl - 2] << 8 | dec[dl - 1]))
handle(dec, dl - 2); /* целый кадр */
else
errors++; /* испорчен — ждём следующий ноль */
}
}
Накопитель acc берут по самому длинному кадру: 254 байта данных и CRC
после COBS — 258 байт; всё, что длиннее, заведомо мусор.
На микроконтроллере то же самое удобно принимать через DMA с флагом
«линия замолчала» (раздел 16.5): разделитель 00 и пауза после кадра
вместе дают надёжную границу.
194.5 Грабли
Программа читает «не то» только иногда. Порт в каноническом режиме:
0x11 и 0x13 съедает управление потоком, 0x0D становится 0x0A,
данные отдаются строками. Пакеты без этих байтов проходят, и ошибка кажется
случайной. Лечение — cfmakeraw один раз при открытии порта.
Микроконтроллер получает собственные байты обратно. Включено эхо
(ECHO). Тот же cfmakeraw.
cfsetispeed(&t, 115200). Скорость — константа B115200, то есть
0x1002; число 115200 даст другую скорость или ошибку.
read висит вечно. VMIN > 0 при VTIME = 0: ждёт, пока наберётся
столько байт. Для прибора — VMIN = 0 и таймаут, или poll.
В данных — сообщения ядра или приглашение ко входу. Порт занят
консолью: ядро пишет в него свои сообщения, служба входа отвечает на байты.
Убрать порт из параметра console= командной строки ядра и запретить
службу входа на нём: systemctl mask serial-getty@ttyS0.
open возвращает «Permission denied». Пользователь не в группе
dialout, которой принадлежат последовательные порты: usermod -aG dialout
имя и войти заново.
Кадр склеился с соседним. Нет разделителя, и приёмник угадывает границы
по паузам — после первой же задержки в передаче угадывает неправильно.
Разделитель 00 после COBS делает границу однозначной.
Проверили CRC до раскодирования COBS. Контрольная сумма считается по исходным данным, а не по закодированным байтам.
Буфер приёма переполнился на мусоре. Если нули долго не приходят (помеха, неверная скорость), накопитель надо сбрасывать по длине: кадр длиннее самого длинного допустимого — заведомо испорчен.
194.6 Итоги
- Порт
ttyпо умолчанию работает для человека:ICANON,ECHO,ICRNLиIXONвключены. В опыте из11 0D 22 33программа получила0A, остальное ждало перевода строки. cfmakerawвыключает всё это; скорость — константойB115200(0x1002).VMIN = 0,VTIME = 5— ждать не дольше полсекунды;VTIME = 0— не ждать.- Поток режут на кадры разделителем
00, а нули внутри убирает COBS: не больше байта на 254 байта данных (254 → 256, 508 → 511). - Кадр = данные + CRC-16 → COBS →
00. Пакет телеметрии из 8 байт дал кадр 12 байт, 1042 мкс на 115 200 бод; испорченный кадр отбрасывается по CRC, следующий принимается с ближайшего нуля. - Один и тот же код кадров работает на обеих сторонах — на STM32 и на Linux.
Полные исходники
link194.c — опыт раздела: сырой режим порта, VMIN/VTIME и кадры COBS + CRC на паре псевдотерминалов.
/* Раздел 194: Linux-половина прибора говорит с микроконтроллером по UART.
* Опыт на паре псевдотерминалов (openpty) — тот же драйвер tty, что и у /dev/ttyS0:
* 1) что делает с байтами «обычный» режим порта и что меняет cfmakeraw;
* 2) VMIN/VTIME: сколько ждёт read;
* 3) кадры COBS + CRC-16/XMODEM: кодирование, накладные расходы, пересинхронизация
* после испорченного кадра.
* Печатает строки ключ=значение; сверяет _verify/linux194_check.py. */
#define _DEFAULT_SOURCE
#include <errno.h>
#include <poll.h>
#include <pty.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
/* ── CRC-16/XMODEM: полином 0x1021, начальное 0 — как в калькуляторе и разделе 43 ── */
static uint16_t crc16(const uint8_t *p, size_t n)
{
uint16_t c = 0;
while (n--) {
c ^= (uint16_t)(*p++) << 8;
for (int i = 0; i < 8; i++)
c = (c & 0x8000u) ? (uint16_t)((c << 1) ^ 0x1021u) : (uint16_t)(c << 1);
}
return c;
}
/* ── COBS: убирает нули из данных, чтобы ноль означал только «конец кадра» ── */
static size_t cobs_encode(const uint8_t *in, size_t n, uint8_t *out)
{
size_t w = 1, code_at = 0;
uint8_t code = 1;
for (size_t i = 0; i < n; i++) {
if (in[i] == 0) {
out[code_at] = code;
code_at = w++;
code = 1;
} else {
out[w++] = in[i];
if (++code == 0xFF) {
out[code_at] = code;
code_at = w++;
code = 1;
}
}
}
out[code_at] = code;
return w;
}
static long cobs_decode(const uint8_t *in, size_t n, uint8_t *out)
{
size_t r = 0, w = 0;
while (r < n) {
uint8_t code = in[r++];
if (code == 0)
return -1;
for (uint8_t i = 1; i < code; i++) {
if (r >= n)
return -1;
out[w++] = in[r++];
}
if (code != 0xFF && r < n)
out[w++] = 0;
}
return (long)w;
}
/* кадр = COBS(данные + CRC старшим байтом вперёд) + 0x00 */
static size_t frame(const uint8_t *data, size_t n, uint8_t *out)
{
uint8_t buf[600];
memcpy(buf, data, n);
uint16_t c = crc16(data, n);
buf[n] = (uint8_t)(c >> 8);
buf[n + 1] = (uint8_t)c;
size_t m = cobs_encode(buf, n + 2, out);
out[m++] = 0;
return m;
}
static double ms_since(const struct timespec *t0)
{
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (t.tv_sec - t0->tv_sec) * 1e3 + (t.tv_nsec - t0->tv_nsec) / 1e6;
}
int main(void)
{
int m, s;
if (openpty(&m, &s, NULL, NULL, NULL) != 0) {
perror("openpty");
return 1;
}
/* ── 1. порт «как есть»: канонический режим, эхо, перевод CR ─────────── */
struct termios t;
tcgetattr(s, &t);
printf("default_icanon=%d\n", (t.c_lflag & ICANON) ? 1 : 0);
printf("default_echo=%d\n", (t.c_lflag & ECHO) ? 1 : 0);
printf("default_icrnl=%d\n", (t.c_iflag & ICRNL) ? 1 : 0);
printf("default_ixon=%d\n", (t.c_iflag & IXON) ? 1 : 0);
printf("B115200=0x%X\n", (unsigned)B115200);
/* четыре двоичных байта. В каноническом режиме: 0x11 — это XON, его съедает
* управление потоком; 0x0D превращается в 0x0A и заканчивает «строку»;
* 0x22 0x33 ждут следующего перевода строки */
const uint8_t bin[4] = {0x11, 0x0D, 0x22, 0x33};
write(m, bin, sizeof bin);
struct pollfd pf = {.fd = s, .events = POLLIN};
uint8_t got[16];
int k = poll(&pf, 1, 200) > 0 ? (int)read(s, got, sizeof got) : 0;
printf("canon_first=");
for (int i = 0; i < k; i++)
printf("%s%02X", i ? " " : "", got[i]);
printf("\n");
printf("canon_waiting=%d\n", poll(&pf, 1, 200)); /* 0x22 0x33 без конца строки не отдаются */
write(m, "\n", 1);
k = poll(&pf, 1, 200) > 0 ? (int)read(s, got, sizeof got) : 0;
printf("canon_second=");
for (int i = 0; i < k; i++)
printf("%s%02X", i ? " " : "", got[i]);
printf("\n");
tcflush(s, TCIOFLUSH);
tcflush(m, TCIOFLUSH);
/* ── 2. сырой режим: байты как есть ───────────────────────────────── */
cfmakeraw(&t);
cfsetispeed(&t, B115200);
cfsetospeed(&t, B115200);
t.c_cc[VMIN] = 0;
t.c_cc[VTIME] = 5; /* десятые доли секунды: 0,5 с */
tcsetattr(s, TCSANOW, &t);
struct termios r;
tcgetattr(s, &r);
printf("raw_icanon=%d\n", (r.c_lflag & ICANON) ? 1 : 0);
printf("raw_echo=%d\n", (r.c_lflag & ECHO) ? 1 : 0);
printf("raw_icrnl=%d\n", (r.c_iflag & ICRNL) ? 1 : 0);
printf("raw_opost=%d\n", (r.c_oflag & OPOST) ? 1 : 0);
printf("raw_speed_ok=%d\n", cfgetispeed(&r) == B115200);
write(m, bin, sizeof bin);
usleep(50000);
k = (int)read(s, got, sizeof got);
printf("raw_bytes=");
for (int i = 0; i < k; i++)
printf("%s%02X", i ? " " : "", got[i]);
printf("\n");
/* VMIN=0, VTIME=5: пустой порт — read вернёт 0 через полсекунды */
struct timespec t0;
clock_gettime(CLOCK_MONOTONIC, &t0);
k = (int)read(s, got, sizeof got);
double waited = ms_since(&t0);
printf("vtime_read=%d\n", k);
printf("vtime_ms_ok=%d\n", waited >= 450 && waited <= 1500);
/* VMIN=0, VTIME=0: не ждёт вовсе */
r.c_cc[VTIME] = 0;
tcsetattr(s, TCSANOW, &r);
clock_gettime(CLOCK_MONOTONIC, &t0);
k = (int)read(s, got, sizeof got);
waited = ms_since(&t0);
printf("poll_read=%d\n", k);
printf("poll_ms_ok=%d\n", waited < 50);
/* ── 3. кадры COBS + CRC ──────────────────────────────────────────── */
printf("crc_123456789=0x%04X\n", crc16((const uint8_t *)"123456789", 9));
uint8_t enc[700];
const uint8_t ex[3] = {0x11, 0x00, 0x22};
size_t n = cobs_encode(ex, 3, enc);
printf("cobs_example=");
for (size_t i = 0; i < n; i++)
printf("%s%02X", i ? " " : "", enc[i]);
printf("\n");
uint8_t big[600];
for (int i = 0; i < 254; i++)
big[i] = (uint8_t)(i + 1); /* 254 байта без единого нуля */
printf("cobs_254_nonzero=%zu\n", cobs_encode(big, 254, enc));
memset(big, 0, 10);
printf("cobs_10_zeros=%zu\n", cobs_encode(big, 10, enc));
for (int i = 0; i < 508; i++)
big[i] = (uint8_t)(i % 255 + 1);
printf("cobs_508_nonzero=%zu\n", cobs_encode(big, 508, enc));
/* пакет телеметрии: номер, температура ×100, напряжение в мВ — с нулями внутри */
const uint8_t pkt[8] = {0x01, 0x00, 0x09, 0xC4, 0x0C, 0xE4, 0x00, 0x00};
uint8_t f[40];
size_t fl = frame(pkt, sizeof pkt, f);
printf("frame_len=%zu\n", fl);
printf("frame_zeros_inside=%d\n", (int)(memchr(f, 0, fl - 1) != NULL));
printf("frame_bytes=");
for (size_t i = 0; i < fl; i++)
printf("%s%02X", i ? " " : "", f[i]);
printf("\n");
/* поток из трёх кадров, в среднем испорчен один байт — приёмник теряет только его */
uint8_t stream[200];
size_t sl = 0;
for (int j = 0; j < 3; j++) {
uint8_t p[8];
memcpy(p, pkt, 8);
p[0] = (uint8_t)(j + 1);
sl += frame(p, 8, stream + sl);
}
stream[fl + 3] ^= 0x40; /* помеха в середине второго кадра */
write(m, stream, sl);
usleep(50000);
uint8_t rx[200];
int got_n = (int)read(s, rx, sizeof rx);
printf("stream_len=%zu\n", sl);
printf("stream_rx=%d\n", got_n);
int good = 0, bad = 0;
uint8_t acc[200], dec[200];
size_t al = 0;
printf("frames_ok=");
for (int i = 0; i < got_n; i++) {
if (rx[i] != 0) {
acc[al++] = rx[i];
continue;
}
long dl = cobs_decode(acc, al, dec);
al = 0;
if (dl < 3 || crc16(dec, (size_t)dl - 2) != (uint16_t)((dec[dl - 2] << 8) | dec[dl - 1])) {
bad++;
continue;
}
printf("%s%d", good ? " " : "", dec[0]);
good++;
}
printf("\n");
printf("frames_good=%d\n", good);
printf("frames_bad=%d\n", bad);
/* 115200 бод, 8N1 — 10 бит на байт: время кадра в линии */
printf("frame_us_115200=%.0f\n", fl * 10.0 / 115200 * 1e6);
printf("done=1\n");
return 0;
}
Дальше: раздел 195 «Обновление по схеме A/B: два слота, подтверждение и откат».