3. CAN硬件驱动层:CAN控制器初始化、中断配置、收发缓冲区管理、错误处理机制
好,咱们今天聊点硬核的。CAN硬件驱动层,说白了就是让芯片里的CAN外设真正跑起来的那层代码。很多初学者喜欢一上来就调应用层,结果发现报文发不出去、收不到,折腾半天才发现是底层配置没搞对。我当年也干过这种事,调了三天,最后发现是时钟没开——嗯,这种坑踩过一次就记住了。
3.1 CAN控制器初始化——别小看这几步
初始化CAN控制器,其实就三件事:开时钟、配引脚、设模式。但每一步都有讲究。
我个人习惯先把外设时钟打开。以STM32为例,CAN1挂载在APB1总线上,你得先使能它的时钟。然后配置GPIO,CAN的TX和RX引脚要设为复用功能。这里有个坑——TX和RX的GPIO速度要匹配,我见过有人把TX设成50MHz,RX设成2MHz,结果通信时好时坏。
接下来是模式配置。CAN控制器通常支持正常模式、环回模式、静默模式。调试阶段我建议先用环回模式,自己发自己收,排除硬件连线问题。等环回通过了,再切到正常模式。
- 使能CAN外设时钟
- 配置GPIO为复用功能(TX推挽输出,RX浮空输入)
- 复位CAN控制器(进入初始化模式)
- 配置波特率(同步段、传播段、相位缓冲段1/2)
- 配置中断使能(接收中断、错误中断等)
- 退出初始化模式,进入正常模式
波特率这块容易让人迷糊。CAN的位时间由4个段组成:同步段、传播段、相位缓冲段1、相位缓冲段2。采样点通常设在85%左右。我一般用这个公式算:波特率 = 时钟频率 / (BRP+1) / (1 + TSEG1 + TSEG2)。举个例子,如果时钟是36MHz,BRP=3,TSEG1=13,TSEG2=2,那波特率就是36M / 4 / 16 = 562.5kbps。
3.2 中断配置——别让CPU空转
CAN通信讲究实时性,轮询方式基本不可取。中断才是正道。
CAN控制器的中断源挺多的:接收中断、发送完成中断、错误中断、总线关闭中断等等。我建议至少开启接收中断和错误中断。接收中断保证你不丢帧,错误中断让你能及时发现问题。
中断优先级怎么设?CAN中断优先级要设得高一些,但不能高过系统滴答定时器。我一般把CAN接收中断设为次高优先级,发送完成中断设为中等优先级。为什么?因为接收是实时性要求最高的,丢一帧可能整个协议栈就乱了。
// 中断配置示例(基于STM32 HAL库)
HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 1, 0); // 接收中断,优先级1
HAL_NVIC_SetPriority(CAN1_TX_IRQn, 3, 0); // 发送中断,优先级3
HAL_NVIC_SetPriority(CAN1_SCE_IRQn, 2, 0); // 错误中断,优先级2
HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);
HAL_NVIC_EnableIRQ(CAN1_TX_IRQn);
HAL_NVIC_EnableIRQ(CAN1_SCE_IRQn);
中断服务函数里要做什么?记住一个原则:快进快出。接收中断里只做两件事:把数据从硬件FIFO搬到软件缓冲区,然后清中断标志。千万别在中断里做协议解析,那会拖死系统。
3.3 收发缓冲区管理——环形缓冲区是个好东西
CAN控制器内部有硬件FIFO,但深度有限。比如STM32的CAN有3个发送邮箱和2个接收FIFO(每个FIFO深度3)。如果报文密集,硬件FIFO很容易溢出。所以我们需要在软件层做缓冲区管理。
我个人最推荐环形缓冲区。实现简单,效率高,没有动态内存分配的问题。
// 环形缓冲区结构体定义
typedef struct {
uint8_t buffer[CAN_RX_BUFFER_SIZE][8]; // 数据存储区
uint32_t id[CAN_RX_BUFFER_SIZE]; // ID存储区
uint8_t dlc[CAN_RX_BUFFER_SIZE]; // 数据长度存储区
volatile uint16_t head; // 写指针
volatile uint16_t tail; // 读指针
uint16_t size; // 缓冲区大小
} can_ring_buffer_t;
为什么用环形缓冲区?你想想看,如果每次收到报文都动态分配内存,频繁的malloc/free会导致内存碎片,嵌入式系统最怕这个。环形缓冲区是固定大小的,用两个指针来管理读写位置,满了就覆盖旧数据或者丢弃新数据,由你决定。
发送缓冲区管理也类似。CANopen协议栈里,PDO和SDO的发送优先级不同,我建议把发送缓冲区设计成优先级队列。高优先级的报文(比如紧急报文)插队到前面发送。实现方式可以用链表,也可以用多个环形缓冲区分别对应不同优先级。
3.4 错误处理机制——别等出事了再慌
CAN总线不是100%可靠的。电磁干扰、节点故障、总线短路,这些都可能发生。所以错误处理机制是驱动层必不可少的部分。
CAN控制器内部有错误计数器,分为发送错误计数器(TEC)和接收错误计数器(REC)。根据这两个计数器的值,节点会处于三种状态之一:
| 状态 | 条件 | 行为 |
|---|---|---|
| 主动错误 | TEC < 128 且 REC < 128 | 正常通信,检测到错误时发送主动错误标志 |
| 被动错误 | TEC > 127 或 REC > 127 | 只能发送被动错误标志,且发送前要等待8个隐性位 |
| 总线关闭 | TEC > 255 | 完全断开与总线的连接,不再发送任何数据 |
我建议在驱动层实现一个错误状态回调函数。当错误计数器变化时,通知应用层当前状态。比如总线关闭时,应用层可以尝试复位CAN控制器,重新加入总线。
// 错误中断处理示例
void CAN1_SCE_IRQHandler(void) {
uint32_t error_code = CAN_GetErrorCode(&hcan1);
if (error_code & CAN_ERROR_EWG) {
// 错误警告,计数器接近128
can_error_warning_callback();
}
if (error_code & CAN_ERROR_EPV) {
// 被动错误
can_error_passive_callback();
}
if (error_code & CAN_ERROR_BOF) {
// 总线关闭
can_bus_off_callback();
// 这里可以尝试重新初始化CAN控制器
CAN_ReInit(&hcan1);
}
// 清除中断标志
CAN_ClearITPendingBit(&hcan1, CAN_IT_ERR);
}
还有一个容易被忽略的点:错误帧的统计。我习惯在驱动层维护一个错误统计结构体,记录总线上发生的错误类型和次数。这些数据对调试非常有用。比如发现CRC错误突然增多,那可能是总线干扰太大,需要检查终端电阻或者屏蔽。
// 错误统计结构体
typedef struct {
uint32_t crc_errors; // CRC错误次数
uint32_t form_errors; // 格式错误次数
uint32_t ack_errors; // 应答错误次数
uint32_t bit_errors; // 位错误次数
uint32_t stuff_errors; // 填充错误次数
uint32_t bus_off_count; // 总线关闭次数
} can_error_stats_t;
好了,CAN硬件驱动层的核心内容就这些。初始化是基础,中断是效率,缓冲区是保障,错误处理是安全。把这四块做好了,上层CANopen协议栈才能跑得稳。下一章咱们聊聊CANopen的对象字典,那才是协议栈的灵魂所在。