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。

小技巧: 调试时可以用示波器看CAN_H和CAN_L的差分信号,数一下位宽,就能反推出实际波特率。我曾经用这招抓到一个时钟配置错误的问题,省了大半天时间。

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会导致内存碎片,嵌入式系统最怕这个。环形缓冲区是固定大小的,用两个指针来管理读写位置,满了就覆盖旧数据或者丢弃新数据,由你决定。

注意: 环形缓冲区的head和tail指针一定要用volatile修饰。因为中断和主循环会同时访问它们,不加volatile的话,编译器优化可能导致数据不一致。我曾经因为这个bug排查了两天,最后发现是优化等级开太高了。

发送缓冲区管理也类似。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);
}
避坑指南: 我曾经在一个项目中遇到总线关闭后无法恢复的问题。原因是复位CAN控制器后,没有等待足够长的时间(至少128个总线空闲位)就重新发送数据。CAN协议规定,总线关闭的节点必须等待128个11位隐性位才能恢复。所以复位后加个延时,或者监听总线空闲信号,再重新加入。

还有一个容易被忽略的点:错误帧的统计。我习惯在驱动层维护一个错误统计结构体,记录总线上发生的错误类型和次数。这些数据对调试非常有用。比如发现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的对象字典,那才是协议栈的灵魂所在。