一句话结论:CAN 最值钱的三条设计是——位填充让接收方永远能重新同步、非破坏性仲裁让高优先级帧零延迟、且失败方自动按 ID 升序排队、错误计数(128 转 error-passive、256 转 bus-off)让坏节点自动退场。本文实测:三节点同时发送,仲裁结果严格按 0x0A0 → 0x120 → 0x400 排序;线上 106 位逐个翻转,漏检 0 个;去填充后全部 4753 种双比特错误 100% 被 CRC15 检出。
一、完整工程下载
压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。
下载 can-frame.zip (20.8 KB,共 10 个文件)
.gitignore
Makefile
README.md
include/
can_frame.h
can_node.h
platformio.ini
src/
can_frame.c
can_node.c
main.c
test/
test_can.c一、CAN 到底解决了什么问题
RS485 也能多机通信,为什么还要 CAN?因为 CAN 在比特层就替你做完了 三件 RS485 完全不管的事:
| 问题 | RS485 / 自定义协议 | CAN |
|---|---|---|
| 两个节点同时开口说话 | 数据撞车,两份都废 | 非破坏性仲裁,优先级高的照发,低的自动退让 |
| 长帧里连续很多个相同位 | 收发双方时钟有偏差就会数错位 | 位填充,每 5 个相同位强制插 1 个反向位,边沿不断 |
| 收到了错数据 | 你自己加校验,加多少随缘 | CRC15 + 固定格式 + ACK 位 + 错误计数,硬件保证 |
| 某个节点坏了乱发 | 总线被它占死 | 错误计数超限 → 自动 error-passive → bus-off 退场 |
所以「CAN 稳」不是软件写得好,是硬件实现 + 比特级规则的功劳。
二、一帧 CAN 长什么样
标准帧(11 位 ID):
1 11 1 1 1 4 0~64 15 1 1 1 7
┌───┬────────┬───┬───┬──┬───────┬───────────────┬─────────────┬───┬───┬────┬────────┐
│SOF│ ID │RTR│IDE│r0│ DLC │ DATA 0~8B │ CRC 15 │DEL│ACK│DEL │ EOF │
└───┴────────┴───┴───┴──┴───────┴───────────────┴─────────────┴───┴───┴────┴────────┘
显性 越小越优先 接收方算出来不对就发错误帧几个必须记住的点:
- 显性(dominant,逻辑 0)会盖住隐性(recessive,逻辑 1)。
这是仲裁的全部基础,也是「ID 越小优先级越高」的原因。
- DLC 超过 8 时,数据长度按 8 字节算。DLC 是 4 位(0~15),
但 9~15 都表示 8 字节,不是无效值。很多自定义协议在这里出错。
- 远程帧(RTR = 1)没有数据,DLC 表示「我想要几字节」。
但它和同名数据帧的仲裁字段完全相同,所以远程帧和数据帧会撞车, 实际项目里建议别用远程帧。
- ACK 位是所有接收方一起拉显性的。发送方只要发现 ACK 位是隐性,
就知道「没有一个人在听」,这是 CAN 唯一能确认「至少有人收到」的机制。
三、位填充:为什么每 5 位就要插一个反的
CAN 是异步总线,没有时钟线。接收方靠边沿来重同步自己的位定时器。 如果数据里连续出现很多个相同位(比如一个 0x00 的字节就是 8 个连续显性位), 线上一段时间没有跳变,接收方只能靠自己的晶振数时间——累积误差就会导致数错位。
解法非常直接:发送方在连续 5 个相同位之后,强制插入 1 个相反位。 接收方知道这个规则,看到 5 个相同位之后就把下一个位丢掉。这样:
- 线上永远不会出现连续 6 个相同位,每 5 个位必有一次跳变,边沿足够密
- 如果接收方看到了 6 个连续相同位,说明出错了(这就是「位填充错误」)
- 填充位不参与 CRC,CRC 只算去填充之后的有效位
实测数据(本文第 2 项):同样是 8 字节数据、同一个 11 位 ID(0x123),
| 数据内容 | 帧位 | 填充位 | 线上位 | 膨胀率 |
|---|---|---|---|---|
| 8 个 0x00 | 98 | 14 | 112 | 14% |
| 8 个 0xFF | 98 | 13 | 111 | 13% |
| 8 个 0x7F | 98 | 9 | 107 | 9% |
| 8 个 0x0F | 98 | 2 | 100 | 2% |
| 8 个 0xAA / 0x55 / 0xF0 / 0x11 | 98 | 1 | 99 | 1% |
也就是说同样一帧,线上位数在 99~112 之间浮动。 做总线负载率估算时,如果按教科书那条「47 + 8×DLC」的公式去算, 你得到的永远只是平均值,最坏情况会低估 14%。 算总线负载必须按最坏情况(全 0 或全 0xFF 之类的数据)留余量, 否则高负载时你会为找不到原因的丢帧抓狂。
四、CRC15:一个 15 位多项式撑起整个检错能力
CAN 用的是 CRC-15,生成多项式 x^15 + x^14 + x^10 + x^8 + x^7 + x^4 + x^3 + 1, 写成十六进制是 0x4599。计算范围是从 SOF 到数据场结束(不含填充位)。
uint16_t can_crc15(const uint8_t *bits, uint32_t n)
{
uint16_t crc = 0;
uint32_t i;
for (i = 0; i < n; i++) {
int inv = (int)((crc >> 14) & 1u) ^ (int)(bits[i] & 1u);
crc = (uint16_t)((crc << 1) & 0x7FFFu);
if (inv) {
crc ^= CAN_CRC15_POLY; /* 0x4599 */
}
}
return crc;
}它的检错能力有明确的数学保证(本文第 4 项实测验证):
- 所有单比特错误 100% 检出
- 所有双比特错误 100% 检出(在 CAN 的最大帧长之内)
- 总长不超过 15 位的突发错误 100% 检出
- 其余错误的漏检率约 2^-15
注意一个常见误解:CRC 不是加密,也不能纠错。它只能告诉你「这帧坏了」。 CAN 的应对是让发送方重发,而不是纠正。
五、错误计数:让坏节点自己退场
每个 CAN 节点有两个计数器:
- TEC(发送错误计数):发送出错 +8,发送成功 −1
- REC(接收错误计数):接收出错 +1(特定情况下 +8),接收成功 −1
状态机:
error-active ──TEC 或 REC > 127──> error-passive ──TEC > 255──> bus-off
↑ │ │
└──────────── 128 次 11 个连续隐性位 ─┴──────────────────────────┘三个状态的行为差别很关键:
| 状态 | 能否发送 | 出错时发什么 | 含义 |
|---|---|---|---|
| error-active | 可以 | 主动错误标志(6 个显性位) | 我认为总线错了,大家都注意 |
| error-passive | 可以,但发完要等 | 被动错误标志(6 个隐性位) | 我可能才是坏的那个,我小声说 |
| bus-off | 完全断开 | 什么都不发 | 我确定自己坏了,退出总线 |
为什么 +8 和 −1 这么不对称? 因为规则要保证「偶尔出错的好节点」 能自愈,「持续出错的坏节点」迅速下线:
- 一个节点每 8 次成功才抵消 1 次错误,正常节点(错误率 < 12%)TEC 始终接近 0
- 一个真正坏掉的节点每次发送都错,TEC 每帧 +8 → 16 帧就 passive,32 帧就 bus-off
- 从 bus-off 恢复需要观察 128 次「11 个连续隐性位」,
也就是说要等总线安静相当长一段时间才能回来——这是个精心设计的惩罚项
六、主机实测(本文数据来源)
| # | 实验 | 实测结果 |
|---|---|---|
| 1 | 编解码往返 | 8 字节帧线上 106 位,解码后 ID/DLC/数据全部一致 |
| 2 | 位填充统计 | 8 种数据模式下:填充位 1~14 个,线上 99~112 位,膨胀 1%~14% |
| 3 | 破坏填充位 | 第 18 位就被判为位填充错误——比 CRC 更早发现 |
| 4 | 线上逐位翻转(103 位全遍历) | 填充错误 14 个、CRC 错误 61 个、帧长不符 27 个、SOF 非法 1 个,漏检 0 |
| 5 | CRC15 单比特(去填充后) | 遍历帧内全部 98 个位,漏检 0 |
| 6 | CRC15 双比特(去填充后) | 遍历全部 4753 种组合,漏检 0 |
| 7 | 三节点同时发送 | 0x0A0 胜出;0x120 在 ID 第 3 位退出,0x400 在第 1 位退出 |
| 8 | 失败方重发 | 三轮后实际发送顺序恒为 0x0A0 → 0x120 → 0x400(ID 升序) |
| 9 | 验收过滤器 | 0x120/0x7F0 只收 0x120~0x12F;0x100/0x700 收 0x100~0x1FF;无过滤器全收 |
| 10 | DLC = 12 | 按 8 字节处理(不报错、也不会读 12 字节) |
| 11 | 连续发送失败 | 第 15 次 TEC=120 仍 active;第 16 次 128 → error-passive;第 32 次 256 → bus-off |
| 12 | 好节点(每 8 成功夹 1 错) | 跑 20 轮后 TEC = 0,仍 error-active |
| 13 | bus-off 恢复 | 观察到 128 次「11 个连续隐性位」后回到 error-active,TEC 清零 |
第 4 项是整篇里最有意思的一组数字:同一个单比特干扰,被什么机制抓住是不固定的。 动到填充位附近 → 填充规则先报警(比 CRC 更早,也意味着错误帧能更早发出); 动到普通数据位 → CRC 报错;动到 SOF → 格式直接不合法。 但无论落在哪里,106 位里没有任何一个位置能溜过去。 这正是 CAN 在物理层就敢保证可靠性的底气。
第 7、8 项是 CAN 最漂亮的地方:仲裁失败不浪费带宽、不重传、不需要软件介入, 失败方下一个位就变接收,等这一帧结束后自动重发。 而且因为 ID 越小越优先,三轮下来的实际发送顺序天然就是 ID 升序—— 这相当于总线免费给了你一个优先级队列。
七、上真机的坑
- 必须 120 Ω 端接,而且只在总线两端各一个。中间节点不要接。
少了端接会有反射,眼图糊掉;多了端接负载太重,幅值不够。 很多「CAN 通信偶发丢帧」最后查出来就是端接电阻。
- CAN_H / CAN_L 是差分对,必须双绞,而且别和电机线走同一个线槽。
这是现场最常被忽略的一条。
- 波特率和采样点。仲裁靠逐位比较,采样点必须落在位的 75%~87.5% 位置。
两个节点波特率一致但采样点差太多,短帧能通、长帧就错—— 典型症状是「小数据量正常,大数据量必挂」。
- 别用远程帧。它和同名数据帧仲裁字段完全相同,会撞车;
而且很多厂商的收发器对 RTR 处理不一致。
- bus-off 之后必须显式恢复。多数 MCU 会置位一个标志就不再自动回来,
你要么重新初始化外设,要么等够 128 次总线空闲。 现场表现是「设备掉线了再也回不来」,一定要在代码里处理这个分支。
- 验收过滤器写在硬件里,但别指望它防盗。
过滤器只能减少 CPU 中断负担,不是安全机制。
完整代码
Makefile
CC ?= gcc
CFLAGS ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS ?=
SRC = src/can_frame.c src/can_node.c
TEST = test/test_can.c
ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)
all: run
$(BIN): $(SRC) $(TEST)
@mkdir -p build
$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)
run: $(BIN)
@$(BIN)
clean:
rm -rf build
.PHONY: all run cleaninclude/can_frame.h
/**
* can_frame.h - CAN 2.0A 帧:比特级编解码、位填充、CRC15、验收过滤
*
* 这一层的目的是把 CAN 的比特级规则讲清楚并且可验证:
* - 帧格式(SOF / ID / RTR / IDE / r0 / DLC / DATA / CRC15)
* - 位填充(每 5 个相同位插 1 个反向位,最大帧长固定)
* - CRC15(多项式 0x4599)
* - 验收过滤器(code + mask)
*
* 上位机不需要接真实总线就能把这几件事全测一遍。
*/
#ifndef CAN_FRAME_H
#define CAN_FRAME_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define CAN_ID_STD_MAX 0x7FFu
#define CAN_DLC_MAX 8u
#define CAN_BITS_MAX 256u /* 单个帧最多这么多位(含填充) */
#define CAN_CRC15_POLY 0x4599u
/** 位填充统计结果 */
typedef struct {
uint32_t raw_bits; /* 有效位数(不含填充) */
uint32_t stuff_bits; /* 插入的填充位数 */
uint32_t total_bits; /* 线上实际位数 = raw + stuff */
} can_stuff_stat_t;
typedef struct {
uint32_t id; /* 标准帧 11 位 */
uint8_t dlc; /* 0~8;>8 按 8 处理 */
uint8_t data[8];
uint8_t ide; /* 1 = 扩展帧(本工程只实现标准帧) */
uint8_t rtr; /* 1 = 远程帧(无数据) */
} can_frame_t;
typedef struct {
uint32_t code; /* 期望的 ID 位模式 */
uint32_t mask; /* 1 = 这一位要比较,0 = 不关心 */
} can_filter_t;
/** 生成不含填充、不含 CRC 的原始位流(SOF..DATA)。返回位数 */
uint32_t can_raw_bits(const can_frame_t *f, uint8_t *out, uint32_t cap);
/** CRC15。输入是位数组(每个元素 0 或 1) */
uint16_t can_crc15(const uint8_t *bits, uint32_t n);
/** 按位填充规则把 raw 展开成线上位流。返回总位数 */
uint32_t can_stuff(const uint8_t *raw, uint32_t n, uint8_t *out, uint32_t cap);
/** 反过来:去掉填充位。返回有效位数;返回 0 表示填充规则被违反 */
uint32_t can_destuff(const uint8_t *in, uint32_t n, uint8_t *out, uint32_t cap);
/** 完整编码:SOF..DATA + CRC15,并做位填充。返回线上位数 */
uint32_t can_encode(const can_frame_t *f, uint8_t *out, uint32_t cap);
/**
* 完整解码并校验 CRC。返回 0 = 成功
* -1 = 填充规则被违反 -2 = SOF 不是显性
* -3 = 位数不够 -4 = 帧长和 DLC 不符
* -5 = CRC 校验失败
*/
int can_decode(const uint8_t *bits, uint32_t n, can_frame_t *out);
/** 统计一个帧的填充情况 */
void can_stuff_stat(const can_frame_t *f, can_stuff_stat_t *st);
/** 验收过滤:命中返回 1 */
int can_filter_match(const can_filter_t *f, uint32_t id);
#ifdef __cplusplus
}
#endif
#endif /* CAN_FRAME_H */include/can_node.h
/**
* can_node.h - CAN 节点状态机:错误计数、错误状态、验收过滤、仲裁
*
* 真实项目里 TEC/REC 由 CAN 外设硬件维护,你只需要读寄存器;
* 这里实现一遍是为了把「为什么 16 帧就 passive、32 帧就 bus-off」算清楚。
*/
#ifndef CAN_NODE_H
#define CAN_NODE_H
#include <stdint.h>
#include "can_frame.h"
#ifdef __cplusplus
extern "C" {
#endif
#define CAN_TEC_PASSIVE_LIMIT 128u
#define CAN_TEC_BUSOFF_LIMIT 256u
#define CAN_BUS_OFF_RECOVER_CNT 128u
#define CAN_FILTER_MAX 4
typedef enum {
CAN_ST_ACTIVE = 0, /* 正常,出错时发主动错误标志 */
CAN_ST_PASSIVE, /* 错误被动,出错时只能发被动错误标志 */
CAN_ST_BUS_OFF /* 已下线,完全不再驱动总线 */
} can_state_t;
typedef struct {
uint32_t tec;
uint32_t rec;
can_state_t state;
uint32_t tx_ok;
uint32_t tx_err;
uint32_t rx_ok;
uint32_t rx_drop; /* 被验收过滤器挡掉的帧数 */
uint32_t busoff_cnt;
uint32_t recover_steps; /* bus-off 恢复计数 */
can_filter_t filter[CAN_FILTER_MAX];
int nfilter;
} can_node_t;
void can_node_init(can_node_t *n);
void can_node_add_filter(can_node_t *n, uint32_t code, uint32_t mask);
int can_node_accepts(const can_node_t *n, uint32_t id);
void can_node_tx_ok(can_node_t *n);
void can_node_tx_err(can_node_t *n);
void can_node_rx(can_node_t *n, uint32_t id);
void can_node_rx_err(can_node_t *n);
/** bus-off 之后每观察到一次「11 个连续隐性位」就调一次 */
int can_node_recover_step(can_node_t *n);
const char *can_state_name(can_state_t s);
/**
* 非破坏性仲裁:多个节点同时开口,谁赢。
* frames / n 是竞争者的原始位流(用 can_raw_bits 得到)。
* 返回胜出者下标;drop_bit[i] 记录第 i 个节点在第几个位退出(-1 = 没退出)。
*/
int can_arbitrate(const can_frame_t *frames, int n, int *drop_bit, uint32_t *arbitration_bits);
#ifdef __cplusplus
}
#endif
#endif /* CAN_NODE_H */platformio.ini
[platformio]
default_envs = bluepill
[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
-Wall
-Wextra
-Isrc
lib_ldf_mode = deep+src/can_frame.c
#include "can_frame.h"
#include <string.h>
/* ---------------- 位流写入(带填充) ---------------- */
typedef struct {
uint8_t *buf;
uint32_t cap;
uint32_t n;
int prev;
uint32_t run;
int stuff;
int overflow;
} bitw_t;
static void bw_put(bitw_t *w, int bit)
{
if (w->n >= w->cap) {
w->overflow = 1;
return;
}
if (w->stuff) {
if (w->n > 0u && bit == w->prev) {
w->run++;
} else {
w->run = 1u;
}
w->prev = bit;
w->buf[w->n++] = (uint8_t)bit;
/* 连续 5 个相同位之后,强制插一个反向位。
* 插入位的 run 从 1 重新开始(因为它和前面相反)。 */
if (w->run == 5u) {
if (w->n >= w->cap) {
w->overflow = 1;
return;
}
w->buf[w->n++] = (uint8_t)(bit ^ 1);
w->prev = bit ^ 1;
w->run = 1u;
}
} else {
w->buf[w->n++] = (uint8_t)bit;
}
}
static void bw_put_field(bitw_t *w, uint32_t val, int nbits)
{
int b;
for (b = nbits - 1; b >= 0; b--) {
bw_put(w, (int)((val >> b) & 1u));
}
}
/* ---------------- 原始位流 ---------------- */
uint32_t can_raw_bits(const can_frame_t *f, uint8_t *out, uint32_t cap)
{
bitw_t w;
uint32_t i;
uint8_t dlc;
w.buf = out;
w.cap = cap;
w.n = 0;
w.prev = 0;
w.run = 0;
w.stuff = 0;
w.overflow = 0;
bw_put(&w, 0); /* SOF:必须是显性 */
bw_put_field(&w, f->id & CAN_ID_STD_MAX, 11);
bw_put(&w, f->rtr ? 1 : 0); /* RTR */
bw_put(&w, f->ide ? 1 : 0); /* IDE */
bw_put(&w, 0); /* r0:保留位,固定显性 */
/* DLC 是 4 位。注意 >8 时表示「8 字节」,不是无效、也不是更多 */
dlc = f->dlc;
if (dlc > CAN_DLC_MAX) {
dlc = CAN_DLC_MAX;
}
bw_put_field(&w, dlc, 4);
if (!f->rtr) {
for (i = 0; i < dlc; i++) {
bw_put_field(&w, f->data[i], 8);
}
}
return w.overflow ? 0u : w.n;
}
uint16_t can_crc15(const uint8_t *bits, uint32_t n)
{
uint16_t crc = 0;
uint32_t i;
for (i = 0; i < n; i++) {
int inv = (int)((crc >> 14) & 1u) ^ (int)(bits[i] & 1u);
crc = (uint16_t)((crc << 1) & 0x7FFFu);
if (inv) {
crc ^= CAN_CRC15_POLY;
}
}
return crc;
}
/* ---------------- 填充 / 去填充 ---------------- */
uint32_t can_stuff(const uint8_t *raw, uint32_t n, uint8_t *out, uint32_t cap)
{
bitw_t w;
uint32_t i;
w.buf = out;
w.cap = cap;
w.n = 0;
w.prev = 0;
w.run = 0;
w.stuff = 1;
w.overflow = 0;
for (i = 0; i < n; i++) {
bw_put(&w, raw[i] & 1u);
}
return w.overflow ? 0u : w.n;
}
uint32_t can_destuff(const uint8_t *in, uint32_t n, uint8_t *out, uint32_t cap)
{
uint32_t i = 0;
uint32_t o = 0;
int prev = -1;
uint32_t run = 0;
while (i < n) {
int b = in[i++];
if (b == prev) {
run++;
if (run >= 6u) {
return 0; /* 6 个连续相同位:线上不可能出现,必是错 */
}
} else {
prev = b;
run = 1u;
}
if (o >= cap) {
return 0;
}
out[o++] = (uint8_t)b;
if (run == 5u) {
/* 按规则,下一个位是填充位,必须和前面相反,并且丢掉。
* 关键:丢掉之后 run 要从 1 重新开始,并且把 prev 更新成
* 填充位的值——否则下一个数据位会被误判成「第 6 个连续位」。 */
if (i >= n) {
return 0;
}
if ((in[i] & 1u) == (uint8_t)prev) {
return 0; /* 填充位不合法 */
}
prev = (int)(in[i] & 1u);
run = 1u;
i++;
}
}
return o;
}
/* ---------------- 完整编解码 ---------------- */
uint32_t can_encode(const can_frame_t *f, uint8_t *out, uint32_t cap)
{
uint8_t raw[CAN_BITS_MAX];
uint32_t rn;
uint16_t crc;
int i;
rn = can_raw_bits(f, raw, sizeof(raw));
if (rn == 0u) {
return 0;
}
crc = can_crc15(raw, rn);
/* CRC 15 位,MSB 先发 */
for (i = 14; i >= 0; i--) {
raw[rn++] = (uint8_t)((crc >> i) & 1u);
}
return can_stuff(raw, rn, out, cap);
}
int can_decode(const uint8_t *bits, uint32_t n, can_frame_t *out)
{
uint8_t raw[CAN_BITS_MAX];
uint32_t rn;
uint32_t dlc = 0;
uint32_t need;
uint32_t i;
uint16_t crc_got = 0;
uint16_t crc_calc;
rn = can_destuff(bits, n, raw, sizeof(raw));
if (rn == 0u) {
return -1;
}
if (raw[0] != 0u) {
return -2; /* SOF 必须是显性 */
}
if (rn < 19u) {
return -3;
}
memset(out, 0, sizeof(*out));
for (i = 0; i < 11u; i++) {
out->id = (out->id << 1) | (uint32_t)raw[1u + i];
}
out->rtr = raw[12];
out->ide = raw[13];
for (i = 0; i < 4u; i++) {
dlc = (dlc << 1) | (uint32_t)raw[15u + i];
}
/* 和发送侧同一条规则:DLC > 8 一律按 8 字节 */
if (dlc > CAN_DLC_MAX) {
dlc = CAN_DLC_MAX;
}
out->dlc = (uint8_t)dlc;
need = 19u + 8u * dlc + 15u;
if (rn != need) {
return -4;
}
if (!out->rtr) {
for (i = 0; i < dlc; i++) {
uint8_t v = 0;
int b;
for (b = 0; b < 8; b++) {
v = (uint8_t)((v << 1) | (uint8_t)raw[19u + 8u * i + (uint32_t)b]);
}
out->data[i] = v;
}
}
for (i = 0; i < 15u; i++) {
crc_got = (uint16_t)((crc_got << 1) | (uint16_t)raw[19u + 8u * dlc + i]);
}
crc_calc = can_crc15(raw, 19u + 8u * dlc);
if (crc_got != crc_calc) {
return -5;
}
return 0;
}
void can_stuff_stat(const can_frame_t *f, can_stuff_stat_t *st)
{
uint8_t raw[CAN_BITS_MAX];
uint8_t stuffed[CAN_BITS_MAX];
uint32_t rn;
uint16_t crc;
uint32_t tn;
int i;
rn = can_raw_bits(f, raw, sizeof(raw));
crc = can_crc15(raw, rn);
for (i = 14; i >= 0; i--) {
raw[rn++] = (uint8_t)((crc >> i) & 1u);
}
tn = can_stuff(raw, rn, stuffed, sizeof(stuffed));
st->raw_bits = rn;
st->total_bits = tn;
st->stuff_bits = tn - rn;
}
int can_filter_match(const can_filter_t *f, uint32_t id)
{
return ((id & f->mask) == (f->code & f->mask)) ? 1 : 0;
}src/can_node.c
#include "can_node.h"
#include <string.h>
void can_node_init(can_node_t *n)
{
memset(n, 0, sizeof(*n));
n->state = CAN_ST_ACTIVE;
}
void can_node_add_filter(can_node_t *n, uint32_t code, uint32_t mask)
{
if (n->nfilter < CAN_FILTER_MAX) {
n->filter[n->nfilter].code = code;
n->filter[n->nfilter].mask = mask;
n->nfilter++;
}
}
int can_node_accepts(const can_node_t *n, uint32_t id)
{
int i;
if (n->nfilter == 0) {
return 1; /* 没配过滤器 = 全收 */
}
for (i = 0; i < n->nfilter; i++) {
if (can_filter_match(&n->filter[i], id)) {
return 1;
}
}
return 0;
}
/* ---------------- 错误计数 ---------------- */
/*
* 为什么是 +8 和 -1?
*
* 发送出错 TEC += 8
* 发送成功 TEC -= 1
*
* 这意味着一个「偶尔出错」的节点需要连续 8 次成功才能抵消 1 次错误。
* 正常节点的错误率远低于 1/8,TEC 会一直贴在 0 附近;
* 而真正坏掉的节点每次发送都错,TEC 每帧涨 8:
* 16 帧 -> 128 -> error-passive
* 32 帧 -> 256 -> bus-off
* 这个不对称是故意设计的:好节点自愈,坏节点迅速退场。
*/
static void node_update_state(can_node_t *n)
{
if (n->state == CAN_ST_BUS_OFF) {
return;
}
if (n->tec >= CAN_TEC_BUSOFF_LIMIT) {
n->state = CAN_ST_BUS_OFF;
n->busoff_cnt++;
n->recover_steps = 0;
return;
}
if (n->tec >= CAN_TEC_PASSIVE_LIMIT || n->rec >= CAN_TEC_PASSIVE_LIMIT) {
n->state = CAN_ST_PASSIVE;
return;
}
n->state = CAN_ST_ACTIVE;
}
void can_node_tx_ok(can_node_t *n)
{
if (n->state == CAN_ST_BUS_OFF) {
return;
}
if (n->tec > 0u) {
n->tec--;
}
n->tx_ok++;
node_update_state(n);
}
void can_node_tx_err(can_node_t *n)
{
if (n->state == CAN_ST_BUS_OFF) {
return;
}
n->tec += 8u;
n->tx_err++;
node_update_state(n);
}
void can_node_rx(can_node_t *n, uint32_t id)
{
if (n->state == CAN_ST_BUS_OFF) {
return;
}
if (can_node_accepts(n, id)) {
n->rx_ok++;
} else {
n->rx_drop++;
}
if (n->rec > 0u) {
n->rec--; /* 成功接收会抵消之前的接收错误 */
}
node_update_state(n);
}
void can_node_rx_err(can_node_t *n)
{
if (n->state == CAN_ST_BUS_OFF) {
return;
}
n->rec += 1u;
node_update_state(n);
}
int can_node_recover_step(can_node_t *n)
{
if (n->state != CAN_ST_BUS_OFF) {
return 0;
}
n->recover_steps++;
if (n->recover_steps >= CAN_BUS_OFF_RECOVER_CNT) {
n->state = CAN_ST_ACTIVE;
n->tec = 0;
n->rec = 0;
n->recover_steps = 0;
return 1; /* 恢复了 */
}
return 0;
}
const char *can_state_name(can_state_t s)
{
switch (s) {
case CAN_ST_ACTIVE:
return "error-active";
case CAN_ST_PASSIVE:
return "error-passive";
case CAN_ST_BUS_OFF:
return "bus-off";
default:
return "unknown";
}
}
/* ---------------- 非破坏性仲裁 ---------------- */
/* 越界的位一律当隐性(1):帧结束了就等于不再驱动总线 */
static int raw_bit(const uint8_t *r, uint32_t len, uint32_t bit)
{
return (bit < len) ? (r[bit] & 1u) : 1;
}
int can_arbitrate(const can_frame_t *frames, int n, int *drop_bit,
uint32_t *arbitration_bits)
{
uint8_t raw[8][CAN_BITS_MAX];
uint32_t len[8];
int alive[8];
int na = n;
int i;
uint32_t bit = 0;
uint32_t maxlen = 0;
int winner = -1;
if (n <= 0) {
return -1;
}
if (n > 8) {
n = 8;
na = n;
}
for (i = 0; i < n; i++) {
len[i] = can_raw_bits(&frames[i], raw[i], sizeof(raw[i]));
alive[i] = 1;
drop_bit[i] = -1;
if (len[i] > maxlen) {
maxlen = len[i];
}
}
/* SOF 大家都是显性,从第 1 个位(ID 的最高位)开始比 */
for (bit = 1; bit < maxlen && na > 1; bit++) {
int bus = 1; /* 默认隐性 */
for (i = 0; i < n; i++) {
if (alive[i] && raw_bit(raw[i], len[i], bit) == 0) {
bus = 0; /* 有人拉显性,总线就是显性 */
break;
}
}
if (bus == 0) {
/* 发隐性的节点:它读到的是 0,但自己发的是 1 —— 仲裁失败,立刻退出 */
for (i = 0; i < n; i++) {
if (alive[i] && raw_bit(raw[i], len[i], bit) == 1) {
alive[i] = 0;
drop_bit[i] = (int)bit;
na--;
}
}
}
}
for (i = 0; i < n; i++) {
if (alive[i]) {
winner = i;
break;
}
}
if (arbitration_bits != NULL) {
*arbitration_bits = (bit > 0u) ? (bit - 1u) : 0u;
}
(void)bit;
return winner;
}src/main.c
/**
* STM32F103 bxCAN 硬件层:过滤器配置 + 收发
*
* 帧格式、填充、CRC、仲裁、错误计数全部由 bxCAN 外设硬件完成,
* 软件只需要配过滤器、投递邮箱、读 FIFO。
*/
#include "stm32f1xx_hal.h"
#include "can_frame.h"
#include "can_node.h"
static CAN_HandleTypeDef hcan;
void can_hw_init(void)
{
CAN_FilterTypeDef flt = {0};
hcan.Instance = CAN1;
hcan.Init.Prescaler = 4; /* 36 MHz / 4 = 9 MHz */
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_13TQ;
hcan.Init.TimeSeg2 = CAN_BS2_2TQ; /* 采样点 = (1+13)/(1+13+2) = 87.5% */
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = DISABLE; /* bus-off 之后我们自己处理 */
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = ENABLE; /* 仲裁失败自动重发 */
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
if (HAL_CAN_Init(&hcan) != HAL_OK) {
Error_Handler();
}
/*
* 验收过滤器:只收 0x120~0x12F。
* 掩码位为 1 表示这一位必须和 ID 相同,为 0 表示不关心。
* 注意:过滤器不是安全机制,只是减少中断负担。
*/
flt.FilterBank = 0;
flt.FilterMode = CAN_FILTERMODE_IDMASK;
flt.FilterScale = CAN_FILTERSCALE_32BIT;
flt.FilterIdHigh = (uint16_t)(0x120u << 5); /* ID 左移 5 位放进寄存器 */
flt.FilterIdLow = 0;
flt.FilterMaskIdHigh = (uint16_t)(0x7F0u << 5);
flt.FilterMaskIdLow = 0;
flt.FilterFIFOAssignment = CAN_RX_FIFO0;
flt.FilterActivation = ENABLE;
if (HAL_CAN_ConfigFilter(&hcan, &flt) != HAL_OK) {
Error_Handler();
}
HAL_CAN_Start(&hcan);
/* bus-off 和错误被动都要开中断,否则现场出问题你什么日志都看不到 */
HAL_CAN_ActivateNotification(&hcan,
CAN_IT_RX_FIFO0_MSG_PENDING |
CAN_IT_ERROR_WARNING |
CAN_IT_ERROR_PASSIVE |
CAN_IT_BUSOFF |
CAN_IT_LAST_ERROR_CODE);
}
int can_send(uint32_t id, const uint8_t *data, uint8_t dlc)
{
CAN_TxHeaderTypeDef h = {0};
uint32_t mailbox;
h.StdId = id & CAN_ID_STD_MAX;
h.IDE = CAN_ID_STD;
h.RTR = CAN_RTR_DATA;
h.DLC = dlc;
h.TransmitGlobalTime = DISABLE;
return (HAL_CAN_AddTxMessage(&hcan, &h, (uint8_t *)data, &mailbox) == HAL_OK)
? 0 : -1;
}
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hc)
{
CAN_RxHeaderTypeDef h;
uint8_t data[8];
if (HAL_CAN_GetRxMessage(hc, CAN_RX_FIFO0, &h, data) != HAL_OK) {
return;
}
/* h.DLC 是硬件给的,9~15 时硬件已经按 8 处理过了 */
(void)data;
(void)h;
}
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hc)
{
uint32_t err = HAL_CAN_GetError(hc);
uint32_t esr = hc->Instance->ESR;
/*
* ESR 里 TEC/REC 各 8 位,直接读出来打日志。
* 现场排查「偶发丢帧」时,这两个数字比什么都直白:
* TEC 一直涨 = 你的发送有问题(接线、端接、波特率)
* REC 一直涨 = 别人的发送你收不对(同样先怀疑物理层)
*/
uint32_t tec = (esr >> 16) & 0xFFu;
uint32_t rec = (esr >> 24) & 0xFFu;
(void)err;
(void)tec;
(void)rec;
}
int main(void)
{
uint8_t d[8] = {1, 2, 3, 4, 5, 6, 7, 8};
HAL_Init();
SystemClock_Config();
can_hw_init();
for (;;) {
if (can_send(0x120, d, 8) != 0) {
/* 发送邮箱满:说明总线负载过高,或者被低优先级帧拖住了 */
HAL_Delay(1);
continue;
}
HAL_Delay(100);
}
}test/test_can.c
/**
* 主机端测试:CAN 帧比特级规则
*
* gcc -std=c99 -Wall -Wextra -Iinclude src/can_frame.c src/can_node.c \
* test/test_can.c -o build/test
*/
#include <stdio.h>
#include <string.h>
#include "can_frame.h"
#include "can_node.h"
static int failed = 0;
static void check(int cond, const char *what)
{
if (!cond) {
printf(" [FAIL] %s\n", what);
failed++;
}
}
static uint8_t g_bits[CAN_BITS_MAX];
static void print_hex(const uint8_t *d, int n)
{
int i;
for (i = 0; i < n; i++) {
printf("%02X", d[i]);
if (i + 1 < n) {
printf(" ");
}
}
}
int main(void)
{
can_frame_t f;
can_frame_t g;
int i;
printf("===== CAN 2.0A 比特级规则实测 =====\n\n");
/* ---------- [1] 编解码往返 ---------- */
printf("[1] 编解码往返\n");
{
uint32_t n;
memset(&f, 0, sizeof(f));
f.id = 0x123;
f.dlc = 8;
for (i = 0; i < 8; i++) {
f.data[i] = (uint8_t)(0x10 * (i + 1));
}
n = can_encode(&f, g_bits, sizeof(g_bits));
printf(" ID=0x%03X DLC=%u 数据 ", f.id, f.dlc);
print_hex(f.data, 8);
printf("\n 编码后线上位数 = %u\n", n);
check(n > 0u, "编码失败");
memset(&g, 0, sizeof(g));
check(can_decode(g_bits, n, &g) == 0, "解码失败");
printf(" 解码回来 ID=0x%03X DLC=%u 数据 ", g.id, g.dlc);
print_hex(g.data, 8);
printf("\n");
check(g.id == f.id && g.dlc == f.dlc, "ID 或 DLC 不一致");
check(memcmp(g.data, f.data, 8) == 0, "数据不一致");
}
/* ---------- [2] 位填充统计 ---------- */
printf("\n[2] 位填充:同一 ID、同一 DLC,不同数据内容的填充位数\n");
{
can_stuff_stat_t st;
struct { const char *name; uint8_t v; } pat[8] = {
{"8 个 0x00", 0x00u},
{"8 个 0xFF", 0xFFu},
{"8 个 0xAA", 0xAAu},
{"8 个 0x55", 0x55u},
{"8 个 0x0F", 0x0Fu},
{"8 个 0xF0", 0xF0u},
{"8 个 0x11", 0x11u},
{"8 个 0x7F", 0x7Fu}
};
uint32_t max_stuff = 0;
uint32_t min_stuff = 1000u;
uint32_t max_total = 0;
uint32_t min_total = 1000u;
int i2;
printf(" %-12s %8s %8s %8s %8s\n",
"数据内容", "帧位", "填充位", "线上位", "膨胀率");
for (i2 = 0; i2 < 8; i2++) {
memset(&f, 0, sizeof(f));
f.id = 0x123;
f.dlc = 8;
for (i = 0; i < 8; i++) {
f.data[i] = pat[i2].v;
}
can_stuff_stat(&f, &st);
printf(" %-12s %8u %8u %8u %7u%%\n", pat[i2].name,
st.raw_bits, st.stuff_bits, st.total_bits,
(st.total_bits * 100u) / st.raw_bits - 100u);
if (st.stuff_bits > max_stuff) { max_stuff = st.stuff_bits; }
if (st.stuff_bits < min_stuff) { min_stuff = st.stuff_bits; }
if (st.total_bits > max_total) { max_total = st.total_bits; }
if (st.total_bits < min_total) { min_total = st.total_bits; }
}
printf(" -> 填充位 %u ~ %u 个,线上位数 %u ~ %u,"
"膨胀率 %u%% ~ %u%%\n",
min_stuff, max_stuff, min_total, max_total,
(min_total * 100u) / 98u - 100u, (max_total * 100u) / 98u - 100u);
printf(" -> 按「47 + 8*DLC」估的 111 位只是个平均数,"
"算总线负载必须带上填充位\n");
check(max_stuff >= min_stuff, "");
}
/* ---------- [3] 填充错误检测 ---------- */
printf("\n[3] 故意破坏填充规则(真实总线上不可能出现 6 个连续相同位)\n");
{
uint32_t n;
int at = -1;
memset(&f, 0, sizeof(f));
f.id = 0x123;
f.dlc = 1;
f.data[0] = 0x00;
n = can_encode(&f, g_bits, sizeof(g_bits));
/* 编码后的位流里必然每隔 5 个相同位插一个反向位。
* 找到「5 个连续相同位 + 后面紧跟的反向填充位」,把那个填充位改成同值,
* 就人为制造出 6 连——这正是线路受干扰时会发生的错误。 */
for (i = 0; (uint32_t)i + 5 < n; i++) {
if (g_bits[i] == g_bits[i + 1] && g_bits[i + 1] == g_bits[i + 2] &&
g_bits[i + 2] == g_bits[i + 3] && g_bits[i + 3] == g_bits[i + 4] &&
g_bits[i + 5] != g_bits[i]) {
g_bits[i + 5] = g_bits[i]; /* 破坏填充位 */
at = i;
break;
}
}
printf(" 编码后线上 %u 位,把第 %d 位之后的填充位改成同值,"
"制造出 6 连位 %d~%d\n", n, at + 5, at, at + 5);
check(at >= 0, "没找到可破坏的填充位");
printf(" 解码返回 %d(-1 = 位填充错误)\n", can_decode(g_bits, n, &g));
check(can_decode(g_bits, n, &g) == -1, "6 连位没有被判为填充错误");
printf(" -> 这是接收方能拿到的最早的错误信号:\n");
printf(" 不用等 CRC,在第 %d 位就已经知道帧废了,"
"可以立刻开始发错误帧\n", at + 6);
/* [3b] 线上位流里每一位都可能被干扰翻转。
* 有意思的是「被什么机制抓住」并不固定:
* 动到填充位附近 -> 填充规则先报警(比 CRC 更早)
* 动到普通数据位 -> CRC 报错
* 动到 SOF -> 格式直接不合法
* 全都跑一遍,看分布。 */
{
uint8_t clean[CAN_BITS_MAX];
uint8_t test[CAN_BITS_MAX];
uint32_t cn;
uint32_t n_stuff = 0;
uint32_t n_crc = 0;
uint32_t n_len = 0;
uint32_t n_sof = 0;
uint32_t n_miss = 0;
uint32_t k;
int rc2;
memset(&f, 0, sizeof(f));
f.id = 0x123;
f.dlc = 8;
for (i = 0; i < 8; i++) {
f.data[i] = (uint8_t)(i * 31 + 7);
}
cn = can_encode(&f, clean, sizeof(clean));
for (k = 0; k < cn; k++) {
memcpy(test, clean, cn);
test[k] = (uint8_t)(test[k] ^ 1u);
rc2 = can_decode(test, cn, &g);
switch (rc2) {
case -1: n_stuff++; break;
case -2: n_sof++; break;
case -4: n_len++; break;
case -5: n_crc++; break;
case 0: n_miss++; break;
default: break;
}
}
printf("\n 线上共 %u 位,逐位翻转后的错误类型分布:\n", cn);
printf(" 位填充错误 (-1) : %u 个\n", n_stuff);
printf(" CRC 错误 (-5) : %u 个\n", n_crc);
printf(" 帧长不符 (-4) : %u 个(翻转破坏了 5 连,导致去填充错位)\n",
n_len);
printf(" SOF 非法 (-2) : %u 个\n", n_sof);
printf(" 漏检 : %u 个\n", n_miss);
check(n_miss == 0u, "有线上的单比特干扰被漏检");
check(n_stuff + n_crc + n_len + n_sof + n_miss == cn, "分类统计不完整");
}
}
/* ---------- [4] CRC15 单比特检错 ---------- */
printf("\n[4] CRC15 检错能力:单比特翻转\n");
{
uint32_t n;
uint8_t copy[CAN_BITS_MAX];
uint32_t raw_n;
uint8_t raw[CAN_BITS_MAX];
uint32_t miss = 0;
uint32_t total = 0;
uint32_t k;
memset(&f, 0, sizeof(f));
f.id = 0x3C8;
f.dlc = 8;
for (i = 0; i < 8; i++) {
f.data[i] = (uint8_t)(i * 31 + 7);
}
raw_n = can_raw_bits(&f, raw, sizeof(raw));
{
uint16_t crc = can_crc15(raw, raw_n);
int b;
for (b = 14; b >= 0; b--) {
raw[raw_n++] = (uint8_t)((crc >> b) & 1u);
}
}
n = raw_n;
for (k = 0; k < n; k++) {
memcpy(copy, raw, n);
copy[k] = (uint8_t)(copy[k] ^ 1u); /* 翻一位 */
/* 只看 CRC 能不能抓住:重算 CRC 和原始的比较 */
{
uint16_t want = can_crc15(raw, n - 15u);
uint16_t got = can_crc15(copy, n - 15u);
uint32_t j;
uint16_t rx = 0;
int same = 1;
for (j = 0; j < 15u; j++) {
rx = (uint16_t)((rx << 1) | (uint16_t)copy[n - 15u + j]);
}
if (want == got && rx == want) {
same = 1;
} else {
same = 0;
}
total++;
if (same) {
miss++;
}
}
}
printf(" 帧长 %u 位,遍历全部 %u 个单比特翻转\n", n, total);
printf(" 漏检 %u 个\n", miss);
check(miss == 0u, "有单比特错误漏检");
}
/* ---------- [5] CRC15 双比特检错 ---------- */
printf("\n[5] CRC15 检错能力:双比特翻转(全组合枚举)\n");
{
uint8_t raw[CAN_BITS_MAX];
uint32_t raw_n;
uint32_t miss = 0;
uint32_t total = 0;
uint32_t a, b;
memset(&f, 0, sizeof(f));
f.id = 0x3C8;
f.dlc = 8;
for (i = 0; i < 8; i++) {
f.data[i] = (uint8_t)(i * 31 + 7);
}
raw_n = can_raw_bits(&f, raw, sizeof(raw));
{
uint16_t crc = can_crc15(raw, raw_n);
int k;
for (k = 14; k >= 0; k--) {
raw[raw_n++] = (uint8_t)((crc >> k) & 1u);
}
}
for (a = 0; a < raw_n; a++) {
for (b = a + 1u; b < raw_n; b++) {
uint8_t copy[CAN_BITS_MAX];
uint16_t want = can_crc15(raw, raw_n - 15u);
uint16_t got;
uint16_t rx = 0;
uint32_t j;
memcpy(copy, raw, raw_n);
copy[a] = (uint8_t)(copy[a] ^ 1u);
copy[b] = (uint8_t)(copy[b] ^ 1u);
got = can_crc15(copy, raw_n - 15u);
for (j = 0; j < 15u; j++) {
rx = (uint16_t)((rx << 1) | (uint16_t)copy[raw_n - 15u + j]);
}
total++;
if (got == want && rx == want) {
miss++;
}
}
}
printf(" 共 %u 种双比特组合,漏检 %u 个\n", total, miss);
check(miss == 0u, "有双比特错误漏检");
}
/* ---------- [6] 非破坏性仲裁 ---------- */
printf("\n[6] 三个节点同时开口,谁赢\n");
{
can_frame_t c[3];
int drop[3];
uint32_t arb_bits = 0;
int winner;
memset(c, 0, sizeof(c));
c[0].id = 0x120; c[0].dlc = 2; c[0].data[0] = 0x11;
c[1].id = 0x0A0; c[1].dlc = 2; c[1].data[0] = 0x22;
c[2].id = 0x400; c[2].dlc = 2; c[2].data[0] = 0x33;
winner = can_arbitrate(c, 3, drop, &arb_bits);
printf(" 竞争者:0x%03X / 0x%03X / 0x%03X(同时开始发送)\n",
c[0].id, c[1].id, c[2].id);
for (i = 0; i < 3; i++) {
if (i == winner) {
printf(" 0x%03X:胜出(全程没被抢走总线)\n", c[i].id);
} else {
/* drop_bit 是绝对位位置;SOF 占第 0 位,所以 ID 的
* 第 n 位恰好就是位置 n(1 起数) */
printf(" 0x%03X:在 ID 的第 %d 位退出仲裁,转为接收\n",
c[i].id, drop[i]);
}
}
check(winner == 1, "仲裁胜出者不是 ID 最小的 0x0A0");
}
/* ---------- [7] 失败方重发,最终顺序 ---------- */
printf("\n[7] 仲裁失败方在下一轮重发(真实 CAN 不需要软件介入)\n");
{
can_frame_t c[3];
int pending[3];
int order[3];
int round;
int round_idx = 0;
memset(c, 0, sizeof(c));
c[0].id = 0x120; c[1].id = 0x0A0; c[2].id = 0x400;
for (i = 0; i < 3; i++) {
c[i].dlc = 1;
c[i].data[0] = (uint8_t)i;
pending[i] = 1;
}
for (round = 0; round < 3; round++) {
can_frame_t sub[3];
int map[3];
int drop[3];
int m = 0;
int w;
uint32_t ab = 0;
for (i = 0; i < 3; i++) {
if (pending[i]) {
sub[m] = c[i];
map[m] = i;
m++;
}
}
w = can_arbitrate(sub, m, drop, &ab);
order[round_idx++] = map[w];
pending[map[w]] = 0;
printf(" 第 %d 轮:%d 个节点竞争 -> 0x%03X 胜出(其余重排队)\n",
round + 1, m, sub[w].id);
}
printf(" 最终发送顺序:0x%03X -> 0x%03X -> 0x%03X\n",
c[order[0]].id, c[order[1]].id, c[order[2]].id);
check(c[order[0]].id == 0x0A0 && c[order[1]].id == 0x120 &&
c[order[2]].id == 0x400, "发送顺序不是 ID 升序");
printf(" -> ID 越小优先级越高,天然形成优先级队列,"
"高优先级帧几乎不排队\n");
}
/* ---------- [8] 验收过滤器 ---------- */
printf("\n[8] 验收过滤器(code + mask)\n");
{
can_node_t a;
can_node_t b;
can_node_t c;
const uint32_t ids[7] = {0x0A0, 0x120, 0x12F, 0x130, 0x1FF, 0x200, 0x400};
can_node_init(&a);
can_node_add_filter(&a, 0x120, 0x7F0); /* 只收 0x120~0x12F */
can_node_init(&b);
can_node_add_filter(&b, 0x100, 0x700); /* 收 0x100~0x1FF */
can_node_init(&c); /* 没过滤器 = 全收 */
printf(" %-8s %-14s %-14s %-8s\n", "ID", "A(0x120/0x7F0)",
"B(0x100/0x700)", "C(无过滤)");
for (i = 0; i < 7; i++) {
printf(" 0x%03X %-14s %-14s %-8s\n", ids[i],
can_node_accepts(&a, ids[i]) ? "收" : "丢",
can_node_accepts(&b, ids[i]) ? "收" : "丢",
can_node_accepts(&c, ids[i]) ? "收" : "丢");
}
check(can_node_accepts(&a, 0x0A0) == 0, "A 不该收 0x0A0");
check(can_node_accepts(&a, 0x120) == 1, "A 该收 0x120");
check(can_node_accepts(&a, 0x130) == 0, "A 不该收 0x130");
check(can_node_accepts(&b, 0x0A0) == 0, "B 不该收 0x0A0");
check(can_node_accepts(&b, 0x1FF) == 1, "B 该收 0x1FF");
check(can_node_accepts(&b, 0x200) == 0, "B 不该收 0x200");
check(can_node_accepts(&c, 0x400) == 1, "C 该收所有帧");
}
/* ---------- [9] DLC > 8 ---------- */
printf("\n[9] DLC = 12(4 位字段,合法值但超过 8)\n");
{
uint32_t n;
memset(&f, 0, sizeof(f));
f.id = 0x111;
f.dlc = 12; /* 非法数据量,但 DLC 字段本身合法 */
for (i = 0; i < 8; i++) {
f.data[i] = (uint8_t)(0x80 + i);
}
n = can_encode(&f, g_bits, sizeof(g_bits));
check(can_decode(g_bits, n, &g) == 0, "DLC=12 的帧解码失败");
printf(" 解码回来 DLC = %u(按 8 字节处理),线上位数 %u\n", g.dlc, n);
check(g.dlc == 8, "DLC 没有被截成 8");
printf(" -> 真实 CAN 就是这么规定的:DLC 9~15 一律表示 8 字节。\n");
printf(" 写驱动时如果按 DLC 直接取长度,DLC=12 会读到 12 字节,越界。\n");
}
/* ---------- [10] 错误计数与状态迁移 ---------- */
printf("\n[10] 一个节点持续发送失败,错误计数怎么走\n");
{
can_node_t n;
uint32_t k;
int shown15 = 0;
int shown16 = 0;
int shown31 = 0;
int shown32 = 0;
can_node_init(&n);
printf(" %6s %6s %6s %-16s\n", "第几次", "TEC", "REC", "状态");
for (k = 1; k <= 33u; k++) {
can_node_tx_err(&n);
if (k == 15 && !shown15) {
shown15 = 1;
printf(" %6u %6u %6u %-16s\n", k, n.tec, n.rec,
can_state_name(n.state));
check(n.state == CAN_ST_ACTIVE, "第 15 次错误还不该 passive");
}
if (k == 16 && !shown16) {
shown16 = 1;
printf(" %6u %6u %6u %-16s <- 越过 127\n", k, n.tec, n.rec,
can_state_name(n.state));
check(n.state == CAN_ST_PASSIVE, "第 16 次错误应该转 passive");
}
if (k == 31 && !shown31) {
shown31 = 1;
printf(" %6u %6u %6u %-16s\n", k, n.tec, n.rec,
can_state_name(n.state));
}
if (k == 32 && !shown32) {
shown32 = 1;
printf(" %6u %6u %6u %-16s <- 越过 255\n", k, n.tec, n.rec,
can_state_name(n.state));
check(n.state == CAN_ST_BUS_OFF, "第 32 次错误应该 bus-off");
}
}
printf(" -> 错误率 100%% 的节点 16 帧被降级、32 帧下线;\n");
printf(" 而错误率低于 1/8 的正常节点 TEC 会一直贴在 0 附近\n");
/* 混合场景:坏节点 TEC 每帧 +8,好节点偶尔错一次后很快归零 */
can_node_init(&n);
for (k = 0; k < 20u; k++) {
can_node_tx_err(&n);
{
uint32_t s;
for (s = 0; s < 8u; s++) {
can_node_tx_ok(&n);
}
}
}
printf(" 好节点(每 8 次成功夹 1 次错)跑 20 轮后:TEC = %u,状态 %s\n",
n.tec, can_state_name(n.state));
check(n.state == CAN_ST_ACTIVE, "好节点不该被降级");
}
/* ---------- [11] bus-off 恢复 ---------- */
printf("\n[11] bus-off 之后怎么回来\n");
{
can_node_t n;
uint32_t k;
int done = 0;
can_node_init(&n);
for (k = 0; k < 32u; k++) {
can_node_tx_err(&n);
}
printf(" 32 次错误后状态 = %s\n", can_state_name(n.state));
check(n.state == CAN_ST_BUS_OFF, "没有进入 bus-off");
can_node_rx(&n, 0x123);
can_node_tx_ok(&n);
printf(" bus-off 期间收到报文 / 发送成功都不改变状态,TEC 仍然 = %u\n",
n.tec);
for (k = 1; k <= CAN_BUS_OFF_RECOVER_CNT; k++) {
if (can_node_recover_step(&n) == 1) {
done = (int)k;
break;
}
}
printf(" 观察到 %d 次「11 个连续隐性位」后恢复到 %s,TEC 清零 = %u\n",
done, can_state_name(n.state), n.tec);
check(done == (int)CAN_BUS_OFF_RECOVER_CNT, "恢复步数不是 128");
check(n.state == CAN_ST_ACTIVE, "没有恢复到 error-active");
printf(" -> 128 次总线空闲是个很重的惩罚:坏节点回不来,"
"好节点也要等很久\n");
}
printf("\n===== %s =====\n",
failed == 0 ? "全部通过:以上数据由本机 gcc 实编译实运行"
: "有失败项!");
return failed == 0 ? 0 : 1;
}实测输出
下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):
===== CAN 2.0A 比特级规则实测 =====
[1] 编解码往返
ID=0x123 DLC=8 数据 10 20 30 40 50 60 70 80
编码后线上位数 = 106
解码回来 ID=0x123 DLC=8 数据 10 20 30 40 50 60 70 80
[2] 位填充:同一 ID、同一 DLC,不同数据内容的填充位数
数据内容 帧位 填充位 线上位 膨胀率
8 个 0x00 98 14 112 14%
8 个 0xFF 98 13 111 13%
8 个 0xAA 98 1 99 1%
8 个 0x55 98 1 99 1%
8 个 0x0F 98 2 100 2%
8 个 0xF0 98 1 99 1%
8 个 0x11 98 1 99 1%
8 个 0x7F 98 9 107 9%
-> 填充位 1 ~ 14 个,线上位数 99 ~ 112,膨胀率 1% ~ 14%
-> 按「47 + 8*DLC」估的 111 位只是个平均数,算总线负载必须带上填充位
[3] 故意破坏填充规则(真实总线上不可能出现 6 个连续相同位)
编码后线上 45 位,把第 17 位之后的填充位改成同值,制造出 6 连位 12~17
解码返回 -1(-1 = 位填充错误)
-> 这是接收方能拿到的最早的错误信号:
不用等 CRC,在第 18 位就已经知道帧废了,可以立刻开始发错误帧
线上共 103 位,逐位翻转后的错误类型分布:
位填充错误 (-1) : 14 个
CRC 错误 (-5) : 61 个
帧长不符 (-4) : 27 个(翻转破坏了 5 连,导致去填充错位)
SOF 非法 (-2) : 1 个
漏检 : 0 个
[4] CRC15 检错能力:单比特翻转
帧长 98 位,遍历全部 98 个单比特翻转
漏检 0 个
[5] CRC15 检错能力:双比特翻转(全组合枚举)
共 4753 种双比特组合,漏检 0 个
[6] 三个节点同时开口,谁赢
竞争者:0x120 / 0x0A0 / 0x400(同时开始发送)
0x120:在 ID 的第 3 位退出仲裁,转为接收
0x0A0:胜出(全程没被抢走总线)
0x400:在 ID 的第 1 位退出仲裁,转为接收
[7] 仲裁失败方在下一轮重发(真实 CAN 不需要软件介入)
第 1 轮:3 个节点竞争 -> 0x0A0 胜出(其余重排队)
第 2 轮:2 个节点竞争 -> 0x120 胜出(其余重排队)
第 3 轮:1 个节点竞争 -> 0x400 胜出(其余重排队)
最终发送顺序:0x0A0 -> 0x120 -> 0x400
-> ID 越小优先级越高,天然形成优先级队列,高优先级帧几乎不排队
[8] 验收过滤器(code + mask)
ID A(0x120/0x7F0) B(0x100/0x700) C(无过滤)
0x0A0 丢 丢 收
0x120 收 收 收
0x12F 收 收 收
0x130 丢 收 收
0x1FF 丢 收 收
0x200 丢 丢 收
0x400 丢 丢 收
[9] DLC = 12(4 位字段,合法值但超过 8)
解码回来 DLC = 8(按 8 字节处理),线上位数 102
-> 真实 CAN 就是这么规定的:DLC 9~15 一律表示 8 字节。
写驱动时如果按 DLC 直接取长度,DLC=12 会读到 12 字节,越界。
[10] 一个节点持续发送失败,错误计数怎么走
第几次 TEC REC 状态
15 120 0 error-active
16 128 0 error-passive <- 越过 127
31 248 0 error-passive
32 256 0 bus-off <- 越过 255
-> 错误率 100% 的节点 16 帧被降级、32 帧下线;
而错误率低于 1/8 的正常节点 TEC 会一直贴在 0 附近
好节点(每 8 次成功夹 1 次错)跑 20 轮后:TEC = 0,状态 error-active
[11] bus-off 之后怎么回来
32 次错误后状态 = bus-off
bus-off 期间收到报文 / 发送成功都不改变状态,TEC 仍然 = 256
观察到 128 次「11 个连续隐性位」后恢复到 error-active,TEC 清零 = 0
-> 128 次总线空闲是个很重的惩罚:坏节点回不来,好节点也要等很久
===== 全部通过:以上数据由本机 gcc 实编译实运行 =====