198е. Mini-project: a CAN sniffer on STM32 — the SLCAN protocol over USB CDC
Section 19 took bxCAN apart from the inside, 19а covered FDCAN, 19б the CANopen protocol
on top of it. A finished instrument for the bus was not there. It appears here:
a sniffer that listens to the bus and hands every frame to the computer as a line
of text. The language of those lines is SLCAN (also called the LAWICEL protocol): the commands
S6, O, t1238… are understood by python-can and other CAN programs, so
you do not need to write your own program on the computer side.
The instrument is built in two variants. On the BluePill (F103) it uses bxCAN and USART1 through a USB–UART adapter: this chip's own USB cannot live together with CAN. On the G474 it uses FDCAN2 and USB CDC from section 20.7. The protocol core is one and the same; it is also built on the computer and checked against a model and against python-can.
References: RM0008 (the bxCAN chapter: silent mode, filters, receive FIFO,
figure 234 with the frame length; the memory shared by USB and CAN) and RM0440
(the FDCAN chapter: message RAM, bus monitoring mode) from the Keil packs, the datasheet
DS12288 (the FDCAN2 and BOOT0 pins of the G474). The format was not checked against a formal description
of SLCAN: the lines were checked against the slcan.py module of python-can 4.6.1 (the Zephyr
environment). The frame length and bit stuffing come from the model in 19б.
Terms used in the documentation
Term Meaning silent mode (SILM) the listen-only mode of bxCAN bus monitoring mode (MON) the same mode in FDCAN identifier mask mode a filter in mask mode: only the mask bits are compared receive FIFO (FMP0, RFOM0, FOVR0) receive queue: how many frames, release a frame, overrun bus idle: 11 consecutive recessive bits the bus is free: 11 recessive bits in a row shared USB/CAN SRAM the buffer memory shared by USB and CAN in the F103 message RAM the FDCAN message memory SLCAN, LAWICEL protocol a text protocol of a CAN adapter over a serial port
198е.1 What the instrument does and what it is built from
The sniffer connects to the bus through a transceiver and stays silent: it does not acknowledge frames, does not send error frames, transmits nothing at all. Each frame it receives goes into a ring, is turned into a line and handed to the computer. Commands from the computer set the bit rate and open the channel.
The BluePill variant:
- CAN_RX and CAN_TX are PB8 and PB9 after remapping (
CAN_REMAP = 10): PA11 and PA12 are taken by the USB connector by default; - a transceiver is mandatory (19.2), a 3.3 V one;
- to the computer: USART1 (PA9, PA10) at 921600 baud through a USB–UART adapter.
Why not the F103's own USB is explained in 19.8, and RM0008 states it precisely: USB and CAN share a dedicated 512-byte memory at address 0x4000 6000 and cannot work at the same time. Hence the adapter. In the G474 the USB memory (0x4000 6000) and the FDCAN message RAM (0x4000 A400) are separate, so the second variant sends the same lines straight to USB CDC.
198е.2 The SLCAN protocol: lines and replies
All communication is ASCII lines, each ending with a CR character (\r). The reply
"done" is CR, a refusal is BEL (\a, code 7). Our core understands these
commands:
| Command | What it does | Reply |
|---|---|---|
S0…S8 |
bit rate 10, 20, 50, 100, 125, 250, 500, 750 kbit/s, 1 Mbit/s | CR; on an open channel, BEL |
O / L |
open the channel: normal mode / listen-only | CR; without a bit rate, BEL |
C |
close the channel | CR |
t T r R |
transmit a frame: 11 or 29 bits, data or remote request | CR; in L mode, BEL |
F |
error flags, two hex digits | F00\r … |
V / N |
version / adapter number | V0101\r, N0198\r |
Z0 / Z1 |
turn timestamps off / on | CR |
A frame is written the same way in both directions: a letter, the identifier (three digits for
11 bits, eight for 29), one DLC digit and the data, two digits per byte.
Remote requests r and R have no data. Lines from a run of the core:
t12388081828384858687\r 11 бит, 0x123, 8 байт
T1ABCDEF08A8ABAEB1B4B7BABD\r 29 бит, 0x1ABCDEF0, 8 байт
t123880818283848586871008\r то же с меткой Z1: 0x1008 мс
The timestamp is four hex digits of milliseconds that wrap around. This is our own
convention: adapters from different vendors count the timestamp differently, and python-can does not read
the tail after the data at all. The bits of the F reply are ours too: 0x01 means the ring
overflowed, 0x02 means the bxCAN receive FIFO overflowed, 0x04 means the node
is error passive, 0x08 means it went bus-off.
The longest frame line is T with eight bytes and a timestamp: 31 characters
including CR. The line buffer in the core is 32 characters.
198е.3 Throughput: where the bottleneck is
The worst case for a sniffer is a bus loaded to 100 % with the shortest frames. Frame length per RM0008 (fig. 234): standard 44 + 8n bits, extended 64 + 8n, plus 3 bits of intermission. Bit stuffing only makes a frame longer, so without it the number of frames per second is the highest. At 500 kbit/s:
| Frame | Bits without stuffing / worst | Frames/s | Characters per line (with Z1) | Characters/s (with Z1) |
|---|---|---|---|---|
| t, DLC 0 | 47 / 55 | 10638 | 6 (10) | 63830 (106383) |
| t, DLC 8 | 111 / 135 | 4505 | 22 (26) | 99099 (117117) |
| T, DLC 0 | 67 / 80 | 7463 | 11 (15) | 82090 (111940) |
| T, DLC 8 | 131 / 160 | 3817 | 27 (31) | 103053 (118321) |
The peak is 103053 characters per second without timestamps and 118321 with them. Now the channels. A UART character is 10 bits (start, 8 data, stop):
- 115200 baud — 11520 characters/s, 11.2 % of the peak. Frames
twith eight bytes get through at 524 per second, which is a bus load of only 11.6 %. - 921600 baud — divider 78 from 72 MHz gives 923077 baud, 92308 characters/s,
89.6 % of the peak. Frames
tDLC 8 get through up to a load of 93.1 %, with timestamps 78.8 %, framesTDLC 8 up to 89.6 %. - USB FS — the bulk transfer ceiling from 198б.5, 1216000 bytes/s, is larger than the peak with timestamps by a factor of 10.3. This is the computed ceiling of the bus, not a measurement of a live stack; how much CDC really carries has not been measured in the handbook.
The bottleneck on the F103 is the UART: even 921600 baud falls a little short of a fully loaded
500 kbit/s bus. The ring smooths out bursts: with 128 frames, at full
load with t DLC 8 frames it fills up in 415 ms at 921600 baud and in
32 ms at 115200. After that frames are lost, and the core raises flag 0x01 in F.
198е.4 F103 firmware: bxCAN listens, the interrupt puts frames into the ring
bxCAN is clocked from APB1 = 36 MHz. A bit is divided into time quanta: one
synchronization quantum, TS1 and TS2 (19.4). The bit rate table in the firmware checks
itself: _Static_assert will not let you build a bit rate that does not divide
exactly.
| Command | Bit rate | Prescaler | Total quanta (TS1 + TS2 in brackets; one more is the sync quantum) | Sample point |
|---|---|---|---|---|
| S0…S7 | 10…750 kbit/s | 450…6 | 8 (6 + 1) | 87.5 % |
| S8 | 1 Mbit/s | 4 | 9 (7 + 1) | 88.9 % |
Opening the channel means entering initialization mode, setting the bit timing and the filter, and leaving:
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;
The SILM bit is the key thing for a sniffer. According to RM0008, in silent mode bxCAN receives frames but drives only recessive bits onto the bus: the acknowledge bit and the error flag are looped back internally. Such a node does not interfere with the bus even if the bit rate is chosen wrongly.
The filter is one 32-bit filter in mask mode: both the identifier and the mask are zeros, so not a single bit is compared. Without a filter bxCAN receives nothing at all (19.5).
Reception happens in the FIFO 0 interrupt (vector 20, shared with USB). The frame is taken out of the mailbox, the mailbox is released at once, and the frame goes into the core ring:
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);
}
Writing RF0R = RFOM0 is safe: the FULL0 and FOVR0 flags are cleared by writing
a one, and zeros have no effect on them.
The main loop does two things: it hands command bytes from USART1 to the core one at a time, and, if there is a frame in the ring, writes its line to the port. The whole firmware (startup code, sniffer, core) is 2260 bytes of flash and 2188 bytes of RAM, of which 2048 is the ring of 128 frames of 16 bytes each.
The G474 variant. FDCAN2 on PB12 and PB13 (AF9): PA11 and PA12 are taken by USB,
and PB8 on the G474 is the BOOT0 pin. Listen-only mode is the MON bit in CCCR;
there are no filters at all (RXGFC = 0: everything goes to FIFO 0). FIFO 0 of FDCAN2 sits
in the message RAM at offset 0x350 + 0xB0: the block of one FDCAN is 212 words,
before the FIFO come 28 filters for 11 bits and 8 for 29 bits, an element is 18 words.
From 170 MHz every bit rate divides exactly except S7: 170000000 / 750000 =
226.67, and to O after S7 the core replies BEL. S1…S8 use 17 quanta,
S0 uses 34 (the prescaler is at most 512), the sample point is 88.2 % everywhere. Lines go out through CDC_Transmit_FS with two buffers:
while one is being sent, the other is being filled.
198е.5 The core: ring, lines, commands
The ring has one writer (the CAN interrupt) and one reader (the main loop). Each moves only its own counter, so disabling interrupts is not needed. When there is no room, a frame does not overwrite an old one but is counted as lost:
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++; /* после данных: цикл не увидит недописанный кадр */
}
A frame line is built without printf, from a table of 16 digits:
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);
}
Commands are parsed by slcan_command: strictly by length (a t line must
have exactly as many data digits as the DLC demands), the identifier is no
greater than 0x7FF or 0x1FFFFFFF. A line longer than 32 characters is rejected entirely,
and the LF character is skipped: a terminal with CR LF line endings is not an error.
198е.6 Testing on the computer: the same core against a model and python-can
The core (slcan.c) is built with gcc in a container with -Wall -Wextra -Werror
-pedantic together with a harness: it feeds the core bytes "from the computer", frames
"from the bus" and prints everything the core outputs. Next to it is a model in Python,
written separately, not as a translation of the C code.
Core against model. 3386 steps: random and edge-case lines (valid, almost valid, garbage, longer than 32 characters, with CR LF), frames with DLC up to 15, a failed open, overflow. All 3386 matched: 3004 replies to commands, 287 frame lines, 273 calls to the board. After 130 frames without output both the core and the model counted 2 lost.
python-can against the core. python-can 4.6.1 writes to a fake port, the bytes go to the C core, and python-can reads the replies:
- opening with
bitrate=500000, listen_only=True: python-can writesC\rS6\r\rL\rL\r, and the core opens the channel once:OPEN 6 1; get_versiongives (1, 1),get_serial_numbergives'0198';- 23 frames (standard and extended, remote requests, DLC 0…8): python-can
parsed all 23 both without timestamps and with
Z1, and all fields matched; - transmission in
Omode: the python-can lines matched the core lines byte for byte, 23 out of 23, and the frames reached the "board" undistorted; inLmode the core replied BEL and transmitted nothing; - opening without a bit rate: python-can writes
O\r, and the core replies BEL.
The firmware was not run on hardware. sniffer_f103.c, start_f103.c,
slcan.c and can_g474.c were built by armclang without warnings; the whole F103 image was
linked by arm-none-eabi-gcc with -Werror, which is where the sizes in 198е.4 come from.
198е.7 Pitfalls
You connected the sniffer and errors appeared on the bus. A node in normal mode
acknowledges every frame of other nodes, and at a wrong bit rate it sends error frames.
A sniffer needs listen-only mode: L instead of O, the SILM bit
(MON in FDCAN).
CAN and USB on the F103 do not work together. The 512-byte memory is shared (RM0008). The way out is to send the lines through a USART and an adapter, or to take a G474, where the memories are separate.
After remapping CAN the debugger disappeared. The SWJ_CFG bits in AFIO_MAPR
are write-only: according to RM0008 their value is undefined when read.
A read-modify-write of MAPR can write garbage there and turn SWD off.
That is why the firmware writes MAPR as a whole: CAN_REMAP = 10, SWJ_CFG = 000.
O replies BEL although the bit rate is set. Leaving initialization waits for
11 recessive bits in a row on CAN_RX. With no transceiver or no bus the INAK bit
does not clear, and plat_can_open gives up on a timeout.
python-can sends L twice. In version 4.6.1 set_bitrate opens the channel
itself, and then the constructor opens it once more. The core rejects the second L on an open
channel, python-can does not check the reply, and everything works.
Better not to let your own core fall over on a repeated open.
A frame with DLC 9 arrives as 12 bytes. In classic CAN, DLC 9…15 means the same 8 bytes, but python-can reads the DLC digit using the CAN FD table. So the core replaces such a DLC with 8 already in the ring.
At 115200 baud frames are lost. The channel carries 11.2 % of the peak, and the ring of
128 frames is full after 32 ms. Flag 0x01 in the F reply tells you there were losses.
Commands sometimes vanish, reception "hangs". In the USART1 interrupt you have to read SR and then DR: only this pair clears the ORE overrun flag (RM0008). If you read DR alone, the ORE interrupt may not clear.
198е.8 Summary
- SLCAN is ASCII lines with CR; a
tframe with eight bytes is 22 characters, the longest line,Twith a timestamp, is 31. - A 500 kbit/s bus at 100 % gives up to 103053 characters/s, with timestamps 118321. A UART at 115200 carries 11.2 % of that, 921600 baud carries 89.6 %; the USB FS ceiling from 198б.5 is larger than the peak with timestamps by a factor of 10.3.
- F103: bxCAN from 36 MHz, S0…S7 with 8 quanta (87.5 %), S8 with 9 quanta; SILM is listen-only, a mask filter of zeros accepts everything.
- USB and CAN in the F103 share 512 bytes of memory, so the lines go through USART1 at 921600 baud (divider 78, 923077 baud). In the G474 the memories are separate: FDCAN2 + USB CDC, and S7 does not divide exactly from 170 MHz.
- The F103 firmware is 2260 bytes of flash and 2188 bytes of RAM, with a ring of 128 frames.
- The C core matched the model on all 3386 steps; python-can parsed all 23 frames, and its transmit lines matched the core lines byte for byte.
Full source code
slcan.c — the SLCAN core: frame ring, lines, commands.
/* Мини-проект «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 — the BluePill firmware: bxCAN and 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 — the same core on the STM32G474: FDCAN2 and 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;
}
}
Next: section 198ж "UDP sensor: broadcast, network discovery and a custom protocol" — from the CAN bus to the network: the device announces itself over UDP.