3. CANFD驱动开发:从初始化到错误恢复
好,咱们进入第三章。CANFD驱动开发,说白了就是让芯片的CANFD外设能正常工作。我做了这么多年嵌入式,发现很多人一上来就写收发代码,结果连初始化都没搞对,折腾半天找不着北。今天咱们就把这块掰开揉碎了讲清楚。
3.1 CANFD初始化流程
初始化这事儿,我习惯按这个顺序来:时钟使能、GPIO配置、CANFD控制器复位、模式设置、位时序配置、过滤器设置,最后使能中断。每一步都有坑,咱们一个个说。
3.1.1 时钟与GPIO配置
先让CANFD模块跑起来。不同芯片的时钟树不一样,但核心就一句话:确保CANFD时钟源正确,且频率满足你的目标速率。
举个例子,我用STM32H7系列时,CANFD时钟通常挂载在FDCAN总线上。代码大概长这样:
// 使能FDCAN1时钟
__HAL_RCC_FDCAN_CLK_ENABLE();
// 配置GPIO:PD0为FDCAN1_RX,PD1为FDCAN1_TX
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF9_FDCAN1;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
这里有个细节:RX引脚要配置为上拉。为什么?CAN总线隐性状态时,RX引脚必须读到高电平。我曾经遇到过因为没配上拉,导致总线一直报错,查了两天才发现是GPIO配置问题。
3.1.2 位时序配置
CANFD的位时序比传统CAN复杂,因为它支持两种速率:仲裁段(标称速率)和数据段(数据速率)。仲裁段通常用250kbps或500kbps,数据段可以跑到2Mbps甚至5Mbps。
配置时要注意:
- 标称位时序:决定仲裁段的波特率,所有节点必须一致
- 数据位时序:决定数据段的波特率,可以不同节点不同速率
- 采样点:我一般设在87.5%左右,抗干扰能力比较好
看个配置示例:
FDCAN_InitTypeDef FdcanInit = {0};
FdcanInit.FrameFormat = FDCAN_FRAME_FD_BRS; // 支持BRS
FdcanInit.Mode = FDCAN_MODE_NORMAL;
// 标称位时序:500kbps
FdcanInit.NominalSyncJumpWidth = 1;
FdcanInit.NominalTimeSeg1 = 13; // TSEG1
FdcanInit.NominalTimeSeg2 = 2; // TSEG2
// 数据位时序:2Mbps
FdcanInit.DataSyncJumpWidth = 1;
FdcanInit.DataTimeSeg1 = 5;
FdcanInit.DataTimeSeg2 = 1;
// 采样点 = (1 + TSEG1) / (1 + TSEG1 + TSEG2)
// 标称采样点 = (1+13)/(1+13+2) = 87.5%
// 数据采样点 = (1+5)/(1+5+1) = 85.7%
3.1.3 过滤器配置
CANFD的过滤器可以帮你过滤掉不关心的报文,减轻CPU负担。我一般用标识符列表模式,只接收我关心的ID。
FDCAN_FilterTypeDef FilterConfig = {0};
FilterConfig.IdType = FDCAN_STANDARD_ID; // 标准帧
FilterConfig.FilterIndex = 0;
FilterConfig.FilterType = FDCAN_FILTER_MASK; // 掩码模式
FilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
FilterConfig.FilterID1 = 0x123; // 要接收的ID
FilterConfig.FilterID2 = 0x7FF; // 掩码:0x7FF表示精确匹配
HAL_FDCAN_ConfigFilter(&hfdcan1, &FilterConfig);
嗯,这里要注意:掩码为0的位表示不关心,为1的位表示必须匹配。我刚开始做的时候搞反了,结果该收的没收,不该收的全进来了。
3.2 报文发送与接收中断处理
初始化搞定后,就该收发报文了。CANFD的收发机制和传统CAN类似,但多了个事件触发发送(TT)和发送带宽控制。不过咱们先从基础讲起。
3.2.1 发送流程
发送报文的核心是:把数据填到发送缓冲区,然后触发发送。CANFD控制器通常有多个发送缓冲区(比如3个),你可以同时缓存多个待发送报文。
FDCAN_TxHeaderTypeDef TxHeader = {0};
uint8_t TxData[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
TxHeader.Identifier = 0x123;
TxHeader.IdType = FDCAN_STANDARD_ID;
TxHeader.TxFrameType = FDCAN_DATA_FRAME;
TxHeader.DataLength = FDCAN_DLC_BYTES_8;
TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
TxHeader.BitRateSwitch = FDCAN_BRS_ON; // 使用高速数据段
// 发送报文
if (HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &TxHeader, TxData) != HAL_OK)
{
// 发送失败处理
Error_Handler();
}
发送时有个坑:如果发送缓冲区满了,AddMessageToTxFifoQ会返回错误。我建议在发送前检查一下缓冲区状态:
if (HAL_FDCAN_GetTxFifoFreeLevel(&hfdcan1) > 0)
{
HAL_FDCAN_AddMessageToTxFifoQ(&hfdcan1, &TxHeader, TxData);
}
else
{
// 缓冲区满,缓存到软件队列
AddToSoftwareTxQueue(&TxHeader, TxData);
}
3.2.2 接收中断处理
接收中断是CANFD驱动里最核心的部分。我习惯用FIFO0接收中断,配合新报文中断来触发接收处理。
// 使能接收中断
HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0);
// 中断服务函数
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
FDCAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[64];
if (RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE)
{
// 从FIFO0读取报文
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK)
{
// 处理接收到的报文
ProcessReceivedMessage(&RxHeader, RxData);
}
}
}
这里有个关键点:接收中断里要尽快把报文从FIFO里读出来。为什么?因为FIFO深度有限(通常只有3-64个),如果不及时读取,新报文会覆盖旧报文,造成丢帧。
我个人的做法是:在中断里只做报文拷贝和标志位设置,具体解析放到任务里。这样既保证了实时性,又不会阻塞中断。
3.3 CANFD错误处理与总线恢复
这部分是实战中最容易出问题的。CANFD的错误处理机制比传统CAN更复杂,因为多了协议错误和位错误的区分。
3.3.1 错误状态与计数器
CANFD控制器内部有两个计数器:发送错误计数器(TEC)和接收错误计数器(REC)。根据这两个计数器的值,节点会处于三种状态:
| 状态 | 条件 | 行为 |
|---|---|---|
| 错误主动 | TEC < 128 且 REC < 128 | 正常收发,发送主动错误帧 |
| 错误被动 | TEC > 127 或 REC > 127 | 只能发送被动错误帧,发送前需等待8个隐性位 |
| 总线关闭 | TEC > 255 | 停止收发,需要协议层恢复 |
我建议在代码里定期检查错误状态:
// 获取错误计数器
uint8_t TEC = HAL_FDCAN_GetTxErrorCount(&hfdcan1);
uint8_t REC = HAL_FDCAN_GetRxErrorCount(&hfdcan1);
if (TEC > 96 || REC > 96)
{
// 接近错误被动,需要关注
LogWarning("CANFD error count high: TEC=%d, REC=%d", TEC, REC);
}
if (TEC > 255)
{
// 总线关闭,需要恢复
BusOffRecovery();
}
3.3.2 常见错误与排查
我在项目中遇到最多的CANFD错误有这几类:
- 位错误(Bit Error):发送节点在总线上读到的位与自己发送的不一致。通常是总线短路或终端电阻问题。
- 填充错误(Stuff Error):连续6个相同电平,违反了位填充规则。一般是总线干扰或节点时钟不同步。
- CRC错误:接收节点计算的CRC与发送节点不一致。数据段速率太高时容易出现。
- 形式错误(Form Error):帧格式不对,比如EOF段不是隐性位。通常是软件bug。
3.3.3 总线恢复机制
当节点进入总线关闭状态后,需要执行总线恢复流程。CANFD协议规定:节点必须检测到128次连续的11个隐性位(即总线空闲),才能退出总线关闭状态。
我一般这样实现:
void BusOffRecovery(void)
{
// 1. 停止CANFD控制器
HAL_FDCAN_Stop(&hfdcan1);
// 2. 等待总线恢复时间(通常128个总线空闲位)
// 对于500kbps,128位 = 256μs
HAL_Delay(1); // 实际项目中建议用定时器精确等待
// 3. 重新初始化
HAL_FDCAN_Init(&hfdcan1);
// 4. 启动CANFD
HAL_FDCAN_Start(&hfdcan1);
// 5. 重新使能中断
HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0);
LogInfo("CANFD bus recovery completed");
}
这里有个细节:恢复后要重新配置过滤器和中断,因为Stop操作会清除这些配置。我刚开始做的时候忘了这一步,恢复后死活收不到报文,排查了半天才发现。
3.3.4 错误中断处理
我建议开启错误中断,这样能第一时间发现总线异常:
// 使能错误中断
HAL_FDCAN_ActivateNotification(&hfdcan1,
FDCAN_IT_BUS_OFF |
FDCAN_IT_ERROR_PASSIVE |
FDCAN_IT_ERROR_WARNING,
0);
// 错误中断回调
void HAL_FDCAN_ErrorCallback(FDCAN_HandleTypeDef *hfdcan)
{
uint32_t ErrorCode = HAL_FDCAN_GetError(&hfdcan1);
if (ErrorCode & FDCAN_ERROR_BUS_OFF)
{
// 总线关闭,启动恢复
BusOffRecovery();
}
if (ErrorCode & FDCAN_ERROR_PASSIVE)
{
// 进入错误被动状态,记录日志
LogWarning("CANFD entered error passive state");
}
if (ErrorCode & FDCAN_ERROR_WARNING)
{
// 错误警告,检查错误计数器
LogInfo("CANFD error warning: TEC=%d, REC=%d",
HAL_FDCAN_GetTxErrorCount(&hfdcan1),
HAL_FDCAN_GetRxErrorCount(&hfdcan1));
}
}
嗯,到这里CANFD驱动开发的核心内容就讲完了。总结一下:初始化要细心,收发要高效,错误处理要全面。下一章咱们会讲CANFD的更高层协议——比如如何实现安全可靠的OTA数据传输。到时候见!