#!/usr/bin/env python3 """PLSR P12 Modbus RTU live-frequency test. The firmware starts a long Q0 move from S0=D1000 and S1=D1100. This tool updates the current-segment frequency at D1010/D1011 with function 0x10, so the two 16-bit words are committed as one Modbus transaction. """ from __future__ import annotations import argparse import struct import time from dataclasses import dataclass import serial from serial.tools import list_ports S0_BASE = 1000 S1_BASE = 1100 LIVE_FREQUENCY_ADDRESS = S0_BASE + 10 SLAVE_DEFAULT = 1 def crc16(data: bytes) -> int: crc = 0xFFFF for byte in data: crc ^= byte for _ in range(8): crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1 return crc & 0xFFFF def add_crc(payload: bytes) -> bytes: crc = crc16(payload) return payload + bytes((crc & 0xFF, crc >> 8)) def signed_dword_words(value: int) -> list[int]: raw = value & 0xFFFFFFFF return [raw & 0xFFFF, (raw >> 16) & 0xFFFF] @dataclass class RtuClient: port: serial.Serial slave: int def exchange(self, request_pdu: bytes, response_size: int) -> bytes: request = add_crc(bytes((self.slave,)) + request_pdu) self.port.reset_input_buffer() self.port.write(request) self.port.flush() response = self.port.read(response_size) if len(response) != response_size: raise RuntimeError( f"响应超时:期望 {response_size} 字节,收到 {len(response)} 字节" ) if crc16(response[:-2]) != int.from_bytes(response[-2:], "little"): raise RuntimeError(f"响应 CRC 错误:{response.hex(' ')}") if response[0] != self.slave: raise RuntimeError(f"站号错误:收到 {response[0]},期望 {self.slave}") if response[1] & 0x80: raise RuntimeError( f"Modbus 异常:功能码 0x{response[1]:02X},异常码 0x{response[2]:02X}" ) return response def read_holding(self, address: int, quantity: int) -> list[int]: pdu = bytes((0x03,)) + struct.pack(">HH", address, quantity) response = self.exchange(pdu, 5 + quantity * 2) if response[1] != 0x03 or response[2] != quantity * 2: raise RuntimeError(f"0x03 响应格式错误:{response.hex(' ')}") return list(struct.unpack(f">{quantity}H", response[3:-2])) def write_multiple(self, address: int, values: list[int]) -> None: encoded = struct.pack(f">{len(values)}H", *values) pdu = ( bytes((0x10,)) + struct.pack(">HHB", address, len(values), len(encoded)) + encoded ) response = self.exchange(pdu, 8) expected = bytes((self.slave, 0x10)) + struct.pack(">HH", address, len(values)) if response[:6] != expected: raise RuntimeError(f"0x10 响应回显错误:{response.hex(' ')}") def write_dword(self, address: int, value: int) -> None: self.write_multiple(address, signed_dword_words(value)) words = self.read_holding(address, 2) if words != signed_dword_words(value): raise RuntimeError( f"D{address} 回读不一致:写入 {signed_dword_words(value)},回读 {words}" ) def choose_port(requested: str | None) -> str: if requested: return requested ports = [item.device for item in list_ports.comports()] if len(ports) == 1: print(f"自动选择串口 {ports[0]}") return ports[0] available = ", ".join(ports) if ports else "未发现串口" raise RuntimeError(f"请用 --port 指定串口。当前串口:{available}") def wait_until(deadline: float) -> None: remaining = deadline - time.monotonic() if remaining > 0: time.sleep(remaining) def main() -> int: parser = argparse.ArgumentParser(description="PLSR P12 Modbus 动态频率自动测试") parser.add_argument("--port", help="串口,例如 COM5;只有一个串口时可省略") parser.add_argument("--baud", type=int, default=9600) parser.add_argument("--slave", type=int, default=SLAVE_DEFAULT) args = parser.parse_args() port_name = choose_port(args.port) with serial.Serial( port=port_name, baudrate=args.baud, bytesize=serial.EIGHTBITS, parity=serial.PARITY_EVEN, stopbits=serial.STOPBITS_ONE, timeout=1.0, write_timeout=1.0, ) as uart: client = RtuClient(uart, args.slave) header = client.read_holding(S0_BASE, 20) s1 = client.read_holding(S1_BASE, 4) if header[0] != 1 or header[12:14] != signed_dword_words(100000): raise RuntimeError( "P12 数据未就绪:请确认已烧录当前固件并复位开发板" ) if any(s1): raise RuntimeError(f"S1 数据异常:{s1}") print("P12 已就绪:S0=D1000,S1=D1100,Q0 正在输出") print("所有 32 位频率均使用 0x10 一次写入两个寄存器。") schedule = [ (0.0, 1000, "初始目标"), (1.0, 4000, "升至 4000Hz"), (1.5, 500, "降至 500Hz"), (2.0, 0, "0 使用 S2 默认 1000Hz"), (2.2, 8000, "超过上限,固件应钳位到 5000Hz"), (2.7, -1, "非法值,固件应保持上一次安全目标"), (2.9, 2000, "恢复到 2000Hz"), ] started = time.monotonic() for offset, frequency, description in schedule: wait_until(started + offset) before = time.monotonic() client.write_dword(LIVE_FREQUENCY_ADDRESS, frequency) latency_ms = (time.monotonic() - before) * 1000.0 print( f"T+{time.monotonic() - started:6.3f}s " f"D1010={frequency:6d} {description} RTU往返={latency_ms:6.1f}ms" ) print("脚本测试完成。请按测试说明核对 Q0 波形与 IAR 状态变量。") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except (RuntimeError, serial.SerialException) as error: print(f"测试失败:{error}") raise SystemExit(1)