3. Bootloader基础
好,咱们直接进入正题。这一章我带你手把手把S32K144的CAN FD驱动搭起来。说实话,CAN FD的驱动说难不难,但细节特别多。我当年第一次调的时候,就因为一个滤波器配置不对,折腾了两天。嗯,咱们今天把这些坑都填上。
3.1 初始化流程:从时钟到模式切换
初始化是基础,基础不牢,后面全白搭。我个人习惯把初始化拆成四步走:
- 时钟使能:先给CAN模块喂时钟,别让它饿着。
- 引脚配置:把CANTX和CANRX的引脚功能选对。
- 时序参数设置:波特率、采样点,这些得算准。
- 模式切换:从初始化模式切到正常模式。
这里有个关键点——时钟源的选择。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 整体架构:一张图看懂
说了这么多,咱们用一张图把整个驱动架构串起来:
这张图把整个驱动分成了四层。你写代码的时候,也按这个层次来组织文件。我个人习惯每个层一个.c/.h文件,这样后期维护起来特别清晰。
总结一下:CAN FD驱动开发的核心就三点——初始化要稳、中断要快、DMA要准。把这三点做好了,你的Bootloader升级就有了一个坚实的基础。