4、CAN驱动开发:CAN收发器(TJA1050)电路设计要点、CAN中断接收与轮询发送、CAN滤波器配置实战

好,咱们进入第四章。这一章聊的是CAN驱动开发,说白了就是让MCU和CAN总线真正“说上话”。很多初学者觉得CAN协议很复杂,其实不然。你只要把硬件电路搭对,中断和轮询的套路搞清楚,滤波器配置别搞反了,基本就稳了。我当年第一次调CAN,就是栽在收发器电路上——信号死活过不去,查了三天发现是共模电感焊反了。嗯,咱们今天就把这些坑一个个填平。

4.1 CAN收发器(TJA1050)电路设计要点

先说说TJA1050这个芯片。它是NXP家的经典收发器,把MCU的CAN控制器输出的差分信号,转换成总线上的CAN_H和CAN_L。反过来也一样。我个人习惯用TJA1050,因为它兼容5V和3.3V的MCU逻辑电平,而且EMC性能不错。

电路设计上,有几个关键点你一定要注意:

  • 电源去耦:VCC引脚必须加一个100nF的陶瓷电容,紧贴芯片放置。我见过有人偷懒没加,结果总线上的噪声直接串到MCU的CAN控制器里,导致接收错误帧。
  • CAN_H和CAN_L的终端电阻:总线两端各需要一只120Ω电阻。这个电阻不能省,也不能随便换阻值。你想想看,CAN总线是靠差分信号传输的,终端电阻的作用是匹配阻抗、消除信号反射。我曾经在一个项目里用了100Ω的电阻,结果高速通信时波形振铃严重,丢包率飙升。
  • 共模扼流圈:在CAN_H和CAN_L上串联一个共模电感,可以有效抑制共模干扰。我建议选TDK或Murata的型号,感值在51μH到100μH之间。注意方向别焊反了,否则信号衰减很大。
  • 保护电路:在CAN_H和CAN_L对地各接一个5V的TVS管(比如SMAJ5.0A),防止总线上的浪涌损坏收发器。嗯,这个在工业现场尤其重要,我遇到过雷击导致整排收发器烧毁的惨案。

核心要点:TJA1050的RS引脚(斜率控制)如果接地,则进入高速模式,适合1Mbps通信。如果接高电平,则进入待机模式。我一般直接接地,除非你需要低功耗。

4.2 CAN中断接收与轮询发送

CAN通信的收发方式,我建议这样搭配:中断接收 + 轮询发送。为什么?

接收用中断,是因为CAN消息是异步到达的。你永远不知道总线上下一个消息什么时候来。如果用轮询接收,CPU得一直盯着CAN控制器,浪费资源。中断方式下,一旦收到消息,硬件自动触发中断,CPU立刻响应。我在一个车载项目中,用中断接收实现了1Mbps下零丢包,效果很好。

发送用轮询,是因为发送是主动行为。你准备好数据,往发送缓冲区一写,然后等待发送完成标志位即可。没必要用中断,因为中断会打断当前任务,反而增加上下文切换开销。当然,如果你需要高优先级消息抢占发送,那可以用中断——但那是进阶玩法了。

下面给一个简单的代码示例,基于STM32的bxCAN外设:

// CAN接收中断服务函数
void CAN1_RX0_IRQHandler(void)
{
    CanRxMsg RxMessage;
    CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
    
    // 处理接收到的数据
    ProcessCanMessage(&RxMessage);
    
    // 清除中断标志
    CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0);
}

// CAN轮询发送函数
uint8_t CanSendMessage(uint32_t id, uint8_t *data, uint8_t len)
{
    CanTxMsg TxMessage;
    TxMessage.StdId = id;
    TxMessage.ExtId = 0;
    TxMessage.IDE = CAN_ID_STD;
    TxMessage.RTR = CAN_RTR_DATA;
    TxMessage.DLC = len;
    
    for(uint8_t i = 0; i < len; i++)
    {
        TxMessage.Data[i] = data[i];
    }
    
    // 等待上一次发送完成
    while(CAN_TransmitStatus(CAN1, 0) != CAN_TxStatus_Ok);
    
    // 发送新消息
    uint8_t mailbox = CAN_Transmit(CAN1, &TxMessage);
    
    // 等待发送完成
    while(CAN_TransmitStatus(CAN1, mailbox) != CAN_TxStatus_Ok);
    
    return 1;
}

小技巧:发送时别忘了检查发送邮箱是否空闲。如果三个邮箱都满了,强行发送会导致数据覆盖。我一般先调用CAN_TransmitStatus检查,确保有空闲邮箱再发。

4.3 CAN滤波器配置实战

CAN滤波器,说白了就是硬件层面的“门卫”。它决定哪些消息能进入接收缓冲区,哪些直接丢弃。配置好了,CPU只处理关心的消息,效率翻倍。配置错了,要么收不到消息,要么收到一堆垃圾。

STM32的bxCAN有28个滤波器组,每个组可以配置为标识符列表模式或掩码模式。我个人习惯用掩码模式,因为它更灵活。举个例子:

  • 掩码模式:你设置一个ID和一个掩码。掩码为1的位必须匹配,掩码为0的位忽略。比如ID=0x321,掩码=0x7FF,那就只接收ID=0x321的消息。如果掩码=0x700,那就接收ID高3位为0x3的所有消息(0x300~0x3FF)。
  • 列表模式:你列出几个ID,只有这些ID的消息能通过。适合固定节点通信。

配置时要注意:滤波器组的编号和FIFO的关联。滤波器组0~13关联FIFO0,14~27关联FIFO1。如果你用FIFO0接收,记得配置对应的滤波器组。

下面是一个配置示例,只接收ID为0x321和0x322的消息:

void CanFilter_Config(void)
{
    CAN_FilterInitTypeDef CAN_FilterInitStructure;
    
    CAN_FilterInitStructure.CAN_FilterNumber = 0;       // 使用滤波器组0
    CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;  // 掩码模式
    CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; // 32位位宽
    
    // 设置ID和掩码
    // 高16位:ID=0x321,掩码=0x7FF(精确匹配)
    // 低16位:ID=0x322,掩码=0x7FF(精确匹配)
    CAN_FilterInitStructure.CAN_FilterIdHigh = 0x321 << 5;
    CAN_FilterInitStructure.CAN_FilterIdLow = 0x322 << 5;
    CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x7FF << 5;
    CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x7FF << 5;
    
    CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_FIFO0; // 关联FIFO0
    CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;        // 使能滤波器
    
    CAN_FilterInit(&CAN_FilterInitStructure);
}

注意:配置滤波器时,ID和掩码的位对齐很重要。标准ID是11位,在32位寄存器中左移5位(因为低5位是扩展ID和RTR位)。如果你搞错了移位,滤波器会完全失效。我曾经在这个问题上浪费了半天,后来用逻辑分析仪抓波形才发现是移位错了。

最后,验证滤波器是否生效,最简单的方法:用CAN分析仪发一条你配置的ID消息,看MCU能否收到。再发一条不在列表里的ID,看是否被过滤掉。嗯,这一步别偷懒,我见过有人配完滤波器直接跑应用,结果收不到数据还以为是硬件坏了。

好了,这一章的内容就这些。CAN驱动开发,说白了就是电路搭好、中断轮询配合好、滤波器配好。你把这三点吃透了,CAN通信这块基本就稳了。下一章咱们聊CANopen协议栈的移植,那又是另一番天地了。