4、CAN驱动开发:CAN控制器初始化、CAN报文收发中断处理、CAN错误处理与总线恢复

好,咱们进入CAN驱动开发这个硬核环节。说实话,很多做以太网网关的工程师,一提到CAN驱动就头大。其实没那么玄乎,CAN控制器说白了就是个状态机,你把它伺候好了,它就能稳定干活。

4.1 CAN控制器初始化——别小看这一步

我个人习惯,写CAN驱动第一件事不是敲代码,而是看数据手册。不同厂家的CAN控制器,寄存器布局差异很大。比如NXP的SJA1000和TI的DCAN,初始化流程就不完全一样。

初始化核心就这几步:

  1. 时钟使能——先让CAN模块通电,不然啥也干不了
  2. 复位模式——大部分CAN控制器需要先进入复位模式才能配寄存器
  3. 配置波特率——这个我踩过坑,后面细说
  4. 设置滤波器——决定哪些报文能进你的邮箱
  5. 退出复位模式——进入正常工作模式

波特率配置的坑

我曾经在一个项目里,明明按公式算好了分频系数,结果CAN总线就是不通。折腾了两天,最后发现是晶振频率和手册上写的不一样。你想想看,晶振偏差1%,波特率就偏1%,总线长度一长,采样点就飘了。

这里给个参考配置代码(以STM32的bxCAN为例):

void CAN_Init(void)
{
    // 1. 使能时钟
    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
    
    // 2. 进入初始化模式
    CAN1->MCR |= CAN_MCR_INRQ;
    while((CAN1->MSR & CAN_MSR_INAK) == 0);
    
    // 3. 配置波特率 500kbps
    // 假设APB1时钟36MHz,分频系数4,时间段1=13,时间段2=2
    CAN1->BTR = (4-1) | (13<<16) | (2<<20);
    
    // 4. 配置滤波器——只接收ID为0x123的报文
    CAN1->FMR |= CAN_FMR_FINIT;  // 进入滤波器初始化模式
    CAN1->sFilterRegister[0].FR1 = 0x123 << 5;  // 标准帧,11位ID左移5位
    CAN1->sFilterRegister[0].FR2 = 0x123 << 5;
    CAN1->FA1R |= CAN_FA1R_FACT0;  // 激活滤波器0
    CAN1->FMR &= ~CAN_FMR_FINIT;  // 退出滤波器初始化模式
    
    // 5. 退出初始化模式
    CAN1->MCR &= ~CAN_MCR_INRQ;
    while((CAN1->MSR & CAN_MSR_INAK));
}

小技巧:初始化完成后,建议读一下CAN控制器的状态寄存器,确认确实退出了初始化模式。我见过有些芯片复位后状态不稳定,多检查一次能省很多调试时间。

4.2 CAN报文收发中断处理——别让中断把你搞崩

CAN报文收发,说白了就两种方式:轮询和中断。做网关嘛,肯定用中断,不然CPU得累死。

中断处理的核心原则:快进快出。中断服务函数里只做最必要的事,比如把数据从硬件FIFO搬到内存缓冲区,然后置个标志位。真正的协议解析、转发逻辑,放到主循环或者任务里做。

我见过一个同事,在中断里做CAN报文解析,还调用了printf打印日志。结果呢?CAN总线一忙,中断嵌套把栈搞爆了,系统直接死机。嗯,血的教训。

标准的中断处理流程:

  1. 进入中断,读取中断标志寄存器,判断是接收中断还是发送中断
  2. 如果是接收中断——从接收FIFO读数据,存到环形缓冲区
  3. 如果是发送中断——清除发送完成标志,释放发送邮箱
  4. 如果是错误中断——转到错误处理流程
  5. 退出中断前,清空中断标志
void CAN_IRQHandler(void)
{
    uint32_t irq_flags = CAN1->RF0R;  // 读接收FIFO状态
    
    if(irq_flags & CAN_RF0R_FMP0)  // 有报文待接收
    {
        CanRxMsg rx_msg;
        rx_msg.StdId = CAN1->sFIFOMailBox[0].RIR >> 21;
        rx_msg.DLC = CAN1->sFIFOMailBox[0].RDTR & 0x0F;
        rx_msg.Data[0] = CAN1->sFIFOMailBox[0].RDLR & 0xFF;
        // ... 读取其他数据字节
        
        // 存入环形缓冲区
        RingBuf_Push(&can_rx_buf, &rx_msg);
        
        // 释放接收邮箱
        CAN1->RF0R |= CAN_RF0R_RFOM0;
    }
    
    if(CAN1->TSR & CAN_TSR_RQCP0)  // 发送完成
    {
        CAN1->TSR |= CAN_TSR_RQCP0;  // 清除标志
        can_tx_busy = 0;  // 释放发送锁
    }
}

注意:中断优先级一定要设好。CAN中断优先级不能太低,否则报文会丢。但也不能太高,否则会抢占系统关键任务。我个人习惯把CAN中断设为中等优先级,比定时器低,比串口高。

4.3 CAN错误处理与总线恢复——这才是体现功力的地方

CAN总线不是永远稳定的。线束老化、连接器松动、电磁干扰,都可能导致错误。CAN控制器本身有错误计数机制,但怎么处理这些错误,就看你的水平了。

CAN错误分三种:

  • 位错误——发送节点发现总线电平和自己发的不一致
  • 填充错误——连续6个相同位,违反了位填充规则
  • CRC错误——接收节点计算的CRC和发送的不一致

每个CAN控制器都有两个计数器:发送错误计数(TEC)和接收错误计数(REC)。当TEC或REC超过某个阈值,节点状态会变化:

状态 条件 行为
主动错误 TEC < 128 且 REC < 128 正常收发,发送主动错误帧
被动错误 TEC > 127 或 REC > 127 只能发送被动错误帧,且发送前要等待8个隐性位
总线关闭 TEC > 255 完全脱离总线,不发送任何数据

我曾经在一个车载项目中,遇到CAN总线频繁进入总线关闭状态。排查了半天,发现是某个节点的CAN收发器供电不稳,导致发送时电平异常。嗯,硬件问题往往比软件问题更难查。

总线恢复策略

总线关闭后,CAN控制器会自动尝试恢复。标准流程是:检测到128次连续的11个隐性位(总线空闲),然后自动回到主动错误状态。但这个过程太慢了,128个位时间,对于500kbps的CAN总线来说,大约需要2.5ms。

我建议的做法:

  1. 检测到总线关闭中断后,立即记录错误日志
  2. 手动复位CAN控制器,重新初始化
  3. 如果连续3次复位后仍然总线关闭,说明硬件有问题,上报给上层应用
  4. 不要无限重试,否则会浪费CPU资源
void CAN_ErrorHandler(void)
{
    uint32_t err_flags = CAN1->ESR;
    
    if(err_flags & CAN_ESR_BOF)  // 总线关闭
    {
        static uint8_t retry_count = 0;
        retry_count++;
        
        if(retry_count <= 3)
        {
            // 手动复位CAN控制器
            CAN1->MCR |= CAN_MCR_RESET;
            delay_ms(10);
            CAN_Init();  // 重新初始化
        }
        else
        {
            // 上报错误,不再重试
            Error_Report(CAN_BUS_OFF);
            retry_count = 0;
        }
    }
    
    if(err_flags & CAN_ESR_EPVF)  // 被动错误
    {
        // 降低发送速率,或者切换备用通道
        CAN_SetSlowMode();
    }
}

避坑指南:我曾经在总线恢复时犯过一个低级错误——恢复后没有清空接收FIFO。结果恢复后立刻收到一堆旧报文,导致上层协议解析错乱。所以恢复流程里一定要加一步:清空所有接收和发送FIFO。

最后说一句,CAN驱动开发没有捷径。多读手册,多看示波器波形,多在实际总线上测试。你踩过的每一个坑,都会变成你简历上的亮点。