跳转到内容
新建笔记

振动分析仪控制协议审查:电量、增益与编码冲突

这是一份振动分析仪项目的电量与模拟前端控制协议审查。原文保留了 8 字节帧、串口参数、增益选项和 GPIO 名称,但数据1的位域定义与配置表互相矛盾。在取得实际固件或抓包前,不能把其中任一套编码当作已经确认的设备协议。

下文保留原始设计信息,把确定的数学关系、历史示例和待确认的硬件行为分开。末尾 Python 程序只验证帧封装和文本一致性,不连接设备,也没有更改现有固件协议。

原记录将 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 示例中的选择,并不是设备固有标识。

字节偏移内容约定
0帧头0xAA
1命令cmd
2~5数据1~4d1,d2,d3,d4
6累加和(cmd+d1+d2+d3+d4) & 0xFF
7帧尾0x55

校验不包含帧头和帧尾。它只能检测部分传输错误,不是 CRC,也不提供认证。数据中可以出现 AA 或 55;因为长度固定,不能简单遇到任意 55 就结束帧。

命令方向数据含义
01PC → STM32电量查询,4 字节填零
81STM32 → PC电量百分比、充电状态、0、0
02PC → STM32设置配置,数据1~4见下文
82STM32 → PC回显数据1、状态码、0、0
03PC → STM32配置查询,4 字节填零
83STM32 → PC返回当前数据1~4
F0原文定义为双向心跳,4 字节填零;回复规则待消除反射问题

响应码 = 请求码 | 0x80 适用于 01/02/03,不能把 F0 当作按此规则区分请求与响应的命令。

电量响应 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 / BAL2141IEPE_BAL_A
IEPE / A / VIB2242IEPE_VIB_A
IEPE / B / BAL3151IEPE_BAL_B
IEPE / B / VIB3252IEPE_VIB_B
DIF / A / BAL1121DIF_BAL_A
DIF / A / VIB1222DIF_VIB_A
DIF / B / BAL1931DIF_BAL_B
DIF / B / VIB1A32DIF_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→10VIB_A0=bit0,VIB_A1=bit1
数据3[1:0]BAL 前级:0→2.5、1→5、2→10、3→20INT_AMP_A0=bit0,INT_AMP_A1=bit1
数据4[1:0]BAL 后级:0→1、1→2.5、2→5、3→10END_A=bit0,END_B=bit1
数据470→5~200 Hz,1→0.5~10 HzINT_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) 静默替换成第一档,拼错传感器或模式也不能默认切到另一选项。

频段和增益均是本项目的历史设计参数,未附原理图或测量报告,不能据此保证实际模拟频响。特别是包含积分器时,不能把总传递函数仅写成前级增益乘后级增益:

H(jω)=Gpre Hint(jω) Gpost.H(j\omega)=G_{\rm pre}\,H_{\rm int}(j\omega)\,G_{\rm post}.

理想积分器的幅值随频率变化;实际电路还包含转折频率和限幅。前后级档位的乘积只能描述这两个放大环节的倍率,不等于整个积分通道的固定增益。

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 55AA 81 50 01 00 00 D2 55
原称 IEPE/A/VIB/5倍AA 02 22 02 00 00 26 55AA 82 22 00 00 00 A4 55
原称 DIF/B/BAL,低频段、前级20、后级5AA 02 19 00 03 82 A0 55AA 82 19 00 00 00 9B 55
查询上行历史配置AA 03 00 00 00 00 03 55AA 83 19 00 03 82 21 55
心跳历史格式AA F0 00 00 00 00 F0 55原文规定相同字节返回,存在下述歧义

校验正确只说明字节和一致,不证明传感器、模式或引脚映射正确。

原文建议每 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. 给协议绑定固件提交、上位机版本和硬件版本。
  2. 确认数据1编码与所有保留值的处理。
  3. 确认双通道与全局 MODE、增益引脚之间的约束。
  4. 消除心跳反射,定义超时、重试和应答匹配。
  5. 逐个记录八种通道组合和全部增益档位的联调结果,再更新为设备协议规范。