3. CAN FD驱动开发:S32K144平台实战

好,咱们直接进入正题。这一章我带你手把手把S32K144的CAN FD驱动搭起来。说实话,CAN FD的驱动说难不难,但细节特别多。我当年第一次调的时候,就因为一个滤波器配置不对,折腾了两天。嗯,咱们今天把这些坑都填上。

3.1 初始化流程:从时钟到模式切换

初始化是基础,基础不牢,后面全白搭。我个人习惯把初始化拆成四步走:

  1. 时钟使能:先给CAN模块喂时钟,别让它饿着。
  2. 引脚配置:把CANTX和CANRX的引脚功能选对。
  3. 时序参数设置:波特率、采样点,这些得算准。
  4. 模式切换:从初始化模式切到正常模式。

这里有个关键点——时钟源的选择。S32K144的CAN FD模块可以用系统时钟,也可以用外部晶振。我建议你用系统时钟,省事。但如果你对时间同步要求特别高,那就得用外部晶振了。

核心要点:CAN FD的初始化必须在初始化模式下完成。你写寄存器前,一定要先确认模块已经进入初始化模式。否则,写进去的值可能不生效。

来看一段实际的初始化代码:

void CAN_FD_Init(void)
{
    // 1. 使能时钟
    PCC->PCCn[PCC_FLEXCAN_INDEX] |= PCC_PCCn_CGC_MASK;
    
    // 2. 软复位
    CAN->MCR |= CAN_MCR_SOFTRST_MASK;
    while(CAN->MCR & CAN_MCR_SOFTRST_MASK);
    
    // 3. 进入初始化模式
    CAN->MCR |= CAN_MCR_MDIS_MASK;
    CAN->MCR |= CAN_MCR_FRZ_MASK;
    CAN->MCR &= ~CAN_MCR_HALT_MASK;
    
    // 4. 配置时序
    CAN->CTRL1 = (40 << CAN_CTRL1_PRESDIV_SHIFT) |  // 预分频
                 (12 << CAN_CTRL1_PSEG1_SHIFT)  |  // 相位段1
                 (3  << CAN_CTRL1_PSEG2_SHIFT)  |  // 相位段2
                 (1  << CAN_CTRL1_RJW_SHIFT);      // 同步跳转宽度
    
    // 5. 使能CAN FD模式
    CAN->CTRL2 |= CAN_CTRL2_ISOCANFDEN_MASK;
    CAN->CTRL2 |= CAN_CTRL2_BOFFDONLY_MASK;
    
    // 6. 退出初始化模式
    CAN->MCR &= ~CAN_MCR_MDIS_MASK;
    while(CAN->MCR & CAN_MCR_LPMACK_MASK);
}

小技巧:退出初始化模式后,一定要等LPMACK位清零。我曾经因为没加这个等待,导致后续配置全乱套。你想想看,模块还在低功耗模式,你发数据它能理你吗?

3.2 中断配置:别让CPU空转

中断是嵌入式开发的灵魂。CAN FD的中断源很多,但常用的就这几个:

中断类型 触发条件 优先级建议
接收中断 收到新报文 中等
发送中断 报文发送完成 低
错误中断 总线错误或状态变化 高
唤醒中断 总线从休眠唤醒 低

我个人习惯把错误中断优先级设最高。为什么?因为总线一出错,你得第一时间知道,不然整个网络都可能瘫痪。我在项目里遇到过,CAN总线因为终端电阻虚焊,时不时丢帧。要不是错误中断及时报警,排查起来得累死。

中断配置的代码其实不复杂:

void CAN_FD_Interrupt_Config(void)
{
    // 使能接收中断
    CAN->IMASK1 |= (1 << RX_MAILBOX);
    
    // 使能错误中断
    CAN->MCR |= CAN_MCR_ERRMSK_MASK;
    
    // 设置NVIC优先级
    NVIC_SetPriority(FlexCAN0_ORed_0_15_MB_IRQn, 5);
    NVIC_SetPriority(FlexCAN0_ORed_Error_IRQn, 0);  // 错误中断最高
    
    // 使能NVIC
    NVIC_EnableIRQ(FlexCAN0_ORed_0_15_MB_IRQn);
    NVIC_EnableIRQ(FlexCAN0_ORed_Error_IRQn);
}

注意:中断服务函数里别做太多事。我见过有人直接在中断里做数据解析,结果导致中断嵌套过深,系统直接崩了。中断里只做标志位设置,具体处理放到主循环或任务里。

3.3 DMA传输:解放CPU

CAN FD的数据段最长64字节,如果每个报文都用CPU搬数据,效率太低了。DMA就是干这个的——让数据自己走,CPU去干更重要的事。

DMA传输的核心思路是:

  • 发送:CPU把数据写到内存缓冲区,DMA自动搬到CAN的发送邮箱。
  • 接收:DMA把CAN接收邮箱的数据搬到内存缓冲区,然后通知CPU。

配置DMA时,有几个参数得注意:

void CAN_FD_DMA_Config(void)
{
    // 使能DMA时钟
    PCC->PCCn[PCC_DMA0_INDEX] |= PCC_PCCn_CGC_MASK;
    
    // 配置DMA通道
    DMA0->TCD[CH_CAN_RX].SADDR = (uint32_t)&CAN->MB[RX_MAILBOX].data;
    DMA0->TCD[CH_CAN_RX].DADDR = (uint32_t)rx_buffer;
    DMA0->TCD[CH_CAN_RX].NBYTES = 64;  // CAN FD最大数据长度
    DMA0->TCD[CH_CAN_RX].CITER = 1;
    DMA0->TCD[CH_CAN_RX].BITER = 1;
    
    // 使能DMA请求
    CAN->MCR |= CAN_MCR_DMA_MASK;
    CAN->IMASK1 &= ~(1 << RX_MAILBOX);  // 关闭对应邮箱中断
}

避坑指南:DMA传输完成后,一定要清传输完成标志。我曾经忘了这一步,结果DMA只工作了一次,后面再也不动了。排查了半天才发现是标志位没清。

3.4 收发函数封装:接口要清爽

驱动写好了,接口得封装得漂亮。我习惯提供四个核心函数:

函数名 功能 参数说明
CAN_FD_Send 发送CAN FD报文 ID、数据指针、长度、标志位
CAN_FD_Receive 接收CAN FD报文 ID、数据指针、超时时间
CAN_FD_GetStatus 获取控制器状态 返回状态结构体
CAN_FD_ErrorHandle 错误处理 错误码

发送函数的实现,我习惯用轮询+超时的方式:

uint8_t CAN_FD_Send(uint32_t id, uint8_t *data, uint8_t len, uint8_t flags)
{
    uint32_t timeout = 10000;
    
    // 等待邮箱空闲
    while((CAN->IFLAG1 & (1 << TX_MAILBOX)) == 0)
    {
        if(--timeout == 0) return CAN_FD_TIMEOUT;
    }
    
    // 配置发送邮箱
    CAN->MB[TX_MAILBOX].CS = (len << 16) | flags;
    CAN->MB[TX_MAILBOX].ID = id;
    
    // 复制数据
    for(uint8_t i = 0; i < len; i++)
    {
        CAN->MB[TX_MAILBOX].data[i] = data[i];
    }
    
    // 触发发送
    CAN->IFLAG1 = (1 << TX_MAILBOX);
    CAN->MCR |= CAN_MCR_TXFWM_MASK;
    
    return CAN_FD_OK;
}

接收函数我建议用回调机制。这样上层应用不用一直轮询,收到数据自动处理:

void CAN_FD_RegisterRxCallback(void (*callback)(uint32_t id, uint8_t *data, uint8_t len))
{
    rx_callback = callback;
}

// 在中断服务函数中调用
void FLEXCAN0_ORed_0_15_MB_IRQHandler(void)
{
    if(CAN->IFLAG1 & (1 << RX_MAILBOX))
    {
        uint32_t id = CAN->MB[RX_MAILBOX].ID;
        uint8_t len = (CAN->MB[RX_MAILBOX].CS >> 16) & 0x0F;
        
        if(rx_callback)
        {
            rx_callback(id, CAN->MB[RX_MAILBOX].data, len);
        }
        
        CAN->IFLAG1 = (1 << RX_MAILBOX);
    }
}

3.5 整体架构:一张图看懂

说了这么多,咱们用一张图把整个驱动架构串起来:

CAN FD驱动架构图 应用层(Bootloader/APP) API接口层(Send/Receive/Status/Error) 驱动核心层(初始化/中断/DMA/邮箱管理) CAN FD控制器 DMA控制器 NVIC中断控制器 数据流向:应用层 → API → 驱动核心 → 硬件层

这张图把整个驱动分成了四层。你写代码的时候,也按这个层次来组织文件。我个人习惯每个层一个.c/.h文件,这样后期维护起来特别清晰。

总结一下:CAN FD驱动开发的核心就三点——初始化要稳、中断要快、DMA要准。把这三点做好了,你的Bootloader升级就有了一个坚实的基础。