CAN 总线这个东西,搞嵌入式的早晚都得碰见。尤其做汽车电子、工业控制、机器人、医疗器械这些方向,CAN 总线几乎是事实标准。我最早接触 CAN 是给某跨平台系统做通信底板,当时对协议理解得不深,拿着 STM32 的库函数一顿配,结果总线一直报错,查了两天才发现是波特率算错了。从那之后我把 CAN 的协议细节、硬件设计和 STM32 的外设机制完整过了一遍,才算真正“搞懂”。这篇博文就把我梳理过的内容从头到尾讲一遍,从物理层到协议层,从硬件设计到 STM32 的 HAL 库实战,再到排查故障的经验,尽量一次说透。
1. CAN 总线到底解决什么问题
1.1 为什么工业现场都在用 CAN
CAN(Controller Area Network)是上世纪 80 年代为了汽车内的线束减重而设计出来的串行通信协议。那个年代一辆车的 ECU 越来越多,每个 ECU 之间如果都拉独立的信号线,线束会重得离谱,故障率也高。CAN 的核心思路就是:所有节点挂在同两根线上,通过报文 ID 区分消息内容,谁想发数据谁就往总线上写,这与点对点的 UART 或者主从式的 I2C 有着本质区别。
CAN 相比其他串行总线有几个关键优势:
- 多主结构,任何节点都可以主动发起通信,不需要主机轮询。
- 非破坏性仲裁,多个节点同时发送时,ID 小的优先,不会损坏数据。
- 错误检测和自动重发机制完善,硬件级别保证可靠性。
- 传输距离远,1 Mbps 下也能跑 40 米左右,低速时(比如 5 kbps)能到 10 公里级别,配合隔离中继还能更远。
- 走差分信号,抗干扰能力强,适合电机、变频器这些噪声恶劣的环境。
后来 CAN 不止用在车上,工业现场总线、伺服驱动器、BMS 电池管理系统、楼宇自动化里到处都是它的身影。可以说,会调试 CAN 总线,已经是嵌入式工程师的基本功了。
1.2 CAN 与 UART、I2C、RS-485 的定位差异
很多人刚接触 CAN 时会拿它和 RS-485 比较,因为两者都是差分信号,都能挂多个节点。但 CAN 和 RS-485 的哲学完全不同:RS-485 只是物理层的电气标准,协议需要自己定;而 CAN 从物理层到数据链路层都有完整规范,包括报文格式、仲裁、错误处理、流量控制。换句话说,RS-485 给你的是“马路和交警”,CAN 给你的是“完整的交通法规”。
I2C 也是多设备总线,但它是主从架构,且是开漏输出,速度上限一般也就几 Mbps,加上地址位数有限,在多节点长距离场景下优势不大。UART 就更不用说了,基本是一对一通信,组网需要额外协议栈。
CAN 的定位,是在强干扰、长距离、多节点的工业环境中提供可靠的数据交换。理解了这个定位,你就知道为什么 CAN 的协议设计得“这么重”——它把可靠性放在了第一位,甚至可以说是“为了可靠不惜代价”。
2. 从物理层到数据链路层:CAN 协议核心机制拆解
2.1 物理层特性:为什么用差分信号
CAN 总线物理层用的是两根线:CAN_H 和 CAN_L。逻辑 1 叫隐性电平,两根线的电压差接近 0;逻辑 0 叫显性电平,CAN_H 和 CAN_L 之间有大约 2V 的电压差。
这里有个很有意思的机制:显性电平会覆盖隐性电平。意思是,如果一个节点发隐性电平、另一个节点发显性电平,总线上最终呈现的是显性电平。打个比方,显性就是“有人按了喇叭”,隐性就是“没人按喇叭”,只要有人按,大家就都能听见。这个特性是 CAN 仲裁机制的基础,很重要,后面讲仲裁时你会反复用到它。
差分信号的好处在于,干扰信号通常同时作用在两根线上,而接收端只看两根线之间的差值,因此共模干扰会被抵消掉,这也是 CAN 能抗干扰的根本原因。另外 CAN 收发器内部有滞回比较器,输入信号需要超过一定阈值才会翻转,进一步减少噪声误触发。
2.2 报文帧格式详解:标准帧和扩展帧
CAN 报文有四种帧类型:
- 数据帧:承载数据,最常用。
- 远程帧:请求其它节点发送数据。
- 错误帧:节点检测到错误时主动发送。
- 过载帧:节点接收能力不足时发送。
实际开发中 90% 的时间只跟数据帧打交道。数据帧又分标准帧和扩展帧,区别主要在 ID 长度:
| 类型 | ID 位数 | ID 范围 | 仲裁场长度 |
|---|---|---|---|
| 标准帧 | 11 位 | 0x000 ~ 0x7FF | 12 位 |
| 扩展帧 | 29 位 | 0x00000000 ~ 0x1FFFFFFF | 32 位 |
标准帧的格式从前往后依次是:帧起始、仲裁场(ID + RTR 位)、控制场(IDE、DLC)、数据场(最多 8 字节)、CRC 场、ACK 场、帧结束。很多人问 CAN FD 的 64 字节和传统 CAN 的 8 字节有什么区别——传统 CAN 数据场上限就是 8 字节,CAN FD 才扩展到 64 字节,但这是后来定的规范。
DLC(Data Length Code)表示数据长度,值可以是 0 到 8,对应 0 到 8 个字节。这里有个容易踩的坑:如果收到一帧 DLC=8 但只填了 3 个字节数据,剩下的字节是未定义的,可能读到乱码,所以发数据时一定要把 DLC 和实际长度严格对应起来。
2.3 非破坏性仲裁机制:为什么 ID 越小优先级越高
多个节点同时发送数据时,CAN 总线怎么决定谁先发?答案就是前面提到的显性和隐性的覆盖特性。
假设节点 A 发 ID=0x100(二进制 00100000000),节点 B 发 ID=0x200(二进制 01000000000)。两个节点同时开始发送,从最高位开始逐位比较:A 的最高位是 0(显性),B 的最高位是 0(显性),一致;第二位,A 是 0,B 也是 0,一致;到第三位,A 是 1(隐性),B 是 0(显性),此时总线上呈现的是显性电平,A 发送的隐性电平被覆盖了。于是 A 判断“有人在发更高优先级的数据”,主动退出,转为接收状态;B 继续发送,完成整帧传输。
整个过程完全由硬件完成,不影响数据,不浪费时间,所以叫非破坏性仲裁。重点结论:ID 越小,优先级别越高。这个机制直接决定了系统设计时要把关键控制报文分配较小的 ID,比如转向、制动这些安全相关报文,ID 必须比车窗、空调这类舒适性报文小。
2.4 错误检测和容错机制:五种错误与三种状态
CAN 的可靠性,核心在错误处理上。它一共有五种错误检测机制:
- 位错误:发送方在发送时同时监控总线电平,如果发送的位和监控到的位不一致,就说明出错了。但要注意:在仲裁期间,发送隐性电平却读到显性电平是正常现象,不算错误;只有在发送显性电平却读到隐性电平时才判错。
- 填充错误:CAN 规定连续发送 5 个相同电平后必须插入一个反相电平,这叫位填充。如果接收方发现连续 6 个相同电平,就说明违反填充规则。
- CRC 错误:接收方重新计算接收帧的 CRC,如果和发送方传来的 CRC 不一致,报错。
- 格式错误:固定位段的电平不符合规范,比如帧结束必须是 7 个隐性位,如果出现显性位,判错。
- ACK 错误:发送方在 ACK 槽发送隐性电平,如果没有节点拉低应答,说明总线上没有节点成功接收。
每个节点都有发送错误计数器和接收错误计数器。错误状态分为三档:错误主动(TEC<128,可以正常发送)、错误被动(128≤TEC≤255,发送延迟,只能等到总线空闲再发)、总线关闭(TEC>255,完全退出总线,不再参与任何通信)。总线关闭是最严重的状态,需要靠软件或硬件恢复机制把节点拉回来,实际项目中要通过读取错误寄存器来判断节点是否处于总线关闭状态,并及时做恢复处理。
3. STM32 上的 CAN 外设:CubeMX 配置与工程初始化
3.1 以 STM32F103 为例:CAN 外设架构
STM32 的 bxCAN 外设是基本扩展 CAN 外设,和标准 CAN 2.0A/B 协议兼容。F103 系列的 CAN1 和 CAN2 共用一组筛选器,主用 CAN1,CAN2 多用于需要多路 CAN 的场景。
bxCAN 由这几块组成:
- 位时序单元:负责波特率生成和采样点控制。
- 发送邮箱:有 3 个,可以缓存待发送的报文,硬件自动调度。
- 接收 FIFO:有 2 个,每个 FIFO 能存 3 帧报文。
- 筛选器:共有 28 组(F103 系列是 28 组),每组可以配置为列表模式或掩码模式。
- 中断和错误管理单元:提供各种中断源和错误状态寄存器。
硬件设计上,STM32F103 的 CAN 引脚默认是 PA11(RX)和 PA12(TX),但通过重映射也可以放到 PB8/PB9。这部分在 CubeMX 里配置起来很简单,但理解底层架构有助于排查问题,尤其是在用多个筛选器或者多个 CAN 外设时。
3.2 CubeMX 图形化配置:引脚下拉和波特率计算
用 CubeMX 配置 CAN 外设,最核心的是波特率。波特率由预分频器、BS1(位段 1)、BS2(位段 2)共同决定,公式为:
$$波特率 = \frac{F_{CAN}}{(BRP + 1) \times (BS1 + BS2 + 3)}$$
其中 $F_{CAN}$ 是 CAN 外设的输入时钟,F103 上 APB1 时钟为 36 MHz,CAN 外设时钟也取自这里。在 CubeMX 界面里:
- Prescaler 就是 BRP 值。
- Time Quanta in Bit Segment 1 对应 BS1。
- Time Quanta in Bit Segment 2 对应 BS2。
- ReSynchronization Jump Width 是重新同步跳转宽度,一般设为 1。
举个实际例子:要配置 250 kbps 的波特率,36 MHz 时钟,需要 144 个时间量子(TQ)每位。设 BRP=11,实际分频就是 12,得到 3 MHz 的位时间频率;再除以 250 kHz,每位占 12 个 TQ。然后让 BS1=7、BS2=2,则 BS1+BS2+3=12,完全符合。这个配置下采样点在第 9 个 TQ 处,占比 75%。
如果你用 500 kbps,需要 6 个 TQ,设 BRP=11,每位只需 3 MHz/500 kHz=6,BS1=2、BS2=1 满足条件,采样点在 4/6≈66.7%。这个值在标准范围内,实测没问题。
3.3 HAL 库初始化代码逐行解读
CubeMX 生成的初始化函数长这样:
void MX_CAN1_Init(void) { CAN_FilterTypeDef sFilterConfig; hcan1.Instance = CAN1; hcan1.Init.Prescaler = 11; hcan1.Init.Mode = CAN_MODE_NORMAL; hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan1.Init.TimeSeg1 = CAN_BS1_7TQ; hcan1.Init.TimeSeg2 = CAN_BS2_2TQ; hcan1.Init.TimeTriggeredMode = DISABLE; hcan1.Init.AutoBusOff = ENABLE; hcan1.Init.AutoWakeUp = DISABLE; hcan1.Init.AutoRetransmission = ENABLE; hcan1.Init.ReceiveFifoLocked = DISABLE; hcan1.Init.TransmitFifoPriority = DISABLE; HAL_CAN_Init(&hcan1); sFilterConfig.FilterBank = 0; sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh = 0x0000; sFilterConfig.FilterIdLow = 0x0000; sFilterConfig.FilterMaskIdHigh = 0x0000; sFilterConfig.FilterMaskIdLow = 0x0000; sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; sFilterConfig.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig); }几个关键点:
AutoBusOff = ENABLE意思是硬件自动退出总线关闭状态,这对稳定性很重要,建议开启。AutoRetransmission = ENABLE表示发送失败后硬件自动重发,防止应用层丢帧。TimeTriggeredMode和时间戳功能一般不开启,除非你做同步控制。- 滤波器组 0 配成掩码模式,ID 高 16 位和低 16 位都填 0,掩码也填 0,表示接收所有报文,适合入门测试;实际项目里要按需过滤。
配置完之后,还需要调用HAL_CAN_Start(&hcan1)让 CAN 进入正常模式,再调用HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING)打开接收中断,之后才能正常收发。
4. 实战:手写 CAN 收发完整流程
4.1 报文发送:Transmit Mailbox 机制
用 HAL 库发送 CAN 报文,标准写法如下:
CAN_TxHeaderTypeDef txHeader; uint8_t txData[8]; uint32_t txMailbox; txHeader.DLC = 8; txHeader.ExtId = 0x180; txHeader.IDE = CAN_ID_EXT; txHeader.RTR = CAN_RTR_DATA; txHeader.TransmitGlobalTime = DISABLE; txData[0] = 0x01; txData[1] = 0x02; txData[2] = 0x03; txData[3] = 0x04; txData[4] = 0x00; txData[5] = 0x00; txData[6] = 0x00; txData[7] = 0x00; HAL_CAN_AddTxMessage(&hcan1, &txHeader, txData, &txMailbox);HAL_CAN_AddTxMessage会把报文放入发送邮箱,硬件随后自动发送。需要注意返回值,如果返回HAL_ERROR或HAL_BUSY,说明三个邮箱都满了,此时你需要做丢帧处理或者等待空闲邮箱。实际项目中,发送频率高而总线负载大时,邮箱满是很常见的,盲目重发只会让情况恶化,建议设计消息队列或者降频。
4.2 报文接收:中断 + 环形缓冲区
接收数据我用的是中断加环形缓冲区的方案。收到报文后,中断函数里把数据拷贝到缓冲区,主循环再从缓冲区取数据进行业务处理。这样不阻塞中断,也不丢数据。
#define RX_BUFFER_SIZE 32 typedef struct { CAN_RxHeaderTypeDef header; uint8_t data[8]; } CAN_RxFrame; volatile CAN_RxFrame rx_buffer[RX_BUFFER_SIZE]; volatile uint8_t rx_head = 0; volatile uint8_t rx_tail = 0; void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { if (hcan->Instance == CAN1) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; uint8_t next_head = (rx_head + 1) % RX_BUFFER_SIZE; if (next_head != rx_tail) { // 缓冲区未满 HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData); rx_buffer[rx_head].header = rxHeader; memcpy(rx_buffer[rx_head].data, rxData, 8); rx_head = next_head; } else { // 缓冲区满,丢弃最旧的一帧 rx_tail = (rx_tail + 1) % RX_BUFFER_SIZE; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData); rx_buffer[rx_head].header = rxHeader; memcpy(rx_buffer[rx_head].data, rxData, 8); rx_head = next_head; } } }这个模式有几个细节值得注意:
- 缓冲区大小必须足够容纳接收峰值,否则会丢帧。可以先估算总线负载,比如 1 Mbps 总线下,一帧 8 字节标准帧加上开销大约 108 位,传输一帧约 108 µs,中断触发频率是约 9 kHz。如果主循环处理不及时,缓冲区必须至少能撑住 32 帧以上。
- 环形缓冲区判断满的条件是
(rx_head + 1) % RX_BUFFER_SIZE == rx_tail,这要求缓冲区大小是 2 的幂,方便取模。 - 中断回调里不能做耗时操作,拷贝 8 字节数据是允许的,但如果要做协议解析、日志打印,必须在主循环里做。
4.3 过滤器配置:掩码模式与列表模式的实际用法
STM32 的 CAN 筛选器是很多人容易搞混的地方。简单说,筛选器有两类模式:
- 列表模式:严格匹配,只接收 ID 完全匹配的报文。
- 掩码模式:按位匹配,掩码位为 0 的位置不关心,为 1 的位置必须匹配。
32 位掩码格式下,ID 被拆成两段,高 16 位和低 16 位分别对应FilterIdHigh/Low和FilterMaskIdHigh/Low。标准帧只有 11 位 ID,存在低 16 位的高位部分;扩展帧有 29 位 ID,跨高 16 位和低 16 位两段。
实际配置时,如果我只想接收 ID=0x180 的扩展帧,可以用列表模式:
sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST; sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh = (0x180 << 3) & 0xFFFF; // 高 16 位 sFilterConfig.FilterIdLow = 0x0000; sFilterConfig.FilterMaskIdHigh = (0x180 << 3) & 0xFFFF; sFilterConfig.FilterMaskIdLow = 0x0000;等等,列表模式不填掩码,直接填两个要匹配的 ID 到FilterIdHigh/Low和FilterMaskIdHigh/Low。我上面写的例子是掩码模式只匹配单帧的写法,列表模式应该这样:
sFilterConfig.FilterMode = CAN_FILTERMODE_IDLIST; sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh = id1_high; sFilterConfig.FilterIdLow = id1_low; sFilterConfig.FilterMaskIdHigh = id2_high; sFilterConfig.FilterMaskIdLow = id2_low;如果系统里报文 ID 比较多,用列表模式会非常消耗筛选器资源,28 组筛选器不够用。更实用的方案是掩码模式配合 ID 规划,比如把所有关键报文的高 8 位设计成同一前缀,只匹配这个前缀,其他位全部屏蔽,这样一组筛选器就能覆盖一批报文。
4.4 波特率计算实战:250 kbps 的完整推导
这部分我单独拿出来说,因为太多人在这里栽跟头。以 F103 的 APB1 时钟 36 MHz 为例,配置 250 kbps 的完整路径:
$$36\ \text{MHz} \div 12 = 3\ \text{MHz}$$
每位需要 $3\ \text{MHz} \div 250\ \text{kHz} = 12$ 个 TQ。
按照BS1 + BS2 + 3 = 12,可选组合有:
- BS1=7、BS2=2:采样点 $(7+2)/12=75%$
- BS1=8、BS2=1:采样点 $(8+1)/12=75%$
- BS1=5、BS2=4:采样点 $(5+2)/12=58.3%$
理论上都可以,但实际推荐 75% 这个采样点——也就是 BS1=7、BS2=2。原因是 75% 采样点对总线传播延迟和时钟容错的容忍度更均衡。试过 58% 采样点的配置,在短线缆下也能工作,但长线缆或节点较多时更容易出错。
如果总线上的设备全部支持同一个波特率,那么波特率误差必须控制在 ±1% 以内,否则总线可能出现间歇性通信故障——不是完全不通,而是偶尔丢帧、报错,这种问题最难排查。
5. 调试实录:常见故障与排查思路
5.1 用示波器看总线状态:波形正确性判断
调试 CAN 第一件事是拿示波器看波形。把示波器探头挂在 CAN_H 和 CAN_L 上,正常通信时应该能看到:
- 隐性状态:两根线都在 2.5V 附近,差分约 0V。
- 显性状态:CAN_H 拉到 3.5V,CAN_L 拉到 1.5V,差分约 2V。
- 数据帧波形是连续的不规则方波,帧与帧之间有明显的空闲电平。
如果波形上看到回波振铃,检查终端电阻;如果电压幅值偏低,比如显性差分只有 1V,检查收发器供电或总线是否过载(挂节点太多、线缆过长);如果波形上是杂乱噪声,可能是没有终端电阻或总线没有被正确驱动。
判断波特率最直接的方式是用示波器测量最短显性脉冲的宽度。在位填充规则下,最短是“1 显性位 + 1 隐性位”一个周期,实测往往能看到规律性的时间间隔。比如 250 kbps 下,单个位时间是 4 µs,脉冲宽度也就是 4 µs 的整数倍。测出来对不上,说明波特率配置有问题。
5.2 错误状态寄存器:让芯片自己告诉我们哪里错了
STM32 的 CAN 外设有错误状态寄存器CAN_ESR,里面存了错误代码(LEC)、发送错误计数(TEC)、接收错误计数(REC)。通过调试器实时查看这几个值,能快速定位故障方向:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| TEC 快速增长,达到 256 进入 BusOff | 发送时总线无 ACK | 检查是否有其他节点在线、波特率是否匹配、终端电阻是否接好 |
| REC 快速增长 | 收到错误帧或物理层信号质量差 | 查波形、查干扰源、查接地 |
| LEC=1(位错误) | 总线竞争或硬件时序问题 | 检查是否有两个节点 ID 完全相同,导致仲裁异常 |
| LEC=3(ACK 错误) | 总线上只有发送方,没有接收方回复 | 检查接收节点是否初始化成功,是否处于环回模式 |
| 通信间歇性成功、间歇性失败 | 波特率误差临界,或终端电阻接触不良 | 精调波特率,检查线缆连接 |
5.3 三个非常隐蔽的坑
第一个坑是共地问题。CAN 是差分信号,理论上不依赖地线,但收发器的工作电压参考还是需要共地。如果两个节点的电源模块相互隔离,而 CAN 收发器的地没拉通,总线上会有很大的共模电压,通信就变得极不稳定。我的做法是:如果系统设计上必须隔离,就选带隔离的 CAN 收发器模块,比如用带 DC-DC 的隔离收发器;如果不隔离,务必保证各节点地电位差在 -7V 到 +12V 以内。
第二个坑是终端电阻的位置和数量。CAN 规范要求总线两端各接一个 120Ω 终端电阻,不是每个节点都接。如果每个节点都接 120Ω,并联之后阻抗变低,信号反射加剧,通信质量反而下降。测量终端电阻时要在总线断电状态下,从任意接入点量,正常应该是约 60Ω。如果量到 120Ω,说明只接了一端;如果量到 0Ω,说明短路;如果量到无穷大,说明两端都没接。
第三个坑是中断优先级和阻塞。CAN 接收中断处理函数里如果执行了耗时操作,比如调用HAL_UART_Transmit同步发送日志,波特率低时一个字符就要几十微秒,一帧日志上百字符就是几毫秒,CAN 接收中断一直被占用,硬件 FIFO 溢出丢帧。中断回调里只做数据拷贝,日志、解析、处理全部丢到主循环,是必须遵守的纪律。
5.4 工具链:CAN 分析仪和日志时间戳
强推每个做 CAN 开发的人都备一个 USB CAN 分析仪,这东西能把总线上的每一帧报文都抓下来,带精确到微秒的时间戳。排查问题时,有了时间戳才能判断丢帧是否集中在某段总线上,或者某个节点发送是否周期性异常。我用过的几款入门分析仪,价格在几十到几百不等,软件支持 PCAN-View 风格的总线监控,够用了。
如果没有分析仪,只靠 STM32 的调试器也能看一些东西:在HAL_CAN_GetRxMessage里打个断点,看接收的 ID 和 DLC 是否正常。但这个方式局限很大,只能看到本节点的收发情况,看不到总线全局状态。有条件还是建议配一个分析仪,调试效率提升明显。
6. 在实际项目中总结的 CAN 开发要点
把前面的内容串成一个项目管理视角的清单,方便你做方案阶段就对关键点心中有数:
- 波特率和采样点:预先根据总线拓扑(长度、节点数)选定波特率和采样点,CAN 2.0 常见配置是 250 kbps / 500 kbps / 1 Mbps,尽量统一全系统。如果系统内新旧设备混用,尽量选较低波特率,兼容性和稳定性更好。
- ID 规划:按报文优先级分配 ID 段。控制类 0x000~0x1FF,状态类 0x200~0x3FF,诊断类 0x400~0x5FF,留出扩展空间。ID 一旦批量生产后再改,牵涉所有节点的软件和文档,成本很高,一定要提前规划。
- 滤波器设计:先确定节点需要接收哪些报文,再决定掩码。不要图省事全接收——全接收会让 MCU 中断负担加重,高负载下更容易丢帧。
- 错误恢复:开启硬件自动 BusOff 恢复,同时在应用层监控
CAN_ESR寄存器,一旦进入 BusOff,主动重新初始化外设,而不是死等硬件恢复。 - 线缆和接地:所用线缆的阻抗尽量靠近 120Ω,双绞线必须使用双绞结构,不要用平行线。接地点要可靠,避免出现地环路。
- 软件架构:接收用中断+环形缓冲区,处理放主循环;发送用队列或者直接邮箱发送,超时退出,防止死等。
我在实际项目里,拿着这套流程排查过不少问题:有一回通信偶发中断,查下来是某个节点的终端电阻虚焊;还有一回波特率算错了一位,导致两个节点都收不到对方报文,但自己发自己却能通过分析仪看到——这种问题不踩过一次,很难想到是波特率算错。CAN 总线的调试,本质上就是在“物理层、数据链路层、应用层”三层之间来回定位,先把硬件波形看明白,再看协议报错,最后查应用逻辑,基本都能解开。
最后分享一个我后来一直沿用的习惯:每块板子预留一个 CAN 调试接口,连上分析仪后可以随时抓包。这个习惯前期只花几十块钱的材料费,后期排查问题能省下大量时间。硬件设计时就把调试接口接出来,比出了问题再飞线要靠谱得多。