#!/usr/bin/env python3 """PLSR P14 four-axis 100 kHz hardware-counter stress test.""" from __future__ import annotations import argparse import time import serial from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words CONTROL_BASE = 1200 CONTROL_WINDOW_WORDS = 338 CALL_REQUEST = CONTROL_BASE + 8 CALL_RESPONSE = CONTROL_BASE + 24 COMMAND_REQUEST = CONTROL_BASE + 40 COMMAND_RESPONSE = CONTROL_BASE + 48 AXIS_STATUS_BASE = CONTROL_BASE + 64 AXIS_STATUS_WORDS = 48 PERFORMANCE_BASE = CONTROL_BASE + 56 STAGE_PERFORMANCE_BASE = CONTROL_BASE + 256 AB_GATE_PERFORMANCE_BASE = CONTROL_BASE + 268 PERFORMANCE_VERSION = 7 STAGE_NAMES = ( "脉冲合并/保护", "关键事件", "命令队列", "普通事件/方向提交", "HAL/路径/Profile", "HSD检查点", ) STAGE_PERFORMANCE_WORDS = len(STAGE_NAMES) * 2 S0_BASES = (1600, 1800, 2000, 2200) S1_BASES = (1700, 1900, 2100, 2300) TEST_FREQUENCY_HZ = 100_000 TEST_PULSES = 200_000 RESULT_OK = 0 RESULT_QUEUED = 1 STATE_ACCEL = 2 STATE_RUN = 3 STATE_COMPLETED = 7 CALL_COMMIT = 1 CALL_START = 2 CMD_SET_POSITION = 5 def put_u32(words: list[int], offset: int, value: int) -> None: words[offset : offset + 2] = signed_dword_words(value) def put_u64(words: list[int], offset: int, value: int) -> None: raw = value & 0xFFFFFFFFFFFFFFFF words[offset : offset + 4] = [ (raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48) ] def get_u32(words: list[int], offset: int) -> int: return words[offset] | (words[offset + 1] << 16) def get_u64(words: list[int], offset: int, signed: bool = False) -> int: raw = sum(words[offset + index] << (16 * index) for index in range(4)) if signed and raw & (1 << 63): return raw - (1 << 64) return raw def wait_response( client: RtuClient, address: int, words: int, sequence: int, timeout: float = 2.0 ) -> list[int]: deadline = time.monotonic() + timeout while time.monotonic() < deadline: response = client.read_holding(address, words) if get_u32(response, 0) == sequence: return response raise RuntimeError(f"等待序号 {sequence} 的应答超时") def send_command( client: RtuClient, sequence: int, axis: int, opcode: int, argument: int = 0 ) -> list[int]: request = [0] * 8 put_u32(request, 0, sequence) request[2] = opcode request[3] = axis put_u64(request, 4, argument) client.write_multiple(COMMAND_REQUEST, request) return wait_response(client, COMMAND_RESPONSE, 8, sequence) def send_call( client: RtuClient, sequence: int, axis: int, operation: int, s2_set: int = 1, ) -> 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, s2_set) request[12] = axis request[13] = 0 # PULSE/DIR request[14] = operation client.write_multiple(CALL_REQUEST, request) return wait_response(client, CALL_RESPONSE, 12, sequence) def check_result(response: list[int], offset: int, expected: int, label: str) -> None: if response[offset] != expected: raise RuntimeError( f"{label} 返回 {response[offset]},期望 {expected};应答={response}" ) def read_axis_status(client: RtuClient, axis: int) -> dict[str, int]: address = AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS words = client.read_holding(address, AXIS_STATUS_WORDS) generation_begin = get_u32(words, 0) generation_end = get_u32(words, 46) if generation_begin != generation_end or generation_begin & 1: raise RuntimeError( f"轴{axis}状态快照不一致:begin={generation_begin}, end={generation_end}" ) return { "state": words[2], "flags": get_u32(words, 3), "error": words[6], "stop_reason": words[7], "last_result": words[8], "last_sequence": get_u32(words, 10), "logical_position": get_u64(words, 16, signed=True), "task_pulses": get_u64(words, 20, signed=True), "physical_pulses": get_u64(words, 28), "counter_mode": words[37], "current_frequency": get_u32(words, 38), "target_frequency": get_u32(words, 40), } def print_process_checkpoint(client: RtuClient, label: str) -> None: words = client.read_holding(PERFORMANCE_BASE, 4) stage_words = client.read_holding( STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS ) stages = ", ".join( f"{name}={get_u32(stage_words, index * 2)}" for index, name in enumerate(STAGE_NAMES) ) print( f"P16阶段[{label}]:自身最大={get_u32(words, 0)} cycles," f"响应最大={get_u32(words, 2)} cycles" ) print(f" 分段最大:{stages}") def prepare_jobs(client: RtuClient) -> None: for axis in range(4): s0 = [0] * 20 put_u32(s0, 0, 1) put_u32(s0, 10, TEST_FREQUENCY_HZ) put_u32(s0, 12, TEST_PULSES) client.write_multiple(S0_BASES[axis], s0) client.write_multiple(S1_BASES[axis], [0] * 4) def main() -> int: parser = argparse.ArgumentParser( description="PLSR P14 四轴100kHz硬件计数与并发压力测试" ) parser.add_argument("--port", help="串口,例如 COM5;只有一个串口时可省略") parser.add_argument("--baud", type=int, default=9600) parser.add_argument("--slave", type=int, default=1) args = parser.parse_args() 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"P14控制窗口未就绪:{header};请烧录当前固件并复位" ) print("P14 已就绪:四轴 PULSE/DIR,100kHz,200000脉冲/轴") prepare_jobs(client) sequence = 100 for axis in range(4): response = send_command( client, sequence, axis, CMD_SET_POSITION, argument=0 ) check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION") sequence += 1 print("四轴位置已清零,S0/S1 已用 0x10 原子写入") print_process_checkpoint(client, "位置清零") for axis in range(4): response = send_call(client, sequence, axis, CALL_COMMIT) check_result(response, 3, RESULT_OK, f"轴{axis} COMMIT") if response[11] != 1: raise RuntimeError(f"轴{axis} COMMIT 未建立有效快照") sequence += 1 print("四轴 COMMIT 校验通过") print_process_checkpoint(client, "COMMIT") physical_baseline = [ read_axis_status(client, axis)["physical_pulses"] for axis in range(4) ] started = time.monotonic() for axis in range(4): response = send_call(client, sequence, axis, CALL_START) check_result(response, 3, RESULT_QUEUED, f"轴{axis} START") sequence += 1 print("四轴 START 已排队;持续读取状态以施加 Modbus/任务并发压力") print_process_checkpoint(client, "START") running_status = [read_axis_status(client, axis) for axis in range(4)] for axis, status in enumerate(running_status): if status["state"] not in {STATE_ACCEL, STATE_RUN}: raise RuntimeError(f"轴{axis} 未进入运行态:{status}") expected_mode = 1 if axis < 2 else 0 if status["counter_mode"] != expected_mode: raise RuntimeError( f"轴{axis}计数模式={status['counter_mode']},期望={expected_mode}" ) if status["current_frequency"] != TEST_FREQUENCY_HZ: raise RuntimeError(f"轴{axis}频率不正确:{status}") print("运行期计数租约正确:Q0/Q1=硬件,Q2/Q3=软件回退") print_process_checkpoint(client, "进入运行态") deadline = time.monotonic() + 10.0 polls = 0 final_status: list[dict[str, int]] = running_status while time.monotonic() < deadline: final_status = [read_axis_status(client, axis) for axis in range(4)] polls += 4 if all(item["state"] == STATE_COMPLETED for item in final_status): break else: raise RuntimeError(f"等待四轴完成超时,最后状态:{final_status}") for axis, status in enumerate(final_status): expected = { "logical_position": TEST_PULSES, "task_pulses": TEST_PULSES, "error": 0, "last_result": RESULT_OK, } bad = {key: (status[key], value) for key, value in expected.items() if status[key] != value} physical_delta = ( status["physical_pulses"] - physical_baseline[axis] ) if physical_delta != TEST_PULSES: bad["physical_pulses_delta"] = ( physical_delta, TEST_PULSES, ) if bad: raise RuntimeError(f"轴{axis}最终计数不正确:{bad};状态={status}") elapsed = time.monotonic() - started print_process_checkpoint(client, "运行完成") performance = client.read_holding(PERFORMANCE_BASE, 8) performance_again = client.read_holding(PERFORMANCE_BASE, 8) if performance != performance_again: performance = performance_again stage_performance = client.read_holding( STAGE_PERFORMANCE_BASE, STAGE_PERFORMANCE_WORDS ) ab_gate_cycles = get_u32( client.read_holding(AB_GATE_PERFORMANCE_BASE, 2), 0 ) core_clock_hz = get_u32(header, 5) performance_version = header[7] cycle_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], } if core_clock_hz == 0 or performance_version != PERFORMANCE_VERSION: raise RuntimeError( f"P16性能诊断头无效:clock={core_clock_hz}, " f"version={performance_version}" ) if any(value == 0 for value in cycle_values.values()): raise RuntimeError(f"P16性能计数未完整运行:{cycle_values}") 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, } overruns = { name: (cycles, budgets[name]) for name, cycles in cycle_values.items() if name in budgets if cycles >= budgets[name] } print("P16 DWT最坏执行时间:") for name, cycles in cycle_values.items(): microseconds = cycles * 1_000_000.0 / core_clock_hz budget_text = ( f"预算<{budgets[name]} cycles" if name in budgets else "观测项(含中断抢占)" ) print( f" {name:<16} {cycles:8d} cycles " f"{microseconds:8.3f}us {budget_text}" ) print("P16 PlsrProcess分段最大执行时间(各段独立峰值):") for index, name in enumerate(STAGE_NAMES): cycles = get_u32(stage_performance, index * 2) microseconds = cycles * 1_000_000.0 / core_clock_hz print(f" {name:<18} {cycles:8d} cycles {microseconds:8.3f}us") print( " AB末周期快速门控 " f"{ab_gate_cycles:8d} cycles " f"{ab_gate_cycles * 1_000_000.0 / core_clock_hz:8.3f}us " "(PULSE/DIR用例未执行时允许为0)" ) if overruns: raise RuntimeError(f"实时执行时间超过对应调度周期:{overruns}") response_cycles = cycle_values["PlsrProcess响应"] if response_cycles >= core_clock_hz // 1_000: print( "提示:PlsrProcess墙钟响应超过1ms,但自身CPU执行时间达标;" "差值来自高优先级PLSR定时器中断抢占。" ) print( f"全部 PASS:四轴均为 {TEST_PULSES} 脉冲," f"耗时 {elapsed:.3f}s,运行期状态读取 {polls} 次" ) print("请再核对逻辑分析仪:Q0~Q3各200000个上升沿、100kHz、无窄脉冲。") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except (RuntimeError, serial.SerialException) as error: print(f"测试失败:{error}") raise SystemExit(1)