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%
我的经验:如果总线长度超过10米,建议把数据段速率降到1Mbps以下。别问我怎么知道的——有一次在产线上,20米长的总线跑2Mbps,误码率高得离谱,最后降到800kbps才稳定。

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);
}
注意:千万不要在中断服务函数里做耗时操作!我见过有人直接在发送中断里调用HAL_Delay,结果系统直接卡死。发送中断里只做标志位设置,具体处理放到主循环或任务里。

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。
避坑指南:我曾经在一个项目里,CANFD总线偶尔出现CRC错误,查了三天没找到原因。最后用示波器一看,发现数据段速率2Mbps时,信号边沿有振铃。解决办法是在CANFD收发器的STB引脚上串一个100Ω电阻,问题就解决了。所以,硬件设计对CANFD的稳定性影响很大

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数据传输。到时候见!