欢迎光临
我们一直在努力

先楫 HPM 与 STM32G491 CAN FD 通信延迟排查:从 4 个周期优化到同周期反馈

先楫 HPM 与 STM32G491 CAN FD 通信延迟排查:从 4 个周期优化到同周期反馈

最近在人形机器人通信链路调试中,发现了一个比较典型、也比较容易被忽略的 CAN FD 时序问题。

先楫板周期下发控制指令,关节模组接收控制量、完成控制处理后,再通过 CAN FD 返回状态数据。最开始从抓包看,一条控制指令从发送到在反馈中体现出来,时间差达到了 8.033 ms,约 4 个 CAN FD 收发周期。

单看 CAN FD 总线本身,这个结果很容易让人先怀疑波特率、总线负载或者驱动性能。但后面的排查证明,真正的问题不在“CAN 帧在线上跑得慢”,而在接收 FIFO 中的旧数据积压,以及控制处理和反馈发送之间的软件时序。

这篇文章记录一下完整的定位过程。


1. 先定义清楚:这里测到的“延迟”是什么

本文说的通信延迟,不是 CAN FD 帧在总线上的物理传播时间,也不是单纯的硬件收发时间。

测试方法是在控制帧中携带 frameNum,模组处理这条控制帧后,在反馈帧中返回对应的 frameNum。通过比较主站当前发送的帧号和反馈中携带的帧号,可以判断:

当前收到的反馈,到底对应几个周期之前的控制指令。

因此本文关注的更准确说法其实是:

控制数据的新鲜度(data age)或者控制—反馈软件流水线延迟。

最初抓包中观察到:

16.661262 s – 16.653229 s = 8.033 ms

约等于 4 个 CAN FD 收发周期。

对于机器人关节控制来说,这种周期级延迟比单纯几十微秒的总线传输时间更值得关注,因为它会直接增加闭环中的数据年龄。


2. 一个很奇怪的现象:换个上电顺序,延迟就变了

进一步测试时发现,延迟和上电顺序存在明显关系。

上电顺序观察到的控制—反馈延迟
先给模组上电,再给先楫板上电 约 1 个周期
先给先楫板上电,再给模组上电 约 4 个周期

这个现象非常关键。

如果问题主要来自 CAN FD 波特率、报文长度或者总线负载,那么仅仅改变上电顺序,不应该让延迟从 1 个周期突然变成 4 个周期。

所以排查重点开始转向:

模组启动过程中,CAN FD 接收链路什么时候开始收数据?软件任务又是什么时候开始消费这些数据?


3. 问题一:模组后上电,Rx FIFO 里已经堆了旧控制帧

模组使用 STM32G491。

模组后上电时,先楫 HPM 已经启动,并且在持续周期发送控制帧。例如总线上可能已经依次出现:

frame 8
frame 9
frame 10
frame 11

问题在于,STM32 上电初始化并不是“所有软件模块在同一个时刻一起开始工作”。

FDCAN 外设可能已经初始化完成,可以正常把总线报文放进 Rx FIFO;但控制任务、反馈任务以及应用层处理流程还没有完全进入稳定运行状态。

于是启动过程中会出现这样的情况:

总线已经发送:frame 8、frame 9、frame 10 …

Rx FIFO 中仍然排着:frame 8、frame 9 …

应用层开始工作以后,从 FIFO 头部按顺序读取

此时模组处理到的是“旧控制帧”,而不是总线上刚刚到达的最新控制量。

请添加图片描述

3.1 为什么这个延迟不会自己追上来?

这才是这个问题最容易忽略的地方。

假设进入稳定运行以后:

  • 先楫板每个周期产生 1 条新控制帧;
  • 模组每个周期也只消费 1 条控制帧。

如果启动时 Rx FIFO 已经积压了若干帧,那么后面虽然模组一直在处理数据,但:

每个周期进来 1 帧,同时只处理 1 帧。

生产速度和消费速度相同,之前形成的队列深度就不会自然减少。

所以系统不会“运行一会儿自动恢复”。

它会一直稳定地落后若干周期。

文档里的 frame 8 / frame 9 / frame 10 是用来说明这个机制的示例。现场测试最终表现为总延迟从约 4 个周期降到 1 个周期,说明启动阶段旧帧积压贡献了额外的周期级数据年龄。

实际积压多少帧,会受到 FDCAN 初始化完成时间、任务启动时间和控制周期相位的影响,并不一定每次固定为 2 帧。


4. 第一轮优化:控制数据只处理 Rx FIFO 中的最新帧

对于这种周期控制协议,旧控制指令通常已经失去意义。

比如主站连续发送:

frame 100:目标位置 10.0°
frame 101:目标位置 10.2°
frame 102:目标位置 10.5°

如果模组现在已经收到 frame 102,再花两个控制周期依次执行 frame 100 和 frame 101,不仅没有收益,反而人为增加控制数据年龄。

因此第一轮优化思路很直接:

收到一批积压帧时,丢弃旧控制帧,只保留并处理 Rx FIFO 中最新的一帧。

修改前,逻辑更接近:

if (FDCAN_RxFifoHasData())
{
read_one_frame_from_fifo();
process_control_frame();
}

优化后的思想是:

while (FDCAN_RxFifoHasData())
{
read_one_frame_from_fifo();

/*
* 持续覆盖,只保留最后读取到的最新帧
*/

latest_frame = rx_frame;
}

process_control_frame(&latest_frame);

具体工程实现可以根据 HAL/LL 或 MCAN 驱动接口调整,核心不是这几行代码本身,而是把接收语义从:

FIFO 消息队列

改成:

最新控制状态缓存

部署以后重新测试,控制—反馈延迟从约 4 个周期降低到 1 个周期。

这说明启动阶段的旧帧积压确实是主要问题之一。

4.1 这里有一个使用前提

“丢掉旧帧,只处理最新帧”并不是所有 CAN 协议都适用。

它适合的是周期位置、速度、力矩目标值,以及其他 latest-value wins 类型的数据。

如果报文承载的是增量命令、轨迹分片、必须逐条执行的事件或者参数写入事务,就不能简单丢弃旧帧。

所以这次优化成立的根本原因,是我们的 CAN FD 控制帧属于周期覆盖型控制量。


5. 还有 1 个周期,到底慢在哪里?

第一轮修改完成以后,问题没有完全结束。

抓包已经从 4 个周期降到了 1 个周期,但 frameNum 仍然能观察到稳定的一拍延迟。

这时再去检查 Rx FIFO 已经没有意义,因为旧帧积压已经解决了。

继续把模组内部的软件执行时序展开后,发现剩下的 1 个周期来自 CAN FD 反馈帧进入 Tx FIFO 的时机。


6. 问题二:反馈帧组织得太早,发送的是“上一拍状态”

原来的处理方式大致是:

  • CAN FD 接收中断到来;
  • 在接收中断中更新新的模组控制参数;
  • 退出 CAN FD 中断之前,立即读取模组状态并填入 CAN FD Tx FIFO;
  • 后续 FOC 中断才真正根据新的控制参数更新模组运行状态。
  • 问题就出在第 3 步。

    CAN FD 接收中断虽然已经收到了本周期最新控制指令,但此时 FOC 控制还没有基于这个新控制量完成运算。

    所以填入 Tx FIFO 的状态,本质上仍然对应上一个控制周期。

    可以把这个时序理解成:

    收到控制帧 N

    CANFD Rx IRQ:
    更新控制参数 N
    立即组织反馈
    此时状态仍然是 N-1

    FOC ISR:
    根据控制参数 N 完成控制处理
    状态更新为 N

    等 CAN FD 回复发出去时,Tx FIFO 里早已经放好了 N-1 的状态。

    于是从抓包看,就表现为:

    控制帧已经是 frame N,但反馈数据仍然对应 frame N-1。

    这就是剩余 1 个周期延迟的来源。

    请添加图片描述


    7. 第二轮优化:等 FOC 更新完成后,再把反馈数据放进 Tx FIFO

    解决思路不是让 CAN FD 发得更快,而是重新安排**“什么时候生成反馈数据”**。

    优化后,CAN FD 接收中断只负责两件事:

    • 更新最新控制参数;
    • 设置 recvNewCanFdFrame = true,表示本周期收到了一条新的控制指令。

    不再在 CAN FD Rx ISR 退出前立即填充 Tx FIFO。

    例如:

    void CANFD_Rx_IRQHandler(void)
    {
    update_control_parameter();

    recvNewCanFdFrame = true;

    /*
    * 这里不再立即组织 CAN FD 反馈数据
    */

    }

    随后进入 FOC 中断,根据最新控制参数完成本周期控制计算和状态更新。

    在 FOC ISR 退出前检查:

    if (recvNewCanFdFrame)
    {
    recvNewCanFdFrame = false;

    /*
    * 软件触发 UART4_IRQ
    */

    trigger_uart4_irq();
    }

    最后在 UART4_IRQ 中,读取已经由 FOC 更新完成的最新模组状态,再填充到 CAN FD Tx FIFO:

    void UART4_IRQHandler(void)
    {
    update_canfd_feedback_from_latest_motor_state();

    fill_canfd_tx_fifo();
    }

    这里 UART4_IRQ 是当前项目中用于承接这一发送处理的软件触发中断,名称本身不是关键。

    真正关键的是执行顺序发生了变化:

    CAN FD 收到最新控制量
    FOC 使用最新控制量完成状态更新
    再组织本周期反馈
    最后放入 CAN FD Tx FIFO

    这样反馈帧中的状态数据才和本周期刚收到的控制指令对应。


    8. 为什么这个优化能消除最后 1 个周期?

    修改前,控制参数和反馈状态属于两个不同时间点:

    控制参数:N
    反馈状态:N-1

    所以即使 CAN FD 总线没有任何堵塞,从 frameNum 看也天然落后一拍。

    修改以后,反馈数据的采样点被移动到了 FOC 更新之后:

    控制参数:N
    FOC 处理:N
    反馈状态:N

    因此反馈不再携带上一周期的旧状态。

    这次优化解决的其实不是传统意义上的“发送性能”,而是一个很典型的:

    数据采样时刻错误。

    在实时控制系统里,“数据什么时候被读取和打包”经常和“数据传输用了多久”同样重要。


    9. 最终测试结果

    完成两轮优化以后重新抓包。

    测试时把控制模式从:

    0

    切换为:

    7

    可以看到先楫板在本周期发送模式 7 后,模组在对应反馈帧中已经可以同步返回模式状态 7。

    同时:

    Tx frameNum == Rx frameNum

    不再观察到之前的周期级滞后。

    速度参数测试也得到相同结果:新的速度控制量可以在对应周期反馈中体现出来。

    最终整个问题可以概括成两部分:

    问题表现根因优化
    启动阶段旧控制帧积压 上电顺序不同,延迟可从 1T 变为约 4T FDCAN 已收帧,但应用处理尚未启动;之后生产/消费速率相同,积压无法自然消失 清理旧帧,只处理最新周期控制帧
    反馈生成时机过早 清掉 FIFO 后仍稳定落后 1T CANFD Rx ISR 中过早把状态放入 Tx FIFO,FOC 还没更新本周期状态 FOC 更新完成后再组织反馈并填入 Tx FIFO

    请添加图片描述

    10. 这次问题给我的几个启发

    10.1 周期通信延迟不一定是总线性能问题

    看到 8 ms 延迟,第一反应很容易去看:

    CAN FD 波特率
    总线利用率
    中断响应时间
    发送 FIFO

    这些当然需要检查,但这次真正占主要部分的是软件流水线。

    一条 CAN FD 帧可能只花很短时间就完成传输,但如果应用层一直处理几周期前的数据,对控制系统来说它仍然是“高延迟”。

    10.2 实时控制更关心数据年龄,而不是消息有没有丢

    普通通信程序往往强调:

    每一帧都不能丢。

    但实时控制有时恰恰相反。

    对于高频周期目标值,旧数据最大的价值可能就是尽快被新数据覆盖。

    控制系统真正需要的是:

    尽量使用最新的控制指令。

    所以 Rx FIFO 的使用策略必须和上层数据语义一致。

    10.3 中断里“马上回复”不一定延迟最低

    乍一看,在 CAN FD 接收中断里立即把反馈放入 Tx FIFO,似乎是最快的做法。

    但如果反馈内容依赖后续 FOC 运算,那么“立即发送”只是更快地发送了一份旧数据。

    这种情况下,更合理的目标不是:

    越早放进 Tx FIFO 越好。

    而应该是:

    在最新状态已经生成以后,尽早把它放进 Tx FIFO。

    二者差一个状态更新边界,结果就是一个完整控制周期。


    11. 总结

    这次 CAN FD 通信延迟问题最终不是一个单点故障,而是两个软件时序问题叠加。

    第一部分来自启动时的 Rx FIFO 旧控制帧积压。模组后上电时,FDCAN 已经开始收帧,但应用层还没有及时消费;进入稳定运行后又因为生产速度和消费速度基本一致,历史积压一直保留下来。

    第二部分来自 反馈数据生成得太早。CAN FD Rx ISR 收到最新指令以后,在 FOC 完成最新状态更新之前就把反馈装入了 Tx FIFO,因此天然返回上一周期状态。

    最终的处理方式也对应两条原则:

    对周期覆盖型控制量,优先保证“最新数据”,不要机械地逐帧补历史数据。

    对依赖控制计算结果的反馈量,要在最新状态真正生成以后再进行采样和发送。

    从最后的抓包结果看,控制模式和速度参数都已经能够在对应周期内完成更新,frameNum 也能保持一致。

    这次问题对我最大的提醒是:

    做实时通信优化时,不要只盯着总线。把“接收、缓存、任务消费、控制计算、状态采样、反馈入队”整个软件时序拉出来,很多所谓的通信延迟其实藏在这里。

    赞(0)
    未经允许不得转载:171主机测评 » 先楫 HPM 与 STM32G491 CAN FD 通信延迟排查:从 4 个周期优化到同周期反馈
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址