1. CAN总线通信技术概述
CAN(Controller Area Network)总线是德国Bosch公司在上世纪80年代为汽车电子系统开发的一种串行通信协议。经过三十多年的发展,这种最初为汽车设计的通信技术,如今已广泛应用于工业自动化、医疗设备、航空航天等领域。我最早接触CAN总线是在2015年参与一个工业控制项目时,当时就被它强大的抗干扰能力和灵活的网络拓扑结构所吸引。
与常见的UART、I2C等通信方式相比,CAN总线具有几个显著特点:首先,它采用差分信号传输(CAN_H和CAN_L),这种设计使其对电磁干扰有着天然的免疫力;其次,CAN总线采用非破坏性仲裁机制,当多个节点同时发送数据时,优先级高的报文会继续传输,而不会像其他总线那样发生冲突导致数据丢失;再者,CAN协议内置了完善的错误检测和处理机制,包括CRC校验、帧检查等,确保数据传输的可靠性。
在工业现场,我曾见过CAN总线在强电磁干扰环境下稳定工作的场景,这正是其他通信协议难以企及的。一个典型的CAN网络可以连接多达110个节点(标准帧格式下),最远传输距离可达10km(在5kbps速率下)。这种特性使其非常适合构建分布式控制系统。
2. STM32的CAN外设详解
STM32系列微控制器几乎全系标配CAN控制器,其中F1系列配备的是bxCAN(Basic Extended CAN),而F4/F7/H7系列则升级为更强大的FDCAN(Flexible Data Rate CAN)。以常用的STM32F103系列为例,其bxCAN控制器主要特性包括:
- 支持CAN 2.0A和2.0B协议
- 波特率最高1Mbps
- 3个发送邮箱
- 2个接收FIFO,每个FIFO可存储3个报文
- 可编程的过滤器组(28个过滤器组)
CAN控制器的硬件结构比较复杂,但理解几个关键部分对开发很有帮助:
-
位时序:CAN总线将每个位时间划分为同步段、传播段、相位缓冲段1和相位缓冲段2。正确配置这些参数对通信稳定性至关重要。例如,在1Mbps速率下,典型的配置可能是:
c复制hcan.Init.Prescaler = 6; // 时钟分频 hcan.Init.TimeSeg1 = CAN_BS1_4TQ; // 时间段1 hcan.Init.TimeSeg2 = CAN_BS2_3TQ; // 时间段2 hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; // 同步跳转宽度 -
过滤器:STM32的CAN过滤器堪称一绝,它可以在硬件层面过滤掉不需要的报文,大大减轻CPU负担。过滤器支持两种模式:
- 标识符列表模式:精确匹配特定ID
- 掩码模式:匹配一组ID
比如要接收ID为0x123的标准数据帧,可以这样配置:
c复制CAN_FilterTypeDef filter; filter.FilterIdHigh = 0x123 << 5; // STDID[10:0]对齐到高位 filter.FilterIdLow = 0; filter.FilterMaskIdHigh = 0x7FF << 5; // 掩码 filter.FilterMaskIdLow = 0; filter.FilterFIFOAssignment = CAN_FILTER_FIFO0; filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan, &filter);
3. 硬件设计要点
设计CAN总线硬件电路时,有几个关键点需要特别注意:
1. 终端电阻匹配
CAN总线两端必须各接一个120Ω的终端电阻,这是很多初学者容易忽略的地方。我曾经调试过一个系统,通信时好时坏,最后发现就是少接了一个终端电阻。终端电阻的作用是阻抗匹配,消除信号反射。在实际应用中:
- 如果节点位于总线中间,通常不需要终端电阻
- 对于只有两个节点的系统,每个节点都应配置终端电阻
- 可以通过测量CAN_H和CAN_L之间的电阻来验证终端电阻是否正确(应为60Ω左右)
2. 收发器选型
常用的CAN收发器芯片有TJA1050、SN65HVD23x等。选择时需要考虑:
- 工作电压(5V或3.3V)
- 传输速率(高速CAN最高1Mbps)
- 工作温度范围(工业级-40℃~125℃)
- 是否支持待机模式(对电池供电设备很重要)
3. PCB布局建议
- 收发器尽量靠近MCU放置
- CAN_H和CAN_L走线应等长且平行,保持差分阻抗120Ω
- 在收发器电源引脚附近放置0.1μF去耦电容
- 避免将CAN走线布置在晶振、开关电源等噪声源附近
一个典型的CAN节点原理图如下:
code复制 +------------+ +-------------+
| STM32 | | CAN收发器 |
| | | |
| CAN_TX ----|------>| TXD |
| CAN_RX ----|<------| RXD |
| | | |
| | | CAN_H ------|---->到总线
| | | CAN_L ------|---->到总线
+------------+ +-------------+
|
GND
4. 软件驱动开发
使用STM32CubeMX可以快速生成CAN初始化代码,但实际开发中还需要处理很多细节。下面分享一个完整的发送接收流程:
1. 初始化配置
c复制hcan.Instance = CAN1;
hcan.Init.Prescaler = 6;
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_4TQ;
hcan.Init.TimeSeg2 = CAN_BS2_3TQ;
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = DISABLE;
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();
}
2. 发送CAN报文
c复制CAN_TxHeaderTypeDef txHeader;
uint8_t data[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
uint32_t mailbox;
txHeader.StdId = 0x123; // 标准ID
txHeader.ExtId = 0x00; // 扩展ID
txHeader.IDE = CAN_ID_STD; // 标准帧
txHeader.RTR = CAN_RTR_DATA; // 数据帧
txHeader.DLC = 8; // 数据长度
if (HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &mailbox) != HAL_OK) {
// 发送失败处理
}
3. 接收CAN报文(中断方式)
首先启用接收中断:
c复制HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
然后在中断回调函数中处理:
c复制void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
CAN_RxHeaderTypeDef rxHeader;
uint8_t data[8];
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, data) == HAL_OK) {
// 处理接收到的数据
if (rxHeader.StdId == 0x123) {
// 特定ID的处理
}
}
}
4. 波特率计算
CAN波特率计算公式:
code复制波特率 = APB1时钟 / (Prescaler * (TimeSeg1 + TimeSeg2 + 1))
例如APB1时钟为36MHz,Prescaler=6,TimeSeg1=4,TimeSeg2=3:
code复制36MHz / (6 * (4 + 3 + 1)) = 1MHz
5. 实战经验与故障排查
在实际项目中,我遇到过各种CAN总线问题,这里分享几个典型案例:
案例1:通信不稳定
症状:偶尔出现报文丢失
排查过程:
- 用示波器观察CAN波形,发现信号过冲
- 检查终端电阻,发现一端未接
- 补接120Ω电阻后问题解决
教训:CAN总线必须正确配置终端电阻
案例2:无法接收到数据
症状:发送正常但接收不到回应
排查过程:
- 检查过滤器配置,发现ID掩码设置错误
- 重新配置过滤器为:
c复制filter.FilterIdHigh = 0x0000; filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x0000; // 接收所有报文 filter.FilterMaskIdLow = 0x0000; - 之后能收到数据,再逐步缩小过滤器范围
案例3:总线持续错误
症状:CAN控制器频繁进入总线关闭状态
排查过程:
- 检查各节点接地,发现存在地电位差
- 改用隔离型CAN收发器
- 增加共模扼流圈
- 问题得到缓解
常见问题速查表:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 无法通信 | 终端电阻缺失 | 检查两端120Ω电阻 |
| 报文丢失 | 波特率不匹配 | 统一所有节点波特率 |
| 错误帧多 | 线路干扰大 | 检查布线,改用屏蔽双绞线 |
| 只能发不能收 | 过滤器配置错误 | 放宽过滤器设置测试 |
| 随机错误 | 地环路问题 | 检查接地,考虑隔离方案 |
6. 高级应用技巧
1. CAN FD兼容设计
新型STM32(如H7系列)支持CAN FD(Flexible Data Rate),它相比传统CAN有两大改进:
- 数据传输速率可变(仲裁段保持1Mbps,数据段可提升)
- 数据长度扩展到64字节
配置CAN FD需要注意:
c复制hfdcan.Init.DataTimeSeg1 = 31;
hfdcan.Init.DataTimeSeg2 = 8;
hfdcan.Init.DataPrescaler = 2;
hfdcan.Init.MessageRAMOffset = 0;
hfdcan.Init.StdFiltersNbr = 1;
hfdcan.Init.ExtFiltersNbr = 1;
2. 多帧传输协议
当需要传输超过8字节数据时,需要实现分段协议。常用的有CANopen的SDO协议。一个简单的自定义多帧协议实现思路:
- 第一帧包含总长度和分段数
- 后续每帧包含序号和部分数据
- 接收方重组数据并校验
3. 总线负载监控
通过CAN控制器的错误计数器可以评估总线健康状况:
c复制uint32_t tec, rec;
HAL_CAN_GetErrorCounters(&hcan, &tec, &rec);
if (tec > 96 || rec > 96) {
// 总线状态不佳警告
}
4. 低功耗设计
对于电池供电设备:
- 使用支持待机模式的收发器(如TJA1051)
- 在空闲时关闭CAN外设时钟
- 通过唤醒中断恢复通信
c复制// 进入低功耗模式前
HAL_CAN_DeactivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
HAL_CAN_Stop(&hcan);
__HAL_CAN_DISABLE(&hcan);
// 唤醒后恢复
__HAL_CAN_ENABLE(&hcan);
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
7. 开发工具推荐
1. 硬件工具
- USB-CAN分析仪(如PCAN-USB, ZLG USBCAN)
- 逻辑分析仪(Saleae, DSLogic)
- 示波器(观察差分信号质量)
2. 软件工具
- CANalyzer/CANoe(专业分析工具)
- BUSMASTER(开源CAN工具)
- candump(Linux下CAN工具)
3. 调试技巧
-
使用环回模式自测:
c复制
hcan.Init.Mode = CAN_MODE_LOOPBACK; -
通过错误中断定位问题:
c复制
HAL_CAN_ActivateNotification(&hcan, CAN_IT_ERROR_WARNING | CAN_IT_ERROR_PASSIVE | CAN_IT_BUSOFF); -
监控发送状态:
c复制void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan) { // 发送成功处理 } void HAL_CAN_TxMailbox0AbortCallback(CAN_HandleTypeDef *hcan) { // 发送失败处理 }
在实际项目中,我习惯先用环回模式验证基本功能,再接入实际总线测试。遇到复杂问题时,结合逻辑分析仪和CAN分析仪同时抓取数据,可以快速定位是硬件问题还是软件问题。