Embedded HandbookПриложения
Все разделы › XXIV. Встраиваемый Linux
STM32F103, STM32F407 · уровень средний · 15 мин чтения
Открыть в интерактивном справочнике

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.

Что порт по умолчанию сделал с четырьмя байтами 11 0D 22 33: IXON съел 0x11 как «продолжай», ICRNL превратил 0x0D в 0x0A, ICANON задержал 0x22 и 0x33 до конца строки — первое read отдало один байт 0A, после ещё одного перевода строки пришли 22 33 0A


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 и VTIME на одних и тех же байтах: когда вернулся read и сколько отдал; прогон на паре псевдотерминалов

Для прибора обычно берут 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, ноль

Кадр COBS с CRC-16 на линии UART и восстановление после испорченного кадра

Порядок на передающей стороне — на микроконтроллере или на Linux, код один и тот же:

  1. к данным дописать CRC-16 (раздел 43.1; здесь CRC-16/XMODEM: полином 0x1021, начальное 0 — для строки 123456789 получается 0x31C3, как во вкладке «CRC» калькуляторов);
  2. закодировать всё это в COBS;
  3. дописать разделитель 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 Итоги

Полные исходники

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: два слота, подтверждение и откат».

Открыть в интерактивном справочнике