4、CAN驱动开发:CAN控制器初始化、CAN报文收发中断处理、CAN错误处理与总线恢复
好,咱们进入CAN驱动开发这个硬核环节。说实话,很多做以太网网关的工程师,一提到CAN驱动就头大。其实没那么玄乎,CAN控制器说白了就是个状态机,你把它伺候好了,它就能稳定干活。
4.1 CAN控制器初始化——别小看这一步
我个人习惯,写CAN驱动第一件事不是敲代码,而是看数据手册。不同厂家的CAN控制器,寄存器布局差异很大。比如NXP的SJA1000和TI的DCAN,初始化流程就不完全一样。
初始化核心就这几步:
- 时钟使能——先让CAN模块通电,不然啥也干不了
- 复位模式——大部分CAN控制器需要先进入复位模式才能配寄存器
- 配置波特率——这个我踩过坑,后面细说
- 设置滤波器——决定哪些报文能进你的邮箱
- 退出复位模式——进入正常工作模式
波特率配置的坑
我曾经在一个项目里,明明按公式算好了分频系数,结果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总线一忙,中断嵌套把栈搞爆了,系统直接死机。嗯,血的教训。
标准的中断处理流程:
- 进入中断,读取中断标志寄存器,判断是接收中断还是发送中断
- 如果是接收中断——从接收FIFO读数据,存到环形缓冲区
- 如果是发送中断——清除发送完成标志,释放发送邮箱
- 如果是错误中断——转到错误处理流程
- 退出中断前,清空中断标志
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。
我建议的做法:
- 检测到总线关闭中断后,立即记录错误日志
- 手动复位CAN控制器,重新初始化
- 如果连续3次复位后仍然总线关闭,说明硬件有问题,上报给上层应用
- 不要无限重试,否则会浪费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驱动开发没有捷径。多读手册,多看示波器波形,多在实际总线上测试。你踩过的每一个坑,都会变成你简历上的亮点。