Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

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