4. CANFD底层驱动开发:FlexCAN初始化流程、报文发送实现、报文接收实现、中断处理机制、CANFD与经典CAN兼容性处理

好,咱们进入正题。这一章是CANFD底层驱动的核心,说白了就是让FlexCAN模块真正跑起来。我当年第一次调FlexCAN时,光初始化就折腾了两天,后来发现是时钟配置搞错了。嗯,咱们一步步来,把坑都填上。

4.1 FlexCAN初始化流程

初始化FlexCAN,我习惯按这个顺序来:时钟使能、模块复位、配置时序参数、设置滤波器、最后使能中断。别急着上来就发数据,先让模块“醒过来”。

核心步骤:

  1. 使能FlexCAN模块时钟(在PCC寄存器里)
  2. 等待模块进入冻结模式(FRZ位+HALT位)
  3. 配置CAN时钟源(选总线时钟还是外部晶振)
  4. 设置位时序(波特率、采样点)
  5. 配置消息缓冲区(MB)或FIFO
  6. 设置接收掩码和滤波器
  7. 退出冻结模式,进入正常模式

这里有个关键点——位时序配置。CANFD的波特率分两段:仲裁段和数据段。仲裁段通常用500kbps,数据段可以到2Mbps甚至5Mbps。我建议仲裁段采样点设在75%~80%之间,数据段可以放宽到70%左右。

// FlexCAN初始化示例(S32K144)
void FLEXCAN_Init(void)
{
    // 1. 使能时钟
    PCC->PCCn[PCC_FLEXCAN0_INDEX] |= PCC_PCCn_CGC_MASK;
    
    // 2. 进入冻结模式
    CAN0->MCR |= CAN_MCR_FRZ_MASK | CAN_MCR_HALT_MASK;
    while(!(CAN0->MCR & CAN_MCR_FRZ_ACK_MASK));
    
    // 3. 配置位时序(仲裁段500kbps,数据段2Mbps)
    CAN0->CTRL1 = CAN_CTRL1_PRESDIV(39) |   // 预分频
                  CAN_CTRL1_PROPSEG(0) |
                  CAN_CTRL1_PSEG1(7) |
                  CAN_CTRL1_PSEG2(4) |
                  CAN_CTRL1_RJW(2);
    
    // 4. 使能CANFD模式
    CAN0->MCR |= CAN_MCR_FDEN_MASK;
    
    // 5. 配置数据段时序
    CAN0->FDCTRL = CAN_FDCTRL_FPRESC(0) |
                   CAN_FDCTRL_TDCEN_MASK;  // 使能收发延迟补偿
    
    // 6. 退出冻结模式
    CAN0->MCR &= ~(CAN_MCR_FRZ_MASK | CAN_MCR_HALT_MASK);
    while(CAN0->MCR & CAN_MCR_FRZ_ACK_MASK);
}

我的经验:配置完时序后,一定要读一下错误计数器(ECR寄存器)。如果看到大量错误,八成是位时序没算对。我曾经因为预分频算错一位,导致整条CAN总线瘫痪,排查了整整一个下午。

4.2 报文发送实现

发送报文,说白了就是把数据塞进消息缓冲区(MB),然后告诉硬件“发出去”。FlexCAN有32个MB,每个MB可以配置成发送或接收。

我个人习惯用轮询发送做调试,用中断发送做产品。为什么?轮询简单,但会阻塞CPU;中断效率高,但代码复杂些。

// 发送CANFD报文(中断方式)
uint8_t FLEXCAN_SendMsg(uint32_t id, uint8_t *data, uint8_t len, uint8_t isFD)
{
    uint8_t mb_idx = 0;
    
    // 找空闲MB
    for(mb_idx = 0; mb_idx < 32; mb_idx++)
    {
        if((CAN0->RAMn[mb_idx].CS & CAN_CS_CODE_MASK) == CAN_CS_CODE_INACTIVE)
        {
            break;
        }
    }
    if(mb_idx >= 32) return 0xFF;  // 没有空闲MB
    
    // 配置MB
    CAN0->RAMn[mb_idx].CS = CAN_CS_CODE_TX_DATA |  // 发送数据帧
                            CAN_CS_IDE_MASK |       // 扩展帧
                            (isFD ? CAN_CS_EDL_MASK : 0) |  // CANFD标志
                            CAN_CS_DLC(len);        // 数据长度
    
    CAN0->RAMn[mb_idx].ID = id;
    
    // 复制数据
    for(uint8_t i = 0; i < len; i++)
    {
        CAN0->RAMn[mb_idx].DATA[i] = data[i];
    }
    
    // 触发发送
    CAN0->IFLAG1 |= (1 << mb_idx);  // 清除中断标志
    CAN0->RAMn[mb_idx].CS |= CAN_CS_CODE_TX_DATA;  // 激活发送
    
    return 0;
}

注意:CANFD报文的数据长度编码(DLC)和经典CAN不一样。经典CAN的DLC最大是8字节,CANFD可以到64字节。但DLC编码不是线性的——比如DLC=9代表12字节,DLC=12代表20字节。这个坑我踩过,发出去的数据对不上。

4.3 报文接收实现

接收报文,我推荐用FIFO+中断的方式。FlexCAN有一个8层深的FIFO,配合中断,基本不会丢帧。你想想看,如果每个报文都去轮询MB,CPU得累死。

// 配置FIFO接收
void FLEXCAN_ConfigFIFO(void)
{
    // 使能FIFO
    CAN0->MCR |= CAN_MCR_FEN_MASK;
    
    // 配置FIFO滤波器(接收所有ID)
    CAN0->RXIMR[0] = 0;  // 掩码全0,接收所有
    
    // 设置FIFO深度为8
    CAN0->CTRL2 &= ~CAN_CTRL2_RFFN_MASK;
    CAN0->CTRL2 |= CAN_CTRL2_RFFN(0);  // 8个FIFO缓冲区
    
    // 使能FIFO中断
    CAN0->IMASK1 |= CAN_IMASK1_BUF5M_MASK;  // FIFO中断在MB5
}

// FIFO中断服务函数
void FLEXCAN0_IRQHandler(void)
{
    if(CAN0->IFLAG1 & CAN_IFLAG1_BUF5I_MASK)
    {
        // 读取FIFO中的数据
        uint32_t id = CAN0->RAMn[5].ID;
        uint8_t dlc = (CAN0->RAMn[5].CS & CAN_CS_DLC_MASK) >> 16;
        uint8_t data[64];
        
        for(uint8_t i = 0; i < dlc; i++)
        {
            data[i] = CAN0->RAMn[5].DATA[i];
        }
        
        // 处理接收到的数据(回调函数)
        if(rx_callback) rx_callback(id, data, dlc);
        
        // 清除中断标志
        CAN0->IFLAG1 = CAN_IFLAG1_BUF5I_MASK;
    }
}

避坑指南:我曾经在FIFO中断里做太多处理,导致中断嵌套,丢了好几帧。后来我把数据处理放到任务队列里,中断里只做标志位设置。记住:中断服务函数要短小精悍

4.4 中断处理机制

FlexCAN的中断系统其实挺清晰的。每个MB都有自己的中断标志位,FIFO也有独立的中断。我建议把中断优先级设高一点,毕竟CAN报文实时性要求高。

中断源 标志位 说明
MB0~MB4 IFLAG1[0:4] 独立MB中断
MB5(FIFO) IFLAG1[5] FIFO满或溢出
错误中断 ESR寄存器 总线错误、仲裁丢失等
唤醒中断 MCR中的WAKMSK 总线活动唤醒

嗯,这里有个细节:中断标志位需要软件清除。而且清除时机很重要——必须在读取完数据后再清除,否则可能丢失数据。我习惯在中断函数最后统一清除标志。

4.5 CANFD与经典CAN兼容性处理

这是实际项目中绕不开的问题。你的节点可能同时连接CANFD设备和经典CAN设备。怎么处理?说白了就是自动识别

FlexCAN有个好功能——CANFD帧检测。当收到一个报文时,硬件会自动判断它是经典CAN还是CANFD帧。你只需要在接收中断里检查EDL位就行。

// 兼容性处理:自动识别CANFD和经典CAN
void FLEXCAN_RxHandler(uint32_t mb_idx)
{
    uint32_t cs = CAN0->RAMn[mb_idx].CS;
    
    if(cs & CAN_CS_EDL_MASK)
    {
        // CANFD帧,数据段波特率可能更高
        // 注意:数据段长度可能超过8字节
        uint8_t dlc = (cs & CAN_CS_DLC_MASK) >> 16;
        uint8_t len = FLEXCAN_DLC2Len(dlc);  // DLC转实际长度
        
        // 处理CANFD数据
        ProcessCANFDMsg(mb_idx, len);
    }
    else
    {
        // 经典CAN帧,数据长度最大8字节
        uint8_t len = (cs & CAN_CS_DLC_MASK) >> 16;
        if(len > 8) len = 8;  // 经典CAN最多8字节
        
        // 处理经典CAN数据
        ProcessClassicCANMsg(mb_idx, len);
    }
}

重要提醒:如果总线上同时有CANFD和经典CAN设备,数据段波特率必须一致。否则CANFD设备发高速数据时,经典CAN设备会报错。我见过一个项目,就因为没注意这个,导致经典CAN节点疯狂报错,总线利用率飙到90%。

另外,收发延迟补偿(TDC)在高速CANFD中特别重要。当数据段波特率超过2Mbps时,信号在总线上的传输延迟会明显影响采样。FlexCAN的TDC功能可以自动补偿这个延迟。我建议在初始化时就使能它。

// 使能收发延迟补偿
CAN0->FDCTRL |= CAN_FDCTRL_TDCEN_MASK;
// 配置补偿偏移量(根据实际布线长度调整)
CAN0->FDCTRL |= CAN_FDCTRL_TDCOFF(5);  // 5个时间量子

好了,这一章的内容就这些。FlexCAN的驱动开发,说白了就是初始化配好、发送别堵、接收别丢、中断别长、兼容别乱。下一章咱们会讲UDS诊断栈的移植,到时候这些底层驱动就是基础了。

本章要点回顾:

  • 初始化一定要先进入冻结模式再配置
  • 发送用中断方式,接收用FIFO+中断
  • 中断服务函数要短,数据处理放外面
  • CANFD和经典CAN兼容靠EDL位识别
  • 高速CANFD必须使能TDC