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 Что делает прибор и из чего он собран
Сниффер включается в шину через трансивер и молчит: не подтверждает кадры, не шлёт кадров ошибки, ничего не передаёт. Каждый принятый кадр он кладёт в кольцо, превращает в строку и отдаёт компьютеру. Команды с компьютера задают скорость и открывают канал.
Вариант на BluePill:
- CAN_RX и CAN_TX — PB8 и PB9 после переназначения (
CAN_REMAP = 10): на PA11 и PA12 по умолчанию сидит разъём USB; - трансивер обязателен (19.2), на 3,3 В;
- к компьютеру — USART1 (PA9, PA10) на 921 600 бод через переходник USB–UART.
Почему не 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 данных, стоп):
- 115 200 бод — 11 520 символов/с, 11,2 % пика. Кадров
tс восемью байтами пройдёт 524 в секунду — это загрузка шины всего 11,6 %. - 921 600 бод — делитель 78 от 72 МГц даёт 923 077 бод, 92 308 символов/с,
89,6 % пика. Кадры
tDLC 8 пройдут до загрузки 93,1 %, с метками — 78,8 %, кадрыTDLC 8 — 89,6 %. - USB FS — потолок массовых передач из 198б.5, 1 216 000 байт/с, больше пика с метками в 10,3 раза. Это расчётный потолок шины, а не замер живого стека; сколько реально провезёт CDC, в справочнике не измерено.
Узкое место на 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:
- открытие
bitrate=500000, listen_only=True— python-can пишетC\rS6\r\rL\rL\r, ядро открывает канал один раз:OPEN 6 1; get_versionдаёт (1, 1),get_serial_number—'0198';- 23 кадра (стандартные и расширенные, запросы, DLC 0…8) — python-can
разобрал все 23 и без меток, и с
Z1, все поля совпали; - передача в режиме
O: строки python-can совпали со строками ядра байт в байт — 23 из 23, кадры дошли до «платы» без искажений; в режимеLядро ответило BEL и ничего не передало; - открытие без скорости: python-can пишет
O\r, ядро отвечает BEL.
Прошивки на железе не запускались. 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 — строки ASCII с CR; кадр
tс восемью байтами — 22 символа, самая длинная строкаTс меткой — 31. - Шина 500 кбит/с на 100 % даёт до 103 053 символов/с, с метками — 118 321. UART 115 200 везёт 11,2 % этого, 921 600 бод — 89,6 %; потолок USB FS из 198б.5 больше пика с метками в 10,3 раза.
- F103: bxCAN от 36 МГц, S0…S7 по 8 квантов (87,5 %), S8 — 9 квантов; SILM — только слушать, фильтр-маска из нулей — принимать всё.
- USB и CAN у F103 делят 512 байт памяти — строки идут через USART1 на 921 600 бод (делитель 78, 923 077 бод). У G474 памяти раздельные: FDCAN2 + USB CDC, S7 от 170 МГц ровно не делится.
- Прошивка F103 — 2260 байт флеш-памяти и 2188 байт ОЗУ, кольцо на 128 кадров.
- Ядро на C совпало с моделью на всех 3386 шагах; python-can разобрал все 23 кадра, его строки передачи совпали со строками ядра байт в байт.
Полные исходники
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.