这是一份振动分析仪项目的电量与模拟前端控制协议审查。原文保留了 8 字节帧、串口参数、增益选项和 GPIO 名称,但数据1的位域定义与配置表互相矛盾。在取得实际固件或抓包前,不能把其中任一套编码当作已经确认的设备协议。
下文保留原始设计信息,把确定的数学关系、历史示例和待确认的硬件行为分开。末尾 Python 程序只验证帧封装和文本一致性,不连接设备,也没有更改现有固件协议。
1. 版本与通信条件
跳转到“1. 版本与通信条件”原记录将 v1.0 记为初版,v2.0 修订日期写为 2024-12,文章首次发布于 2025-12-17。没有附固件版本或提交号,不能据此判断服务器或设备当前正在使用哪一版。
| 项目 | 原记录中的约定 |
|---|---|
| 连接 | PC 经 CH340 USB 转串口连接 STM32 |
| 波特率 | 115200 bit/s |
| 串口格式 | 8 数据位、无校验、2 停止位,即 8N2 |
| 响应超时建议 | 500 ms |
| 重试建议 | 最多重试 3 次;是否包括首发必须由实现明确 |
USB 数据包边界不等于 UART 应用帧边界,程序应按字节流累积和解析。端口名 COM3 只是原 Windows 示例中的选择,并不是设备固有标识。
2. 8 字节帧结构
跳转到“2. 8 字节帧结构”| 字节偏移 | 内容 | 约定 |
|---|---|---|
| 0 | 帧头 | 0xAA |
| 1 | 命令 | cmd |
| 2~5 | 数据1~4 | d1,d2,d3,d4 |
| 6 | 累加和 | (cmd+d1+d2+d3+d4) & 0xFF |
| 7 | 帧尾 | 0x55 |
校验不包含帧头和帧尾。它只能检测部分传输错误,不是 CRC,也不提供认证。数据中可以出现 AA 或 55;因为长度固定,不能简单遇到任意 55 就结束帧。
| 命令 | 方向 | 数据含义 |
|---|---|---|
01 | PC → STM32 | 电量查询,4 字节填零 |
81 | STM32 → PC | 电量百分比、充电状态、0、0 |
02 | PC → STM32 | 设置配置,数据1~4见下文 |
82 | STM32 → PC | 回显数据1、状态码、0、0 |
03 | PC → STM32 | 配置查询,4 字节填零 |
83 | STM32 → PC | 返回当前数据1~4 |
F0 | 原文定义为双向 | 心跳,4 字节填零;回复规则待消除反射问题 |
响应码 = 请求码 | 0x80 适用于 01/02/03,不能把 F0 当作按此规则区分请求与响应的命令。
3. 电量与设置状态
跳转到“3. 电量与设置状态”电量响应 81 的数据1为 0~100;数据2为 0=放电、1=充电;数据3、4为零。解析器应拒绝超范围电量和未知状态,不把任何非 1 值都默认为放电。
设置响应 82 的数据2状态如下:
| 值 | 原定义 |
|---|---|
00 | 设置成功 |
01 | 传感器或工作模式参数错误 |
02 | 增益配置错误 |
03 | 硬件故障 |
FF | 未知错误 |
这些响应约定不证明硬件实际执行了设置。若需要确认,设置后还应查询 83 并比对全部配置字段。
4. 数据1:必须解决的编码冲突
跳转到“4. 数据1:必须解决的编码冲突”原文声明:传感器占 bit[7:5],001=DIF、010=IEPE;通道占 bit4,0=A、1=B;模式占 bit[3:0],0001=BAL、0010=VIB。因此按声明只能得到:
data1 = (sensor << 5) | (channel << 4) | mode但原表和 GPIO 分支用了另一组数值:
| 传感器/通道/模式 | 原表与原样例值 | 严格按声明位域推导 | 原 GPIO 名称 |
|---|---|---|---|
| IEPE / A / BAL | 21 | 41 | IEPE_BAL_A |
| IEPE / A / VIB | 22 | 42 | IEPE_VIB_A |
| IEPE / B / BAL | 31 | 51 | IEPE_BAL_B |
| IEPE / B / VIB | 32 | 52 | IEPE_VIB_B |
| DIF / A / BAL | 11 | 21 | DIF_BAL_A |
| DIF / A / VIB | 12 | 22 | DIF_VIB_A |
| DIF / B / BAL | 19 | 31 | DIF_BAL_B |
| DIF / B / VIB | 1A | 32 | DIF_VIB_B |
例如 0x22 >> 5 = 1,按位域代表 DIF,而原表称 IEPE;0x19 >> 5 = 0,按位域是保留传感器,而且低 4 位为 9,不是 BAL。原 Python 根据位域构造,原 STM32 解析也使用移位,但前面的映射表使用另一套值,二者不能直接互通。
这里不擅自给设备换码。 应取得正在运行的固件、上位机版本与至少一组已知配置的抓包,确认真实编码。如果固件采用查表编码,文档应明确写成枚举表;如果采用位域,应统一修正表格、样例和双方代码,并按兼容性要求发布协议新版本。
5. 数据2~4:保留原增益与频段约定
跳转到“5. 数据2~4:保留原增益与频段约定”| 字段 | 位 | 原定义 | 对应 GPIO |
|---|---|---|---|
| 数据2 | [1:0] | VIB 增益:0→0.5、1→1、2→5、3→10 | VIB_A0=bit0,VIB_A1=bit1 |
| 数据3 | [1:0] | BAL 前级:0→2.5、1→5、2→10、3→20 | INT_AMP_A0=bit0,INT_AMP_A1=bit1 |
| 数据4 | [1:0] | BAL 后级:0→1、1→2.5、2→5、3→10 | END_A=bit0,END_B=bit1 |
| 数据4 | 7 | 0→5~200 Hz,1→0.5~10 Hz | INT_FREQ_SEL |
数据2、3的高6位以及数据4的 bit[6:2] 保留填零。数据4由频段位与后级码组合,所以高频段四个值为 00/01/02/03,低频段四个值为 80/81/82/83。
原文约定 VIB 模式使用数据2,BAL 模式使用数据3、4,不适用字段建议填零;固件是否忽略还是拒绝非零保留字段需统一实现。无效增益不能像原 Python 示例那样通过 dict.get(...,0) 静默替换成第一档,拼错传感器或模式也不能默认切到另一选项。
频段和增益均是本项目的历史设计参数,未附原理图或测量报告,不能据此保证实际模拟频响。特别是包含积分器时,不能把总传递函数仅写成前级增益乘后级增益:
理想积分器的幅值随频率变化;实际电路还包含转折频率和限幅。前后级档位的乘积只能描述这两个放大环节的倍率,不等于整个积分通道的固定增益。
6. GPIO 提交需要完整状态
跳转到“6. GPIO 提交需要完整状态”原定义 MODE=1 为动平衡、MODE=0 为振动,每个通道的四个选择引脚分别对应 IEPE/DIF 与 BAL/VIB。应先验证整帧、解码为候选配置,再按硬件规定的顺序切换,最后更新软件状态并回复。
原伪代码仅演示 A 通道,没有实现 B 通道;它也没有真正检查全部增益和保留位,却可能返回成功。检查 A 通道四选一不能替代 B 通道检查,更不能在切换过程中留下两个互斥引脚同时有效。
只有一个 MODE 引脚时,两个通道是否能够采用不同模式需要原理图确认。不能一边宣称双通道独立,一边忽略全局模式或共享增益引脚的约束。实际采用先断后接、禁止采集期间切换还是其他过程,应由模拟电路与固件设计共同确定。
7. 原始帧样例及其适用范围
跳转到“7. 原始帧样例及其适用范围”以下保留原字节序列。累加校验已重算;配置相关帧只作为历史字节样例,语义仍受第4节冲突影响。
| 历史用途 | 请求 | 响应 |
|---|---|---|
| 电量查询,返回80%充电中 | AA 01 00 00 00 00 01 55 | AA 81 50 01 00 00 D2 55 |
| 原称 IEPE/A/VIB/5倍 | AA 02 22 02 00 00 26 55 | AA 82 22 00 00 00 A4 55 |
| 原称 DIF/B/BAL,低频段、前级20、后级5 | AA 02 19 00 03 82 A0 55 | AA 82 19 00 00 00 9B 55 |
| 查询上行历史配置 | AA 03 00 00 00 00 03 55 | AA 83 19 00 03 82 21 55 |
| 心跳历史格式 | AA F0 00 00 00 00 F0 55 | 原文规定相同字节返回,存在下述歧义 |
校验正确只说明字节和一致,不证明传感器、模式或引脚映射正确。
8. 心跳、超时和重试
跳转到“8. 心跳、超时和重试”原文建议每 5 秒心跳,双方收到 F0 就原样返回。若双方都无条件执行该规则,一个包会不断触发另一个包。可选方案包括固定 PC 发起、设备仅回应、PC 消费回应而不再回复;或者在新协议中增加请求/响应标志及序号。最终方案必须写进双方实现。
校验失败丢弃不响应时,上位机应使用事务总截止时间,不能每读到一点数据就无限重置超时。串口读一次 read(8) 可能因超时返回不足8字节,应保留残片继续解析;见 pySerial read/timeout 语义。
当前帧没有事务序号。建议一次只发送一个待应答命令,核对响应码和回显字段,并处理迟到帧;设置响应只回显数据1,无法单独证明增益对应本次请求,因此应进行配置回读。重试还需要命令幂等约定,不能默认所有未来命令都可重复执行。
9. 可运行的离线一致性检查
跳转到“9. 可运行的离线一致性检查”下面使用 Python 3.9 及以上的标准库,不需要串口设备。declared_data1() 明确只按文字位域推导,不应直接用于未确认协议的设备控制。
from dataclasses import dataclass
@dataclass(frozen=True)class Frame: command: int data: bytes
def encode(command: int, data: bytes = bytes(4)) -> bytes: if not isinstance(command, int) or not 0 <= command <= 255: raise ValueError("command must be a byte") if not isinstance(data, bytes) or len(data) != 4: raise ValueError("data must contain exactly four bytes") payload = bytes([command]) + data return b"\xaa" + payload + bytes([sum(payload) & 0xFF]) + b"\x55"
def decode(raw: bytes) -> Frame: if len(raw) != 8 or raw[0] != 0xAA or raw[7] != 0x55: raise ValueError("invalid frame boundaries") if sum(raw[1:6]) & 0xFF != raw[6]: raise ValueError("invalid checksum") return Frame(raw[1], raw[2:6])
class Parser: """固定 8 字节帧;坏帧后向后滑动,保留下一帧的候选帧头。""" def __init__(self): self.pending = bytearray()
def feed(self, chunk: bytes) -> list[Frame]: frames = [] for byte in chunk: self.pending.append(byte) while self.pending and self.pending[0] != 0xAA: del self.pending[0] if len(self.pending) == 8: try: frame = decode(bytes(self.pending)) except ValueError: del self.pending[0] while self.pending and self.pending[0] != 0xAA: del self.pending[0] else: frames.append(frame) self.pending.clear() return frames
def decode_battery(frame: Frame) -> tuple[int, bool]: if frame.command != 0x81 or len(frame.data) != 4: raise ValueError("not a battery response") level, charging, reserved1, reserved2 = frame.data if level > 100 or charging not in (0, 1) or reserved1 or reserved2: raise ValueError("invalid battery fields") return level, bool(charging)
def declared_data1(sensor: str, channel: str, mode: str) -> int: """仅推导文档声明的位域,不代表现有固件采用这种编码。""" sensors = {"DIF": 1, "IEPE": 2} channels = {"A": 0, "B": 1} modes = {"BAL": 1, "VIB": 2} if sensor not in sensors or channel not in channels or mode not in modes: raise ValueError("unknown configuration name") return (sensors[sensor] << 5) | (channels[channel] << 4) | modes[mode]
if __name__ == "__main__": request = encode(0x01) assert request.hex(" ") == "aa 01 00 00 00 00 01 55" battery = bytes.fromhex("aa 81 50 01 00 00 d2 55") assert decode_battery(decode(battery)) == (80, True) for split in range(len(battery) + 1): parser = Parser() result = parser.feed(battery[:split]) + parser.feed(battery[split:]) assert result == [decode(battery)] malformed = bytearray(battery) malformed[6] ^= 1 parser = Parser() result = parser.feed(b"garbage" + bytes(malformed) + battery + request) assert [frame.command for frame in result] == [0x81, 0x01] assert len(parser.pending) == 0 assert declared_data1("IEPE", "A", "VIB") == 0x42 assert declared_data1("DIF", "B", "BAL") == 0x31 assert encode(0x83, bytes.fromhex("19 00 03 82")).hex(" ") == "aa 83 19 00 03 82 21 55" try: decode_battery(Frame(0x81, bytes([101, 0, 0, 0]))) except ValueError: pass else: raise AssertionError("invalid battery percentage accepted") print("protocol framing checks passed; configuration encoding remains unconfirmed")已运行的检查覆盖全部拆包位置、连续帧、前导垃圾、坏校验后恢复、电量字段范围以及两个配置冲突。它没有验证线缆、CH340 驱动、MCU 固件或模拟增益。
10. 版本修订应补齐的证据
跳转到“10. 版本修订应补齐的证据”原 v1→v2 的意图保留为:删除单端传感器、增加 A/B 通道、补齐前后级和频段配置、由两个预留字段改为四个有效数据字段,以及补 GPIO 映射。真正发布前还需完成:
- 给协议绑定固件提交、上位机版本和硬件版本。
- 确认数据1编码与所有保留值的处理。
- 确认双通道与全局 MODE、增益引脚之间的约束。
- 消除心跳反射,定义超时、重试和应答匹配。
- 逐个记录八种通道组合和全部增益档位的联调结果,再更新为设备协议规范。