Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

372 строки
13 KiB

  1. #!/usr/bin/env python3
  2. """PLSR P14 four-axis 100 kHz hardware-counter stress test."""
  3. from __future__ import annotations
  4. import argparse
  5. import time
  6. import serial
  7. from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words
  8. CONTROL_BASE = 1200
  9. CONTROL_WINDOW_WORDS = 338
  10. CALL_REQUEST = CONTROL_BASE + 8
  11. CALL_RESPONSE = CONTROL_BASE + 24
  12. COMMAND_REQUEST = CONTROL_BASE + 40
  13. COMMAND_RESPONSE = CONTROL_BASE + 48
  14. AXIS_STATUS_BASE = CONTROL_BASE + 64
  15. AXIS_STATUS_WORDS = 48
  16. PERFORMANCE_BASE = CONTROL_BASE + 56
  17. STAGE_PERFORMANCE_BASE = CONTROL_BASE + 256
  18. AB_GATE_PERFORMANCE_BASE = CONTROL_BASE + 268
  19. PERFORMANCE_VERSION = 7
  20. STAGE_NAMES = (
  21. "脉冲合并/保护",
  22. "关键事件",
  23. "命令队列",
  24. "普通事件/方向提交",
  25. "HAL/路径/Profile",
  26. "HSD检查点",
  27. )
  28. STAGE_PERFORMANCE_WORDS = len(STAGE_NAMES) * 2
  29. S0_BASES = (1600, 1800, 2000, 2200)
  30. S1_BASES = (1700, 1900, 2100, 2300)
  31. TEST_FREQUENCY_HZ = 100_000
  32. TEST_PULSES = 200_000
  33. RESULT_OK = 0
  34. RESULT_QUEUED = 1
  35. STATE_ACCEL = 2
  36. STATE_RUN = 3
  37. STATE_COMPLETED = 7
  38. CALL_COMMIT = 1
  39. CALL_START = 2
  40. CMD_SET_POSITION = 5
  41. def put_u32(words: list[int], offset: int, value: int) -> None:
  42. words[offset : offset + 2] = signed_dword_words(value)
  43. def put_u64(words: list[int], offset: int, value: int) -> None:
  44. raw = value & 0xFFFFFFFFFFFFFFFF
  45. words[offset : offset + 4] = [
  46. (raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48)
  47. ]
  48. def get_u32(words: list[int], offset: int) -> int:
  49. return words[offset] | (words[offset + 1] << 16)
  50. def get_u64(words: list[int], offset: int, signed: bool = False) -> int:
  51. raw = sum(words[offset + index] << (16 * index) for index in range(4))
  52. if signed and raw & (1 << 63):
  53. return raw - (1 << 64)
  54. return raw
  55. def wait_response(
  56. client: RtuClient, address: int, words: int, sequence: int, timeout: float = 2.0
  57. ) -> list[int]:
  58. deadline = time.monotonic() + timeout
  59. while time.monotonic() < deadline:
  60. response = client.read_holding(address, words)
  61. if get_u32(response, 0) == sequence:
  62. return response
  63. raise RuntimeError(f"等待序号 {sequence} 的应答超时")
  64. def send_command(
  65. client: RtuClient, sequence: int, axis: int, opcode: int, argument: int = 0
  66. ) -> list[int]:
  67. request = [0] * 8
  68. put_u32(request, 0, sequence)
  69. request[2] = opcode
  70. request[3] = axis
  71. put_u64(request, 4, argument)
  72. client.write_multiple(COMMAND_REQUEST, request)
  73. return wait_response(client, COMMAND_RESPONSE, 8, sequence)
  74. def send_call(
  75. client: RtuClient,
  76. sequence: int,
  77. axis: int,
  78. operation: int,
  79. s2_set: int = 1,
  80. ) -> list[int]:
  81. request = [0] * 16
  82. put_u32(request, 0, sequence)
  83. request[2] = 0 # S0 device D
  84. put_u32(request, 3, S0_BASES[axis])
  85. request[5] = 0 # S1 device D
  86. put_u32(request, 6, S1_BASES[axis])
  87. request[8] = 0 # S2 constant
  88. put_u32(request, 10, s2_set)
  89. request[12] = axis
  90. request[13] = 0 # PULSE/DIR
  91. request[14] = operation
  92. client.write_multiple(CALL_REQUEST, request)
  93. return wait_response(client, CALL_RESPONSE, 12, sequence)
  94. def check_result(response: list[int], offset: int, expected: int, label: str) -> None:
  95. if response[offset] != expected:
  96. raise RuntimeError(
  97. f"{label} 返回 {response[offset]},期望 {expected};应答={response}"
  98. )
  99. def read_axis_status(client: RtuClient, axis: int) -> dict[str, int]:
  100. address = AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS
  101. words = client.read_holding(address, AXIS_STATUS_WORDS)
  102. generation_begin = get_u32(words, 0)
  103. generation_end = get_u32(words, 46)
  104. if generation_begin != generation_end or generation_begin & 1:
  105. raise RuntimeError(
  106. f"轴{axis}状态快照不一致:begin={generation_begin}, end={generation_end}"
  107. )
  108. return {
  109. "state": words[2],
  110. "flags": get_u32(words, 3),
  111. "error": words[6],
  112. "stop_reason": words[7],
  113. "last_result": words[8],
  114. "last_sequence": get_u32(words, 10),
  115. "logical_position": get_u64(words, 16, signed=True),
  116. "task_pulses": get_u64(words, 20, signed=True),
  117. "physical_pulses": get_u64(words, 28),
  118. "counter_mode": words[37],
  119. "current_frequency": get_u32(words, 38),
  120. "target_frequency": get_u32(words, 40),
  121. }
  122. def print_process_checkpoint(client: RtuClient, label: str) -> None:
  123. words = client.read_holding(PERFORMANCE_BASE, 4)
  124. stage_words = client.read_holding(
  125. STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS
  126. )
  127. stages = ", ".join(
  128. f"{name}={get_u32(stage_words, index * 2)}"
  129. for index, name in enumerate(STAGE_NAMES)
  130. )
  131. print(
  132. f"P16阶段[{label}]:自身最大={get_u32(words, 0)} cycles,"
  133. f"响应最大={get_u32(words, 2)} cycles"
  134. )
  135. print(f" 分段最大:{stages}")
  136. def prepare_jobs(client: RtuClient) -> None:
  137. for axis in range(4):
  138. s0 = [0] * 20
  139. put_u32(s0, 0, 1)
  140. put_u32(s0, 10, TEST_FREQUENCY_HZ)
  141. put_u32(s0, 12, TEST_PULSES)
  142. client.write_multiple(S0_BASES[axis], s0)
  143. client.write_multiple(S1_BASES[axis], [0] * 4)
  144. def main() -> int:
  145. parser = argparse.ArgumentParser(
  146. description="PLSR P14 四轴100kHz硬件计数与并发压力测试"
  147. )
  148. parser.add_argument("--port", help="串口,例如 COM5;只有一个串口时可省略")
  149. parser.add_argument("--baud", type=int, default=9600)
  150. parser.add_argument("--slave", type=int, default=1)
  151. args = parser.parse_args()
  152. with serial.Serial(
  153. port=choose_port(args.port),
  154. baudrate=args.baud,
  155. bytesize=serial.EIGHTBITS,
  156. parity=serial.PARITY_EVEN,
  157. stopbits=serial.STOPBITS_ONE,
  158. timeout=1.0,
  159. write_timeout=1.0,
  160. ) as uart:
  161. client = RtuClient(uart, args.slave)
  162. header = client.read_holding(CONTROL_BASE, 8)
  163. if header[:5] != [
  164. 0x504C,
  165. 0x5352,
  166. 0x0100,
  167. CONTROL_WINDOW_WORDS,
  168. 0x0007,
  169. ]:
  170. raise RuntimeError(
  171. f"P14控制窗口未就绪:{header};请烧录当前固件并复位"
  172. )
  173. print("P14 已就绪:四轴 PULSE/DIR,100kHz,200000脉冲/轴")
  174. prepare_jobs(client)
  175. sequence = 100
  176. for axis in range(4):
  177. response = send_command(
  178. client, sequence, axis, CMD_SET_POSITION, argument=0
  179. )
  180. check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION")
  181. sequence += 1
  182. print("四轴位置已清零,S0/S1 已用 0x10 原子写入")
  183. print_process_checkpoint(client, "位置清零")
  184. for axis in range(4):
  185. response = send_call(client, sequence, axis, CALL_COMMIT)
  186. check_result(response, 3, RESULT_OK, f"轴{axis} COMMIT")
  187. if response[11] != 1:
  188. raise RuntimeError(f"轴{axis} COMMIT 未建立有效快照")
  189. sequence += 1
  190. print("四轴 COMMIT 校验通过")
  191. print_process_checkpoint(client, "COMMIT")
  192. physical_baseline = [
  193. read_axis_status(client, axis)["physical_pulses"]
  194. for axis in range(4)
  195. ]
  196. started = time.monotonic()
  197. for axis in range(4):
  198. response = send_call(client, sequence, axis, CALL_START)
  199. check_result(response, 3, RESULT_QUEUED, f"轴{axis} START")
  200. sequence += 1
  201. print("四轴 START 已排队;持续读取状态以施加 Modbus/任务并发压力")
  202. print_process_checkpoint(client, "START")
  203. running_status = [read_axis_status(client, axis) for axis in range(4)]
  204. for axis, status in enumerate(running_status):
  205. if status["state"] not in {STATE_ACCEL, STATE_RUN}:
  206. raise RuntimeError(f"轴{axis} 未进入运行态:{status}")
  207. expected_mode = 1 if axis < 2 else 0
  208. if status["counter_mode"] != expected_mode:
  209. raise RuntimeError(
  210. f"轴{axis}计数模式={status['counter_mode']},期望={expected_mode}"
  211. )
  212. if status["current_frequency"] != TEST_FREQUENCY_HZ:
  213. raise RuntimeError(f"轴{axis}频率不正确:{status}")
  214. print("运行期计数租约正确:Q0/Q1=硬件,Q2/Q3=软件回退")
  215. print_process_checkpoint(client, "进入运行态")
  216. deadline = time.monotonic() + 10.0
  217. polls = 0
  218. final_status: list[dict[str, int]] = running_status
  219. while time.monotonic() < deadline:
  220. final_status = [read_axis_status(client, axis) for axis in range(4)]
  221. polls += 4
  222. if all(item["state"] == STATE_COMPLETED for item in final_status):
  223. break
  224. else:
  225. raise RuntimeError(f"等待四轴完成超时,最后状态:{final_status}")
  226. for axis, status in enumerate(final_status):
  227. expected = {
  228. "logical_position": TEST_PULSES,
  229. "task_pulses": TEST_PULSES,
  230. "error": 0,
  231. "last_result": RESULT_OK,
  232. }
  233. bad = {key: (status[key], value) for key, value in expected.items()
  234. if status[key] != value}
  235. physical_delta = (
  236. status["physical_pulses"] - physical_baseline[axis]
  237. )
  238. if physical_delta != TEST_PULSES:
  239. bad["physical_pulses_delta"] = (
  240. physical_delta,
  241. TEST_PULSES,
  242. )
  243. if bad:
  244. raise RuntimeError(f"轴{axis}最终计数不正确:{bad};状态={status}")
  245. elapsed = time.monotonic() - started
  246. print_process_checkpoint(client, "运行完成")
  247. performance = client.read_holding(PERFORMANCE_BASE, 8)
  248. performance_again = client.read_holding(PERFORMANCE_BASE, 8)
  249. if performance != performance_again:
  250. performance = performance_again
  251. stage_performance = client.read_holding(
  252. STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS
  253. )
  254. ab_gate_cycles = get_u32(
  255. client.read_holding(AB_GATE_PERFORMANCE_BASE, 2), 0
  256. )
  257. core_clock_hz = get_u32(header, 5)
  258. performance_version = header[7]
  259. cycle_values = {
  260. "PlsrProcess自身": get_u32(performance, 0),
  261. "PlsrProcess响应": get_u32(performance, 2),
  262. "TIM6控制ISR": get_u32(performance, 4),
  263. "输出定时器ISR": performance[6],
  264. "TIM9/12计数ISR": performance[7],
  265. }
  266. if core_clock_hz == 0 or performance_version != PERFORMANCE_VERSION:
  267. raise RuntimeError(
  268. f"P16性能诊断头无效:clock={core_clock_hz}, "
  269. f"version={performance_version}"
  270. )
  271. if any(value == 0 for value in cycle_values.values()):
  272. raise RuntimeError(f"P16性能计数未完整运行:{cycle_values}")
  273. budgets = {
  274. "PlsrProcess自身": core_clock_hz // 1_000,
  275. "TIM6控制ISR": core_clock_hz // 10_000,
  276. "输出定时器ISR": core_clock_hz // TEST_FREQUENCY_HZ,
  277. "TIM9/12计数ISR": core_clock_hz // TEST_FREQUENCY_HZ,
  278. }
  279. overruns = {
  280. name: (cycles, budgets[name])
  281. for name, cycles in cycle_values.items()
  282. if name in budgets
  283. if cycles >= budgets[name]
  284. }
  285. print("P16 DWT最坏执行时间:")
  286. for name, cycles in cycle_values.items():
  287. microseconds = cycles * 1_000_000.0 / core_clock_hz
  288. budget_text = (
  289. f"预算<{budgets[name]} cycles"
  290. if name in budgets
  291. else "观测项(含中断抢占)"
  292. )
  293. print(
  294. f" {name:<16} {cycles:8d} cycles "
  295. f"{microseconds:8.3f}us {budget_text}"
  296. )
  297. print("P16 PlsrProcess分段最大执行时间(各段独立峰值):")
  298. for index, name in enumerate(STAGE_NAMES):
  299. cycles = get_u32(stage_performance, index * 2)
  300. microseconds = cycles * 1_000_000.0 / core_clock_hz
  301. print(f" {name:<18} {cycles:8d} cycles {microseconds:8.3f}us")
  302. print(
  303. " AB末周期快速门控 "
  304. f"{ab_gate_cycles:8d} cycles "
  305. f"{ab_gate_cycles * 1_000_000.0 / core_clock_hz:8.3f}us "
  306. "(PULSE/DIR用例未执行时允许为0)"
  307. )
  308. if overruns:
  309. raise RuntimeError(f"实时执行时间超过对应调度周期:{overruns}")
  310. response_cycles = cycle_values["PlsrProcess响应"]
  311. if response_cycles >= core_clock_hz // 1_000:
  312. print(
  313. "提示:PlsrProcess墙钟响应超过1ms,但自身CPU执行时间达标;"
  314. "差值来自高优先级PLSR定时器中断抢占。"
  315. )
  316. print(
  317. f"全部 PASS:四轴均为 {TEST_PULSES} 脉冲,"
  318. f"耗时 {elapsed:.3f}s,运行期状态读取 {polls} 次"
  319. )
  320. print("请再核对逻辑分析仪:Q0~Q3各200000个上升沿、100kHz、无窄脉冲。")
  321. return 0
  322. if __name__ == "__main__":
  323. try:
  324. raise SystemExit(main())
  325. except (RuntimeError, serial.SerialException) as error:
  326. print(f"测试失败:{error}")
  327. raise SystemExit(1)