You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

665 lines
26 KiB

  1. #!/usr/bin/env python3
  2. """PLSR P16 Modbus 性能统计自动测试(对应规格:PLSR_MODBUS_PERFORMANCE_TEST.md)。
  3. 本脚本在四轴 100kHz 输出与 Modbus 持续轮询的并发压力下,测量并判定
  4. 规格中定义的各项性能指标,运行完成后自动读取 P16 DWT 统计并做预算检查:
  5. - 版本号一致性(任务要求的“32/64 位版本号一致性检查”):
  6. D1205~D1206 的 32 位 CPU 计时频率非零、D1207 性能统计版本为 V7、
  7. 控制窗口协议版本 0x0100;P16 主统计块(32 位周期数与 16 位饱和字段)
  8. 连续两次读取必须完全一致。
  9. - 单条命令响应时间:0x03/0x10 完整往返耗时,判定预算 = 波特率折算的
  10. 线上传输时间(8E1,每字符 11 位)+ 10ms 处理余量。
  11. - 连续状态读取吞吐:四轴状态块(48 字×4)持续轮询速率,要求不低于
  12. 波特率理论上限的 50%。
  13. - 重复序号幂等:运行期重发完全相同序号的命令,固件必须只回放既有应答、
  14. 不得重复执行(沿用 P13 控制测试的 PAUSE 重复序号用例)。
  15. - 并发读写压力模拟:四轴 100kHz 期间持续读取轴状态,并穿插重写相同的
  16. S0/S1 数据(幂等写入)与读取 P16 统计块,验证并发下计数精确。
  17. - P16 自动预算:PlsrProcess 自身 < 1ms、TIM6 控制 ISR < 0.1ms、输出/计数
  18. ISR < 10us,四项均须实际执行(数值不得为 0);墙钟响应单独显示,
  19. 超过 1ms 只给抢占提示、不判失败。
  20. 与规格/现有脚本的差异说明:
  21. - 规格“测试方法”一节引用 P14 脚本;本脚本独立实现相同的四轴 100kHz +
  22. 持续轮询场景(规格要求),并额外测量主机侧性能指标,满足规格
  23. “自动预算”与“记录到测试报告”的要求。
  24. - 规格要求“无毛刺、无残余输出”,该两项只能由逻辑分析仪验收,本脚本
  25. 按规格保留为人工核对提示,与 plsr_modbus_counter_stress_test.py 口径一致。
  26. - P16 主统计块读取比 P14 脚本更严格:必须连续两次读取一致(规格要求
  27. 统计快照一致性),三次尝试仍不一致即判定失败。
  28. """
  29. from __future__ import annotations
  30. import argparse
  31. import time
  32. import serial
  33. from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words
  34. CONTROL_BASE = 1200
  35. CONTROL_WINDOW_WORDS = 338
  36. CALL_REQUEST = CONTROL_BASE + 8
  37. CALL_RESPONSE = CONTROL_BASE + 24
  38. COMMAND_REQUEST = CONTROL_BASE + 40
  39. COMMAND_RESPONSE = CONTROL_BASE + 48
  40. AXIS_STATUS_BASE = CONTROL_BASE + 64
  41. AXIS_STATUS_WORDS = 48
  42. PERFORMANCE_BASE = CONTROL_BASE + 56 # D1256:P16 主统计块
  43. STAGE_PERFORMANCE_BASE = CONTROL_BASE + 256 # D1456:P16 分阶段统计块
  44. AB_GATE_PERFORMANCE_BASE = CONTROL_BASE + 268 # D1468:AB末周期快速门控
  45. PERFORMANCE_VERSION = 7
  46. STAGE_NAMES = (
  47. "脉冲合并/保护",
  48. "关键事件",
  49. "命令队列",
  50. "普通事件/方向提交",
  51. "HAL/路径/Profile",
  52. "HSD检查点",
  53. )
  54. STAGE_PERFORMANCE_WORDS = len(STAGE_NAMES) * 2
  55. S0_BASES = (1600, 1800, 2000, 2200)
  56. S1_BASES = (1700, 1900, 2100, 2300)
  57. TEST_FREQUENCY_HZ = 100_000
  58. TEST_PULSES = 200_000
  59. RESULT_OK = 0
  60. RESULT_QUEUED = 1
  61. STATE_ACCEL = 2
  62. STATE_RUN = 3
  63. STATE_PAUSED = 6
  64. STATE_COMPLETED = 7
  65. CALL_COMMIT = 1
  66. CALL_START = 2
  67. CMD_PAUSE = 3
  68. CMD_RESUME = 4
  69. CMD_SET_POSITION = 5
  70. BITS_PER_CHARACTER = 11 # Modbus RTU 8E1:每字符 11 位
  71. PROCESSING_BUDGET_MS = 10.0 # 单条命令响应预算中的处理余量(主机+从站开销)
  72. THROUGHPUT_MIN_RATIO = 0.5 # 实测吞吐不低于波特率理论上限的比例
  73. LATENCY_SAMPLES = 20 # 每种命令的响应时间采样次数
  74. RUN_DEADLINE_SECONDS = 15.0 # 四轴运行完成的等待上限
  75. METRICS: list[tuple[str, bool, str]] = []
  76. def put_u32(words: list[int], offset: int, value: int) -> None:
  77. words[offset : offset + 2] = signed_dword_words(value)
  78. def put_u64(words: list[int], offset: int, value: int) -> None:
  79. raw = value & 0xFFFFFFFFFFFFFFFF
  80. words[offset : offset + 4] = [
  81. (raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48)
  82. ]
  83. def get_u32(words: list[int], offset: int) -> int:
  84. return words[offset] | (words[offset + 1] << 16)
  85. def get_u64(words: list[int], offset: int, signed: bool = False) -> int:
  86. raw = sum(words[offset + index] << (16 * index) for index in range(4))
  87. if signed and raw & (1 << 63):
  88. return raw - (1 << 64)
  89. return raw
  90. def wait_response(
  91. client: RtuClient, address: int, words: int, sequence: int, timeout: float = 2.0
  92. ) -> list[int]:
  93. deadline = time.monotonic() + timeout
  94. while time.monotonic() < deadline:
  95. response = client.read_holding(address, words)
  96. if get_u32(response, 0) == sequence:
  97. return response
  98. raise RuntimeError(f"等待序号 {sequence} 的应答超时")
  99. def send_command(
  100. client: RtuClient, sequence: int, axis: int, opcode: int, argument: int = 0
  101. ) -> list[int]:
  102. request = [0] * 8
  103. put_u32(request, 0, sequence)
  104. request[2] = opcode
  105. request[3] = axis
  106. put_u64(request, 4, argument)
  107. client.write_multiple(COMMAND_REQUEST, request)
  108. return wait_response(client, COMMAND_RESPONSE, 8, sequence)
  109. def send_call(
  110. client: RtuClient,
  111. sequence: int,
  112. axis: int,
  113. operation: int,
  114. s2_set: int = 1,
  115. ) -> list[int]:
  116. request = [0] * 16
  117. put_u32(request, 0, sequence)
  118. request[2] = 0 # S0 device D
  119. put_u32(request, 3, S0_BASES[axis])
  120. request[5] = 0 # S1 device D
  121. put_u32(request, 6, S1_BASES[axis])
  122. request[8] = 0 # S2 constant
  123. put_u32(request, 10, s2_set)
  124. request[12] = axis
  125. request[13] = 0 # PULSE/DIR
  126. request[14] = operation
  127. client.write_multiple(CALL_REQUEST, request)
  128. return wait_response(client, CALL_RESPONSE, 12, sequence)
  129. def check_result(response: list[int], offset: int, expected: int, label: str) -> None:
  130. if response[offset] != expected:
  131. raise RuntimeError(
  132. f"{label} 返回 {response[offset]},期望 {expected};应答={response}"
  133. )
  134. def read_axis_status(client: RtuClient, axis: int) -> dict[str, int]:
  135. address = AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS
  136. words = client.read_holding(address, AXIS_STATUS_WORDS)
  137. generation_begin = get_u32(words, 0)
  138. generation_end = get_u32(words, 46)
  139. if generation_begin != generation_end or generation_begin & 1:
  140. raise RuntimeError(
  141. f"轴{axis}状态快照不一致:begin={generation_begin}, end={generation_end}"
  142. )
  143. return {
  144. "state": words[2],
  145. "flags": get_u32(words, 3),
  146. "error": words[6],
  147. "stop_reason": words[7],
  148. "last_result": words[8],
  149. "last_sequence": get_u32(words, 10),
  150. "logical_position": get_u64(words, 16, signed=True),
  151. "task_pulses": get_u64(words, 20, signed=True),
  152. "physical_pulses": get_u64(words, 28),
  153. "counter_mode": words[37],
  154. "current_frequency": get_u32(words, 38),
  155. "target_frequency": get_u32(words, 40),
  156. }
  157. def wait_status(
  158. client: RtuClient,
  159. axis: int,
  160. states: set[int],
  161. sequence: int | None = None,
  162. timeout: float = 5.0,
  163. ) -> dict[str, int]:
  164. deadline = time.monotonic() + timeout
  165. latest: dict[str, int] | None = None
  166. while time.monotonic() < deadline:
  167. latest = read_axis_status(client, axis)
  168. sequence_ok = sequence is None or latest["last_sequence"] == sequence
  169. if latest["state"] in states and sequence_ok:
  170. return latest
  171. raise RuntimeError(f"等待轴{axis}状态 {sorted(states)} 超时,最后状态:{latest}")
  172. def wait_running_output(
  173. client: RtuClient, axis: int, sequence: int, timeout: float = 5.0
  174. ) -> dict[str, int]:
  175. """等待真实脉冲恢复,不能只依据 ACCEL/RUN 状态标签。"""
  176. deadline = time.monotonic() + timeout
  177. latest: dict[str, int] | None = None
  178. while time.monotonic() < deadline:
  179. latest = read_axis_status(client, axis)
  180. pulse_active = (latest["flags"] & (1 << 1)) != 0
  181. if (
  182. latest["last_sequence"] == sequence
  183. and latest["state"] in {STATE_ACCEL, STATE_RUN}
  184. and pulse_active
  185. and latest["current_frequency"] > 0
  186. ):
  187. return latest
  188. raise RuntimeError(f"等待轴{axis}实际脉冲恢复超时,最后状态:{latest}")
  189. def print_process_checkpoint(client: RtuClient, label: str) -> None:
  190. words = client.read_holding(PERFORMANCE_BASE, 4)
  191. stage_words = client.read_holding(
  192. STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS
  193. )
  194. stages = ", ".join(
  195. f"{name}={get_u32(stage_words, index * 2)}"
  196. for index, name in enumerate(STAGE_NAMES)
  197. )
  198. print(
  199. f"P16阶段[{label}]:自身最大={get_u32(words, 0)} cycles,"
  200. f"响应最大={get_u32(words, 2)} cycles"
  201. )
  202. print(f" 分段最大:{stages}")
  203. def read_performance_block(client: RtuClient) -> list[int] | None:
  204. """读取 P16 主统计块;连续两次读取一致才算有效(快照一致性)。"""
  205. for _ in range(3):
  206. first = client.read_holding(PERFORMANCE_BASE, 8)
  207. second = client.read_holding(PERFORMANCE_BASE, 8)
  208. if first == second:
  209. return first
  210. return None
  211. def wire_time_ms(baud: int, request_bytes: int, response_bytes: int) -> float:
  212. """RTU 8E1 线上传输时间:每字符 11 位。"""
  213. return (request_bytes + response_bytes) * BITS_PER_CHARACTER * 1000.0 / baud
  214. def throughput_theory(baud: int) -> float:
  215. """四轴状态轮询(4×48字,请求 8B + 响应 101B)的波特率理论速率(轮/s)。"""
  216. bytes_per_poll = 4 * (8 + 101)
  217. return baud / (bytes_per_poll * BITS_PER_CHARACTER)
  218. def sample_round_trips(
  219. client: RtuClient,
  220. baud: int,
  221. actions: list[tuple[str, int, int, object]],
  222. samples: int,
  223. ) -> dict[str, tuple[float, float, float, float]]:
  224. """对每种命令采样往返耗时,返回 {名称: (最小, 中位, 最大, 预算) ms}。"""
  225. results = {}
  226. for name, request_bytes, response_bytes, action in actions:
  227. collected = []
  228. for _ in range(samples):
  229. started = time.perf_counter()
  230. action()
  231. collected.append((time.perf_counter() - started) * 1000.0)
  232. ordered = sorted(collected)
  233. results[name] = (
  234. ordered[0],
  235. ordered[len(ordered) // 2],
  236. ordered[-1],
  237. wire_time_ms(baud, request_bytes, response_bytes) + PROCESSING_BUDGET_MS,
  238. )
  239. return results
  240. def build_s0_job() -> list[int]:
  241. s0 = [0] * 20
  242. put_u32(s0, 0, 1)
  243. put_u32(s0, 10, TEST_FREQUENCY_HZ)
  244. put_u32(s0, 12, TEST_PULSES)
  245. return s0
  246. def prepare_jobs(client: RtuClient) -> None:
  247. for axis in range(4):
  248. client.write_multiple(S0_BASES[axis], build_s0_job())
  249. client.write_multiple(S1_BASES[axis], [0] * 4)
  250. def record_metric(name: str, passed: bool, detail: str) -> None:
  251. """记录一项指标并实时打印 通过/失败 + 实测值。"""
  252. METRICS.append((name, passed, detail))
  253. print(f" [{'通过' if passed else '失败'}] {name}:{detail}")
  254. def main() -> int:
  255. parser = argparse.ArgumentParser(
  256. description="PLSR P16 Modbus 性能统计自动测试"
  257. "(PLSR_MODBUS_PERFORMANCE_TEST.md)"
  258. )
  259. parser.add_argument("--port", help="串口,例如 COM5;只有一个串口时可省略")
  260. parser.add_argument("--baud", type=int, default=9600)
  261. parser.add_argument("--slave", type=int, default=1)
  262. parser.add_argument(
  263. "--polls",
  264. type=int,
  265. default=10,
  266. help="静态吞吐测试的完整四轴轮询轮数(默认 10)",
  267. )
  268. args = parser.parse_args()
  269. with serial.Serial(
  270. port=choose_port(args.port),
  271. baudrate=args.baud,
  272. bytesize=serial.EIGHTBITS,
  273. parity=serial.PARITY_EVEN,
  274. stopbits=serial.STOPBITS_ONE,
  275. timeout=1.0,
  276. write_timeout=1.0,
  277. ) as uart:
  278. client = RtuClient(uart, args.slave)
  279. header = client.read_holding(CONTROL_BASE, 8)
  280. if header[:5] != [
  281. 0x504C,
  282. 0x5352,
  283. 0x0100,
  284. CONTROL_WINDOW_WORDS,
  285. 0x0007,
  286. ]:
  287. raise RuntimeError(
  288. f"P16 控制窗口未就绪:{header};请烧录当前固件并复位"
  289. )
  290. print("P16 控制窗口就绪:D1200~D1537,协议 V1.0")
  291. # ---- 指标1:版本号一致性(32位CPU时钟 + 16位性能版本 + 统计块快照) ----
  292. core_clock_hz = get_u32(header, 5)
  293. performance_version = header[7]
  294. baseline_block = read_performance_block(client)
  295. version_ok = (
  296. core_clock_hz != 0
  297. and performance_version == PERFORMANCE_VERSION
  298. and baseline_block is not None
  299. )
  300. consistency_text = (
  301. "一致" if baseline_block is not None else "连续三次读取不一致"
  302. )
  303. record_metric(
  304. "版本号一致性",
  305. version_ok,
  306. f"协议版本={header[2]:#06x},32位CPU计时频率(D1205~D1206)="
  307. f"{core_clock_hz}Hz,性能统计版本(D1207)=V{performance_version},"
  308. f"P16统计块两次读取{consistency_text}",
  309. )
  310. if not version_ok:
  311. raise RuntimeError("版本号一致性检查失败,后续指标失去判定基准")
  312. print_process_checkpoint(client, "复位后基线")
  313. print("P16 已就绪:四轴 PULSE/DIR,100kHz,200000脉冲/轴")
  314. # ---- 指标2:单条命令响应时间 ----
  315. s0_job = build_s0_job()
  316. actions = [
  317. (
  318. "0x03读取控制头(8字)",
  319. 8,
  320. 21,
  321. lambda: client.read_holding(CONTROL_BASE, 8),
  322. ),
  323. (
  324. "0x10写入S0(20字)",
  325. 49,
  326. 8,
  327. lambda: client.write_multiple(S0_BASES[0], s0_job),
  328. ),
  329. (
  330. "0x10写入S1(4字)",
  331. 17,
  332. 8,
  333. lambda: client.write_multiple(S1_BASES[0], [0] * 4),
  334. ),
  335. ]
  336. latency = sample_round_trips(client, args.baud, actions, LATENCY_SAMPLES)
  337. latency_ok = True
  338. latency_parts = []
  339. for name, (minimum, median, maximum, budget) in latency.items():
  340. latency_ok = latency_ok and median <= budget
  341. latency_parts.append(
  342. f"{name} 中位{median:.1f}ms"
  343. f"(最小{minimum:.1f}/最大{maximum:.1f},预算{budget:.1f}ms)"
  344. )
  345. record_metric("单条命令响应时间", latency_ok, ";".join(latency_parts))
  346. # ---- 指标3:连续状态读取吞吐(静态轮询,无运行压力) ----
  347. static_started = time.monotonic()
  348. for _ in range(args.polls):
  349. for axis in range(4):
  350. read_axis_status(client, axis)
  351. static_elapsed = time.monotonic() - static_started
  352. static_polls = args.polls * 4
  353. static_rate = static_polls / static_elapsed
  354. theory = throughput_theory(args.baud)
  355. ratio = static_rate / theory
  356. record_metric(
  357. "连续状态读取吞吐",
  358. ratio >= THROUGHPUT_MIN_RATIO,
  359. f"静态四轴轮询 {static_polls} 次耗时 {static_elapsed:.3f}s,"
  360. f"速率 {static_rate:.2f} 轮/s,波特率理论上限 {theory:.2f} 轮/s"
  361. f"(占比 {ratio:.0%})",
  362. )
  363. prepare_jobs(client)
  364. sequence = 100
  365. for axis in range(4):
  366. response = send_command(
  367. client, sequence, axis, CMD_SET_POSITION, argument=0
  368. )
  369. check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION")
  370. sequence += 1
  371. print("四轴位置已清零,S0/S1 已用 0x10 原子写入")
  372. print_process_checkpoint(client, "位置清零")
  373. for axis in range(4):
  374. response = send_call(client, sequence, axis, CALL_COMMIT)
  375. check_result(response, 3, RESULT_OK, f"轴{axis} COMMIT")
  376. if response[11] != 1:
  377. raise RuntimeError(f"轴{axis} COMMIT 未建立有效快照")
  378. sequence += 1
  379. print("四轴 COMMIT 校验通过")
  380. print_process_checkpoint(client, "COMMIT")
  381. physical_baseline = [
  382. read_axis_status(client, axis)["physical_pulses"]
  383. for axis in range(4)
  384. ]
  385. started = time.monotonic()
  386. for axis in range(4):
  387. response = send_call(client, sequence, axis, CALL_START)
  388. check_result(response, 3, RESULT_QUEUED, f"轴{axis} START")
  389. sequence += 1
  390. print("四轴 START 已排队;持续读取状态以施加 Modbus/任务并发压力")
  391. print_process_checkpoint(client, "START")
  392. running_status = [read_axis_status(client, axis) for axis in range(4)]
  393. for axis, status in enumerate(running_status):
  394. if status["state"] not in {STATE_ACCEL, STATE_RUN}:
  395. raise RuntimeError(f"轴{axis} 未进入运行态:{status}")
  396. expected_mode = 1 if axis < 2 else 0
  397. if status["counter_mode"] != expected_mode:
  398. raise RuntimeError(
  399. f"轴{axis}计数模式={status['counter_mode']},期望={expected_mode}"
  400. )
  401. if status["current_frequency"] != TEST_FREQUENCY_HZ:
  402. raise RuntimeError(f"轴{axis}频率不正确:{status}")
  403. print("运行期计数租约正确:Q0/Q1=硬件,Q2/Q3=软件回退")
  404. print_process_checkpoint(client, "进入运行态")
  405. # ---- 指标4:重复序号幂等(轴0,运行期命令处理) ----
  406. pause_sequence = sequence
  407. response = send_command(client, pause_sequence, 0, CMD_PAUSE)
  408. check_result(response, 4, RESULT_QUEUED, f"轴0 PAUSE#{pause_sequence}")
  409. status = wait_status(client, 0, {STATE_PAUSED}, sequence=pause_sequence)
  410. # 重发完全相同的请求:必须只回放既有应答、不得重复执行
  411. replay = send_command(client, pause_sequence, 0, CMD_PAUSE)
  412. status_again = read_axis_status(client, 0)
  413. idem_ok = (
  414. replay == response
  415. and status_again["state"] == STATE_PAUSED
  416. and status_again["last_sequence"] == pause_sequence
  417. )
  418. record_metric(
  419. "重复序号幂等",
  420. idem_ok,
  421. f"重发 PAUSE#{pause_sequence}:应答与首次完全一致={replay == response},"
  422. f"状态保持暂停,last_sequence={status_again['last_sequence']}"
  423. f"(期望 {pause_sequence})",
  424. )
  425. sequence += 1
  426. response = send_command(client, sequence, 0, CMD_RESUME)
  427. check_result(response, 4, RESULT_QUEUED, f"轴0 RESUME#{sequence}")
  428. status = wait_running_output(client, 0, sequence=sequence)
  429. resumed_pulses = status["task_pulses"]
  430. time.sleep(0.5)
  431. status = read_axis_status(client, 0)
  432. if status["task_pulses"] <= resumed_pulses:
  433. raise RuntimeError(f"RESUME#{sequence} 后轴0脉冲计数未增长:{status}")
  434. print(
  435. f"轴0 PAUSE/RESUME 完成:恢复输出,当前 {status['current_frequency']}Hz"
  436. )
  437. print_process_checkpoint(client, "暂停/恢复")
  438. # ---- 指标5:并发读写压力模拟(运行期持续轮询 + 幂等写入) ----
  439. loop_started = time.monotonic()
  440. deadline = loop_started + RUN_DEADLINE_SECONDS
  441. polls = 0
  442. writes = 0
  443. final_status: list[dict[str, int]] = running_status
  444. while time.monotonic() < deadline:
  445. final_status = [read_axis_status(client, axis) for axis in range(4)]
  446. polls += 4
  447. # 写压力:重写与 prepare_jobs 完全相同的 S0/S1(幂等,
  448. # 不影响 COMMIT 后已建立的运行快照)
  449. client.write_multiple(S0_BASES[0], s0_job)
  450. client.write_multiple(S1_BASES[0], [0] * 4)
  451. # 并发下验证 P16 统计块可正常读取
  452. client.read_holding(PERFORMANCE_BASE, 8)
  453. writes += 2
  454. if all(item["state"] == STATE_COMPLETED for item in final_status):
  455. break
  456. else:
  457. raise RuntimeError(f"等待四轴完成超时,最后状态:{final_status}")
  458. run_elapsed = time.monotonic() - loop_started
  459. # 并发压力下四轴计数精确性核对
  460. count_errors: list[str] = []
  461. for axis, status in enumerate(final_status):
  462. expected = {
  463. "logical_position": TEST_PULSES,
  464. "task_pulses": TEST_PULSES,
  465. "error": 0,
  466. "last_result": RESULT_OK,
  467. }
  468. bad = {
  469. key: (status[key], value)
  470. for key, value in expected.items()
  471. if status[key] != value
  472. }
  473. physical_delta = status["physical_pulses"] - physical_baseline[axis]
  474. if physical_delta != TEST_PULSES:
  475. bad["physical_pulses_delta"] = (physical_delta, TEST_PULSES)
  476. if bad:
  477. count_errors.append(f"轴{axis}: {bad}")
  478. record_metric(
  479. "并发读写压力",
  480. not count_errors,
  481. f"四轴100kHz运行期间读取状态 {polls} 次、穿插幂等写入 {writes} 次,"
  482. f"轮询吞吐 {polls / run_elapsed:.2f} 次/s;四轴计数均精确为 "
  483. f"{TEST_PULSES}、error=0"
  484. + (f";异常:{';'.join(count_errors)}" if count_errors else ""),
  485. )
  486. elapsed = time.monotonic() - started
  487. print_process_checkpoint(client, "运行完成")
  488. # ---- 指标6:P16 统计块快照一致性(运行后,含基线单调性) ----
  489. block = read_performance_block(client)
  490. if block is None:
  491. record_metric(
  492. "P16统计块快照一致性", False, "三次读取未得到连续一致结果"
  493. )
  494. else:
  495. regressions = [
  496. f"D{PERFORMANCE_BASE + offset}: {baseline_block[offset]}→{block[offset]}"
  497. for offset in range(len(block))
  498. if block[offset] < baseline_block[offset]
  499. ]
  500. detail = "连续两次读取完全一致(32位周期数与16位饱和字段)"
  501. if regressions:
  502. detail += ";但相对复位后基线发生回退:" + "、".join(regressions)
  503. record_metric("P16统计块快照一致性", not regressions, detail)
  504. # ---- 指标7~10:P16 自动预算 ----
  505. stage_performance = client.read_holding(
  506. STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS
  507. )
  508. ab_gate_cycles = get_u32(
  509. client.read_holding(AB_GATE_PERFORMANCE_BASE, 2), 0
  510. )
  511. cycle_values: dict[str, int] = {}
  512. if block is not None:
  513. cycle_values = {
  514. "PlsrProcess自身": get_u32(block, 0),
  515. "PlsrProcess响应": get_u32(block, 2),
  516. "TIM6控制ISR": get_u32(block, 4),
  517. "输出定时器ISR": block[6],
  518. "TIM9/12计数ISR": block[7],
  519. }
  520. print("P16 DWT最坏执行时间:")
  521. for name, cycles in cycle_values.items():
  522. microseconds = cycles * 1_000_000.0 / core_clock_hz
  523. print(f" {name:<16} {cycles:8d} cycles {microseconds:8.3f}us")
  524. print("P16 PlsrProcess分段最大执行时间(各段独立峰值,来自不同轮次不能相加):")
  525. for index, name in enumerate(STAGE_NAMES):
  526. cycles = get_u32(stage_performance, index * 2)
  527. microseconds = cycles * 1_000_000.0 / core_clock_hz
  528. print(f" {name:<18} {cycles:8d} cycles {microseconds:8.3f}us")
  529. print(
  530. " AB末周期快速门控 "
  531. f"{ab_gate_cycles:8d} cycles "
  532. f"{ab_gate_cycles * 1_000_000.0 / core_clock_hz:8.3f}us "
  533. "(本PULSE/DIR用例未执行时允许为0)"
  534. )
  535. budgets = {
  536. "PlsrProcess自身": (core_clock_hz // 1_000, "1ms"),
  537. "TIM6控制ISR": (core_clock_hz // 10_000, "0.1ms"),
  538. "输出定时器ISR": (core_clock_hz // TEST_FREQUENCY_HZ, "10us"),
  539. "TIM9/12计数ISR": (core_clock_hz // TEST_FREQUENCY_HZ, "10us"),
  540. }
  541. for name, (budget, budget_text) in budgets.items():
  542. if block is None:
  543. record_metric(f"P16 {name}预算", False, "统计块不可用,无法判定")
  544. continue
  545. cycles = cycle_values[name]
  546. microseconds = cycles * 1_000_000.0 / core_clock_hz
  547. passed = cycles != 0 and cycles < budget
  548. detail = (
  549. f"实测 {cycles} cycles = {microseconds:.3f}us,"
  550. f"预算 < {budget} cycles({budget_text})"
  551. )
  552. if cycles == 0:
  553. detail += ";数值为 0,路径未被实际执行"
  554. record_metric(f"P16 {name}预算", passed, detail)
  555. # 墙钟响应与阶段峰值超过 1ms 时仅给提示(规格:不与自身 CPU 混算)
  556. if block is not None:
  557. response_cycles = cycle_values["PlsrProcess响应"]
  558. if response_cycles >= core_clock_hz // 1_000:
  559. print(
  560. "提示:PlsrProcess墙钟响应超过1ms,但自身CPU执行时间达标;"
  561. "差值来自高优先级PLSR定时器中断抢占。"
  562. )
  563. for index, name in enumerate(STAGE_NAMES):
  564. cycles = get_u32(stage_performance, index * 2)
  565. if cycles >= core_clock_hz // 1_000:
  566. print(
  567. f"提示:阶段“{name}”峰值超过1ms,需定位峰值路径;"
  568. "各段最大值来自不同轮次,不能直接相加。"
  569. )
  570. # ---- 汇总 ----
  571. print("\n性能测试指标汇总:")
  572. passed_count = sum(1 for _, passed, _ in METRICS if passed)
  573. for name, passed, detail in METRICS:
  574. print(f" [{'通过' if passed else '失败'}] {name}:{detail}")
  575. print(f"最终判定:{passed_count}/{len(METRICS)} 项通过")
  576. failed = [name for name, passed, _ in METRICS if not passed]
  577. if failed:
  578. raise RuntimeError("未通过指标:" + "、".join(failed))
  579. print(
  580. f"全部 PASS:四轴均为 {TEST_PULSES} 脉冲,耗时 {elapsed:.3f}s,"
  581. f"运行期状态读取 {polls} 次"
  582. )
  583. print("请再核对逻辑分析仪:Q0~Q3 各200000个上升沿、100kHz、无窄脉冲。")
  584. return 0
  585. if __name__ == "__main__":
  586. try:
  587. raise SystemExit(main())
  588. except (RuntimeError, serial.SerialException) as error:
  589. print(f"测试失败:{error}")
  590. raise SystemExit(1)