云计算百科
云计算领域专业知识百科平台

STM32 + i.MX6ULL CAN 双向命令/ACK 实战(02):50 ms 超时为何反而“假丢包”

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

STM32 + i.MX6ULL CAN 诊断与 OTA(02):双向命令/ACK,以及 50 ms 超时为什么是“假丢包”

系列目录:从“CAN 能通”到“可诊断、可升级” 上一篇:01 旧 CAN 项目基线审计 仓库:stm32-imx6ull-can-diag-ota 本文对应 tag:a-v0.2-bidirectional

AImate 原来只有 STM32 → Linux 的周期上报:

0x111 PIR
0x112 温湿度
0x113 心跳

Linux → STM32 的 0x221/0x222/0x228 只有 ID 和帧格式,MCU 端没有实现。

这一篇做三件事:

  • 把单向链路补成双向命令/ACK;
  • 做 1000 次命令压测;
  • 解释一个很容易误判的现象:把 ACK 超时改短,丢包率反而升高。

  • TL;DR

    • 双向命令/ACK 已跑通:0x221/0x222 命令,0x228 分片 ACK。
    • 1000 次命令:1000/1000 成功,0 重试,P99 22.06 ms。
    • 50 ms 超时那次:2006 次尝试 / 1006 次重试,但 1000/1000 全部成功。
    • 这不是 CAN 丢包,是 50 ms 超时小于 RTT p99(49.85 ms)。
    • 另一个问题是 STM32 的 APP_REPORT_TICK_MS = 100,命令每 100 ms 才处理一次;改成 20 ms 后 RTT 从约 99 ms 降到约 20 ms。
    • 重复 seq 验证:dup_cmd 300 → 301,没有重复执行。

    1. 背景:原来只有单向上报

    旧工程已经能:

    • 500 kbps 标准帧;
    • 0x111/0x112/0x113 周期/事件上报;
    • Linux 端 candump 能收到。

    但 Linux 发命令时,STM32 端没有解析,也没有 ACK。 所以“双向 CAN 通信”其实只完成了物理层和单向上报。

    这次要补的是应用层:

    Linux 发 0x221/0x222
    ↓
    STM32 校验 CRC、执行命令
    ↓
    STM32 回 0x228 ACK
    ↓
    Linux 按 cmd + seq 匹配


    2. 协议:命令、ACK、seq

    2.1 命令帧 0x221/0x222

    8 字节固定:

    偏移类型字段说明
    0 uint8 cmd 命令码
    1 uint8 seq 每条新命令 +1;重传复用同一 seq
    2…5 uint8[4] args 参数,小端,未用填 0
    6…7 uint16 LE crc16 CRC-16/CCITT-FALSE,覆盖 0…5

    CRC 参数:

    poly = 0x1021
    init = 0xFFFF
    refin = false
    refout = false
    xorout = 0

    例子:

    0x222#10 00 00 00 00 00 94 14

    • cmd=0x10:查询版本;
    • seq=0x00;
    • 后两字节是 CRC16。

    2.2 ACK 帧 0x228

    ACK 也固定 8 字节,但按 4 字节分片:

    偏移类型字段说明
    0 uint8 cmd 回显命令码
    1 uint8 seq 回显 seq
    2 uint8 status 0=OK,1=未知命令,2=参数错误,3=不支持
    3 uint8 frag 分片号,从 0 开始
    4…7 uint8[4] data 载荷分片,不足补 0

    为什么需要分片?

    0x12 查询统计返回 11 字节,8 字节 ACK 装不下,所以拆成 3 帧:

    frag0: cmd seq status 0 data[0..3]
    frag1: cmd seq status 1 data[4..7]
    frag2: cmd seq status 2 data[8..10] 00

    接收端按 cmd + seq 匹配,再按 frag 排序拼接。

    2.3 seq、重试、去重

    • Linux 每条新命令 seq++;
    • 超时重发时 seq 不变,帧字节完全一样;
    • STM32 用 (cmd, seq) 做缓存,深度 4;
    • 命中缓存:dup_cmd++,原样回放 ACK,不重复执行;
    • 未命中:执行命令,缓存应答,再发 ACK。

    这是“ACK 丢失后重传不会执行两次”的关键。


    3. 第一次压测:1000 条命令要 98 秒

    第一版 STM32 任务周期是:

    #define APP_REPORT_TICK_MS 100U

    Report_Task 每 100 ms 醒一次,命令处理 AppCmd_Task() 在这个任务里。

    压测结果:

    压测命令数 : 1000
    成功 / 失败 : 1000 / 0
    总尝试 / 重试: 1000 / 0
    耗时 : 98.66s
    平均速率 : 10.1 cmd/s
    RTT min/avg/max : 8.81 / 98.38 / 103.03 ms
    RTT p50/p99 : 99.72 / 102.86 ms

    没有丢包,也没有重试,但每条命令都要等约 100 ms。

    为什么平均是 98 ms,不是 50 ms

    因为测试是串行的:

    STM32 任务醒来,发 ACK
    ↓
    Linux 立刻收到 ACK,马上发下一条
    ↓
    新命令刚好错过这一轮
    ↓
    只能等下一次 100 ms 任务唤醒

    每条命令都刚好等满一个 tick,平均接近 100 ms。 1000 条 × 100 ms ≈ 100 秒,和实测 98.66 秒对得上。


    4. 把超时改短,反而“丢包”

    第一版 Linux ACK 超时是 50 ms。

    结果:

    压测命令数 : 1000
    成功 / 失败 : 1000 / 0
    总尝试 / 重试: 2006 / 1006
    耗时 : 99.67s
    RTT min/avg/max : 5.71 / 45.88 / 51.50 ms
    RTT p50/p99 : 46.77 / 49.85 ms

    看起来像“丢包率很高”,但实际上:

    • 1000 条命令最终 1000/1000 成功;
    • 2006 次尝试 = 1000 条命令 + 1006 次重试;
    • p99 49.85 ms,max 51.50 ms,而超时是 50 ms。

    也就是说:

    超时窗口刚好卡在 RTT p99 上,大量命令会先超时,再重发;1006 次重试约等于每条命令多等一次 50 ms。

    这不是 CAN 链路丢包,是 超时设得太小。

    ack_rx 为什么不是命令数

    早期日志里出现过:

    cmd_tx=3 ack_rx=6

    3 条命令,6 个 ACK 分片:

    版本 1 片 + 配置 2 片 + 统计 3 片 = 6 片

    所以 ack_rx 是分片数,不是命令成功数。 命令成功数看 ok。


    5. 根因:不是 CAN,是调度粒度

    两个问题要分开:

    现象根因
    50 ms 超时导致大量重试 超时小于 RTT p99,属于“假丢包”
    每条命令约 100 ms STM32 命令处理每 100 ms 才轮一次

    CAN 外设发一帧很快,慢的是:

    CAN RX FIFO
    → Report_Task 100 ms 醒来
    → AppCmd_Task 解析
    → 执行命令
    → CAN_Send ACK

    所以优化方向不是“改 CAN 波特率”,而是:

  • 让 Linux 超时大于 RTT;
  • 让 STM32 更频繁地处理命令,或者用中断唤醒。

  • 6. 修复:先调超时,再调 tick

    6.1 超时改成 150 ms

    50 ms 超时窗口小于 max 51.50 ms,必然有大量重试。 把 Linux 超时改成 150 ms:

    总尝试 / 重试: 1000 / 0
    耗时 : 98.66s
    RTT p99 : 102.86 ms

    重试归零,但每条命令仍然约 100 ms,因为 tick 还是 100 ms。

    结论:

    超时必须大于 RTT p99;但只改超时,只能消除“假丢包”,不能降低延迟。

    6.2 tick 从 100 ms 改成 20 ms

    /* Core/CustomDrivers/app_report.h */
    #define APP_REPORT_TICK_MS 20U

    周期帧仍按各自的时间判断,不受影响;只是命令处理更频繁。

    结果:

    压测命令数 : 1000
    成功 / 失败 : 1000 / 0
    总尝试 / 重试: 1000 / 0
    耗时 : 20.04s
    平均速率 : 49.9 cmd/s
    RTT min/avg/max : 18.57 / 19.75 / 26.24 ms
    RTT p50/p99 : 19.74 / 22.06 ms

    对比:

    tick超时尝试次数重试耗时RTT p99
    100 ms 50 ms 2006 1006 99.67s 49.85 ms
    100 ms 150 ms 1000 0 98.66s 102.86 ms
    20 ms 150 ms 1000 0 20.04s 22.06 ms

    最终选择:

    APP_REPORT_TICK_MS = 20
    ACK timeout = 150 ms

    P99 22 ms,满足后面 UDS 0x22 < 50 ms 的目标。


    7. 去重验证

    发两次同一个 seq=0x77 的查询:

    cansend can0 222#107700000000C66E
    cansend can0 222#107700000000C66E

    再查两次统计:

    cansend can0 222#1278000000007F80
    cansend can0 222#1279000000002E2A

    抓到:

    can0 228 [8] 12 78 00 00 3B 06 00 00
    can0 228 [8] 12 78 00 01 2C 01 5F 09
    can0 228 [8] 12 78 00 02 00 00 78 00

    can0 228 [8] 10 77 00 00 10 12 00 00
    can0 228 [8] 10 77 00 00 10 12 00 00

    can0 228 [8] 12 79 00 00 3E 06 00 00
    can0 228 [8] 12 79 00 01 2D 01 64 09
    can0 228 [8] 12 79 00 02 00 00 79 00

    统计解析:

    查询cmd_rxcrc_errdup_cmdack_txack_faillast_seq
    0x12 seq=0x78 1595 0 300 2399 0 0x78
    0x12 seq=0x79 1598 0 301 2404 0 0x79

    变化:

    cmd_rx +3 = 两次 0x77 + 最后一次 0x79
    dup_cmd +1 = 第二次 0x77 命中缓存
    ack_tx +5 = 两次 0x77 各 1 帧 + 0x79 统计 3 帧
    crc_err = 0
    ack_fail = 0

    结论:

    第二次 0x77 没有重复执行,而是命中缓存并回放 ACK。


    8. 踩坑清单

  • ack_rx 是分片数,不是命令数 看命令成功数要用 ok,看分片用 ack_rx。

  • 超时小于 RTT p99,会制造“假丢包” 50 ms 超时那次 1006 次重试,但全部成功;不是链路丢包。

  • 串行命令/ACK + 轮询 tick 会相位锁定 每次 ACK 后立刻发下一条,新命令刚好错过本轮,等待一个完整 tick。

  • 0x228 的源地址位是历史遗留 ID 位域里源地址仍是 Linux,但实际发送方是 STM32;接收按完整 ID 匹配。

  • 去重缓存要存“应答内容”,不是只存一个标志 否则重传时查询结果(uptime、统计值)会变化。


  • 9. 下一步

    通信层到此冻结:

    命令/ACK:完成
    seq/CRC/重试/去重:完成
    丢包/RTT 统计:完成
    tag:a-v0.2-bidirectional

    下一步是 ISO-TP:

    SF / FF / CF / FC
    BlockSize / STmin
    N_Cr 超时
    4 KB 分片/重组

    然后 UDS:

    0x10 / 0x3E / 0x22 / 0x19 / 0x14


    10. 仓库 / 数据

    • 仓库:https://github.com/longlin05/stm32-imx6ull-can-diag-ota
    • tag:a-v0.2-bidirectional
    • 1000 次压测日志:test/records/20261103-can-stress-1000.log
    • 50 ms 超时日志:test/records/20261103-can-timeout-50ms.log
    • 去重日志:test/records/20261103-can-dedup.log
    • 测试报告:docs/test-report.md
    • 下一篇:03 ISO-TP 多帧

    标签:STM32 CAN i.MX6ULL 嵌入式Linux ISO-TP

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » STM32 + i.MX6ULL CAN 双向命令/ACK 实战(02):50 ms 超时为何反而“假丢包”
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!