Administrator
发布于 2026-09-25 / 0 阅读
0
0

把 CAN 拆到比特级:位填充、CRC15、仲裁与错误计数

一句话结论: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   │
└───┴────────┴───┴───┴──┴───────┴───────────────┴─────────────┴───┴───┴────┴────────┘
  显性  越小越优先                                     接收方算出来不对就发错误帧

几个必须记住的点:

  1. 显性(dominant,逻辑 0)会盖住隐性(recessive,逻辑 1)。

这是仲裁的全部基础,也是「ID 越小优先级越高」的原因。

  1. DLC 超过 8 时,数据长度按 8 字节算。DLC 是 4 位(0~15),

但 9~15 都表示 8 字节,不是无效值。很多自定义协议在这里出错。

  1. 远程帧(RTR = 1)没有数据,DLC 表示「我想要几字节」。

但它和同名数据帧的仲裁字段完全相同,所以远程帧和数据帧会撞车, 实际项目里建议别用远程帧。

  1. ACK 位是所有接收方一起拉显性的。发送方只要发现 ACK 位是隐性,

就知道「没有一个人在听」,这是 CAN 唯一能确认「至少有人收到」的机制。

三、位填充:为什么每 5 位就要插一个反的

CAN 是异步总线,没有时钟线。接收方靠边沿来重同步自己的位定时器。 如果数据里连续出现很多个相同位(比如一个 0x00 的字节就是 8 个连续显性位), 线上一段时间没有跳变,接收方只能靠自己的晶振数时间——累积误差就会导致数错位。

解法非常直接:发送方在连续 5 个相同位之后,强制插入 1 个相反位。 接收方知道这个规则,看到 5 个相同位之后就把下一个位丢掉。这样:

  • 线上永远不会出现连续 6 个相同位,每 5 个位必有一次跳变,边沿足够密
  • 如果接收方看到了 6 个连续相同位,说明出错了(这就是「位填充错误」)
  • 填充位不参与 CRC,CRC 只算去填充之后的有效位

实测数据(本文第 2 项):同样是 8 字节数据、同一个 11 位 ID(0x123),

数据内容帧位填充位线上位膨胀率
8 个 0x00981411214%
8 个 0xFF981311113%
8 个 0x7F9891079%
8 个 0x0F9821002%
8 个 0xAA / 0x55 / 0xF0 / 0x11981991%

也就是说同样一帧,线上位数在 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
5CRC15 单比特(去填充后)遍历帧内全部 98 个位,漏检 0
6CRC15 双比特(去填充后)遍历全部 4753 种组合,漏检 0
7三节点同时发送0x0A0 胜出;0x120 在 ID 第 3 位退出,0x400 在第 1 位退出
8失败方重发三轮后实际发送顺序恒为 0x0A0 → 0x120 → 0x400(ID 升序)
9验收过滤器0x120/0x7F0 只收 0x120~0x12F;0x100/0x700 收 0x100~0x1FF;无过滤器全收
10DLC = 12按 8 字节处理(不报错、也不会读 12 字节)
11连续发送失败第 15 次 TEC=120 仍 active;第 16 次 128 → error-passive;第 32 次 256 → bus-off
12好节点(每 8 成功夹 1 错)跑 20 轮后 TEC = 0,仍 error-active
13bus-off 恢复观察到 128 次「11 个连续隐性位」后回到 error-active,TEC 清零

第 4 项是整篇里最有意思的一组数字:同一个单比特干扰,被什么机制抓住是不固定的。 动到填充位附近 → 填充规则先报警(比 CRC 更早,也意味着错误帧能更早发出); 动到普通数据位 → CRC 报错;动到 SOF → 格式直接不合法。 但无论落在哪里,106 位里没有任何一个位置能溜过去。 这正是 CAN 在物理层就敢保证可靠性的底气。

第 7、8 项是 CAN 最漂亮的地方:仲裁失败不浪费带宽、不重传、不需要软件介入, 失败方下一个位就变接收,等这一帧结束后自动重发。 而且因为 ID 越小越优先,三轮下来的实际发送顺序天然就是 ID 升序—— 这相当于总线免费给了你一个优先级队列。

七、上真机的坑

  1. 必须 120 Ω 端接,而且只在总线两端各一个。中间节点不要接。

少了端接会有反射,眼图糊掉;多了端接负载太重,幅值不够。 很多「CAN 通信偶发丢帧」最后查出来就是端接电阻。

  1. CAN_H / CAN_L 是差分对,必须双绞,而且别和电机线走同一个线槽。

这是现场最常被忽略的一条。

  1. 波特率和采样点。仲裁靠逐位比较,采样点必须落在位的 75%~87.5% 位置。

两个节点波特率一致但采样点差太多,短帧能通、长帧就错—— 典型症状是「小数据量正常,大数据量必挂」。

  1. 别用远程帧。它和同名数据帧仲裁字段完全相同,会撞车;

而且很多厂商的收发器对 RTR 处理不一致。

  1. bus-off 之后必须显式恢复。多数 MCU 会置位一个标志就不再自动回来,

你要么重新初始化外设,要么等够 128 次总线空闲。 现场表现是「设备掉线了再也回不来」,一定要在代码里处理这个分支。

  1. 验收过滤器写在硬件里,但别指望它防盗。

过滤器只能减少 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 clean

include/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 实编译实运行 =====

评论