PCAN-USB 只能发送几百帧后停止:CloneCheck 门控的定位与修复
本文最后更新于16 天前,其中的信息可能已经过时,如有错误请留言评论。

在 STM32G474 自研 USB-CAN 设备上实现 PCAN-USB classic 兼容层后,遇到过一个很有迷惑性的故障:PCANBasic 的 CAN_Write() 连续返回成功,但 CAN 总线上只能看到几百帧,随后彻底停止发送。CAN 错误计数始终为 0,扩大缓冲区、关闭占用软件、降级驱动都没有解决。

本文记录完整的取证、驱动逆向、协议还原、固件修复和真机验证过程。结论是:问题不在 CAN 控制器和 USB 缓冲,而是固件没有响应 PCAN classic 驱动的 0x1E DEVICE_DATA challenge,驱动将设备标记为 CloneCheck=Error (2),随后在随机门限处主动停止提交 USB 收发请求。

说明:本文涉及的 PEAK VID/PID 与私有协议仅用于实验室兼容性验证,不应作为量产设备身份。商业产品应使用合法分配的 USB 身份和自有协议。

一、测试环境

  • MCU:STM32G474CBT6
  • 固件:DluCanFD,PCAN-USB classic 协议适配层
  • PCAN 驱动:4.6.4.16846;同时对比 5.1.2
  • 接收端:ZCAN EBLINK
  • CAN 波特率:500 kbit/s
  • 发送方式:PCANBasic,约 1 ms 一帧
  • 固件设备版本:bcdDevice=0x5602

二、故障现象

第一次物理重新插拔后,PCANBasic 连续调用 CAN_Write() 3000 次,所有调用都返回成功,但 ZCAN 实际只收到 800 帧,最后一帧出现在约 1.016 秒。双方 CAN 错误计数均为 0。

不重新插拔,立即再次发送 1000 帧,ZCAN 收到 0 帧。继续调用 CAN_Write(),直到第 32767 帧才返回 QXMTFULL (0x80)

场景 PCANBasic 写入 ZCAN 实收 总线错误
重新插拔后的首次测试 3000 次成功 800 0
不插拔立即复测 1000 次成功 0 0
持续写入 第 32767 帧出现 QXMTFULL 无新增 0

这组数据有两个关键特征:

  1. 停止点位于几百帧且每次可能不同,不像固定大小的 MCU 环形缓冲溢出。
  2. CAN_Write() 成功只代表报文进入驱动队列,不代表驱动已经向 USB 端点提交 URB。

三、排查过的错误方向

1. ZCANPRO 独占设备

关闭 ZCANPRO 并重新初始化 ZCAN 通道后,现象不变。独占确实会影响测试,但不是这次几百帧停止的根因。

2. CAN 总线、终端电阻和错误状态

两端的 TX/RX error counter、bus-off 状态均正常,已收到的 800 帧内容也完全正确,因此可以排除物理层持续错误和 CAN 控制器 bus-off。

3. USB/CAN 缓冲区不足

如果 MCU 发送缓冲或 USB 端点拥塞,通常会在固件侧看到队列满、端点写失败或稳定的容量边界。实际停止点具有随机性,而且主机队列最终积累到 32767 帧才报告 QXMTFULL,说明报文根本没有继续进入 USB 提交路径。

4. 降级 PCAN 驱动

将驱动降到 4.6.4.16846 后仍然复现。后续静态对比确认 4.6.4 和 5.1.2 都包含相同类型的发送门控,因此降级不能解决协议响应缺失。

四、真正根因:CloneCheck 进入发送门控

对 classic 驱动的发送路径反汇编后发现,TX 函数在提交 EP2 URB 前会调用一个检查函数。该函数读取设备对象中的 CloneCheck 状态;当状态为 Error (2) 时,会生成约 100..999 次的随机计数阈值。达到阈值后,驱动直接跳过 USB 提交。

具体函数偏移会随驱动版本变化。在本次分析的二进制中,TX 路径位于约 0x3049C,检查函数位于约 0x32300,状态字段位于设备对象 +0xA30。这些偏移只是定位证据,不应作为稳定接口使用。

这可以同时解释:

  • 为什么每次只能发送几百帧,而不是固定数量;
  • 为什么 CAN error counter 始终为 0;
  • 为什么第一次停止后,不重新枚举就连第一帧也不再下发;
  • 为什么 PCANBasic 仍能短时间返回成功,最终才因主机队列堆积报告 QXMTFULL。

继续追踪设备初始化流程发现,驱动会通过 EP1 命令通道执行 0x1E DEVICE_DATA challenge。原固件没有实现该命令:SET 被忽略,GET 返回全零,驱动因此把 CloneCheck 状态置为 2。

五、0x1E DEVICE_DATA 协议还原

PCAN classic 命令记录固定为 16 字节:

byte 0      function
byte 1      number:01=GET,02=SET
byte 2..15  args[14]

驱动首先发送两个 SET 记录,合并出 17 字节 challenge:

1E 02 00 <11 bytes>                         11 00
1E 02 01 <6 bytes + 5 bytes zero padding>   11 00

合并后:
byte 0      = 01
byte 1..16  = 16 字节随机 challenge

随后通过两个 GET 读取响应:

byte 0      = 1E
byte 1      = 01
byte 2      = selector
byte 3..13  = 分片数据
byte 14..15 = 总长度 17,小端

合并后的响应格式为:

byte 0      = key index,当前为 00
byte 1..16  = AES-128(challenge[1..16], fixed key)

AES 语义为 ECB 单块加密,无 padding、无 IV、无字节倒序。固定 key 字节不在本文公开;下面的已知向量用于验证轮变换、状态排列和端序:

challenge = 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
cipher    = 27 3B 87 FA 10 BB 8E 27 F7 F3 16 76 86 4F 4A E7

六、固件修复

修复保持在 Middleware/usbcan/pcan 协议适配层,没有修改通用 USB core,也没有扩大 CAN/USB 缓冲区。

1. challenge 分片状态机

  • 保存 17 字节 challenge 和两个分片的有效位。
  • 收到 selector 0 时先清除旧状态,再校验总长度为 17、marker 为 1,并复制前 11 字节。
  • selector 1 只有在 selector 0 有效后才接受,复制剩余 6 字节。
  • 初始化和 USB 协议 reset 都清除 challenge 状态,禁止复用旧响应。
  • challenge 不完整、selector 非法或长度错误时,GET 保持返回长度 0。

2. AES-128 软件实现

使用 256 字节 S-box、16 字节状态和 16 字节就地轮密钥扩展:

  • 初始 AddRoundKey;
  • 9 轮 SubBytes、ShiftRows、MixColumns、AddRoundKey;
  • 末轮省略 MixColumns;
  • 无动态内存,不依赖 STM32 AES 外设;
  • 单次加密主要栈占用约 32 字节。

这里没有额外拆分 C 文件。DEVICE_DATA 是 PCAN 协议私有行为,继续由原有适配器承担,避免给 USB core 增加协议耦合。

3. 测试脚本纠错

原真机脚本只判断“收到数量大于 0”,因此曾把 800/3000 错误标记为成功。修复后通过标准改为:

发送成功数 == 请求发送数
实际接收数 == 实际发送数
两个方向都满足时 overall_ok 才为 true

七、验证结果

静态与构建验证

  • 协议线格式测试:24/24 通过。
  • 测试脚本直接解析 C 源码中的 key 与 S-box,并用已知向量校验 AES 输出。
  • 两段 GET 响应逐字节验证。
  • Keil Arm Compiler 6.24:0 Error、0 Warning。
  • 构建尺寸:Code 58788 B,RO-data 3688 B。

真机验证

烧录后首次严格测试为 2999/3000。丢失的一帧位于接收 worker 启动窗口,但最后一帧仍持续到约 3.781 秒,已经明确越过原驱动门限。随后在同一次 USB 枚举下、不重新插拔,立即执行 5000 帧双向测试:

方向 发送 接收 结果
PCAN → ZCAN 5000 5000 通过
ZCAN → PCAN 5000 5000 通过

测试结束后,PCAN 状态为 0,ZCAN 双向 error counter 均为 0,overall_ok=true。更关键的是测试之间没有重新插拔:旧故障状态下第二次测试应为 0 帧,而修复后可以继续完整发送 5000 帧,说明 CloneCheck 门控已经解除。

八、烧录与升级注意事项

本工程采用 Bootloader 与 App 分区:

0x08000000 .. 0x08007FFF  Bootloader
0x08008000 .. 0x0801EFFF  App
0x0801F000 .. 0x0801F7FF  BootInfo
0x0801F800 .. 0x0801FFFF  Config
  • AppUpgrade.bin:供设备 USB Bootloader 升级,只包含 App。
  • 使用烧录器直接写 AppUpgrade.bin 时,目标地址必须是 0x08008000,并需要考虑 BootInfo/CRC 更新。
  • DluCanFD_full.bin:完整出厂镜像,烧到 0x08000000,包含 Bootloader、App 和已回填的 BootInfo。
  • Keil 下载 AXF/HEX 时,文件自身携带各 Load Region 地址。

更新 PCAN 兼容固件后建议物理重新插拔一次,让 Windows 销毁旧设备对象并重新执行 challenge。仅复位 CAN 通道不足以清除驱动已经缓存的 CloneCheck 错误状态。

九、经验总结

  1. API 返回成功不等于数据已经到达硬件。 主机驱动可能只完成入队,USB URB 是否提交必须单独取证。
  2. 随机的几百次门限更像策略状态,而不是容量边界。 缓冲问题往往具有稳定容量和可观测的满队列路径。
  3. 驱动降级不是协议缺失的修复。 多个版本保留相同 challenge 时,固件必须正确实现协议。
  4. 测试通过标准必须是数量守恒。 对转发和压力测试,只判断“收到过帧”会掩盖严重截断。
  5. 协议私有逻辑应留在协议适配层。 这次修复无需修改 USB core,也不需要扩大缓冲区。

最终根因链路可以概括为:

固件未实现 0x1E DEVICE_DATA
→ challenge GET 返回零
→ 驱动设置 CloneCheck=Error (2)
→ 随机 100..999 次后停止提交 USB URB
→ CAN_Write 继续入主机队列
→ 最终 QXMTFULL

这次问题真正困难的地方,不是 AES 实现本身,而是区分“应用 API 成功”“驱动入队成功”“USB 提交成功”和“CAN 总线实际发送成功”这四个不同层级。只有把证据逐层对齐,才能避免一直在缓冲区和物理层上兜圈。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇