☰
嵌入式必会总线:CAN协议原理与STM32调试避坑指南
2026/10/11 1:10:18 网站建设 项目流程

做嵌入式这几年,要说哪个总线最绕不开,我第一个想到的就是 CAN。从汽车 ECU、BMS 电池管理,到工业设备、医疗仪器、机器人关节,到处都在用它。很多刚入门的同学一上来就卡在概念堆里:显性隐性、仲裁、位填充、采样点……听着就头大。这篇文章我尽量按自己工作的思路来写,先把 CAN 为什么存在讲清楚,再讲协议骨架、硬件设计,最后落到 STM32 上怎么把收发跑起来,中间会穿插大量我实际调总线踩过的坑。适合刚接触 CAN 还不知道从哪下手的开发者,也适合写过一点收发但总被错误帧搞到焦头烂额的人。

1. 为什么是 CAN:一个老协议凭什么还不过时

CAN(Controller Area Network)诞生于 1986 年前后,是博世为汽车车内通信设计的串行总线协议。现在看它,帧最长才 8 字节、最大速率 1Mbps,数据量是真不大,但这并不妨碍它在工业现场、车载网络里继续统治三十年。原因很简单:它不是追求“大带宽”的协议,而是追求“在恶劣电磁环境下,多节点协作不打架、出错能自愈”的协议。这一点到现在也没有哪个通用串口协议能完全替代。

我刚接触 CAN 时最大的困惑是:明明嵌入式领域有 UART、SPI、I2C 这些更简单的通信方式,为什么偏偏搞一个复杂的 CAN?直到自己做了一个多节点采集系统,才真正明白单纯的串口通信在工业场景里有多脆弱。

1.1 传统点对点通信的痛点在哪

UART 是最常用的串行通信方式,结构简单、调起来快。但它天然面向点对点:一个 TX 接一个 RX,两个设备聊得挺好,三个设备就要面临“谁先说”的问题。加 RS-485 转成总线后可以多节点挂线,但本质上还是半双工轮询模型,需要主机仲裁谁有发送权。一旦一个从机故障拉死总线,整条线路直接瘫痪。更要命的是,UART 没有冲突检测、没有优先级概念、没有内建的错误重传机制,数据错不错基本靠应用层自己兜底。

CAN 解决的正是这几个问题。它不是主从结构,而是多主结构:任何一个节点,只要检测到总线空闲就可以发起发送。多个节点同时发的时候,谁重要谁先走,这是靠底层硬件仲裁完成的,不占用额外时间。而且每条报文自带 15 位 CRC 校验、ACK 应答,发送失败控制器会自动重发。这些特性对汽车这种安全优先的场景是刚需,对工业现场的长距离抗干扰也是刚需。

1.2 CAN 的关键特性与适合场景

我习惯把 CAN 的关键特性总结成下面几条:

  • 多主通信,任意节点可主动发报文,不需要主机轮询
  • 非破坏性仲裁,多个节点同时抢总线时,ID 小的报文先发,仲裁过程不浪费带宽
  • 报文自带 CRC、ACK,出错自动重发,应用层不用写繁琐的校验重传
  • 差分信号传输,CAN_H 和 CAN_L 两根线互为参考,共模干扰基本被抵消
  • 单总线可挂 100+ 节点(实际受收发器驱动能力、线缆长度和波特率影响)
  • 支持错误主动、错误被动、总线关闭三态错误管理,单个坏节点不影响整条总线

基于这些特性,CAN 特别适合“节点多、距离长、环境脏、要求响应确定”的场景。比如电机驱动器之间的实时控制、传感器数据汇总、设备状态监控。数据量很大、对带宽敏感的场景,CAN FD 或者以太网可能是更好的选择,这个话题后面单独讲。

2. CAN 协议:帧格式、仲裁机制与位时序

理解了“为什么需要 CAN”,接下来就进入协议细节。CAN 协议本身分物理层和链路层,其中链路层的帧结构、仲裁、位填充这些概念,是读懂调试现象的基础。我尽量不用术语堆砌,而是把它们讲成一个个能记住的规则。

2.1 帧格式:一条报文长什么样

CAN 2.0 规定了几种帧:数据帧、远程帧、错误帧、过载帧。日常 99% 的通信都在用数据帧。标准数据帧的完整结构是这样的:

  • SOF:1 bit,显性,表示帧开始
  • 仲裁场:11 位标识符(标准帧)或 29 位(扩展帧),加 RTR 位
  • 控制场:IDE 位、保留位、DLC 数据长度(0~8)
  • 数据场:最多 8 字节,这是经典 CAN 最大的限制
  • CRC 场:15 位 CRC 校验加一位 CRC 分隔符
  • ACK 场:ACK 槽和分隔符,发送方发送隐性位,接收方如果校验通过就回显性位应答
  • EOF:7 位隐性位,表示帧结束
  • IFS:3 位隐性位,帧间间隔

有一个细节容易被忽略:发送方在连续发送 5 个相同电平的位之后,必须插入一个反相的填充位,这叫位填充。目的是保证总线上的跳变沿足够多,让所有接收节点能持续做位同步。如果接收方发现连续出现 5 个以上相同位,没有找到填充位,就会判定填充错误并发送错误帧,把这条帧打掉。很多“总线上全是错误帧”的调试现场,根源就出在发送方和接收方的位时间参数不一致,导致位填充解析错误。

远程帧我一句话带过:它是“请求对方发数据”的帧,但在实际工程中最好不要用。因为远程帧的 ID 和对应数据帧的 ID 相同,仲裁规则下数据帧优先,容易在总线上制造冲突和不确定行为。我在项目里从来都是让各节点按周期主动上报,不做远程请求这种反向控制。

2.2 仲裁机制:谁重要谁先走

CAN 总线的物理层是“线与”逻辑:显性位(Dominant,逻辑 0)会压过隐性位(Recessive,逻辑 1)。所有节点监听总线时,只要有一个节点发送显性位,其他节点看到的电平就是显性。仲裁正是利用这个特性实现的。

假设两个节点同时开始发送一帧。它们从 SOF 开始逐位发送自己的标识符,发送过程中每个节点都在回读总线电平。如果一个节点发送的是隐性 1,却回读到显性 0,说明总线上有优先级更高的节点也在发送,它就立刻停止发送,转为接收状态,等总线空闲后自动重发自己的报文。这个仲裁过程是逐位进行的,完全硬件完成,不丢任何数据,所以叫“非破坏性仲裁”。

用个生活类比:就像一条单车道,多辆车同时想进路口,不是靠人喊停,而是靠“编号小的车直接顶开编号大的车”,主动让行的车不用倒回起点重新排队,等前车过去后它接着走就行。CAN 的优先级规则是标识符数值越小优先级越高,因此应用层设计时,紧急报文要分配小的 ID,普通状态报文分配大的 ID,这个原则从协议层面上就决定了你的实时性。

2.3 位时序与采样点:波特率的底层逻辑

CAN 的一个时间位(bit time)不是简单的 1/波特率,而是由四段组成的:同步段(SYNC_SEG)、传播段、相位缓冲段 1(PS1)和相位缓冲段 2(PS2)。STM32 的 bxCAN 外设把这些段合并成了 BS1、BS2 和 SJW 三个可配置参数,加上预分频值 Prescaler,就确定了最终波特率。

波特率公式是:

波特率 = APB1 时钟 /(Prescaler ×(1 + BS1 + BS2))

采样点公式是:

采样点 =(1 + BS1)/(1 + BS1 + BS2)

采样点决定了控制器在一个位时间的什么时刻去读总线电平。采样点太靠前,容错窗口不够;太靠后,接近下一位起点,容易误采。行业常见做法是采样点放在 75% ~ 90% 之间,经典 CAN 通常取 80% 或 87.5%。在短距离总线上,我习惯取 87.5%,因为在 STM32 上这个参数组合最整齐,也比较好配。

SJW(同步跳转宽度)是重新同步时允许调整的最大时间单位,一般设 1~2 tq 就够了。总线特别长、抖动大的场合可以适当加大,但别超过 BS2 的值。这些参数优先满足采样点,其次兼顾 SJW,优先级的顺序不能搞反。

3. 物理层设计:收发器、终端电阻与布线

很多人觉得 CAN 调不通是程序问题,其实一半以上是物理层问题。收发器选型、终端电阻、线缆拓扑,任何一个环节做错,程序写得再对也白搭。这块我踩过不少坑,值得单独讲透。

3.1 收发器选型与电平适配

CAN 控制器(比如 STM32 内部的 bxCAN)输出的是 TTL 电平的 TX/RX 信号,不能直接驱动差分总线,必须经过 CAN 收发器转换成 CAN_H 和 CAN_L 差分信号。常见的收发器型号有 TJA1050、TJA1040、MCP2551、SIT1050 这类,5V 供电,兼容 ISO 11898 标准。也有专门给 3.3V MCU 设计的收发器,选型时注意手册里的 VCC 和 I/O 电平范围。

以 STM32F103 为例,PA11 是 CAN1_RX,PA12 是 CAN1_TX。F103 的这两个引脚是 5V 容忍的,所以直接连 5V 收发器没有电平问题。但这是特例。很多新出的芯片并不是所有 IO 都 5V 容忍,或者系统供电只有 3.3V,这时候不能想当然直连,要用分压、电平转换芯片或选 3.3V 版本的收发器。我见过不止一次有人拿 3.3V 引脚去接 5V 收发的 RXD 输出,把引脚读坏或者电平识别不稳定。

收发器的 TXD/RXD 和 MCU 的 TX/RX 是交叉连接的:MCU 的 TX 接收发器的 TXD,MCU 的 RX 接收发器的 RXD。连线本身没有脑子,但你把名字对上就不会错。RXD 空闲电平通常为高,对应 CAN 的隐性电平。

3.2 终端电阻:为什么必须 120 欧

CAN 总线两端各需要一只 120Ω 终端电阻。这个电阻的作用是匹配双绞线的特征阻抗,防止信号在总线末端反射。反射会导致电平畸变,轻则误码率上升,重则大量错误帧。

有一种常见错误设计:每个节点设计时都默认“我已经预留了 120 欧”,结果总线上挂了 N 个节点,每个节点都焊上电阻,等效阻抗变成 60Ω、40Ω,信号电平被拉低,距离一长就出错。正确做法是只让总线物理最远端的两个节点装终端电阻,中间节点不装。调试时如果只用两个节点互连,两个节点都装 120Ω;如果只有一个测试板和分析仪,分析仪那头一般不装,那就只装板子那头的电阻。

更讲究一点的方案是“分裂终端”:用两只 60Ω 电阻串联,中点通过 4.7nF 电容接到地,这样对共模噪声有更好的滤波效果。条件允许的工业设备,我会优先用这个接法,实测对 EMC 测试确实有帮助。

3.3 布线拓扑与线缆要求

CAN 总线推荐拓扑是“主干 + 短支线”:所有节点从一条主线上就近引出,支线越短越好。20cm 以内的支线在 500kbps 下问题不大,超过 30cm,波形就会明显畸变。理想情况是节点直接做在主线中间,不断线。

线缆用特征阻抗约 120Ω 的屏蔽双绞线,CAN_H 和 CAN_L 各走一根。屏蔽层一般单点接地,不要形成地环流。CAN_GND 在多数小系统里可以和电源地共用,但长距离传输时强烈建议有独立的地线。电源地、屏蔽地、信号地如果处理不好,实测最容易出现“距离短没问题,距离一拉长或者电机一启动就疯狂错误帧”的怪现象。

还有一条铁律:CAN_H 和 CAN_L 不能接反。接反不会烧,但整个总线无法通信,波形上 CAN_H 和 CAN_L 的电平完全反过来。这种问题用万用表量一遍就能发现,但新手往往先怀疑代码,浪费半天。

4. STM32 的 bxCAN 外设:从初始化到收发报文

STM32 的 CAN 控制器叫 bxCAN,F103 上有一路 CAN1,F105/107 有 CAN1 和 CAN2,G0/G4/H7 系列用的是升级版 FDCAN,但经典 CAN 这部分思路是相通的。下面我用标准外设库的写法演示,HAL 库用户照着配置项也能一一对应。

4.1 引脚与时钟配置

F103 的 CAN1 引脚固定是 PA11(RX)和 PA12(TX)。先开 GPIOA、AFIO 的 APB2 时钟,再开 CAN1 的 APB1 时钟,然后配置引脚模式:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; // CAN1_RX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; // CAN1_TX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);

RX 配置成上拉输入是有讲究的:CAN 总线空闲时是隐性电平,RX 引脚应该读到高。TX 用推挽输出,这是 CAN 控制器标准接法。如果 RX 没配成上拉,悬空状态下引脚电平不定,初始化后可能立刻触发一堆假错误帧。

4.2 CAN 控制器初始化与波特率参数计算

CAN 初始化参数里最核心的就是波特率。假设 F103 系统时钟 72MHz,APB1 分频后是 36MHz,我想配 250kbps。套公式:36MHz / 250k = 144,也就是 Prescaler ×(1 + BS1 + BS2)= 144。取 Prescaler = 9,BS1 = 13 tq,BS2 = 2 tq,总共 1 + 13 + 2 = 16 tq,9 × 16 = 144,250kbps 正好。采样点 =(1 + 13)/ 16 = 87.5%。

CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_TTCM = DISABLE; CAN_InitStructure.CAN_ABOM = ENABLE; CAN_InitStructure.CAN_AWUM = ENABLE; CAN_InitStructure.CAN_NART = DISABLE; CAN_InitStructure.CAN_RFLM = DISABLE; CAN_InitStructure.CAN_TXFP = DISABLE; CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_13tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler = 9; CAN_Init(CAN1, &CAN_InitStructure);

几个配置项逐个说:CAN_TTCM 是时间触发模式,一般不用。CAN_ABOM 是自动总线恢复,强烈建议打开,否则节点因为错误进入 bus off 后不会自动回到总线。CAN_NART 是禁止自动重发,默认值 DISABLE 表示出错自动重发,要保持关闭。CAN_TXFP 是发送优先级按 FIFO 顺序而不是 ID 排序,项目中一般 DISABLE。CAN_Mode 有 Normal、LoopBack、Silent、Silent_LoopBack 四种,调试和自测很有用。

波特率表和采样点对应关系,我整理了一份常用参考:

目标波特率PrescalerBS1BS2采样点
1 Mbps47188.9%
500 kbps415288.9%
250 kbps913287.5%
125 kbps1813287.5%

只要 APB1 还是 36MHz,这组参数直接可用。如果换了主频,记得重新套公式算,别直接抄。

4.3 哈希过滤器配置

bxCAN 的过滤器属于寄存器级硬件匹配。它的作用不是“增强安全”,而是帮 CPU 挡掉无关中断。假如总线上每秒有几千帧数据,而你的节点只关心几个 ID,没有过滤器,CPU 会被无关帧疯狂打断。

最常用的设置方式是“掩码模式 + 32 位过滤器”。先看最简单的写法,不屏蔽任何 ID,全部放行:

CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(CAN1, &CAN_FilterInitStructure);

掩码位为 0 表示该位不参与比较,全 0 掩码就等于不过滤。如果想只接收标准帧 ID 0x123,标准帧的 ID 在 32 位过滤器里占据 FilterIdHigh 的 bit[15:3],所以需要把 ID 左移 5 位:

CAN_FilterInitStructure.CAN_FilterIdHigh = (uint16_t)(0x123 << 5); CAN_FilterInitStructure.CAN_FilterIdLow = 0; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (uint16_t)(0x7FF << 5); CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0;

掩码 0x7FF << 5 表示只比较 ID 这 11 位,其他位忽略。过滤器配置在初始化流程里只需要做一次,之后对 FR 寄存器置位激活即可。实际项目中如果你不确定该放行哪些 ID,先全部放行、在接收中断里判断 ID,功能验证正确后再收窄过滤器,这样排查问题更简单。

4.4 报文发送与接收中断实现

发送报文用 CanTxMsg 结构体。填写完成后调用 CAN_Transmit,返回值可能是一个邮箱号 0/1/2,也可能是 CAN_TxStatus_NoMailBox 表示三个发送邮箱都占满了。占用的时候要么等,要么先把旧的失败报文清掉:

CanTxMsg TxMessage; TxMessage.StdId = 0x123; TxMessage.IDE = CAN_Id_Standard; TxMessage.RTR = CAN_RTR_Data; TxMessage.DLC = 8; TxMessage.Data[0] = 0x00; TxMessage.Data[1] = 0x11; // ... 填充 Data[2]~Data[7] uint8_t mailbox = CAN_Transmit(CAN1, &TxMessage); if (mailbox != CAN_TxStatus_NoMailBox) { while (CAN_TransmitStatus(CAN1, mailbox) != CAN_TxStatus_Ok) { // 加超时保护,防止卡死 } }

注意 CAN_Transmit 只是把报文放进硬件发送邮箱,不是已经发完。真正的发送完成状态要等 CAN_TransmitStatus 返回 Ok。很多初学者把 CAN_Transmit 当成“已经发出去”,然后紧接着改下一包数据,结果内存被覆盖,发送内容乱七八糟。

接收方面,我先讲中断方式。配置 CAN_IT_FMP0 的 FIFO0 消息挂起中断,再配好 NVIC:

NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);

F103 的中断向量是 USB_LP_CAN1_RX0_IRQn,它和 USB 低优先级中断共用向量,不初始化 USB 外设就没事。中断处理函数里取报文后清挂起位:

void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) != RESET) { CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); // 根据 RxMessage.StdId / DLC / Data[] 做业务处理 } }

CAN_Receive 一次只能从 FIFO 里取出一帧。如果 FIFO 积累多帧,中断会再次触发。FIFO 溢出时,CAN_RFLM 开启锁定模式还是关闭,会决定溢出后是丢弃最新帧还是丢弃最旧帧,这点要根据业务选。比如传感器数值控制环路,宁可丢旧帧也要最新状态,那就别开锁定模式;如果是记录型设备,锁住 FIFO 避免数据混乱更好。

5. 调试 CAN 总线:让 ESR 寄存器自己说话

程序写好、烧进去,总线参数对不对,只能靠调试手段验证。CAN 最让我舒服的一点是,几乎所有状态都能从寄存器读出来,不用瞎猜。养成看 CAN_ESR 寄存器的习惯,能省下一周排查时间。

5.1 解读 ESR:发送错误计数、接收错误计数和 LEC

STM32 的 CAN_ESR 寄存器里,20~22 位是 LEC(上次错误代码),16~23 位含发送错误计数(TEC),24~31 位是接收错误计数(REC)。可以用一句内联读法拿到它们:

uint8_t TEC = (CAN1->ESR >> 16) & 0xFF; uint8_t REC = (CAN1->ESR >> 24) & 0xFF; uint8_t LEC = (CAN1->ESR >> 4) & 0x7;

调试时把这三个值通过串口打印出来,观察它们的变化。正常通信时 TEC 和 REC 都应该是 0。一旦出现非 0,说明有错误帧产生。LEC 的具体编码各芯片参考手册写得不同,但常见值可以对照:

LEC 值含义大概率现场原因
0无错误一切正常
1填充错误波特率不匹配、总线干扰
2格式错误波特率不匹配、帧结构破坏
3ACK 错误总线上没有第二个节点应答
4隐性位错误总线短路、收发器异常
5显性位错误多节点同时发送造成电平冲突
6CRC 错误时钟偏差大、线缆劣化、干扰

每次出现错误并不是立刻导致通信失败,CAN 有“错误主动、错误被动、总线关闭”三个阶段:错误计数小,发错误帧提醒同伴;错误超过阈值进入被动状态,不再主动发错误帧;TEC 超过 255 进入总线关闭,彻底脱离总线。这个设计保证了单个坏节点很难把整条总线的其他人都拖死。

5.2 波特率不匹配的典型表现

如果某个节点波特率配置和总线不一致,这个节点发出去的电平宽度和其他节点期望的位时间对不上,其他节点会疯狂回错。从现象上看,整条总线上错误帧比正常帧还多,你的节点收不到任何有效报文。

更坑的场景是:总线上一个节点配错波特率,其他正常节点也会被它连累。因为 CAN 是共享总线,错误帧会打断正常帧的接收,导致所有节点状态都不健康。排查这种问题,第一件事就是把总线上所有节点的波特率配置打印出来核对,第二件事是确认每个节点的采样点参数,第三件才是检查硬件线缆。我处理过的一个现场就是这样:两块板子波特率都配的 500k,但一个采样点在 60%,一个在 88%,单独和电脑分析仪通信都正常,两块板子互连就不稳定。把采样点参数拉齐后就彻底正常了。

5.3 单板自测:环回模式

没有第二块板子、没有总线分析仪时怎么快速验证程序?STM32 提供了环回模式(LoopBack),把 CAN 控制器的发送输出在芯片内部直接接到接收输入上。发出去的报文自己就能收到,不需要外部收发器,也不需要总线上有第二个节点。

初始化时把 CAN_Mode 改成 CAN_Mode_LoopBack,其他配置不变。然后发送一帧,进接收中断,看能不能收到自己发的报文。能收到,说明 CAN 外设初始化、过滤器、中断链路都是通的;收不到,先查 GPIO、时钟、NVIC,别急着怀疑收发器。

这个模式用于排查“到底是控制器没工作,还是物理层有问题”非常好用。环回通了,再接收发器、接总线,一层一层往上加,问题自然就定位了。我还常用 Silent 静默模式做只监听测试:只收不发,适合接入现有总线上先观察总线负担和报文规律,不干扰生产系统。

5.4 常见问题速查表

多年下来,CAN 调试里反反复复出现的问题就那么几种。我把它们整理成一张速查表:

现象优先怀疑方向处理思路
一发送 TEC 就涨,最后 bus off只有一个节点、无终端电阻、波特率不符加第二个节点,核对两终端 120Ω,统一波特率
两个节点单独都对,互连就乱码采样点不一致、分支线太长拉齐采样点参数,缩短支线
收到全错误帧波特率不匹配、总线干扰用分析仪监听,核对实际波特率;检查屏蔽接地
发送邮箱一直 NoMailBox上次发送失败未处理、ABOM 没开处理发送失败状态,打开 CAN_ABOM
环回模式正常,接收发器不通收发器供电、TX/RX 接反、终端电阻量 VCC/TXD/RXD 电平,互换验证
距离一长就掉线线缆阻抗不匹配、接地不良换标准双绞线,检查屏蔽单点接地

6. 真正做项目时,比收发报文更重要的设计

把 CAN 收发跑通只是入门,真正到项目中,决定系统稳不稳的反而是协议层设计、节点管理和异常恢复。这部分没有标准答案,但有一些通用的设计原则值得参考。

6.1 应用层协议怎么定

CAN 数据场只有 8 字节,ID 也只有 11 位或 29 位,所以设计协议的第一步就是把 ID 分配好。我习惯把 ID 按“优先级 + 源节点 + 数据类型”分段规划。比如 11 位 ID 里,高 3 位表示优先级:000 是紧急报警,001 是控制指令,010 是状态心跳,011 是参数配置,后面 8 位给源节点和消息类型。ID 越小优先级越高,把最紧急的报文排到最小 ID,这是硬件层面必须保证的。

数据场方面,8 字节要学会省着用。状态报文里的电量、温度、转速,能用 1 字节表达就不拿 4 字节浮点硬塞。浮点转成定标整数发送,是业内最常见做法。约定好单位和大端小端,两端按同一套字节序解析,这一条不写进协议文档,后面一定会有人踩雷。

还需要定义每类报文的发送周期。节点的状态帧按固定周期发,比如 100ms 一条;报警帧按事件触发,立刻发;参数帧按请求应答,不主动上传。周期和事件的节奏一旦定好,总线负载率很容易预估:把每帧的位宽乘上每秒帧数,加总就是总线占用率,经典 CAN 建议长期负载率控制在 50%~60% 以下,突发情况才有余量。

6.2 节点心跳、离线判定与故障恢复

每个节点无论有没有业务数据,都应该周期发一条心跳报文。接收端只要超过一定时间没收到某节点的心跳,就能判定它掉线了。这个机制比业务数据超时可靠得多,因为心跳只有固定几个字节,不受业务状态影响。

发送端的故障恢复也要主动做。打开 CAN_ABOM 后,节点总线关闭之后会自动恢复,但恢复后要重新登记身份、确认参数有效,才能重新参与业务。我见过一个项目,节点 bus off 后自动恢复了,但恢复后没有重新发注册帧,业务数据一直不更新,整个系统处于“物理在线、逻辑离线”的诡异状态。启动和恢复流程里放一个状态机:初始化 → 登记 → 等待确认 → 正常运行 → 故障退出,会让整个系统严谨很多。

6.3 下一步:CAN FD 与协议栈

经典 CAN 的 8 字节限制对很多场景确实不够用。CAN FD 在相同物理层基础上,把数据场扩展到 64 字节,还支持数据段用更高波特率发送(最高可达 5Mbps、8Mbps 甚至更高),仲裁段仍然兼容经典 CAN。所以新项目如果芯片支持,我建议优先用 CAN FD 的硬件,同时保留经典 CAN 兼容模式,两头都舒服。STM32F103 不支持 CAN FD,需要用支持 FDCAN 的新系列芯片,像 G0、G4、H7 系列都很常见。

更高一层还有 CANopen、J1939、UDS 这些现成协议栈。自己做私有协议自由度大,但 N 个节点之间怎么同步、怎么诊断、怎么参数管理,全部都要自己写。如果项目时间紧、节点多,直接找成熟协议栈,在它上面做应用扩展,稳定性会好很多。

我个人在实际操作中的体会是,CAN 的精髓不在于把 TX/RX 接对,而在于理解它是“所有节点同时在听、靠物理电平分出胜负”的协作协议。你要是只拿它当 RS-485 的替代品,能通,但只有真正理解仲裁、错误、位同步这些底层机制,才会在疑难 bug 面前有方向。调 CAN 时养成先看 ESR 寄存器、先打环回、先把采样点拉齐的习惯,这一点比多写一百行应用代码都有用。先跑通单板自检,再上真总线,最后做干扰测试,这个顺序基本能帮你避开 80% 的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询