|
- #!/usr/bin/env python3
- """PLSR dual-AB 100 kHz hardware-counter and fast-gate stress test."""
-
- from __future__ import annotations
-
- import argparse
- import time
- from dataclasses import dataclass
-
- import serial
-
- from plsr_modbus_counter_stress_test import (
- AB_GATE_PERFORMANCE_BASE,
- AXIS_STATUS_WORDS,
- CALL_COMMIT,
- CALL_REQUEST,
- CALL_RESPONSE,
- CALL_START,
- CMD_SET_POSITION,
- CONTROL_BASE,
- CONTROL_WINDOW_WORDS,
- PERFORMANCE_BASE,
- PERFORMANCE_VERSION,
- RESULT_OK,
- RESULT_QUEUED,
- RtuClient,
- S0_BASES,
- S1_BASES,
- check_result,
- get_u32,
- put_u32,
- read_axis_status,
- send_command,
- wait_response,
- )
- from plsr_modbus_frequency_test import choose_port
-
-
- TEST_FREQUENCY_HZ = 100_000
- TEST_LOW_FREQUENCY_HZ = 50_000
- AB_OUTPUT_MODE = 1
- AB_S2_SET = 3
- AB_OWNERS = (0, 2)
- CMD_PAUSE = 3
- CMD_RESUME = 4
- STATE_ACCEL = 2
- STATE_RUN = 3
- STATE_DECEL = 4
- STATE_PAUSED = 6
- STATE_COMPLETED = 7
- RUNNING_STATES = {STATE_ACCEL, STATE_RUN, STATE_DECEL}
-
-
- @dataclass(frozen=True)
- class AbCase:
- name: str
- pulses_axis0: int
- pulses_axis2: int
- require_independent_stop: bool
- exercise_dynamic_pause: bool = False
-
-
- CASES = {
- "independent": AbCase(
- name="独立停止(Q0/Q1先停,Q2/Q3继续)",
- pulses_axis0=100_000,
- pulses_axis2=-200_000,
- require_independent_stop=True,
- ),
- "simultaneous": AbCase(
- name="等长双AB并发完成(相同目标周期数)",
- pulses_axis0=-200_000,
- pulses_axis2=200_000,
- require_independent_stop=False,
- ),
- "dynamic": AbCase(
- name="双AB变频与单组PAUSE/RESUME",
- pulses_axis0=300_000,
- pulses_axis2=-300_000,
- require_independent_stop=False,
- exercise_dynamic_pause=True,
- ),
- }
-
-
- def send_ab_call(
- client: RtuClient, sequence: int, axis: int, operation: int
- ) -> list[int]:
- request = [0] * 16
- put_u32(request, 0, sequence)
- request[2] = 0 # S0 device D
- put_u32(request, 3, S0_BASES[axis])
- request[5] = 0 # S1 device D
- put_u32(request, 6, S1_BASES[axis])
- request[8] = 0 # S2 constant
- put_u32(request, 10, AB_S2_SET)
- request[12] = axis
- request[13] = AB_OUTPUT_MODE
- request[14] = operation
- client.write_multiple(CALL_REQUEST, request)
- return wait_response(client, CALL_RESPONSE, 12, sequence)
-
-
- def wait_command_applied(
- client: RtuClient, axis: int, sequence: int, timeout: float = 3.0
- ) -> dict[str, int]:
- deadline = time.monotonic() + timeout
- latest: dict[str, int] | None = None
- while time.monotonic() < deadline:
- latest = read_axis_status(client, axis)
- if latest["last_sequence"] == sequence:
- if latest["last_result"] != RESULT_OK:
- raise RuntimeError(
- f"轴{axis}命令#{sequence}执行失败:{latest}"
- )
- return latest
- raise RuntimeError(f"等待轴{axis}命令#{sequence}执行超时:{latest}")
-
-
- def wait_axis_state(
- client: RtuClient,
- axis: int,
- expected: set[int],
- timeout: float = 4.0,
- ) -> dict[str, int]:
- deadline = time.monotonic() + timeout
- latest: dict[str, int] | None = None
- while time.monotonic() < deadline:
- latest = read_axis_status(client, axis)
- if latest["error"] != 0:
- raise RuntimeError(f"轴{axis}等待状态时进入错误:{latest}")
- if latest["state"] in expected:
- return latest
- raise RuntimeError(
- f"等待轴{axis}状态{sorted(expected)}超时,最后状态={latest}"
- )
-
-
- def write_live_frequency(client: RtuClient, frequency_hz: int) -> None:
- words = [0, 0]
- put_u32(words, 0, frequency_hz)
- for axis in AB_OWNERS:
- # One FC16 writes the complete signed INT32 target atomically.
- client.write_multiple(S0_BASES[axis] + 10, words)
-
-
- def wait_live_frequency(
- client: RtuClient, frequency_hz: int, timeout: float = 3.0
- ) -> dict[int, dict[str, int]]:
- deadline = time.monotonic() + timeout
- latest: dict[int, dict[str, int]] = {}
- while time.monotonic() < deadline:
- latest = {axis: read_axis_status(client, axis) for axis in AB_OWNERS}
- if all(
- status["state"] in RUNNING_STATES
- and status["target_frequency"] == frequency_hz
- and status["current_frequency"] == frequency_hz
- for status in latest.values()
- ):
- return latest
- raise RuntimeError(
- f"双AB未稳定到{frequency_hz}Hz,最后状态={latest}"
- )
-
-
- def exercise_dynamic_pause_resume(
- client: RtuClient, sequence: int
- ) -> int:
- write_live_frequency(client, TEST_LOW_FREQUENCY_HZ)
- slowed = wait_live_frequency(client, TEST_LOW_FREQUENCY_HZ)
- time.sleep(0.05)
- slowed_again = {
- axis: read_axis_status(client, axis) for axis in AB_OWNERS
- }
- if any(
- abs(slowed_again[axis]["task_pulses"])
- <= abs(slowed[axis]["task_pulses"])
- for axis in AB_OWNERS
- ):
- raise RuntimeError(f"双AB降频后计数未继续增长:{slowed_again}")
-
- write_live_frequency(client, TEST_FREQUENCY_HZ)
- wait_live_frequency(client, TEST_FREQUENCY_HZ)
-
- response = send_command(client, sequence, 0, CMD_PAUSE, 0)
- check_result(response, 4, RESULT_QUEUED, "轴0 AB PAUSE")
- wait_command_applied(client, 0, sequence)
- sequence += 1
- paused = wait_axis_state(client, 0, {STATE_PAUSED})
- other_before = read_axis_status(client, 2)
- time.sleep(0.05)
- paused_again = read_axis_status(client, 0)
- other_after = read_axis_status(client, 2)
- if (
- paused_again["state"] != STATE_PAUSED
- or paused_again["physical_pulses"] != paused["physical_pulses"]
- or abs(other_after["task_pulses"])
- <= abs(other_before["task_pulses"])
- ):
- raise RuntimeError(
- "AB PAUSE独立性失败:"
- f"paused={paused}, paused_again={paused_again}, "
- f"other_before={other_before}, other_after={other_after}"
- )
-
- response = send_command(client, sequence, 0, CMD_RESUME, 0)
- check_result(response, 4, RESULT_QUEUED, "轴0 AB RESUME")
- wait_command_applied(client, 0, sequence)
- sequence += 1
- resumed = wait_axis_state(client, 0, RUNNING_STATES)
- resume_deadline = time.monotonic() + 1.0
- while time.monotonic() < resume_deadline:
- resumed_again = read_axis_status(client, 0)
- if abs(resumed_again["task_pulses"]) > abs(resumed["task_pulses"]):
- break
- else:
- raise RuntimeError(f"AB RESUME后计数未恢复:{resumed_again}")
- print("动态控制已通过:双AB 100k→50k→100k,Q0/Q1在00边界暂停并恢复")
- return sequence
-
-
- def prepare_job(client: RtuClient, axis: int, signed_pulses: int) -> None:
- s0 = [0] * 20
- put_u32(s0, 0, 1)
- put_u32(s0, 10, TEST_FREQUENCY_HZ)
- put_u32(s0, 12, signed_pulses)
- client.write_multiple(S0_BASES[axis], s0)
- client.write_multiple(S1_BASES[axis], [0] * 4)
-
-
- def validate_final_status(
- axis: int,
- status: dict[str, int],
- signed_pulses: int,
- physical_baseline: int,
- ) -> None:
- expected_physical = abs(signed_pulses)
- checks = {
- "状态": (status["state"], STATE_COMPLETED),
- "错误": (status["error"], 0),
- "执行结果": (status["last_result"], RESULT_OK),
- "逻辑位置": (status["logical_position"], signed_pulses),
- "任务周期数": (status["task_pulses"], signed_pulses),
- "物理周期增量": (
- status["physical_pulses"] - physical_baseline,
- expected_physical,
- ),
- }
- bad = {name: value for name, value in checks.items() if value[0] != value[1]}
- if bad:
- raise RuntimeError(f"轴{axis} AB最终状态不正确:{bad};状态={status}")
-
-
- def run_case(
- client: RtuClient, case: AbCase, sequence: int
- ) -> tuple[int, int]:
- print(f"\n开始用例:{case.name}")
- signed_targets = {0: case.pulses_axis0, 2: case.pulses_axis2}
-
- for axis in AB_OWNERS:
- response = send_command(client, sequence, axis, CMD_SET_POSITION, 0)
- check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION")
- wait_command_applied(client, axis, sequence)
- sequence += 1
- prepare_job(client, axis, signed_targets[axis])
-
- for axis in AB_OWNERS:
- response = send_ab_call(client, sequence, axis, CALL_COMMIT)
- check_result(response, 3, RESULT_OK, f"轴{axis} AB COMMIT")
- if response[11] != 1:
- raise RuntimeError(f"轴{axis} AB COMMIT未建立有效快照")
- sequence += 1
-
- baselines = {
- axis: read_axis_status(client, axis)["physical_pulses"]
- for axis in AB_OWNERS
- }
- started = time.monotonic()
- for axis in AB_OWNERS:
- response = send_ab_call(client, sequence, axis, CALL_START)
- check_result(response, 3, RESULT_QUEUED, f"轴{axis} AB START")
- sequence += 1
-
- running: dict[int, dict[str, int]] = {}
- deadline = time.monotonic() + 4.0
- while time.monotonic() < deadline:
- running = {axis: read_axis_status(client, axis) for axis in AB_OWNERS}
- if all(item["state"] in RUNNING_STATES for item in running.values()):
- break
- else:
- raise RuntimeError(f"双AB未同时进入运行态:{running}")
- for axis, status in running.items():
- if status["counter_mode"] != 1:
- raise RuntimeError(f"轴{axis}未取得AB硬件计数器:{status}")
- if status["current_frequency"] != TEST_FREQUENCY_HZ:
- raise RuntimeError(f"轴{axis}未达到100kHz:{status}")
- print("运行期租约正确:Q0/Q1→TIM9,Q2/Q3→TIM12,双组均为硬件计数")
-
- if case.exercise_dynamic_pause:
- sequence = exercise_dynamic_pause_resume(client, sequence)
-
- deadline = time.monotonic() + 8.0
- latest = running
- independent_stop_seen = False
- while time.monotonic() < deadline:
- latest = {axis: read_axis_status(client, axis) for axis in AB_OWNERS}
- if (
- case.require_independent_stop
- and latest[0]["state"] == STATE_COMPLETED
- and latest[2]["state"] in RUNNING_STATES
- ):
- q2_before = abs(latest[2]["task_pulses"])
- q0_physical = latest[0]["physical_pulses"]
- time.sleep(0.05)
- stopped_again = read_axis_status(client, 0)
- running_again = read_axis_status(client, 2)
- independent_stop_seen = (
- stopped_again["state"] == STATE_COMPLETED
- and stopped_again["physical_pulses"] == q0_physical
- and running_again["state"] in RUNNING_STATES
- and abs(running_again["task_pulses"]) > q2_before
- )
- if independent_stop_seen:
- print("独立停止已观测:Q0/Q1保持低且计数冻结,Q2/Q3继续计数")
- if all(item["state"] == STATE_COMPLETED for item in latest.values()):
- break
- else:
- raise RuntimeError(f"等待双AB完成超时:{latest}")
- if case.require_independent_stop and not independent_stop_seen:
- raise RuntimeError("未观测到第一组停止后第二组继续运行的独立停止窗口")
-
- for axis in AB_OWNERS:
- validate_final_status(
- axis, latest[axis], signed_targets[axis], baselines[axis]
- )
- elapsed = time.monotonic() - started
- print(
- f"用例 PASS:Q0/Q1={abs(case.pulses_axis0)}完整AB周期,"
- f"Q2/Q3={abs(case.pulses_axis2)}完整AB周期,耗时{elapsed:.3f}s"
- )
- return sequence, int(elapsed * 1_000)
-
-
- def validate_dwt(client: RtuClient, header: list[int]) -> None:
- core_clock_hz = get_u32(header, 5)
- if core_clock_hz == 0 or header[7] != PERFORMANCE_VERSION:
- raise RuntimeError(
- f"P16诊断头无效:clock={core_clock_hz}, version={header[7]}"
- )
- performance = client.read_holding(PERFORMANCE_BASE, 8)
- performance_again = client.read_holding(PERFORMANCE_BASE, 8)
- if performance != performance_again:
- performance = performance_again
- ab_gate_cycles = get_u32(
- client.read_holding(AB_GATE_PERFORMANCE_BASE, 2), 0
- )
- values = {
- "PlsrProcess自身": get_u32(performance, 0),
- "PlsrProcess响应": get_u32(performance, 2),
- "TIM6控制ISR": get_u32(performance, 4),
- "输出定时器ISR": performance[6],
- "TIM9/12计数ISR": performance[7],
- "AB末周期快速门控": ab_gate_cycles,
- }
- budgets = {
- "PlsrProcess自身": core_clock_hz // 1_000,
- "TIM6控制ISR": core_clock_hz // 10_000,
- "输出定时器ISR": core_clock_hz // TEST_FREQUENCY_HZ,
- "TIM9/12计数ISR": core_clock_hz // TEST_FREQUENCY_HZ,
- # Gate must finish within one 100kHz quarter-period (2.5us).
- "AB末周期快速门控": core_clock_hz // 400_000,
- }
- print("\nP16/AB DWT最坏执行时间:")
- for name, cycles in values.items():
- budget = budgets.get(name)
- budget_text = f"预算<{budget}" if budget is not None else "观测项"
- print(
- f" {name:<18} {cycles:8d} cycles "
- f"{cycles * 1_000_000.0 / core_clock_hz:8.3f}us {budget_text}"
- )
- missing = [name for name, cycles in values.items() if cycles == 0]
- if missing:
- raise RuntimeError("DWT路径未实际执行:" + "、".join(missing))
- overruns = {
- name: (values[name], budget)
- for name, budget in budgets.items()
- if values[name] >= budget
- }
- if overruns:
- raise RuntimeError(f"AB实时预算超限:{overruns}")
-
-
- def print_logic_analyzer_acceptance(selected: list[str]) -> None:
- print("\n逻辑分析仪验收(CH0~CH3=Q0~Q3,建议100MS/s,四通道同步):")
- if selected == ["independent"]:
- print(" 上升沿:CH0=CH1=100000,CH2=CH3=200000。")
- elif selected == ["simultaneous"]:
- print(" 上升沿:CH0=CH1=CH2=CH3=200000。")
- elif selected == ["dynamic"]:
- print(" 上升沿:CH0=CH1=CH2=CH3=300000(含Q0/Q1暂停窗口)。")
- else:
- print(
- " 连续采全部用例时累计上升沿:CH0=CH1=600000,"
- "CH2=CH3=700000;精确逐用例验收建议分别用 --case 采集。"
- )
- print(
- " 100kHz区间周期10.000us/高宽5.000us;dynamic的50kHz区间"
- "周期20.000us/高宽10.000us;无<1us窄脉冲。"
- )
- print(" 正向:00→10→11→01→00;反向:00→01→11→10→00。")
- print(
- " 独立停止用例:Q0/Q1正向且先停,Q2/Q3反向并继续;"
- "等长用例方向相反。"
- )
- print(
- " dynamic用例:调频前后保持严格±90°且无额外边沿;Q0/Q1只在00"
- "边界进入低电平暂停,Q2/Q3连续运行,RESUME从00重建相序。"
- )
- print(
- " 每组首跳前必须为00;A/B首个上升沿相隔2.50us(建议容差±0.05us),"
- "不得近似同时上升。"
- )
- print(" 末周期必须完整回到00,停止后至少1ms全低、无残余边沿。")
-
-
- def main() -> int:
- parser = argparse.ArgumentParser(
- description="PLSR 双AB 100kHz硬件计数、独立停止与快速门控压力测试"
- )
- parser.add_argument("--port", help="串口,例如COM5;只有一个串口时可省略")
- parser.add_argument("--baud", type=int, default=9600)
- parser.add_argument("--slave", type=int, default=1)
- parser.add_argument(
- "--case",
- choices=("all", "independent", "simultaneous", "dynamic"),
- default="all",
- help="默认依次执行独立停止、等长并发、动态调频/暂停三个用例",
- )
- args = parser.parse_args()
-
- selected = (
- ["independent", "simultaneous", "dynamic"]
- if args.case == "all"
- else [args.case]
- )
- with serial.Serial(
- port=choose_port(args.port),
- baudrate=args.baud,
- bytesize=serial.EIGHTBITS,
- parity=serial.PARITY_EVEN,
- stopbits=serial.STOPBITS_ONE,
- timeout=1.0,
- write_timeout=1.0,
- ) as uart:
- client = RtuClient(uart, args.slave)
- header = client.read_holding(CONTROL_BASE, 8)
- if header[:5] != [
- 0x504C,
- 0x5352,
- 0x0100,
- CONTROL_WINDOW_WORDS,
- 0x0007,
- ]:
- raise RuntimeError(
- f"AB控制窗口未就绪:{header};请烧录当前固件并硬复位"
- )
- if header[7] != PERFORMANCE_VERSION:
- raise RuntimeError(
- f"AB测试要求性能统计V{PERFORMANCE_VERSION},当前V{header[7]}"
- )
- if AXIS_STATUS_WORDS != 48:
- raise RuntimeError("上位机轴状态结构版本不匹配")
- print("双AB测试就绪:K3,100kHz,Q0/Q1与Q2/Q3")
-
- sequence = max(1, (time.monotonic_ns() >> 20) & 0x7FFFFFFF)
- for index, name in enumerate(selected):
- sequence, _elapsed_ms = run_case(client, CASES[name], sequence)
- if index + 1 < len(selected):
- time.sleep(0.25)
-
- validate_dwt(client, header)
- print_logic_analyzer_acceptance(selected)
- print("\n全部自动检查PASS;最终结论仍需逻辑分析仪四通道波形通过。")
- return 0
-
-
- if __name__ == "__main__":
- try:
- raise SystemExit(main())
- except (RuntimeError, serial.SerialException) as error:
- print(f"测试失败:{error}")
- raise SystemExit(1)
|