车载CAN上云前,先管住串口帧失步
云端少了一条 CAN 消息,第一反应往往是查 4G 信号。但在 CAN 收发器与蜂窝模组之间,还有一段串口链路:它不长,却足以让一个错误的长度字节打乱后续解析。这里从电路边界入手,沿着一帧数据走到接收代码,再用可运行的回放工具验证恢复策略。
先认清三段链路
CANH/CANL 从 CN1 进入,经过 U6 收发器变成 3.3 V 逻辑信号;U5(AT32F425)处理 CAN 控制器,再通过 UART 将消息交给上层模组。调试时应分别看总线、U5 内部处理和 UART 字节流,不能把“MQTT 没收到”直接判成蜂窝网络故障。

图 1:U5 的 CAN1_TX/CAN1_RX 与 CAN_UART_TX/RX 是两组不同信号。串口帧校验发生在后者,不能拿它代替 CAN 控制器自己的错误计数。

图 2:U6 接到 CN1,D5 是总线侧的瞬态保护。图中存在保护器件,不等于已经完成汽车电源和 EMC 认证;保护能力仍要结合器件参数、布局与实际环境验证。
这块板还通过 K1 和 R19 切换 120 Ω 终端。终端电阻只应按总线拓扑配置:设备处于线缆中段时,再并上 120 Ω 可能使等效负载过重。上电后的继电器默认状态也要以实际触点和控制逻辑核对,不能只凭“可切换”三个字判断总线已经正确端接。

图 3:R19 为 120 Ω;Q3 驱动 K1,D6 给线圈提供续流通路。软件改变终端状态前,先确认这台设备是否位于总线端点。
供电也要分开看。图 4 的输入标注上限为 24 V,降压后得到 3.8 V,再为其他电路提供电源。这个标注是设计输入范围,不能推导出已通过汽车抛负载、反接或瞬态测试。本次只分析 UART 帧边界,不给出可直接接入整车的安全承诺。

真正容易失步的是长度字节
两端的外层串口格式为:
AA 55 | CMD | LEN | PAYLOAD (LEN bytes) | XOR
XOR 是从 AA 起到载荷最后一个字节逐字节异或。协议里八个命令的载荷长度是确定的:00/01 为 13 字节,02/03 为 1 字节,04/05 为 4 字节,06/07 为 3 字节。例如设置终端打开的帧为 AA 55 02 01 01 FD,前五个字节异或后得到 FD。
接收代码中有一处值得警惕:读到大于 64 的 LEN 后,直接把它改成 64,再按 64 字节去取数据和校验。这样虽然限制了写入量,却改变了线上帧的含义。假设噪声将长度变成 0x41:发送端后面若真有 65 字节,接收端会把第 65 字节当校验;若这是短帧被误读成超长帧,接收端会持续等待,甚至吞入下一帧的字节。两种情形都会拖慢重新对齐。仅凭静态代码不能断言设备在现场已经丢帧,但边界处理确实应改。
更稳妥的顺序是:先用命令约束长度,再接收载荷;不合理的头部立即丢弃,从下一个可能的 AA 55 重新搜索。 校验失败时也按同一策略恢复,而完整且合法的载荷即便含有 AA 55,也应作为载荷交付,不能误当新帧。
下面是主机端回放用的核心实现。它独立于设备 SDK,不会修改已烧录固件:
from dataclasses import dataclass
SYNC = b"\xaa\x55"
LENGTHS = {0: 13, 1: 13, 2: 1, 3: 1, 4: 4, 5: 4, 6: 3, 7: 3}
@dataclass(frozen=True)
class Frame:
command: int
payload: bytes
class Decoder:
def __init__(self):
self.pending = bytearray()
self.rejected = 0
def reset(self):
self.pending.clear()
def feed(self, chunk: bytes) -> list[Frame]:
frames = []
for value in chunk:
self.pending.append(value)
while self.pending:
start = self.pending.find(SYNC)
if start < 0:
self.pending[:] = b"\xaa" if self.pending[-1] == 0xaa else b""
break
if start:
del self.pending[:start]
if len(self.pending) < 4:
break
command, length = self.pending[2:4]
if length > 64 or LENGTHS.get(command) != length:
self.rejected += 1
del self.pending[0]
continue
size = 5 + length
if len(self.pending) < size:
break
check = 0
for byte in self.pending[:size - 1]:
check ^= byte
if check != self.pending[size - 1]:
self.rejected += 1
del self.pending[0]
continue
frames.append(Frame(command, bytes(self.pending[4:size - 1])))
del self.pending[:size]
return frames
这里按每个输入字节推进状态。坏头或坏校验只移走一个字节,后面的合法帧仍有机会被找到。已知命令的最大载荷仅 13 字节,因此待处理区保持有界;reset() 用于传输层确认丢字节或超时后丢弃半帧。这个版本只验证外层包格式,不检查 CAN ID、DLC、时间戳或业务权限;异或校验也不能抵抗刻意构造的数据。
怎样证明它至少能正确恢复
最小回放先喂入文档示例,再在坏长度后面紧接一个合法帧:
from can_frame import Decoder, Frame, encode
good = encode(0x02, b"\x01")
decoder = Decoder()
assert decoder.feed(bytes.fromhex("AA 55 01 41") + b"x" * 20 + good) == [
Frame(0x02, b"\x01")
]
assert decoder.rejected >= 1
在 Python 3.9+ 下,完整最小工程的 10 项标准库单元测试已通过:八个命令的每个拆包位置、连续帧、未知命令、超长长度、坏校验、载荷内伪帧头及超时重置都覆盖到了。这证明的是主机端字节流解析逻辑,不是 AT32F425 固件已编译通过,更不是实板 CAN 到 MQTT 全链路测试。移植到固件时,还需结合环形缓冲区并发、超时计时和 CAN 控制器错误状态重新验证。

图 5:顶面中下方可找到 CN1、K1 与 R19 的布局位置;U5 与 U6 位于右侧区域。讨论串口帧恢复时,先确认 CAN 硬件通路及端接位置,再追 UART 数据,排错顺序才不会颠倒。

图 6:底面按底层观察方向显示,文字镜像属于视图特征。顶底面截图只用于核对连接与回流路径;没有实测阻抗和 EMC 数据,不能据此断言车载环境可靠性。
这次改进的边界很明确:它把“不可信的长度字节”留在解析器外侧,并让坏帧之后的合法帧重新被看见。下一步若要做成可复现的设备版本,应先把同样的判定移植到两个固件接收端,再用真实串口故障注入、CAN 总线分析仪和断网重连场景逐项验证。仅有主机回放结果时,不应把“可恢复”写成“整机已经稳定运行”。
创作来源与复用说明:工程为立创开源广场「车载4G CANDTU」,作者/团队:eda_kwazdnpkc,项目网址:https://oshwhub.com/eda_kwazdnpkc/ml3074gcandtu,页面标注 GPL 3.0。图 1~6 从该项目 V1 嘉立创 EDA 编辑器重新取图,裁切局部和压缩,仅用于对应的技术分析;原理图和 PCB 未作电气修改。协议依据为项目附件《CAN2UART协议文档.pdf》,问题定位对照 V1 附件 F425_UART2CAN.7z 与 ML307R-SRC.7z。本文代码为独立编写的主机端回放工具,验证范围仅为上述 10 项单元测试;完整最小工程可从本页附件下载,原始附件可从项目页获取。项目页同时提示学习交流、研究和非商业使用;复用工程材料时请遵守原页面要求。
技术交流,欢迎关注微信公众号:美男子玩编程。





