Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32F103, STM32G474 · уровень средний · 28 мин чтения
Открыть в интерактивном справочнике

198е. Мини-проект: CAN-сниффер на STM32 — протокол SLCAN поверх USB CDC

Раздел 19 разобрал bxCAN изнутри, 19а — FDCAN, 19б — протокол CANopen поверх него. Законченного прибора для шины там нет. Здесь он появится: сниффер, который слушает шину и отдаёт каждый кадр на компьютер строкой текста. Язык строк — SLCAN (его ещё называют протоколом LAWICEL): команды S6, O, t1238… понимает python-can и другие программы для CAN, так что писать свою программу на стороне компьютера не нужно.

Прибор собран в двух вариантах. На BluePill (F103) — bxCAN и USART1 через переходник USB–UART: собственный USB этого кристалла с CAN не уживается. На G474 — FDCAN2 и USB CDC из раздела 20.7. Ядро протокола одно и то же, оно же собирается на компьютере и сверяется с моделью и с python-can.

Опоры: RM0008 (глава bxCAN: режим «только слушать», фильтры, очередь приёма, рисунок 234 с длиной кадра; общая память USB и CAN) и RM0440 (глава FDCAN: память сообщений, режим наблюдения) из паков Keil, даташит DS12288 (ножки FDCAN2 и BOOT0 у G474). С формальным описанием SLCAN формат не сверялся: строки сверены с модулем slcan.py из python-can 4.6.1 (окружение Zephyr). Длина кадра и вставка битов — модель из 19б.

Как это называется в документации

Оригинал Перевод
silent mode (SILM) режим «только слушать» у bxCAN
bus monitoring mode (MON) тот же режим у FDCAN: режим наблюдения
identifier mask mode фильтр в режиме маски: сравниваются только биты маски
receive FIFO (FMP0, RFOM0, FOVR0) очередь приёма: сколько кадров, отпустить кадр, переполнение
bus idle: 11 consecutive recessive bits шина свободна: 11 рецессивных бит подряд
shared USB/CAN SRAM общая память буферов USB и CAN у F103
message RAM память сообщений FDCAN
SLCAN, LAWICEL protocol текстовый протокол адаптера CAN поверх последовательного порта

198е.1 Что делает прибор и из чего он собран

Сниффер включается в шину через трансивер и молчит: не подтверждает кадры, не шлёт кадров ошибки, ничего не передаёт. Каждый принятый кадр он кладёт в кольцо, превращает в строку и отдаёт компьютеру. Команды с компьютера задают скорость и открывают канал.

Путь кадра в сниффере: шина CAN и трансивер, bxCAN F103 в режиме только слушать с фильтром принимать всё, кольцо ядра на 128 кадров, главный цикл делает строку SLCAN, USART1 на 921 600 бод и переходник USB–UART к python-can; внизу поток при полной загрузке и замечание про общую память USB и CAN у F103

Вариант на BluePill:

Почему не USB самого F103, сказано в 19.8, а RM0008 говорит точно: USB и CAN делят выделенную память на 512 байт по адресу 0x4000 6000 и одновременно работать не могут. Отсюда переходник. У G474 память USB (0x4000 6000) и память сообщений FDCAN (0x4000 A400) раздельные, поэтому второй вариант отдаёт те же строки прямо в USB CDC.


198е.2 Протокол SLCAN: строки и ответы

Всё общение — строки ASCII, каждая кончается символом CR (\r). Ответ «выполнено» — CR, отказ — BEL (\a, код 7). Наше ядро понимает такие команды:

Команда Что делает Ответ
S0…S8 скорость 10, 20, 50, 100, 125, 250, 500, 750 кбит/с, 1 Мбит/с CR; на открытом канале — BEL
O / L открыть канал: обычный режим / только слушать CR; без скорости — BEL
C закрыть канал CR
t T r R передать кадр: 11 или 29 бит, данные или запрос CR; в режиме L — BEL
F флаги ошибок, две шестнадцатеричные цифры F00\r …
V / N версия / номер адаптера V0101\r, N0198\r
Z0 / Z1 выключить / включить метки времени CR

Кадр в обе стороны пишется одинаково: буква, идентификатор (три цифры для 11 бит, восемь для 29), одна цифра DLC и данные по две цифры на байт. У запросов r и R данных нет. Строки из прогона ядра:

t12388081828384858687\r          11 бит, 0x123, 8 байт
T1ABCDEF08A8ABAEB1B4B7BABD\r     29 бит, 0x1ABCDEF0, 8 байт
t123880818283848586871008\r      то же с меткой Z1: 0x1008 мс

Метка — четыре шестнадцатеричные цифры миллисекунд по кругу. Это наше соглашение: у адаптеров разных фирм счёт метки разный, а python-can хвост после данных не читает вовсе. Биты ответа F тоже наши: 0x01 — кольцо переполнялось, 0x02 — переполнялась очередь приёма bxCAN, 0x04 — узел в пассивном состоянии, 0x08 — отключился от шины.

Самая длинная строка кадра — T с восемью байтами и меткой: 31 символ вместе с CR. Буфер строки в ядре — 32 символа.


198е.3 Пропускная способность: где узкое место

Худший случай для сниффера — шина, забитая на 100 % самыми короткими кадрами. Длина кадра по RM0008 (рис. 234): стандартный 44 + 8n бит, расширенный 64 + 8n, плюс 3 бита паузы. Вставка битов кадр только удлиняет — значит, без неё кадров в секунду больше всего. При 500 кбит/с:

Кадр Бит без вставок / худший Кадров/с Символов в строке (с Z1) Символов/с (с Z1)
t, DLC 0 47 / 55 10 638 6 (10) 63 830 (106 383)
t, DLC 8 111 / 135 4 505 22 (26) 99 099 (117 117)
T, DLC 0 67 / 80 7 463 11 (15) 82 090 (111 940)
T, DLC 8 131 / 160 3 817 27 (31) 103 053 (118 321)

Пик — 103 053 символа в секунду без меток и 118 321 с метками. Теперь каналы. Символ UART — 10 бит (старт, 8 данных, стоп):

Узкое место на F103 — UART: даже 921 600 бод чуть не дотягивают до полной шины 500 кбит/с. Кольцо сглаживает всплески: на 128 кадрах при полной загрузке кадрами t DLC 8 оно наполнится через 415 мс на 921 600 бод и за 32 мс на 115 200. Дальше кадры теряются, и ядро поднимает флаг 0x01 в F.


198е.4 Прошивка F103: bxCAN слушает, прерывание кладёт в кольцо

bxCAN тактуется от APB1 = 36 МГц. Бит делится на кванты: один квант синхронизации, TS1 и TS2 (19.4). Таблица скоростей в прошивке проверяет себя сама: _Static_assert не даст собрать скорость, которая не делится ровно.

Команда Скорость Делитель Квантов всего (в скобках TS1 + TS2; ещё один — квант синхронизации) Точка выборки
S0…S7 10…750 кбит/с 450…6 8 (6 + 1) 87,5 %
S8 1 Мбит/с 4 9 (7 + 1) 88,9 %

Открытие канала — вход в режим инициализации, битовый тайминг, фильтр и выход:

    CAN1->BTR = (listen ? CAN_BTR_SILM : 0u)    /* «только слушать»: на шину — ни одного доминантного бита */
              | ((uint32_t)(bt[speed].ts2 - 1u) << 20)
              | ((uint32_t)(bt[speed].ts1 - 1u) << 16)
              | (uint32_t)(bt[speed].brp - 1u);  /* SJW = 1 квант */

    CAN1->FMR |= CAN_FMR_FINIT;                 /* фильтр 0: маска из нулей — принимать всё */
    CAN1->FA1R &= ~1u;
    CAN1->FS1R |= 1u;
    CAN1->FM1R &= ~1u;
    CAN1->sFilterRegister[0].FR1 = 0;
    CAN1->sFilterRegister[0].FR2 = 0;
    CAN1->FFA1R &= ~1u;
    CAN1->FA1R |= 1u;
    CAN1->FMR &= ~CAN_FMR_FINIT;

Бит SILM — главное для сниффера. По RM0008 в режиме «только слушать» bxCAN принимает кадры, но на шину выдаёт только рецессивные биты: бит подтверждения и флаг ошибки заворачиваются внутрь. Такой узел не вмешивается в шину, даже если скорость выбрана неверно.

Фильтр — один 32-битный в режиме маски: и идентификатор, и маска — нули, то есть ни один бит не сравнивается. Без фильтра bxCAN не принимает ничего (19.5).

Приём — в прерывании очереди 0 (вектор 20, общий с USB). Кадр вынимается из ящика, ящик сразу отпускается, кадр уходит в кольцо ядра:

void USB_LP_CAN1_RX0_IRQHandler(void)
{
    while (CAN1->RF0R & CAN_RF0R_FMP0) {
        uint32_t rir = CAN1->sFIFOMailBox[0].RIR;
        uint32_t lo = CAN1->sFIFOMailBox[0].RDLR, hi = CAN1->sFIFOMailBox[0].RDHR;
        slcan_frame f;
        f.flags = (uint8_t)(((rir & CAN_RI0R_IDE) ? SLCAN_EXT : 0u) | ((rir & CAN_RI0R_RTR) ? SLCAN_RTR : 0u));
        f.id = (rir & CAN_RI0R_IDE) ? (rir >> 3) : (rir >> 21);
        f.dlc = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0xFu);
        f.ts = (uint16_t)ms;
        for (unsigned i = 0; i < 4u; i++) {
            f.data[i] = (uint8_t)(lo >> (8u * i));
            f.data[4u + i] = (uint8_t)(hi >> (8u * i));
        }
        CAN1->RF0R = CAN_RF0R_RFOM0;    /* отпустить ящик: следующий кадр очереди */
        slcan_rx_push(&f);
    }

Запись RF0R = RFOM0 безопасна: флаги FULL0 и FOVR0 снимаются записью единицы, нули на них не действуют.

Главный цикл делает две вещи: байты команд из USART1 по одному отдаёт ядру и, если в кольце есть кадр, пишет его строку в порт. Прошивка целиком (стартовый код, сниффер, ядро) — 2260 байт флеш-памяти и 2188 байт ОЗУ, из них 2048 — кольцо на 128 кадров по 16 байт.

Вариант на G474. FDCAN2 на PB12 и PB13 (AF9): PA11 и PA12 заняты USB, а PB8 у G474 — это ножка BOOT0. Режим «только слушать» — бит MON в CCCR, фильтров нет вовсе (RXGFC = 0: всё в очередь 0). Очередь 0 FDCAN2 лежит в памяти сообщений по смещению 0x350 + 0xB0: блок одного FDCAN — 212 слов, перед очередью 28 фильтров по 11 бит и 8 по 29 бит, элемент — 18 слов. От 170 МГц ровно делятся все скорости, кроме S7: 170 000 000 / 750 000 = 226,67, и на O после S7 ядро ответит BEL. S1…S8 идут по 17 квантов, S0 — по 34 (делитель не больше 512), точка выборки везде 88,2 %. Строки уходят через CDC_Transmit_FS двумя буферами: пока один передаётся, второй заполняется.


198е.5 Ядро: кольцо, строки, команды

Кольцо — один писатель (прерывание CAN) и один читатель (главный цикл). Каждый двигает только свой счётчик, поэтому запрет прерываний не нужен. Места нет — кадр не затирает старый, а считается потерянным:

void slcan_rx_push(const slcan_frame *f)
{
    if (head - tail >= SLCAN_RING) {    /* места нет: считаем, но не затираем */
        lost++;
        lost_flag = 1;
        return;
    }
    ring[head % SLCAN_RING] = *f;
    if (ring[head % SLCAN_RING].dlc > 8u)
        ring[head % SLCAN_RING].dlc = 8u;   /* DLC 9…15 в классическом CAN — те же 8 байт */
    head++;                             /* после данных: цикл не увидит недописанный кадр */
}

Строка кадра собирается без printf — таблицей из 16 цифр:

unsigned slcan_format(const slcan_frame *f, char *out)
{
    char *p = out;
    int ext = (f->flags & SLCAN_EXT) != 0;
    *p++ = (f->flags & SLCAN_RTR) ? (ext ? 'R' : 'r') : (ext ? 'T' : 't');
    p = hexn(p, f->id, ext ? 8u : 3u);
    *p++ = (char)('0' + f->dlc);
    if (!(f->flags & SLCAN_RTR))
        for (unsigned i = 0; i < f->dlc; i++)
            p = hexn(p, f->data[i], 2u);
    if (stamps)
        p = hexn(p, f->ts, 4u);         /* Z1: четыре цифры миллисекунд */
    *p++ = '\r';
    return (unsigned)(p - out);
}

Команды разбирает slcan_command: строго по длине (строка t обязана иметь ровно столько цифр данных, сколько велит DLC), идентификатор не больше 0x7FF или 0x1FFFFFFF. Строка длиннее 32 символов отвергается целиком, символ LF пропускается: терминал с концом строки CR LF — не ошибка.


198е.6 Проверка на компьютере: то же ядро против модели и python-can

Ядро (slcan.c) собирается gcc в контейнере -Wall -Wextra -Werror -pedantic вместе с обвязкой: она подаёт ядру байты «с компьютера», кадры «с шины» и печатает всё, что ядро отдаёт. Рядом — модель на Python, написанная отдельно, не переводом C.

Ядро против модели. 3386 шагов: случайные и граничные строки (верные, почти верные, мусор, длиннее 32 символов, с CR LF), кадры с DLC до 15, отказ открытия, переполнение. Совпали все 3386: 3004 ответа на команды, 287 строк кадров, 273 вызова платы. После 130 кадров без выдачи и ядро, и модель насчитали 2 потерянных.

python-can против ядра. python-can 4.6.1 пишет в поддельный порт, байты уходят в ядро на C, ответы читает python-can:

Прошивки на железе не запускались. sniffer_f103.c, start_f103.c, slcan.c и can_g474.c собраны armclang без предупреждений; F103 целиком скомпонован arm-none-eabi-gcc с -Werror — отсюда размеры в 198е.4.


198е.7 Грабли

Подключили сниффер — на шине пошли ошибки. Узел в обычном режиме подтверждает каждый чужой кадр, а при неверной скорости шлёт кадры ошибки. Снифферу нужен режим «только слушать»: L вместо O, бит SILM (у FDCAN — MON).

CAN и USB на F103 вместе не работают. Память 512 байт общая (RM0008). Выход — отдать строки через USART и переходник или взять G474, где памяти раздельные.

После переназначения CAN пропал отладчик. Биты SWJ_CFG в AFIO_MAPR только для записи: по RM0008 при чтении значение не определено. Чтение-изменение-запись MAPR может записать туда мусор и выключить SWD. Поэтому прошивка пишет MAPR целиком: CAN_REMAP = 10, SWJ_CFG = 000.

O отвечает BEL, хотя скорость задана. Выход из инициализации ждёт 11 рецессивных бит подряд на CAN_RX. Нет трансивера или шины — бит INAK не снимается, и plat_can_open сдаётся по таймауту.

python-can шлёт L дважды. В версии 4.6.1 set_bitrate сам открывает канал, а затем конструктор открывает его ещё раз. Второе L на открытом канале ядро отвергает, python-can ответа не проверяет, и всё работает. Своё ядро на повторное открытие лучше не ронять.

Кадр с DLC 9 приходит как 12 байт. В классическом CAN DLC 9…15 значит те же 8 байт, а python-can читает цифру DLC по таблице CAN FD. Поэтому ядро заменяет такой DLC на 8 ещё в кольце.

На 115 200 бод кадры теряются. Канал везёт 11,2 % пика, кольцо на 128 кадров полно через 32 мс. Флаг 0x01 в ответе F скажет, что потери были.

Команды иногда пропадают, приём «зависает». В прерывании USART1 надо читать SR, а потом DR: только эта пара снимает флаг переполнения ORE (RM0008). Если читать один DR, прерывание от ORE может не сняться.


198е.8 Итоги

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

slcan.c — ядро SLCAN: кольцо кадров, строки, команды.

/* Мини-проект «CAN-сниффер»: ядро SLCAN — кольцо кадров, строки, команды. */
#include "slcan.h"

#define OK   '\r'                       /* команда выполнена */
#define BEL  '\a'                       /* отказ */

static volatile slcan_frame ring[SLCAN_RING];
static volatile uint32_t head;          /* пишет только прерывание CAN */
static volatile uint32_t tail;          /* пишет только главный цикл */
static volatile uint32_t lost;          /* кадры, которым не хватило места */
static volatile uint8_t  lost_flag;

static uint8_t speed = 0xFFu;           /* S0…S8; 0xFF — скорость не задана */
static uint8_t is_open, listen_only, stamps;

static const char HEX[] = "0123456789ABCDEF";

/* ── кольцо: один писатель, один читатель ── */
void slcan_rx_push(const slcan_frame *f)
{
    if (head - tail >= SLCAN_RING) {    /* места нет: считаем, но не затираем */
        lost++;
        lost_flag = 1;
        return;
    }
    ring[head % SLCAN_RING] = *f;
    if (ring[head % SLCAN_RING].dlc > 8u)
        ring[head % SLCAN_RING].dlc = 8u;   /* DLC 9…15 в классическом CAN — те же 8 байт */
    head++;                             /* после данных: цикл не увидит недописанный кадр */
}

int slcan_rx_pop(slcan_frame *f)
{
    if (tail == head)
        return 0;
    *f = ring[tail % SLCAN_RING];
    tail++;
    return 1;
}

uint32_t slcan_lost(void)
{
    return lost;
}

/* ── кадр → строка: t123 8 1122…  T12345678 8 …  r123 0  R12345678 0 ── */
static char *hexn(char *p, uint32_t v, unsigned digits)
{
    while (digits--)
        *p++ = HEX[(v >> (4u * digits)) & 0xFu];
    return p;
}

unsigned slcan_format(const slcan_frame *f, char *out)
{
    char *p = out;
    int ext = (f->flags & SLCAN_EXT) != 0;
    *p++ = (f->flags & SLCAN_RTR) ? (ext ? 'R' : 'r') : (ext ? 'T' : 't');
    p = hexn(p, f->id, ext ? 8u : 3u);
    *p++ = (char)('0' + f->dlc);
    if (!(f->flags & SLCAN_RTR))
        for (unsigned i = 0; i < f->dlc; i++)
            p = hexn(p, f->data[i], 2u);
    if (stamps)
        p = hexn(p, f->ts, 4u);         /* Z1: четыре цифры миллисекунд */
    *p++ = '\r';
    return (unsigned)(p - out);
}

/* ── строка → кадр (команды передачи) ── */
static int hexval(char c)
{
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    return -1;
}

static int gethex(const char *s, unsigned digits, uint32_t *v)
{
    *v = 0;
    for (unsigned i = 0; i < digits; i++) {
        int h = hexval(s[i]);
        if (h < 0)
            return -1;
        *v = (*v << 4) | (uint32_t)h;
    }
    return 0;
}

int slcan_parse(const char *s, unsigned n, slcan_frame *f)
{
    int ext = (s[0] == 'T' || s[0] == 'R');
    int rtr = (s[0] == 'r' || s[0] == 'R');
    unsigned idl = ext ? 8u : 3u;
    uint32_t v;
    if (n < 2u + idl || gethex(s + 1, idl, &f->id) != 0)
        return -1;
    if (f->id > (ext ? 0x1FFFFFFFu : 0x7FFu))
        return -1;
    if (s[1 + idl] < '0' || s[1 + idl] > '8')
        return -1;
    f->dlc = (uint8_t)(s[1 + idl] - '0');
    f->flags = (uint8_t)((ext ? SLCAN_EXT : 0u) | (rtr ? SLCAN_RTR : 0u));
    f->ts = 0;
    if (n != 2u + idl + (rtr ? 0u : 2u * f->dlc))
        return -1;                      /* длина строки обязана совпасть с DLC */
    for (unsigned i = 0; i < 8u; i++)
        f->data[i] = 0;
    for (unsigned i = 0; !rtr && i < f->dlc; i++) {
        if (gethex(s + 2 + idl + 2 * i, 2u, &v) != 0)
            return -1;
        f->data[i] = (uint8_t)v;
    }
    return 0;
}

/* ── команды: ответ '\r' — выполнено, '\a' — отказ ── */
unsigned slcan_command(const char *s, unsigned n, char *reply)
{
    slcan_frame f;
    char *p = reply;
    if (n == 0u) {                      /* пустая строка: python-can шлёт её после S */
        *p++ = OK;
        return 1u;
    }
    switch (s[0]) {
    case 'S':                           /* S0…S8 — скорость, только на закрытом канале */
        if (n != 2u || s[1] < '0' || s[1] > '8' || is_open)
            break;
        speed = (uint8_t)(s[1] - '0');
        *p++ = OK;
        return 1u;
    case 'O':                           /* открыть: обычный режим */
    case 'L':                           /* открыть: только слушать */
        if (n != 1u || is_open || speed > 8u || plat_can_open(speed, s[0] == 'L') != 0)
            break;
        is_open = 1;
        listen_only = (s[0] == 'L');
        *p++ = OK;
        return 1u;
    case 'C':                           /* закрыть; на закрытом — тоже «выполнено» */
        if (n != 1u)
            break;
        if (is_open)
            plat_can_close();
        is_open = 0;
        *p++ = OK;
        return 1u;
    case 't': case 'T': case 'r': case 'R':
        if (!is_open || listen_only || slcan_parse(s, n, &f) != 0 || plat_can_send(&f) != 0)
            break;                      /* «только слушать» передавать не умеет */
        *p++ = OK;
        return 1u;
    case 'F': {                         /* флаги: F + две цифры */
        if (n != 1u)
            break;
        uint8_t fl = (uint8_t)(plat_status() | (lost_flag ? SLCAN_F_LOST : 0u));
        lost_flag = 0;
        *p++ = 'F';
        p = hexn(p, fl, 2u);
        *p++ = OK;
        return (unsigned)(p - reply);
    }
    case 'V':                           /* версия: железо 01, программа 01 */
        if (n != 1u)
            break;
        for (const char *v = "V0101\r"; *v; v++)
            *p++ = *v;
        return (unsigned)(p - reply);
    case 'N':                           /* номер адаптера */
        if (n != 1u)
            break;
        for (const char *v = "N0198\r"; *v; v++)
            *p++ = *v;
        return (unsigned)(p - reply);
    case 'Z':                           /* метки времени: Z0 — выкл, Z1 — вкл */
        if (n != 2u || (s[1] != '0' && s[1] != '1'))
            break;
        stamps = (uint8_t)(s[1] == '1');
        *p++ = OK;
        return 1u;
    default:
        break;
    }
    *p++ = BEL;
    return 1u;
}

/* ── сборка строки по байту: разделитель — '\r', '\n' пропускается ── */
unsigned slcan_feed(char c, char *reply)
{
    static char line[SLCAN_LINE];
    static unsigned len;
    static uint8_t too_long;
    if (c == '\n')
        return 0u;                      /* терминал с CR LF — не ошибка */
    if (c != '\r') {
        if (len < SLCAN_LINE)
            line[len++] = c;
        else
            too_long = 1;               /* хвост выбрасываем, строку потом отвергнем */
        return 0u;
    }
    unsigned n = len, bad = too_long;
    len = 0;
    too_long = 0;
    if (bad) {
        reply[0] = BEL;
        return 1u;
    }
    return slcan_command(line, n, reply);
}

sniffer_f103.c — прошивка BluePill: bxCAN и USART1.

/* Мини-проект «CAN-сниффер»: BluePill (STM32F103C8), регистры через CMSIS.
 *   PB8/PB9 — CAN_RX/CAN_TX после ремапа (PA11/PA12 заняты разъёмом USB);
 *   PA9/PA10 — USART1 к переходнику USB–UART, 921 600 бод.
 * USB самого F103 здесь не годится: его буферы и bxCAN делят одни 512 байт
 * памяти (RM0008, 23.2 и 24.2) — вместе они не работают.
 * Такт: кварц 8 МГц × 9 = 72 МГц, APB1 = 36 МГц — от него тактуется bxCAN. */
#include "stm32f10x.h"
#include "slcan.h"

#define FCPU   72000000u
#define FCAN   36000000u                /* APB1 */
#define BAUD   921600u
#define UART_BRR ((FCPU + BAUD / 2u) / BAUD)
_Static_assert(UART_BRR == 78u, "72 МГц / 78 = 923 077 бод, +0,16 %");

/* S0…S8: скорость, делитель, TS1, TS2 (в квантах, без «минус один») */
#define SPEEDS(X) \
    X(10000u, 450u, 6u, 1u) X(20000u, 225u, 6u, 1u) X(50000u, 90u, 6u, 1u)   \
    X(100000u, 45u, 6u, 1u) X(125000u, 36u, 6u, 1u) X(250000u, 18u, 6u, 1u)  \
    X(500000u, 9u, 6u, 1u)  X(750000u, 6u, 6u, 1u)  X(1000000u, 4u, 7u, 1u)
#define ROW(rate, brp, ts1, ts2) { brp, ts1, ts2 },
#define EXACT(rate, brp, ts1, ts2) \
    _Static_assert(FCAN % ((brp) * (1u + (ts1) + (ts2))) == 0u \
                   && FCAN / ((brp) * (1u + (ts1) + (ts2))) == (rate), "скорость не делится ровно");
SPEEDS(EXACT)
static const struct { uint16_t brp; uint8_t ts1, ts2; } bt[9] = { SPEEDS(ROW) };

static volatile uint32_t ms;            /* SysTick: миллисекунды — для меток Z1 */
static volatile uint8_t  fovr;          /* очередь приёма переполнялась */
static volatile uint8_t  rxq[64];       /* байты от компьютера */
static volatile uint32_t rxh, rxt;

void SysTick_Handler(void)
{
    ms++;
}

void USART1_IRQHandler(void)
{
    uint32_t sr = USART1->SR;           /* SR, затем DR — снимает и RXNE, и ORE */
    uint8_t c = (uint8_t)USART1->DR;
    if ((sr & USART_SR_RXNE) && rxh - rxt < sizeof rxq)
        rxq[rxh++ % sizeof rxq] = c;
}

/* кадр из очереди 0 bxCAN — в кольцо ядра; очередь освобождается сразу */
void USB_LP_CAN1_RX0_IRQHandler(void)
{
    while (CAN1->RF0R & CAN_RF0R_FMP0) {
        uint32_t rir = CAN1->sFIFOMailBox[0].RIR;
        uint32_t lo = CAN1->sFIFOMailBox[0].RDLR, hi = CAN1->sFIFOMailBox[0].RDHR;
        slcan_frame f;
        f.flags = (uint8_t)(((rir & CAN_RI0R_IDE) ? SLCAN_EXT : 0u) | ((rir & CAN_RI0R_RTR) ? SLCAN_RTR : 0u));
        f.id = (rir & CAN_RI0R_IDE) ? (rir >> 3) : (rir >> 21);
        f.dlc = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0xFu);
        f.ts = (uint16_t)ms;
        for (unsigned i = 0; i < 4u; i++) {
            f.data[i] = (uint8_t)(lo >> (8u * i));
            f.data[4u + i] = (uint8_t)(hi >> (8u * i));
        }
        CAN1->RF0R = CAN_RF0R_RFOM0;    /* отпустить ящик: следующий кадр очереди */
        slcan_rx_push(&f);
    }
    if (CAN1->RF0R & CAN_RF0R_FOVR0) {
        CAN1->RF0R = CAN_RF0R_FOVR0;    /* флаг снимается записью единицы */
        fovr = 1;
    }
}

int plat_can_open(uint8_t speed, int listen)
{
    uint32_t t;
    CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_NART;    /* из сна — в инициализацию; без автоповтора */
    for (t = 100000u; !(CAN1->MSR & CAN_MSR_INAK); t--)
        if (t == 0u)
            return 1;
    CAN1->BTR = (listen ? CAN_BTR_SILM : 0u)    /* «только слушать»: на шину — ни одного доминантного бита */
              | ((uint32_t)(bt[speed].ts2 - 1u) << 20)
              | ((uint32_t)(bt[speed].ts1 - 1u) << 16)
              | (uint32_t)(bt[speed].brp - 1u);  /* SJW = 1 квант */

    CAN1->FMR |= CAN_FMR_FINIT;                 /* фильтр 0: маска из нулей — принимать всё */
    CAN1->FA1R &= ~1u;
    CAN1->FS1R |= 1u;
    CAN1->FM1R &= ~1u;
    CAN1->sFilterRegister[0].FR1 = 0;
    CAN1->sFilterRegister[0].FR2 = 0;
    CAN1->FFA1R &= ~1u;
    CAN1->FA1R |= 1u;
    CAN1->FMR &= ~CAN_FMR_FINIT;

    CAN1->IER = CAN_IER_FMPIE0;                 /* прерывание: в очереди 0 есть кадр */
    NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
    CAN1->MCR &= ~CAN_MCR_INRQ;                 /* ждёт 11 рецессивных бит на RX */
    for (t = 100000u; CAN1->MSR & CAN_MSR_INAK; t--)
        if (t == 0u)
            return 2;
    return 0;
}

void plat_can_close(void)
{
    CAN1->MCR |= CAN_MCR_INRQ;
    NVIC_DisableIRQ(USB_LP_CAN1_RX0_IRQn);
}

int plat_can_send(const slcan_frame *f)
{
    if (!(CAN1->TSR & CAN_TSR_TME0))
        return 1;                               /* ящик 0 занят — отказ, повторит компьютер */
    CAN1->sTxMailBox[0].TDTR = f->dlc;
    CAN1->sTxMailBox[0].TDLR = (uint32_t)f->data[0] | ((uint32_t)f->data[1] << 8)
                             | ((uint32_t)f->data[2] << 16) | ((uint32_t)f->data[3] << 24);
    CAN1->sTxMailBox[0].TDHR = (uint32_t)f->data[4] | ((uint32_t)f->data[5] << 8)
                             | ((uint32_t)f->data[6] << 16) | ((uint32_t)f->data[7] << 24);
    CAN1->sTxMailBox[0].TIR = ((f->flags & SLCAN_EXT) ? ((f->id << 3) | CAN_TI0R_IDE) : (f->id << 21))
                            | ((f->flags & SLCAN_RTR) ? CAN_TI0R_RTR : 0u) | CAN_TI0R_TXRQ;
    return 0;
}

uint8_t plat_status(void)
{
    uint32_t esr = CAN1->ESR;
    uint8_t s = (uint8_t)((fovr ? SLCAN_F_OVERRUN : 0u) | ((esr & CAN_ESR_EPVF) ? SLCAN_F_PASSIVE : 0u)
                          | ((esr & CAN_ESR_BOFF) ? SLCAN_F_BUSOFF : 0u));
    fovr = 0;
    return s;
}

static void clock_72mhz(void)
{
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY)) {
    }
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
    RCC->CFGR = RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9 | RCC_CFGR_PPRE1_DIV2;
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY)) {
    }
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
    }
}

static void put(const char *s, unsigned n)
{
    for (unsigned i = 0; i < n; i++) {
        while (!(USART1->SR & USART_SR_TXE)) {
        }
        USART1->DR = (uint8_t)s[i];
    }
}

int main(void)
{
    char out[SLCAN_OUT];
    slcan_frame f;
    clock_72mhz();
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_USART1EN;
    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
    AFIO->MAPR = AFIO_MAPR_CAN_REMAP_REMAP2;   /* PB8 RX, PB9 TX; SWJ_CFG = 000 — отладчик жив */
    GPIOB->CRH = (GPIOB->CRH & ~0xFFu) | 0x4u | (0xBu << 4);   /* PB8 вход, PB9 — AF двухтактный */
    GPIOA->CRH = (GPIOA->CRH & ~0xFF0u) | (0xBu << 4) | (0x4u << 8);   /* PA9 TX, PA10 RX */
    USART1->BRR = UART_BRR;
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE;
    NVIC_EnableIRQ(USART1_IRQn);
    SysTick->LOAD = FCPU / 1000u - 1u;
    SysTick->VAL = 0;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;

    for (;;) {
        while (rxt != rxh) {                    /* команды — по байту в ядро */
            unsigned n = slcan_feed((char)rxq[rxt % sizeof rxq], out);
            rxt++;
            if (n)
                put(out, n);
        }
        if (slcan_rx_pop(&f))                   /* кадры — по строке за проход */
            put(out, slcan_format(&f, out));
    }
}

can_g474.c — то же ядро на STM32G474: FDCAN2 и USB CDC.

/* Мини-проект «CAN-сниффер»: то же ядро SLCAN на STM32G474 — FDCAN2 + USB CDC.
 *   PB12/PB13 — FDCAN2_RX/TX (AF9): PA11/PA12 заняты USB, а PB8 (FDCAN1_RX) — это BOOT0;
 *   USB CDC — из CubeMX (раздел 20.7): CDC_Receive_FS зовёт sniffer_cdc_rx(),
 *   главный цикл — sniffer_poll(). Такт, USB и HAL настраивает CubeMX.
 * У G474 память USB (0x4000 6000) и память сообщений FDCAN (0x4000 A400)
 * разные — CDC и CAN работают одновременно (RM0440, карта памяти). */
#include "stm32g474xx.h"
#include "slcan.h"

extern uint8_t  CDC_Transmit_FS(uint8_t *buf, uint16_t len);   /* 0 = USBD_OK */
extern uint32_t HAL_GetTick(void);

#define FCAN 170000000u                 /* ядро FDCAN от PCLK1 (FDCANSEL = 10) */

/* S0…S8: скорость, делитель, TSEG1, TSEG2 в квантах; 0 — ровно не делится */
#define SPEEDS(X) \
    X(10000u, 500u, 29u, 4u) X(20000u, 500u, 14u, 2u) X(50000u, 200u, 14u, 2u)  \
    X(100000u, 100u, 14u, 2u) X(125000u, 80u, 14u, 2u) X(250000u, 40u, 14u, 2u) \
    X(500000u, 20u, 14u, 2u) X(750000u, 0u, 0u, 0u) X(1000000u, 10u, 14u, 2u)
#define ROW(rate, brp, s1, s2) { brp, s1, s2 },
#define EXACT(rate, brp, s1, s2) \
    _Static_assert((brp) == 0u || (FCAN % ((brp) * (1u + (s1) + (s2))) == 0u \
                   && FCAN / ((brp) * (1u + (s1) + (s2))) == (rate) && (brp) <= 512u), "скорость");
SPEEDS(EXACT)
static const struct { uint16_t brp; uint8_t s1, s2; } bt[9] = { SPEEDS(ROW) };

/* память сообщений: FDCAN2 — второй блок по 0x350, очередь 0 — со смещения 0xB0,
 * элемент — 18 слов (2 слова заголовка + 64 байта данных) */
#define RAM2    (SRAMCAN_BASE + 0x350u)
#define F0SA    (RAM2 + 0x0B0u)
_Static_assert(28u * 4u + 8u * 8u == 0x0B0u, "фильтры 11 и 29 бит перед очередью 0");
_Static_assert(212u * 4u == 0x350u, "блок одного FDCAN — 212 слов");

void FDCAN2_IT0_IRQHandler(void)
{
    while (FDCAN2->RXF0S & FDCAN_RXF0S_F0FL) {
        uint32_t gi = (FDCAN2->RXF0S >> FDCAN_RXF0S_F0GI_Pos) & 3u;
        const volatile uint32_t *e = (const volatile uint32_t *)(F0SA + gi * 18u * 4u);
        uint32_t r0 = e[0], r1 = e[1], lo = e[2], hi = e[3];
        slcan_frame f;
        f.flags = (uint8_t)(((r0 >> 30) & 1u ? SLCAN_EXT : 0u) | ((r0 >> 29) & 1u ? SLCAN_RTR : 0u));
        f.id = (r0 >> 30) & 1u ? (r0 & 0x1FFFFFFFu) : ((r0 >> 18) & 0x7FFu);
        f.dlc = (uint8_t)((r1 >> 16) & 0xFu);  /* 9…15 ядро превратит в 8 */
        f.ts = (uint16_t)HAL_GetTick();
        for (unsigned i = 0; i < 4u; i++) {
            f.data[i] = (uint8_t)(lo >> (8u * i));
            f.data[4u + i] = (uint8_t)(hi >> (8u * i));
        }
        FDCAN2->RXF0A = gi;                    /* элемент прочитан */
        slcan_rx_push(&f);
    }
    FDCAN2->IR = FDCAN_IR_RF0N;
}

int plat_can_open(uint8_t speed, int listen)
{
    if (bt[speed].brp == 0u)
        return 1;                              /* S7 от 170 МГц ровно не делится */
    RCC->CCIPR = (RCC->CCIPR & ~RCC_CCIPR_FDCANSEL) | RCC_CCIPR_FDCANSEL_1;
    RCC->APB1ENR1 |= RCC_APB1ENR1_FDCANEN;
    RCC->AHB2ENR |= RCC_AHB2ENR_GPIOBEN;
    GPIOB->AFR[1] = (GPIOB->AFR[1] & ~(0xFFu << 16)) | (9u << 16) | (9u << 20);
    GPIOB->MODER = (GPIOB->MODER & ~(0xFu << 24)) | (0xAu << 24);   /* PB12, PB13 — AF */

    FDCAN2->CCCR |= FDCAN_CCCR_INIT;
    while (!(FDCAN2->CCCR & FDCAN_CCCR_INIT)) {
    }
    FDCAN2->CCCR |= FDCAN_CCCR_CCE | (listen ? FDCAN_CCCR_MON : 0u);  /* MON — «только слушать» */
    FDCAN2->NBTP = ((uint32_t)(bt[speed].brp - 1u) << FDCAN_NBTP_NBRP_Pos)
                 | ((uint32_t)(bt[speed].s1 - 1u) << FDCAN_NBTP_NTSEG1_Pos)
                 | ((uint32_t)(bt[speed].s2 - 1u) << FDCAN_NBTP_NTSEG2_Pos);  /* SJW = 1 */
    FDCAN2->RXGFC = 0;                         /* без фильтров: всё — в очередь 0 (так и после сброса) */
    FDCAN2->IE = FDCAN_IE_RF0NE;
    FDCAN2->ILE = FDCAN_ILE_EINT0;
    NVIC_EnableIRQ(FDCAN2_IT0_IRQn);
    FDCAN2->CCCR &= ~FDCAN_CCCR_INIT;
    return 0;
}

void plat_can_close(void)
{
    FDCAN2->CCCR |= FDCAN_CCCR_INIT;
    NVIC_DisableIRQ(FDCAN2_IT0_IRQn);
    FDCAN2->CCCR = (FDCAN2->CCCR | FDCAN_CCCR_CCE) & ~FDCAN_CCCR_MON;
}

int plat_can_send(const slcan_frame *f)
{
    (void)f;
    return 1;                                  /* этот вариант только слушает */
}

uint8_t plat_status(void)
{
    uint32_t psr = FDCAN2->PSR;
    return (uint8_t)(((psr & FDCAN_PSR_EP) ? SLCAN_F_PASSIVE : 0u) | ((psr & FDCAN_PSR_BO) ? SLCAN_F_BUSOFF : 0u));
}

/* ── USB CDC: приём — в кольцо байтов, передача — двумя буферами ── */
static volatile uint8_t  rxq[256];
static volatile uint32_t rxh, rxt;
static uint8_t  txb[2][256];
static unsigned txn, cur;

void sniffer_cdc_rx(const uint8_t *buf, uint32_t len)    /* из CDC_Receive_FS */
{
    for (uint32_t i = 0; i < len && rxh - rxt < sizeof rxq; i++)
        rxq[rxh++ % sizeof rxq] = buf[i];
}

static void out(const char *s, unsigned n)
{
    for (unsigned i = 0; i < n; i++)
        txb[cur][txn++] = (uint8_t)s[i];
}

void sniffer_poll(void)                                  /* из главного цикла */
{
    char line[SLCAN_OUT];
    slcan_frame f;
    while (rxt != rxh && txn + SLCAN_OUT <= sizeof txb[0]) {
        unsigned n = slcan_feed((char)rxq[rxt % sizeof rxq], line);
        rxt++;
        out(line, n);
    }
    while (txn + SLCAN_OUT <= sizeof txb[0] && slcan_rx_pop(&f))
        out(line, slcan_format(&f, line));
    /* отданный буфер не трогать до конца передачи: «принято» — значит прошлый ушёл */
    if (txn && CDC_Transmit_FS(txb[cur], (uint16_t)txn) == 0u) {
        cur ^= 1u;
        txn = 0;
    }
}

Дальше: раздел 198ж «Датчик по UDP: широковещание, обнаружение в сети и свой протокол» — от шины CAN к сети: прибор сам сообщает о себе по UDP.

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