您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

501 行
18 KiB

  1. #!/usr/bin/env python3
  2. """PLSR dual-AB 100 kHz hardware-counter and fast-gate stress test."""
  3. from __future__ import annotations
  4. import argparse
  5. import time
  6. from dataclasses import dataclass
  7. import serial
  8. from plsr_modbus_counter_stress_test import (
  9. AB_GATE_PERFORMANCE_BASE,
  10. AXIS_STATUS_WORDS,
  11. CALL_COMMIT,
  12. CALL_REQUEST,
  13. CALL_RESPONSE,
  14. CALL_START,
  15. CMD_SET_POSITION,
  16. CONTROL_BASE,
  17. CONTROL_WINDOW_WORDS,
  18. PERFORMANCE_BASE,
  19. PERFORMANCE_VERSION,
  20. RESULT_OK,
  21. RESULT_QUEUED,
  22. RtuClient,
  23. S0_BASES,
  24. S1_BASES,
  25. check_result,
  26. get_u32,
  27. put_u32,
  28. read_axis_status,
  29. send_command,
  30. wait_response,
  31. )
  32. from plsr_modbus_frequency_test import choose_port
  33. TEST_FREQUENCY_HZ = 100_000
  34. TEST_LOW_FREQUENCY_HZ = 50_000
  35. AB_OUTPUT_MODE = 1
  36. AB_S2_SET = 3
  37. AB_OWNERS = (0, 2)
  38. CMD_PAUSE = 3
  39. CMD_RESUME = 4
  40. STATE_ACCEL = 2
  41. STATE_RUN = 3
  42. STATE_DECEL = 4
  43. STATE_PAUSED = 6
  44. STATE_COMPLETED = 7
  45. RUNNING_STATES = {STATE_ACCEL, STATE_RUN, STATE_DECEL}
  46. @dataclass(frozen=True)
  47. class AbCase:
  48. name: str
  49. pulses_axis0: int
  50. pulses_axis2: int
  51. require_independent_stop: bool
  52. exercise_dynamic_pause: bool = False
  53. CASES = {
  54. "independent": AbCase(
  55. name="独立停止(Q0/Q1先停,Q2/Q3继续)",
  56. pulses_axis0=100_000,
  57. pulses_axis2=-200_000,
  58. require_independent_stop=True,
  59. ),
  60. "simultaneous": AbCase(
  61. name="等长双AB并发完成(相同目标周期数)",
  62. pulses_axis0=-200_000,
  63. pulses_axis2=200_000,
  64. require_independent_stop=False,
  65. ),
  66. "dynamic": AbCase(
  67. name="双AB变频与单组PAUSE/RESUME",
  68. # At 9600bps the two START transactions, consistent status reads and
  69. # two atomic frequency writes can consume most of a 300000-cycle
  70. # 100kHz job. Keep enough motion time for the complete dynamic
  71. # sequence instead of racing normal completion.
  72. pulses_axis0=600_000,
  73. pulses_axis2=-600_000,
  74. require_independent_stop=False,
  75. exercise_dynamic_pause=True,
  76. ),
  77. }
  78. def send_ab_call(
  79. client: RtuClient, sequence: int, axis: int, operation: int
  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, AB_S2_SET)
  89. request[12] = axis
  90. request[13] = AB_OUTPUT_MODE
  91. request[14] = operation
  92. client.write_multiple(CALL_REQUEST, request)
  93. return wait_response(client, CALL_RESPONSE, 12, sequence)
  94. def wait_command_applied(
  95. client: RtuClient, axis: int, sequence: int, timeout: float = 3.0
  96. ) -> dict[str, int]:
  97. deadline = time.monotonic() + timeout
  98. latest: dict[str, int] | None = None
  99. while time.monotonic() < deadline:
  100. latest = read_axis_status(client, axis)
  101. if latest["last_sequence"] == sequence:
  102. if latest["last_result"] != RESULT_OK:
  103. raise RuntimeError(
  104. f"轴{axis}命令#{sequence}执行失败:{latest}"
  105. )
  106. return latest
  107. raise RuntimeError(f"等待轴{axis}命令#{sequence}执行超时:{latest}")
  108. def wait_axis_state(
  109. client: RtuClient,
  110. axis: int,
  111. expected: set[int],
  112. timeout: float = 4.0,
  113. ) -> dict[str, int]:
  114. deadline = time.monotonic() + timeout
  115. latest: dict[str, int] | None = None
  116. while time.monotonic() < deadline:
  117. latest = read_axis_status(client, axis)
  118. if latest["error"] != 0:
  119. raise RuntimeError(f"轴{axis}等待状态时进入错误:{latest}")
  120. if latest["state"] in expected:
  121. return latest
  122. raise RuntimeError(
  123. f"等待轴{axis}状态{sorted(expected)}超时,最后状态={latest}"
  124. )
  125. def write_live_frequency(client: RtuClient, frequency_hz: int) -> None:
  126. words = [0, 0]
  127. put_u32(words, 0, frequency_hz)
  128. for axis in AB_OWNERS:
  129. # One FC16 writes the complete signed INT32 target atomically.
  130. client.write_multiple(S0_BASES[axis] + 10, words)
  131. def wait_live_frequency(
  132. client: RtuClient, frequency_hz: int, timeout: float = 3.0
  133. ) -> dict[int, dict[str, int]]:
  134. deadline = time.monotonic() + timeout
  135. latest: dict[int, dict[str, int]] = {}
  136. while time.monotonic() < deadline:
  137. latest = {axis: read_axis_status(client, axis) for axis in AB_OWNERS}
  138. if all(
  139. status["state"] in RUNNING_STATES
  140. and status["target_frequency"] == frequency_hz
  141. and status["current_frequency"] == frequency_hz
  142. for status in latest.values()
  143. ):
  144. return latest
  145. raise RuntimeError(
  146. f"双AB未稳定到{frequency_hz}Hz,最后状态={latest}"
  147. )
  148. def exercise_dynamic_pause_resume(
  149. client: RtuClient, sequence: int
  150. ) -> int:
  151. write_live_frequency(client, TEST_LOW_FREQUENCY_HZ)
  152. slowed = wait_live_frequency(client, TEST_LOW_FREQUENCY_HZ)
  153. time.sleep(0.05)
  154. slowed_again = {
  155. axis: read_axis_status(client, axis) for axis in AB_OWNERS
  156. }
  157. if any(
  158. abs(slowed_again[axis]["task_pulses"])
  159. <= abs(slowed[axis]["task_pulses"])
  160. for axis in AB_OWNERS
  161. ):
  162. raise RuntimeError(f"双AB降频后计数未继续增长:{slowed_again}")
  163. write_live_frequency(client, TEST_FREQUENCY_HZ)
  164. wait_live_frequency(client, TEST_FREQUENCY_HZ)
  165. response = send_command(client, sequence, 0, CMD_PAUSE, 0)
  166. check_result(response, 4, RESULT_QUEUED, "轴0 AB PAUSE")
  167. wait_command_applied(client, 0, sequence)
  168. sequence += 1
  169. paused = wait_axis_state(client, 0, {STATE_PAUSED})
  170. other_before = read_axis_status(client, 2)
  171. time.sleep(0.05)
  172. paused_again = read_axis_status(client, 0)
  173. other_after = read_axis_status(client, 2)
  174. if (
  175. paused_again["state"] != STATE_PAUSED
  176. or paused_again["physical_pulses"] != paused["physical_pulses"]
  177. or abs(other_after["task_pulses"])
  178. <= abs(other_before["task_pulses"])
  179. ):
  180. raise RuntimeError(
  181. "AB PAUSE独立性失败:"
  182. f"paused={paused}, paused_again={paused_again}, "
  183. f"other_before={other_before}, other_after={other_after}"
  184. )
  185. response = send_command(client, sequence, 0, CMD_RESUME, 0)
  186. check_result(response, 4, RESULT_QUEUED, "轴0 AB RESUME")
  187. wait_command_applied(client, 0, sequence)
  188. sequence += 1
  189. resumed = wait_axis_state(client, 0, RUNNING_STATES)
  190. resume_deadline = time.monotonic() + 1.0
  191. while time.monotonic() < resume_deadline:
  192. resumed_again = read_axis_status(client, 0)
  193. if abs(resumed_again["task_pulses"]) > abs(resumed["task_pulses"]):
  194. break
  195. else:
  196. raise RuntimeError(f"AB RESUME后计数未恢复:{resumed_again}")
  197. print("动态控制已通过:双AB 100k→50k→100k,Q0/Q1在00边界暂停并恢复")
  198. return sequence
  199. def prepare_job(client: RtuClient, axis: int, signed_pulses: int) -> None:
  200. s0 = [0] * 20
  201. put_u32(s0, 0, 1)
  202. put_u32(s0, 10, TEST_FREQUENCY_HZ)
  203. put_u32(s0, 12, signed_pulses)
  204. client.write_multiple(S0_BASES[axis], s0)
  205. client.write_multiple(S1_BASES[axis], [0] * 4)
  206. def validate_final_status(
  207. axis: int,
  208. status: dict[str, int],
  209. signed_pulses: int,
  210. physical_baseline: int,
  211. ) -> None:
  212. expected_physical = abs(signed_pulses)
  213. checks = {
  214. "状态": (status["state"], STATE_COMPLETED),
  215. "错误": (status["error"], 0),
  216. "执行结果": (status["last_result"], RESULT_OK),
  217. "逻辑位置": (status["logical_position"], signed_pulses),
  218. "任务周期数": (status["task_pulses"], signed_pulses),
  219. "物理周期增量": (
  220. status["physical_pulses"] - physical_baseline,
  221. expected_physical,
  222. ),
  223. }
  224. bad = {name: value for name, value in checks.items() if value[0] != value[1]}
  225. if bad:
  226. raise RuntimeError(f"轴{axis} AB最终状态不正确:{bad};状态={status}")
  227. def run_case(
  228. client: RtuClient, case: AbCase, sequence: int
  229. ) -> tuple[int, int]:
  230. print(f"\n开始用例:{case.name}")
  231. signed_targets = {0: case.pulses_axis0, 2: case.pulses_axis2}
  232. for axis in AB_OWNERS:
  233. response = send_command(client, sequence, axis, CMD_SET_POSITION, 0)
  234. check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION")
  235. wait_command_applied(client, axis, sequence)
  236. sequence += 1
  237. prepare_job(client, axis, signed_targets[axis])
  238. for axis in AB_OWNERS:
  239. response = send_ab_call(client, sequence, axis, CALL_COMMIT)
  240. check_result(response, 3, RESULT_OK, f"轴{axis} AB COMMIT")
  241. if response[11] != 1:
  242. raise RuntimeError(f"轴{axis} AB COMMIT未建立有效快照")
  243. sequence += 1
  244. baselines = {
  245. axis: read_axis_status(client, axis)["physical_pulses"]
  246. for axis in AB_OWNERS
  247. }
  248. started = time.monotonic()
  249. for axis in AB_OWNERS:
  250. response = send_ab_call(client, sequence, axis, CALL_START)
  251. check_result(response, 3, RESULT_QUEUED, f"轴{axis} AB START")
  252. sequence += 1
  253. running: dict[int, dict[str, int]] = {}
  254. deadline = time.monotonic() + 4.0
  255. while time.monotonic() < deadline:
  256. running = {axis: read_axis_status(client, axis) for axis in AB_OWNERS}
  257. if all(item["state"] in RUNNING_STATES for item in running.values()):
  258. break
  259. else:
  260. raise RuntimeError(f"双AB未同时进入运行态:{running}")
  261. for axis, status in running.items():
  262. if status["counter_mode"] != 1:
  263. raise RuntimeError(f"轴{axis}未取得AB硬件计数器:{status}")
  264. if status["current_frequency"] != TEST_FREQUENCY_HZ:
  265. raise RuntimeError(f"轴{axis}未达到100kHz:{status}")
  266. print("运行期租约正确:Q0/Q1→TIM9,Q2/Q3→TIM12,双组均为硬件计数")
  267. if case.exercise_dynamic_pause:
  268. sequence = exercise_dynamic_pause_resume(client, sequence)
  269. deadline = time.monotonic() + 8.0
  270. latest = running
  271. independent_stop_seen = False
  272. while time.monotonic() < deadline:
  273. latest = {axis: read_axis_status(client, axis) for axis in AB_OWNERS}
  274. if (
  275. case.require_independent_stop
  276. and not independent_stop_seen
  277. and latest[0]["state"] == STATE_COMPLETED
  278. and latest[2]["state"] in RUNNING_STATES
  279. ):
  280. q2_before = abs(latest[2]["task_pulses"])
  281. q0_physical = latest[0]["physical_pulses"]
  282. time.sleep(0.05)
  283. stopped_again = read_axis_status(client, 0)
  284. running_again = read_axis_status(client, 2)
  285. independent_stop_seen = (
  286. stopped_again["state"] == STATE_COMPLETED
  287. and stopped_again["physical_pulses"] == q0_physical
  288. and running_again["state"] in RUNNING_STATES
  289. and abs(running_again["task_pulses"]) > q2_before
  290. )
  291. if independent_stop_seen:
  292. print("独立停止已观测:Q0/Q1保持低且计数冻结,Q2/Q3继续计数")
  293. if all(item["state"] == STATE_COMPLETED for item in latest.values()):
  294. break
  295. else:
  296. raise RuntimeError(f"等待双AB完成超时:{latest}")
  297. if case.require_independent_stop and not independent_stop_seen:
  298. raise RuntimeError("未观测到第一组停止后第二组继续运行的独立停止窗口")
  299. for axis in AB_OWNERS:
  300. validate_final_status(
  301. axis, latest[axis], signed_targets[axis], baselines[axis]
  302. )
  303. elapsed = time.monotonic() - started
  304. print(
  305. f"用例 PASS:Q0/Q1={abs(case.pulses_axis0)}完整AB周期,"
  306. f"Q2/Q3={abs(case.pulses_axis2)}完整AB周期,耗时{elapsed:.3f}s"
  307. )
  308. return sequence, int(elapsed * 1_000)
  309. def validate_dwt(client: RtuClient, header: list[int]) -> None:
  310. core_clock_hz = get_u32(header, 5)
  311. if core_clock_hz == 0 or header[7] != PERFORMANCE_VERSION:
  312. raise RuntimeError(
  313. f"P16诊断头无效:clock={core_clock_hz}, version={header[7]}"
  314. )
  315. performance = client.read_holding(PERFORMANCE_BASE, 8)
  316. performance_again = client.read_holding(PERFORMANCE_BASE, 8)
  317. if performance != performance_again:
  318. performance = performance_again
  319. ab_gate_cycles = get_u32(
  320. client.read_holding(AB_GATE_PERFORMANCE_BASE, 2), 0
  321. )
  322. values = {
  323. "PlsrProcess自身": get_u32(performance, 0),
  324. "PlsrProcess响应": get_u32(performance, 2),
  325. "TIM6控制ISR": get_u32(performance, 4),
  326. "输出定时器ISR": performance[6],
  327. "TIM9/12计数ISR": performance[7],
  328. "AB末周期快速门控": ab_gate_cycles,
  329. }
  330. budgets = {
  331. "PlsrProcess自身": core_clock_hz // 1_000,
  332. "TIM6控制ISR": core_clock_hz // 10_000,
  333. "输出定时器ISR": core_clock_hz // TEST_FREQUENCY_HZ,
  334. "TIM9/12计数ISR": core_clock_hz // TEST_FREQUENCY_HZ,
  335. # Gate must finish within one 100kHz quarter-period (2.5us).
  336. "AB末周期快速门控": core_clock_hz // 400_000,
  337. }
  338. print("\nP16/AB DWT最坏执行时间:")
  339. for name, cycles in values.items():
  340. budget = budgets.get(name)
  341. budget_text = f"预算<{budget}" if budget is not None else "观测项"
  342. print(
  343. f" {name:<18} {cycles:8d} cycles "
  344. f"{cycles * 1_000_000.0 / core_clock_hz:8.3f}us {budget_text}"
  345. )
  346. missing = [name for name, cycles in values.items() if cycles == 0]
  347. if missing:
  348. raise RuntimeError("DWT路径未实际执行:" + "、".join(missing))
  349. overruns = {
  350. name: (values[name], budget)
  351. for name, budget in budgets.items()
  352. if values[name] >= budget
  353. }
  354. if overruns:
  355. raise RuntimeError(f"AB实时预算超限:{overruns}")
  356. def print_logic_analyzer_acceptance(selected: list[str]) -> None:
  357. print("\n逻辑分析仪验收(CH0~CH3=Q0~Q3,建议100MS/s,四通道同步):")
  358. if selected == ["independent"]:
  359. print(" 上升沿:CH0=CH1=100000,CH2=CH3=200000。")
  360. elif selected == ["simultaneous"]:
  361. print(" 上升沿:CH0=CH1=CH2=CH3=200000。")
  362. elif selected == ["dynamic"]:
  363. print(" 上升沿:CH0=CH1=CH2=CH3=600000(含Q0/Q1暂停窗口)。")
  364. else:
  365. print(
  366. " 连续采全部用例时累计上升沿:CH0=CH1=900000,"
  367. "CH2=CH3=1000000;精确逐用例验收建议分别用 --case 采集。"
  368. )
  369. print(
  370. " 100kHz区间周期10.000us/高宽5.000us;dynamic的50kHz区间"
  371. "周期20.000us/高宽10.000us;无<1us窄脉冲。"
  372. )
  373. print(" 正向:00→10→11→01→00;反向:00→01→11→10→00。")
  374. print(
  375. " 独立停止用例:Q0/Q1正向且先停,Q2/Q3反向并继续;"
  376. "等长用例方向相反。"
  377. )
  378. print(
  379. " dynamic用例:调频前后保持严格±90°且无额外边沿;Q0/Q1只在00"
  380. "边界进入低电平暂停,Q2/Q3连续运行,RESUME从00重建相序。"
  381. )
  382. print(
  383. " 每组首跳前必须为00;A/B首个上升沿相隔2.50us(建议容差±0.05us),"
  384. "不得近似同时上升。"
  385. )
  386. print(" 末周期必须完整回到00,停止后至少1ms全低、无残余边沿。")
  387. def main() -> int:
  388. parser = argparse.ArgumentParser(
  389. description="PLSR 双AB 100kHz硬件计数、独立停止与快速门控压力测试"
  390. )
  391. parser.add_argument("--port", help="串口,例如COM5;只有一个串口时可省略")
  392. parser.add_argument("--baud", type=int, default=9600)
  393. parser.add_argument("--slave", type=int, default=1)
  394. parser.add_argument(
  395. "--case",
  396. choices=("all", "independent", "simultaneous", "dynamic"),
  397. default="all",
  398. help="默认依次执行独立停止、等长并发、动态调频/暂停三个用例",
  399. )
  400. args = parser.parse_args()
  401. selected = (
  402. ["independent", "simultaneous", "dynamic"]
  403. if args.case == "all"
  404. else [args.case]
  405. )
  406. with serial.Serial(
  407. port=choose_port(args.port),
  408. baudrate=args.baud,
  409. bytesize=serial.EIGHTBITS,
  410. parity=serial.PARITY_EVEN,
  411. stopbits=serial.STOPBITS_ONE,
  412. timeout=1.0,
  413. write_timeout=1.0,
  414. ) as uart:
  415. client = RtuClient(uart, args.slave)
  416. header = client.read_holding(CONTROL_BASE, 8)
  417. if header[:5] != [
  418. 0x504C,
  419. 0x5352,
  420. 0x0100,
  421. CONTROL_WINDOW_WORDS,
  422. 0x0007,
  423. ]:
  424. raise RuntimeError(
  425. f"AB控制窗口未就绪:{header};请烧录当前固件并硬复位"
  426. )
  427. if header[7] != PERFORMANCE_VERSION:
  428. raise RuntimeError(
  429. f"AB测试要求性能统计V{PERFORMANCE_VERSION},当前V{header[7]}"
  430. )
  431. if AXIS_STATUS_WORDS != 48:
  432. raise RuntimeError("上位机轴状态结构版本不匹配")
  433. print("双AB测试就绪:K3,100kHz,Q0/Q1与Q2/Q3")
  434. sequence = max(1, (time.monotonic_ns() >> 20) & 0x7FFFFFFF)
  435. for index, name in enumerate(selected):
  436. sequence, _elapsed_ms = run_case(client, CASES[name], sequence)
  437. if index + 1 < len(selected):
  438. time.sleep(0.25)
  439. validate_dwt(client, header)
  440. print_logic_analyzer_acceptance(selected)
  441. print(
  442. "\n内部状态与实时预算自动检查PASS;这不代表端子波形通过,"
  443. "最终结论必须以逻辑分析仪四通道验收为准。"
  444. )
  445. return 0
  446. if __name__ == "__main__":
  447. try:
  448. raise SystemExit(main())
  449. except (RuntimeError, serial.SerialException) as error:
  450. print(f"测试失败:{error}")
  451. raise SystemExit(1)