news 2026/11/5 3:59:18

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 + i.MX6ULL CAN 双向命令/ACK 实战(02):50 ms 超时为何反而“假丢包”

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 端没有实现。

这一篇做三件事:

  1. 把单向链路补成双向命令/ACK;
  2. 做 1000 次命令压测;
  3. 解释一个很容易误判的现象:把 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 字节固定:

偏移类型字段说明
0uint8cmd命令码
1uint8seq每条新命令 +1;重传复用同一 seq
2…5uint8[4]args参数,小端,未用填 0
6…7uint16 LEcrc16CRC-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 字节分片:

偏移类型字段说明
0uint8cmd回显命令码
1uint8seq回显 seq
2uint8status0=OK,1=未知命令,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_MS100U

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 msSTM32 命令处理每 100 ms 才轮一次

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

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

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

  1. 让 Linux 超时大于 RTT;
  2. 让 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 */#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 p99
100 ms50 ms2006100699.67s49.85 ms
100 ms150 ms1000098.66s102.86 ms
20 ms150 ms1000020.04s22.06 ms

最终选择:

APP_REPORT_TICK_MS = 20 ACK timeout = 150 ms

P99 22 ms,满足后面 UDS0x22 < 50 ms的目标。


7. 去重验证

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

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_seq
0x12 seq=0x7815950300239900x78
0x12 seq=0x7915980301240400x79

变化:

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. 踩坑清单

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

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

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

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

  5. 去重缓存要存“应答内容”,不是只存一个标志
    否则重传时查询结果(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 多帧

标签:STM32CANi.MX6ULL嵌入式LinuxISO-TP

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 3:59:04

物流成本分析三大指标:成本率、周转率、准时率怎么用?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 3:58:21

测试代码相关

<!-- 全局音频播放器&#xff0c;由父窗口统一管理 --><audio id"masterAudio" style"display:none;"></audio><script>// 提供给子页面调用&#xff1a;借用子页面的刷卡按键动作来激活父窗口的播放权限window.unlockParentAudio…

作者头像 李华
网站建设 2026/11/5 3:56:17

Eclipse 新手入门:从安装配置到代码调试全流程

摘要:本文面向 Java 初学者,以实战视角完整讲解 Eclipse 的下载、安装与核心配置流程。内容涵盖 JDK 环境检查、官方安装包获取、中文语言包配置、工作台界面解析、首个 Java 项目创建、代码编写规范、程序运行与断点调试、常见启动报错排查、透视图与工作区管理,以及新手避…

作者头像 李华
网站建设 2026/11/5 3:55:47

代扣业务资质要求高吗?哪些行业能做自动扣款

经常有企业老板问&#xff0c;代扣这件事是不是门槛特别高、是不是只有大公司才能做&#xff0c;其实这个问题要分两头看&#xff0c;资质要求确实比普通收款多一些&#xff0c;但远没有想象中那么高&#xff0c;关键看你属于哪个行业、业务规不规范。下面把哪些行业能做、每个…

作者头像 李华
网站建设 2026/11/5 3:52:47

真实项目拆解:低代码平台如何隔离流程设计与运行?

老炮淘金:13 个真实企业项目的架构高光 S03 基于一套企业真实工业互联网低代码平台源码实读 关键词:低代码 BPMN Flowable 设计态与运行态 流程引擎 多租户 👋 欢迎阅读 🏠个人主页: 知守观 📘我的专栏: 老炮淘金:13个真实企业项目的架构高光 💻当前内容:…

作者头像 李华