STM32 + i.MX6ULL CAN 双向命令/ACK 实战(02):50 ms 超时为何反而“假丢包” STM32 i.MX6ULL CAN 诊断与 OTA02双向命令/ACK以及 50 ms 超时为什么是“假丢包”系列目录从“CAN 能通”到“可诊断、可升级”上一篇01 旧 CAN 项目基线审计仓库stm32-imx6ull-can-diag-ota本文对应 taga-v0.2-bidirectionalAImate 原来只有 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 p9949.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、seq2.1 命令帧0x221/0x2228 字节固定偏移类型字段说明0uint8cmd命令码1uint8seq每条新命令 1重传复用同一 seq2…5uint8[4]args参数小端未用填 06…7uint16 LEcrc16CRC-16/CCITT-FALSE覆盖 0…5CRC 参数poly 0x1021 init 0xFFFF refin false refout false xorout 0例子0x222#10 00 00 00 00 00 94 14cmd0x10查询版本seq0x00后两字节是 CRC16。2.2 ACK 帧0x228ACK 也固定 8 字节但按 4 字节分片偏移类型字段说明0uint8cmd回显命令码1uint8seq回显 seq2uint8status0OK1未知命令2参数错误3不支持3uint8frag分片号从 0 开始4…7uint8[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 任务周期是#defineAPP_REPORT_TICK_MS100UReport_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 msmax 51.50 ms而超时是 50 ms。也就是说超时窗口刚好卡在 RTT p99 上大量命令会先超时再重发1006 次重试约等于每条命令多等一次 50 ms。这不是 CAN 链路丢包是超时设得太小。ack_rx为什么不是命令数早期日志里出现过cmd_tx3 ack_rx63 条命令6 个 ACK 分片版本 1 片 配置 2 片 统计 3 片 6 片所以ack_rx是分片数不是命令成功数。命令成功数看ok。5. 根因不是 CAN是调度粒度两个问题要分开现象根因50 ms 超时导致大量重试超时小于 RTT p99属于“假丢包”每条命令约 100 msSTM32 命令处理每 100 ms 才轮一次CAN 外设发一帧很快慢的是CAN RX FIFO → Report_Task 100 ms 醒来 → AppCmd_Task 解析 → 执行命令 → CAN_Send ACK所以优化方向不是“改 CAN 波特率”而是让 Linux 超时大于 RTT让 STM32 更频繁地处理命令或者用中断唤醒。6. 修复先调超时再调 tick6.1 超时改成 150 ms50 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 */#defineAPP_REPORT_TICK_MS20U周期帧仍按各自的时间判断不受影响只是命令处理更频繁。结果压测命令数 : 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 p99100 ms50 ms2006100699.67s49.85 ms100 ms150 ms1000098.66s102.86 ms20 ms150 ms1000020.04s22.06 ms最终选择APP_REPORT_TICK_MS 20 ACK timeout 150 msP99 22 ms满足后面 UDS0x22 50 ms的目标。7. 去重验证发两次同一个seq0x77的查询cansend can0222#107700000000C66Ecansend can0222#107700000000C66E再查两次统计cansend can0222#1278000000007F80cansend can0222#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_seq0x12 seq0x7815950300239900x780x12 seq0x7915980301240400x79变化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 统计完成 taga-v0.2-bidirectional下一步是 ISO-TPSF / FF / CF / FC BlockSize / STmin N_Cr 超时 4 KB 分片/重组然后 UDS0x10 / 0x3E / 0x22 / 0x19 / 0x1410. 仓库 / 数据仓库https://github.com/longlin05/stm32-imx6ull-can-diag-otataga-v0.2-bidirectional1000 次压测日志test/records/20261103-can-stress-1000.log50 ms 超时日志test/records/20261103-can-timeout-50ms.log去重日志test/records/20261103-can-dedup.log测试报告docs/test-report.md下一篇03 ISO-TP 多帧标签STM32CANi.MX6ULL嵌入式LinuxISO-TP