25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

174 lines
6.1 KiB

  1. #!/usr/bin/env python3
  2. """PLSR P12 Modbus RTU live-frequency test.
  3. The firmware starts a long Q0 move from S0=D1000 and S1=D1100. This tool
  4. updates the current-segment frequency at D1010/D1011 with function 0x10, so
  5. the two 16-bit words are committed as one Modbus transaction.
  6. """
  7. from __future__ import annotations
  8. import argparse
  9. import struct
  10. import time
  11. from dataclasses import dataclass
  12. import serial
  13. from serial.tools import list_ports
  14. S0_BASE = 1000
  15. S1_BASE = 1100
  16. LIVE_FREQUENCY_ADDRESS = S0_BASE + 10
  17. SLAVE_DEFAULT = 1
  18. def crc16(data: bytes) -> int:
  19. crc = 0xFFFF
  20. for byte in data:
  21. crc ^= byte
  22. for _ in range(8):
  23. crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
  24. return crc & 0xFFFF
  25. def add_crc(payload: bytes) -> bytes:
  26. crc = crc16(payload)
  27. return payload + bytes((crc & 0xFF, crc >> 8))
  28. def signed_dword_words(value: int) -> list[int]:
  29. raw = value & 0xFFFFFFFF
  30. return [raw & 0xFFFF, (raw >> 16) & 0xFFFF]
  31. @dataclass
  32. class RtuClient:
  33. port: serial.Serial
  34. slave: int
  35. def exchange(self, request_pdu: bytes, response_size: int) -> bytes:
  36. request = add_crc(bytes((self.slave,)) + request_pdu)
  37. self.port.reset_input_buffer()
  38. self.port.write(request)
  39. self.port.flush()
  40. response = self.port.read(response_size)
  41. if len(response) != response_size:
  42. raise RuntimeError(
  43. f"响应超时:期望 {response_size} 字节,收到 {len(response)} 字节"
  44. )
  45. if crc16(response[:-2]) != int.from_bytes(response[-2:], "little"):
  46. raise RuntimeError(f"响应 CRC 错误:{response.hex(' ')}")
  47. if response[0] != self.slave:
  48. raise RuntimeError(f"站号错误:收到 {response[0]},期望 {self.slave}")
  49. if response[1] & 0x80:
  50. raise RuntimeError(
  51. f"Modbus 异常:功能码 0x{response[1]:02X},异常码 0x{response[2]:02X}"
  52. )
  53. return response
  54. def read_holding(self, address: int, quantity: int) -> list[int]:
  55. pdu = bytes((0x03,)) + struct.pack(">HH", address, quantity)
  56. response = self.exchange(pdu, 5 + quantity * 2)
  57. if response[1] != 0x03 or response[2] != quantity * 2:
  58. raise RuntimeError(f"0x03 响应格式错误:{response.hex(' ')}")
  59. return list(struct.unpack(f">{quantity}H", response[3:-2]))
  60. def write_multiple(self, address: int, values: list[int]) -> None:
  61. encoded = struct.pack(f">{len(values)}H", *values)
  62. pdu = (
  63. bytes((0x10,))
  64. + struct.pack(">HHB", address, len(values), len(encoded))
  65. + encoded
  66. )
  67. response = self.exchange(pdu, 8)
  68. expected = bytes((self.slave, 0x10)) + struct.pack(">HH", address, len(values))
  69. if response[:6] != expected:
  70. raise RuntimeError(f"0x10 响应回显错误:{response.hex(' ')}")
  71. def write_dword(self, address: int, value: int) -> None:
  72. self.write_multiple(address, signed_dword_words(value))
  73. words = self.read_holding(address, 2)
  74. if words != signed_dword_words(value):
  75. raise RuntimeError(
  76. f"D{address} 回读不一致:写入 {signed_dword_words(value)},回读 {words}"
  77. )
  78. def choose_port(requested: str | None) -> str:
  79. if requested:
  80. return requested
  81. ports = [item.device for item in list_ports.comports()]
  82. if len(ports) == 1:
  83. print(f"自动选择串口 {ports[0]}")
  84. return ports[0]
  85. available = ", ".join(ports) if ports else "未发现串口"
  86. raise RuntimeError(f"请用 --port 指定串口。当前串口:{available}")
  87. def wait_until(deadline: float) -> None:
  88. remaining = deadline - time.monotonic()
  89. if remaining > 0:
  90. time.sleep(remaining)
  91. def main() -> int:
  92. parser = argparse.ArgumentParser(description="PLSR P12 Modbus 动态频率自动测试")
  93. parser.add_argument("--port", help="串口,例如 COM5;只有一个串口时可省略")
  94. parser.add_argument("--baud", type=int, default=9600)
  95. parser.add_argument("--slave", type=int, default=SLAVE_DEFAULT)
  96. args = parser.parse_args()
  97. port_name = choose_port(args.port)
  98. with serial.Serial(
  99. port=port_name,
  100. baudrate=args.baud,
  101. bytesize=serial.EIGHTBITS,
  102. parity=serial.PARITY_EVEN,
  103. stopbits=serial.STOPBITS_ONE,
  104. timeout=1.0,
  105. write_timeout=1.0,
  106. ) as uart:
  107. client = RtuClient(uart, args.slave)
  108. header = client.read_holding(S0_BASE, 20)
  109. s1 = client.read_holding(S1_BASE, 4)
  110. if header[0] != 1 or header[12:14] != signed_dword_words(100000):
  111. raise RuntimeError(
  112. "P12 数据未就绪:请确认已烧录当前固件并复位开发板"
  113. )
  114. if any(s1):
  115. raise RuntimeError(f"S1 数据异常:{s1}")
  116. print("P12 已就绪:S0=D1000,S1=D1100,Q0 正在输出")
  117. print("所有 32 位频率均使用 0x10 一次写入两个寄存器。")
  118. schedule = [
  119. (0.0, 1000, "初始目标"),
  120. (1.0, 4000, "升至 4000Hz"),
  121. (1.5, 500, "降至 500Hz"),
  122. (2.0, 0, "0 使用 S2 默认 1000Hz"),
  123. (2.2, 8000, "超过上限,固件应钳位到 5000Hz"),
  124. (2.7, -1, "非法值,固件应保持上一次安全目标"),
  125. (2.9, 2000, "恢复到 2000Hz"),
  126. ]
  127. started = time.monotonic()
  128. for offset, frequency, description in schedule:
  129. wait_until(started + offset)
  130. before = time.monotonic()
  131. client.write_dword(LIVE_FREQUENCY_ADDRESS, frequency)
  132. latency_ms = (time.monotonic() - before) * 1000.0
  133. print(
  134. f"T+{time.monotonic() - started:6.3f}s "
  135. f"D1010={frequency:6d} {description} RTU往返={latency_ms:6.1f}ms"
  136. )
  137. print("脚本测试完成。请按测试说明核对 Q0 波形与 IAR 状态变量。")
  138. return 0
  139. if __name__ == "__main__":
  140. try:
  141. raise SystemExit(main())
  142. except (RuntimeError, serial.SerialException) as error:
  143. print(f"测试失败:{error}")
  144. raise SystemExit(1)