diff --git a/.gitignore b/.gitignore index 0b74749..45c820d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,11 @@ # Editor and operating-system files .vscode/ .idea/ +**/__pycache__/ +*.pyc +HostComputer/build/ +HostComputer/dist/ +HostComputer/long_stress_logs/ *.user *.suo *.tmp diff --git a/Core/Src/main.c b/Core/Src/main.c index 5c7b0a6..e83e175 100644 --- a/Core/Src/main.c +++ b/Core/Src/main.c @@ -26,6 +26,7 @@ #include "modbus_rtu_slave.h" #include "plc_device.h" #include "plsr_core.h" +#include "plsr_build_config.h" #include "plsr_modbus_control.h" #include "plsr_self_test.h" #include "stdio.h" @@ -38,12 +39,6 @@ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ -#define PLSR_BOARD_TEST_CW_CCW (9U) -#define PLSR_BOARD_TEST_FAST_REFRESH (10U) -#define PLSR_BOARD_TEST_DYNAMIC_FREQ (11U) -#define PLSR_BOARD_TEST_MODBUS_DATA (12U) -#define PLSR_BOARD_TEST_MODBUS_CONTROL (13U) -#define PLSR_BOARD_TEST_SELECT PLSR_BOARD_TEST_MODBUS_CONTROL #define PLSR_MODBUS_CONTROL_TEST_BASE (1200U) /* USER CODE END PD */ @@ -86,8 +81,12 @@ static void AppTaskStart(void *pArg) /* * F4作为Modbus RTU从站,触摸屏作为主站 * 初始化函数会立即启动USART1的DMA空闲接收 - */ - (void)ModbusSlaveInit(&huart1, MODBUS_SLAVE_DEFAULT_ADDRESS); + */ + if (ModbusSlaveInit(&huart1, MODBUS_SLAVE_DEFAULT_ADDRESS) != HAL_OK) + { + Error_Handler(); + } +#if PLSR_ENABLE_BOARD_SELF_TEST != 0U #if PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_MODBUS_DATA /* P12 must be queued after the Modbus register store is ready. */ if (PlsrModbusDataSelfTestQueue() != PLSR_RESULT_QUEUED) @@ -95,13 +94,29 @@ static void AppTaskStart(void *pArg) Error_Handler(); } #elif PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_MODBUS_CONTROL - if ((PlsrModbusControlSelfTestPrepare() != PLSR_RESULT_OK) - || (PlsrModbusControlInit(PLSR_MODBUS_CONTROL_TEST_BASE) - != PLSR_RESULT_OK)) + if (PlsrModbusControlSelfTestPrepare() != PLSR_RESULT_OK) + { + Error_Handler(); + } +#elif (PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_HW_COUNTER) \ + || (PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_DUAL_AB) + if (PlsrHardwareCounterSelfTestPrepare() != PLSR_RESULT_OK) + { + Error_Handler(); + } +#elif PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_LONG_STRESS + if (PlsrLongStressSelfTestPrepare() != PLSR_RESULT_OK) { Error_Handler(); } #endif +#endif + /* Production command/status service; never couple it to a board fixture. */ + if (PlsrModbusControlInit(PLSR_MODBUS_CONTROL_TEST_BASE) + != PLSR_RESULT_OK) + { + Error_Handler(); + } //ModbusRetainedRegistersLoad(); while (1) { @@ -181,7 +196,9 @@ int main(void) /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_DMA_Init(); +#if APP_ENABLE_USB_CDC != 0U MX_USB_DEVICE_Init(); +#endif MX_USART1_UART_Init(); if (BackupSramInit() != HAL_OK) { @@ -224,6 +241,7 @@ int main(void) } /* 上电自测统一延时1s,当前P9配置见下方调用(验证后关闭)。 */ +#if PLSR_ENABLE_BOARD_SELF_TEST != 0U HAL_Delay(1000U); #if PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_CW_CCW /* P9: Q0=CW, Q1=CCW; only one channel may pulse. */ @@ -234,6 +252,7 @@ int main(void) #elif PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_DYNAMIC_FREQ /* P11: automatic live-frequency schedule without Watch edits. */ (void)PlsrDynamicFrequencySelfTestQueue(); +#endif #endif //(void)PlsrDirectionLogicSelfTestQueue(); OSStart(); diff --git a/Document/PLSR_document/PLSR信捷对标追踪矩阵.md b/Document/PLSR_document/PLSR信捷对标追踪矩阵.md index 51d5019..d8f64aa 100644 --- a/Document/PLSR_document/PLSR信捷对标追踪矩阵.md +++ b/Document/PLSR_document/PLSR信捷对标追踪矩阵.md @@ -92,8 +92,8 @@ | 硬限位输入和常开/常闭 | 31、61等 | 输入点及极性可配置 | 已纳入 | | 正反向齿隙补偿 | 31、41~43 | V1.0实现 | 已纳入 | | 补偿加减速时间 | 41 | S2参数 | 已纳入 | -| FOLLOW性能参数 | 23~24、44 | 范围1~100 | 已纳入 | -| FOLLOW前馈补偿 | 23~24、44 | 范围0~100% | 已纳入 | +| FOLLOW性能参数 | 23~24、44 | 范围1~100 | 共用S2字段的存储/快照/校验已纳入;FOLLOW执行属于独立指令,不属于PLSR | +| FOLLOW前馈补偿 | 23~24、44 | 范围0~100% | 共用S2字段的存储/快照/校验已纳入;FOLLOW执行属于独立指令,不属于PLSR | | 0.1 ms/1 ms刷新 | 22~24、44 | S2配置,独立硬件控制周期 | 已纳入 | ## 6. 监控、事件和本项目增强 @@ -117,3 +117,20 @@ 2. `待实机确认`项目已通过信捷实机或厂家书面资料关闭。 3. 所有`已纳入`和`等效实现`项目具有测试用例和结果。 4. Y0~Y3的PLSR固定软元件地址不得作为差异项;差异仅限编程界面或明确记录的增强行为。 + +## 8. 当前实现与验证状态(2026-08-10) + +本节只记录实现证据;第1~6节的“已纳入/等效实现”仍不等同于代码或板测完成,严格关闭仍以 +第7节四项准则为准。 + +| 范围 | 当前证据 | 状态 | +|---|---|---| +| 四轴PULSE/DIR与计数 | P14真机100kHz,Q0~Q3逻辑分析仪各严格200000个上升沿,无窄脉冲、启动毛刺和停止后残余 | 已板测关闭 | +| 实时预算 | P16 `PlsrProcess`自身53748 cycles(319.929us)<168000,响应85655 cycles;输出和计数ISR均在预算内 | 已板测关闭 | +| 软限位 | P15四轴正负方向、500/2000Hz矩阵,最终位置误差不超过±1脉冲 | 已板测关闭 | +| X/M/HM位设备 | 三套独立10000位映像、FC02读取X、标准线圈访问M、PLSR `readBit`已实现 | 代码完成;X物理GPIO映射与HM保持未关闭 | +| 双AB 100kHz | 安全首沿、TIM9/TIM12双硬件计数、00边界PAUSE、延后慢收尾及D1468完整快速窗口已通过Host/IAR | 待四通道真机验证,历史7501首沿问题尚不能关闭 | +| 通信长稳 | USB IRQ优先级4、Modbus 0x47、D1516~D1537 USB设备端统计、P18/K4夹具和自动长稳脚本已完成 | 待30分钟及更长Modbus/USB/四轴板测 | +| 持久化 | HSD双检查点、SFD Flash A/B+CRC、保守全轴有效位及分阶段掉电/故障工具已完成 | 待VBAT断电、坏CRC、异常复位和反复擦写板测 | + +因此截至本状态日仍不得宣称“严格功能对标全部完成”。 diff --git a/Document/PLSR_document/PLSR方案设计书_V1.0.md b/Document/PLSR_document/PLSR方案设计书_V1.0.md index fabf7b3..9415a3a 100644 --- a/Document/PLSR_document/PLSR方案设计书_V1.0.md +++ b/Document/PLSR_document/PLSR方案设计书_V1.0.md @@ -3,7 +3,7 @@ > 文档版本:V1.0 Rev.C > 需求基线:[PLSR需求规格统计_V1.3.md](./PLSR需求规格统计_V1.3.md) Rev.B > 目标平台:XDM-60T4-E / STM32F407IG / uC/OS-II -> 编制日期:2026-08-05 +> 编制日期:2026-08-10 > 文档状态:设计基线;标记为“待上板验证”的项目在验证完成前不得关闭 ## 目录 @@ -68,7 +68,7 @@ V1.0 不实现多轴直线或圆弧插补、闭环位置修正、电子凸轮、 | PLSR | 多段脉冲定位指令,逻辑调用形式为 `PLSR S0 S1 S2 D` | | S0 | 动态首地址指定的路径数据块,保存总段数及各段频率、脉冲数、等待和跳转 | | S1 | 动态首地址指定的用户参数块,保存相对/绝对模式和起始段 | -| S2 | K0~K4 运动参数套组,保存默认速度、速度限制、曲线、补偿、FOLLOW和刷新周期 | +| S2 | K0~K4 运动参数套组,保存默认速度、速度限制、曲线、补偿、刷新周期及与独立FOLLOW指令共用的字段 | | D | 输出资源操作数,兼容语义下选择 Y0~Y3,本项目对应 Q0~Q3 高速资源 | | PULSE/DIR | 一个端口输出脉冲,另一个端口输出方向电平 | | AB 相 | 两路相差四分之一周期的正交输出,通过相位先后表示方向 | @@ -129,9 +129,9 @@ Q2/Q3 固定点对,最多同时运行两轴。四轴之间不做插补,各 | MCU | STM32F407IG,168 MHz | 保持现有时钟配置 | | OS | uC/OS-II,Tick为1 ms | 不提高OS Tick | | Modbus | USART1+DMA,任务轮询 | 作为调用与监控适配层 | -| USB | OTG FS中断当前优先级0 | 调低到运动中断之后,建议优先级6 | -| PLSR定时器 | 当前未初始化 | 新增TIM6控制基准、TIM9/TIM12硬件计数及TIM10/11/13/14脉冲输出 | -| Modbus历史 | 代码仍写入1000起始历史区 | 删除历史写入,释放给S0/S1动态数据池 | +| USB | OTG FS中断优先级已设为4 | 低于输出/计数中断优先级1和TIM6优先级2,仍需长稳并发板测 | +| PLSR定时器 | TIM6、TIM9/TIM12及TIM10/11/13/14已接入 | 保持固定资源映射并通过租约隔离冲突 | +| Modbus历史 | 1000起始历史区写入已删除 | S0/S1动态数据池不得恢复旧历史映射 | | Backup SRAM | 已完成基础初始化 | 扩展为带版本和CRC的双检查点 | | 急停 | 无外部硬件切断 | 仅实现软件立即关闭和状态锁存 | @@ -312,7 +312,8 @@ S0校验内容: - S0与S1不重叠。 S1只包含定位模式和起始段。S2只允许K0~K4,并校验默认速度、最高速度、起止频率、曲线、 -FOLLOW、补偿和刷新周期。D按输出模式检查Y0~Y3或Y0/Y2固定点对。 +补偿、刷新周期及FOLLOW共用字段的合法范围;FOLLOW共用字段不参与PLSR运算。D按输出模式检查 +Y0~Y3或Y0/Y2固定点对。 #### 3.2.2 模块输入输出接口说明 @@ -438,8 +439,9 @@ f(u) = f_start + (f_end - f_start) * g(u) 下一段入口速度。反向段必须先减速到0再切换方向或相序。 S2最高速度为0或超出硬件开放范围时拒绝启动。起始、目标和终止速度超过合法最高速度时限幅并发布 -`SPEED_CLAMPED`。FOLLOW性能范围为1~100,前馈补偿范围为0~100;具体运算公式在编码前按 -信捷手册相应参数定义冻结,禁止自行发明含义。 +`SPEED_CLAMPED`。FOLLOW性能范围为1~100,前馈补偿范围为0~100;两者属于信捷独立 +FOLLOW/FOLLOW_AB指令使用的共用S2字段。本PLSR模块保留存储、快照和范围校验,但不把它们 +代入PLSR速度曲线,也不自行定义FOLLOW运算公式。 #### 3.4.2 模块输入输出接口说明 @@ -561,6 +563,15 @@ AB相固定配对如下: 完整AB周期,落后相第四次跳变完成后关闭最后一周期。一个完整四状态周期只计1个指令脉冲,四次 相位跳变不得分别计数。 +AB启动必须在GPIO仍持有安全低电平时完成以下顺序:停止两个定时器、强制OC无效并UG、把两个CNT +装入严格大于CCR且保持四分之一周期差的位置、使能通道、切回AF、切入PWM1后再同时开放计数。 +禁止在运行中重写CNT维持相位。双AB分别以Q0→TIM9和Q3→TIM12作为完整周期计数源;到达目标前 +武装落后相的`00`边界快速门控,并通过D1468~D1469记录该快速路径最大周期数。上述新序列和门控 +代码已完成Host/IAR验证。PAUSE同样只在下一真实`00`边界停表,已经开始的周期计入完成数;双组 +近同时完成时,ISR只快速gate并锁存,GPIO、计数器释放和事件发布延后到任务态。D1468覆盖从IRQ +入口到二次扫描及公共ISR收尾的完整快速窗口,而非局部函数时间。在四通道真机确认严格边沿数、 +2.5us首相位间隔、变频及PAUSE/RESUME波形前仍标记为待上板验证。 + ##### 3.5.1.2 16位硬件计数扩展 TIM9和TIM12均为16位计数器。计数器配置为外部时钟模式1,ARR保持为`0xFFFF`,每收到一次选定的 @@ -819,6 +830,11 @@ PLSR内核通过统一设备接口访问逻辑软元件,Modbus只负责字节 32/64位状态采用版本化快照发布。Modbus断开不产生STOP;非法通信参数不得改变正在运行的任务。 本设计书不新增PLSR全局通信地址区,也不恢复已删除的Modbus历史功能。 +Modbus数据存储提供相互隔离的X、M、HM三套10000位映像。FC01/FC05/FC0F访问M,FC02只读访问X; +`PlsrModbusDataSourceInit()`把PLSR `readBit`连接到对应映像。该层只解决数据命名空间和协议一致性: +当前工程尚无可据以冻结的X端子GPIO映射,HM映像也尚未接入经板测的保持介质,因此硬限位、WAIT、 +EXT的真实X输入链路和HM掉电保持不得宣称关闭。 + #### 3.8.2 模块输入输出接口说明 ```c @@ -856,7 +872,9 @@ void PlsrDevice_TriggerSegmentEvent(uint8_t axis, uint16_t segment); `position_valid`。高速ISR只更新当前块计数;任务在块边界、段边界或检查点时合并64位数据。 发布HSD固定32位累计前检查INT32范围。超出范围时保持最近合法发布值,锁存 -`COUNTER_OVERFLOW`,不得回绕或静默截断。 +`COUNTER_OVERFLOW`,不得回绕或静默截断。HSD记录只有一个全局位置有效元数据位,因此检查点按 +四轴`position_valid`且无发布溢出的保守AND写入;单独校准一轴不得把其他未校准轴一起标为可信, +任何轴超出32位发布范围时也不得让旧HSD位置在重启后冒充当前可信坐标。 当量换算按每转指令脉冲数和每转移动量形成有理数比例,计算前约分并检查溢出,每轴保存换算余数, 避免连续多段累计舍入漂移。任一分母为0时返回兼容错误码2。 @@ -915,6 +933,13 @@ void PlsrPersist_SaveCheckpoint(uint32_t reason); 诊断模块记录每轴目标/实际频率、PSC/ARR/CCR、当前计数模式、TIM9/TIM12块累计、软件脉冲数、 物理边沿诊断值、当前段、停止原因、状态机非法转换、资源冲突和ISR执行周期。 +P16诊断协议V7使用D1256~D1263发布整体/ISR最大周期,D1456~D1467发布六个任务阶段独立最大值, +D1468~D1469发布AB末周期快速门控最大值,D1470~D1515发布缓存的持久化诊断与受控验证应答, +D1516~D1537使用首尾generation发布USB CDC initialized及八个32位收发/错误统计。 +Modbus只读功能码0x47提供40 WORD运行诊断;各32位字段不会撕裂,整组字段允许跨相邻通信事件; +USB CDC另记录收发、BUSY、失败和重挂统计。USB OTG FS优先级为4,长稳脚本可并发施加Modbus轮询、 +原子动态频率写入、坏CRC、计划断线及可选USB OUT流量。 + `SELF_TEST` 分为两级: - 默认不输出自测:地址表、S0/S1边界、跳转、分频、状态机、计数和CRC; @@ -1258,16 +1283,23 @@ Flash操作或通信发送,并使用DWT周期计数器测量最坏执行时间 | RISK-01 | 四轴100 kHz仍有两轴可能采用软件计数,最坏约200k周期事件/秒 | 分别验证两硬件+两软件计数组合,记录ISR最坏周期、CPU占用和通信压力结果 | | RISK-02 | TIM9/TIM12只能累计,不能反向自动关闭输出源 | 最终比较只武装尾边沿,逐数量边界验证无额外或窄脉冲 | | RISK-03 | AB配对定时器时钟分别为168/84 MHz | 全频段测量两组AB频率、相位及方向 | -| RISK-04 | USB当前中断优先级0 | 调低优先级并完成USB压力测试 | +| RISK-04 | USB中断可能抢占运动实时路径 | 优先级已降为4;完成USB/Modbus/四轴长稳压力后关闭 | | RISK-05 | 输出电路极性和可靠带宽待实测 | 测量Q0~Q3带载波形后冻结参数 | | RISK-06 | 0.1 ms曲线计算量 | 定点化和预计算,DWT实测最坏执行时间小于周期预算 | | RISK-07 | 16位通信写入造成32位动态频率撕裂 | 使用影子+COMMIT版本和内核二次校验 | | RISK-08 | Backup SRAM不能保证掉电瞬间零误差 | 异常复位清position_valid,并记录恢复等级 | | RISK-09 | 现有历史写入破坏S0/S1数据 | 删除历史写入并完成全地址越界测试 | | RISK-10 | 信捷保留字段被扩展复用 | 固定描述表、静态断言和对标矩阵评审 | -| RISK-11 | FOLLOW公式尚未在需求中展开 | 编码前按手册冻结公式和测试向量 | +| RISK-11 | 将共用S2字段误当作PLSR的FOLLOW运算 | 已关闭:FOLLOW/FOLLOW_AB按信捷手册属于独立指令,不在PLSR V1.0执行范围 | | RISK-12 | TIM9/TIM12内部OC事件相位和跨时钟域同步延迟 | 测量Q0~Q3的计数对应边沿、首脉冲、末脉冲和动态调频后累计一致性 | +截至2026-08-10,P14端子实测Q0~Q3均为100kHz且各严格200000个上升沿;P16实测 +`PlsrProcess`自身最大53748 cycles(319.929us)、响应最大85655 cycles,P14/P16已关闭。P15四轴 +软限位矩阵亦已板测通过。双AB安全首沿/双计数器/00边界PAUSE/D1468完整快速窗口、0x47及 +D1516~D1537 USB自动诊断、P18/K4长稳夹具和持久化分阶段掉电工具属于代码完成、Host/IAR已通过 +但最终板测未关闭项;真实X GPIO映射和HM保持介质仍待硬件定义。上述新批次落盘后必须重跑 +P14/P15/P16回归,不能用修改前的通过记录替代发布回归。 + ### 5.2 错误输出信息 #### 5.2.1 信捷兼容错误码 diff --git a/Document/PLSR_document/PLSR需求规格统计_V1.3.md b/Document/PLSR_document/PLSR需求规格统计_V1.3.md index c9ff4ae..0ee39ab 100644 --- a/Document/PLSR_document/PLSR需求规格统计_V1.3.md +++ b/Document/PLSR_document/PLSR需求规格统计_V1.3.md @@ -1,7 +1,7 @@ # PLSR 需求规格书 > 文档版本:V1.3 Rev.B(正式需求规格基线)
-> 更新日期:2026-08-05
+> 更新日期:2026-08-10
> 目标平台:XDM-60T4-E
> 软件平台:STM32F407 + uC/OS-II
> 对标基线:信捷 XD/XL 系列 PLC 定位控制手册 PLSR(印刷页 5~87,重点为 49~56) @@ -38,6 +38,7 @@ - [4.3 后续版本或加分项](#43-后续版本或加分项) - [4.4 不在当前范围内](#44-不在当前范围内) - [4.5 实施验证事项](#45-实施验证事项) + - [4.6 当前实现与验证快照](#46-当前实现与验证快照) - [5 参考资料](#5-参考资料) ## 1 概述 @@ -97,7 +98,7 @@ V1.0 的目标范围如下: | 后续方式 | 在上一段内提前变频,使段结束时已经达到下一段频率 | | S0 | 路径数据块,保存段数、频率、脉冲数、等待和跳转参数 | | S1 | 用户执行参数,仅保存定位模式和起始段 | -| S2 | K0~K4运动参数套组,保存速度限制、起止频率、加减速、补偿、FOLLOW和刷新周期 | +| S2 | K0~K4运动参数套组,保存速度限制、起止频率、加减速、补偿、刷新周期及与独立FOLLOW指令共用的参数字段 | | D | 输出轴或脉冲输出点 | | 控制周期 | PLSR 更新状态和速度规划的周期,由 S2 配置为 1 ms 或 0.1 ms | | 检查点 | 为掉电恢复而保存的一致性累计数据快照 | @@ -314,7 +315,7 @@ PULSE/DIR模式每个有效上升沿计1个指令脉冲;CW/CCW模式在当前 | --- | --- | --- | | S0 | 路径数据块 | 总段数、每段频率、脉冲数、等待及跳转 | | S1 | 用户执行参数 | 仅包含相对/绝对模式和起始执行段;异步调用属于指令接口而非S1字段 | -| S2 | 运动参数套组 | K0~K4,包含默认频率、最高频率、起止频率、加减速、曲线、补偿、FOLLOW和刷新周期 | +| S2 | 运动参数套组 | K0~K4,包含默认频率、最高频率、起止频率、加减速、曲线、补偿、刷新周期及与独立FOLLOW指令共用的参数字段 | | D | 输出轴 | 指定Y0~Y3脉冲轴;输出模式、方向点、极性和方向延时由该轴配置参数决定 | #### 2.4.1 指令接口 @@ -378,8 +379,8 @@ S0首地址由调用方动态指定。占用范围从`S0+0`开始,到最后一 - 齿隙补偿加减速时间。 - 直线、S形、正弦三种加减速模式。 - 最高速度限制、起始速度和终止速度。 -- FOLLOW性能参数,范围1~100。 -- FOLLOW前馈补偿百分比,范围0~100。 +- FOLLOW性能参数,范围1~100;该字段供信捷独立FOLLOW/FOLLOW_AB指令使用,PLSR只负责兼容存储和范围校验。 +- FOLLOW前馈补偿百分比,范围0~100;该字段不参与PLSR运动曲线计算。 - 脉冲频率刷新周期,0.1 ms或1 ms。 K0参数固定使用`HSD460~539`中对应轴的地址;K1~K4及公共参数固定使用 @@ -738,6 +739,11 @@ I6000~I6399按轴和段映射:第N段事件入口为该轴事件基址加`N- 7. 运行中写影子参数不得改变当前任务,允许运行修改的目标频率除外。 8. 协议应包含版本号,新增字段时不得改变既有寄存器语义。 +位设备访问必须保持X、M、HM三个独立命名空间,不得通过地址重叠或别名互相覆盖。标准Modbus线圈 +FC01/FC05/FC0F映射M位映像,离散输入FC02只读映射X位映像;PLSR的`readBit`按操作数类型读取 +对应映像。X物理输入扫描必须由明确的板级GPIO映射写入X映像,HM若要求掉电保持则必须采用经 +验证的保持存储策略;仅存在RAM映像不等于这两项已经上板关闭。 + PLSR内存地址、数据类型和固定占用规则见 [PLSR地址映射](./PLSR地址映射.md)。地址分配必须避开现有演示、 Backup @@ -994,7 +1000,7 @@ Flash数据无效或保存失败 | AT-17 | 四轴独立运行 | 四轴同时以100 kHz运行时无丢脉冲、重复脉冲和异常周期,且状态和累计数据互不串扰 | | AT-18 | AB 相输出 | 两个AB轴可独立运行;一个完整正交周期计1个指令脉冲,周期频率、相位方向和数量正确 | | AT-19 | S0等待与跳转 | 六种等待条件、动态参数来源、提前/延后触发和0/指定/自身跳转语义与信捷一致 | -| AT-20 | 公共参数 | 单位换算、软限位、方向延时、齿隙补偿和FOLLOW参数均通过测试 | +| AT-20 | 公共参数 | 单位换算、软限位、方向延时和齿隙补偿通过功能测试;FOLLOW共用字段通过存储及范围校验测试,不作为PLSR运算项 | | AT-21 | 零值及边界 | INT32最小值、频率0、脉冲数0跳段和绝对零位移行为符合本规格 | | AT-22 | 段事件和监控 | 每段事件、运行标志、当前段计数和累计计数可被稳定读取 | | AT-23 | 固定软元件 | HSD、SM、SD和I6000~I6399的地址、位宽、轴映射及可观察语义与信捷一致 | @@ -1052,6 +1058,21 @@ Flash数据无效或保存失败 5. Backup SRAM检查点的具体更新时机和诊断数据允许损失量。 6. 段脉冲数为0时无脉冲输出、跳过当前段并正确进入下一段或完成任务。 +### 4.6 当前实现与验证快照 + +以下快照用于区分“需求已纳入”“代码已实现”和“已完成板测”,不改变第4.1节验收准则,也不能 +单独作为“严格功能对标全部完成”的结论: + +| 项目 | 截至2026-08-10的状态 | 关闭边界 | +| --- | --- | --- | +| P14四轴PULSE/DIR | 已板测通过 | Q0~Q3均为100kHz、各严格200000个物理上升沿,无窄脉冲、启动毛刺和停止后残余 | +| P15软限位矩阵 | 已板测通过 | 四轴正负方向、500/2000Hz均在目标软限位±1脉冲内停止 | +| P16实时性能 | 已板测通过 | `PlsrProcess`自身最大53748 cycles(319.929us),预算小于168000 cycles;响应最大85655 cycles | +| X/M/HM位数据源 | 数据结构和通信代码已实现 | 已分离三个10000位映像,M接FC01/05/0F、X接FC02、PLSR `readBit`已接入;实际X GPIO扫描映射及HM掉电保持尚未关闭 | +| 双AB 100kHz | 代码、Host及IAR阶段完成 | 安全首沿、TIM9/TIM12双硬件计数、真实00边界PAUSE、任务态延后收尾和D1468完整快速窗口已写;必须以四通道逻辑分析仪关闭首次边沿、相序、相位、严格周期数、动态调频、PAUSE/RESUME和独立停止 | +| Modbus/USB长稳 | 代码和自动脚本完成 | USB优先级已降为4,0x47运行诊断、D1516~D1537设备端USB统计和P18/K4无软限位台架夹具已提供;30分钟及更长并发板测尚未关闭 | +| HSD/SFD持久化 | A/B、CRC、诊断窗口及分阶段板测工具完成 | 检查点按四轴有效且无32位发布溢出的保守AND写入;VBAT断主电、Flash擦写/坏CRC、异常复位和反复保存的正式板测尚未关闭;破坏性诊断默认禁用 | + ## 5 参考资料 1. [PLSR 嵌入式方向任务要求](./任务要求.png) diff --git a/Document/PLSR_document/交接提示词_2026-08-10.md b/Document/PLSR_document/交接提示词_2026-08-10.md new file mode 100644 index 0000000..7915556 --- /dev/null +++ b/Document/PLSR_document/交接提示词_2026-08-10.md @@ -0,0 +1,80 @@ +# PLSR 项目交接提示词(2026-08-10) + +> **用途**:将此文件全文粘贴到新的 Codex/对话中,即可无缝接替上一段长对话(ZCode-deepseek 会话)的全部任务。 +> 交接人:ZCode 会话(deepseek);接收人:Codex 新会话。 +> 项目:信捷 XDM 兼容 PLSR 脉冲定位模块(STM32F407 + IAR EWARM 8.3 + uC/OS-II + Modbus RTU 从站)。 + +--- + +## 一、项目一句话 + +在 STM32F407 上实现信捷 XD/XL 系列 PLC 的 `PLSR S0 S1 S2 D` 多段脉冲定位指令(对标 XDM-60T4-E),上板真机验证,当前处于**收尾阶段:只剩遗留精度验证与压力测试/清理**。 + +- 仓库:`F:\Xinje_Modbus_IAR\TrainCamp_yuwenhao_modbus`(git,当前分支 `deepseek`,主分支 `master`) +- 指令语义:S0=段表(每段 10 字:频率/脉冲/等待码/跳转),S1=模式/起始段,S2=参数组(K0=HSD460-539,K1-K4=SFD950+),D=Y0-Y3 +- 十态状态机:UNINITIALIZED/IDLE/ACCEL/RUN/DECEL/WAIT/PAUSED/COMPLETED/STOPPED/ERROR +- 速度规划:Q32.32 定点,中断中无浮点,直线/S曲线/正弦加减速,动态调频 +- 输出特性:Y 输出为**集电极开路(OC/NPN 漏型)负逻辑**,正向=低电平=ON +- 参考手册:`Document/PLSR_document/信捷XD_XL系列PLC定位控制手册_PD02_20260510_V1.3.pdf` + +## 二、⚠️ 进入任务前必读(当前工作区状态) + +1. **工作区有 22 个文件未提交**(P14 硬件计数新增 437 行 + 软限位修复 + AB 预热机制等全在工作区,未 commit)——先 `git status` 查看,与老对话确认提交策略后再动工。 +2. 最近提交:`fdbbe71` P13(Modbus 控制窗口)、`7750e43` P6(四轴并发自测)、`c776438` P5(软限位保护)。 +3. 上板自测 `plsr_self_test.c` 仍在(收尾时要删)。 +4. 真机验证环境:STM32F407 + 逻辑分析仪导出 16 通道 .bin(100MS/s,2字节/采样小端 uint16),波形分析脚本 `Document/PLSR_document/波形/plot_waveform.py`。 + +## 三、主线:5 批规划与完成度(已核实代码) + +| 批次 | 内容 | 状态 | 证据 | +|---|---|---|---| +| ① | Modbus 数据源正式接入(真实 D/HD/FD → S0/S1/动态调频) | ✅ 完成 | `plsr_modbus_data.c` readWord=PlsrModbusReadWord;S0=D1600/S1=D1700 经 0x10 原子写入;动态调频 D1000/D1100 已真机验证 | +| ② | Modbus 命令与状态接口(START/STOP/PAUSE/状态/错误码/位置/计数) | ✅ 完成 | fdbbe71:COMMIT/START 分离、停止/暂停/继续/位置设置/清零/保存、重复序号幂等、四轴状态、32/64 位版本一致性;错误码 5/6 + RESET_ERROR 已验 | +| ③ | FOLLOW 与前馈补偿 | ✅ 关闭(不实现) | P14 按手册确认 FOLLOW/FOLLOW_AB 是**独立随动指令**,PLSR 仅保留参数字段存储校验,不实现随动运算。**勿再开发** | +| ④ | 遗留精度问题 | ⚠️ 见下表 | — | +| ⑤ | 压力测试与正式收尾 | ❌ 大部分未做 | 见第五节 | + +## 四、遗留精度问题(④批)——按优先级 + +| # | 问题 | 状态 | 验证/修复方法 | +|---|---|---|---| +| 1 | **P14 硬件计数 -1**(Q0/Q1=199999 而非 200000;TIM9/TIM12 硬件计数;Q2/Q3 软件回退精确) | 🟡 代码已修**未上板验证**、未提交 | 修复:计数器延迟到源 OC 启动 UG 完成后再配置/使能,消除无物理脉冲的内部首沿;验收=四轴均 200000。用 `HostComputer/plsr_modbus_counter_stress_test.py`(四轴 100kHz 压力) | +| 2 | **AB 启动边沿**(段1 起步 A-R/B-R 同升,各多 1 沿 → 7501/7501;首周期从 11 开始非 00→10→11;稳定复现 6 版固件) | 🔴 预热机制 `abStartupPriming` 已写(plsr_hal_f407.c:845/893/1065)**未上板验证** | ① 两 ISR 各翻转调试 GPIO 抓先后;② `SetPwmMode1` 移到 `SetCc1e` 之前;③ 启动后读两路 CNT 确认初值。验收=严格 7500 | +| 3 | **CW/CCW 段尾截断窄脉冲**(段末最后脉冲仅 2.5~5µs 高,未走完完整周期) | 🟡 未关闭(主验收已过,微秒毛刺标准不达标) | 停止应延迟到最后脉冲 CC1 匹配(下降沿)之后 | +| 4 | 软限位边界 | ✅ 已关闭 | P15 高频采样补偿(2000Hz 提前 1 脉冲进入减速)→ 四轴正/负 500/2000Hz 矩阵 ±1 脉冲窗口全过 | +| 5 | AB 段3 反向切换 10ms 过渡期 / 方向信号 Q5 1µs 毛刺 | 🟢 低优先级,可暂缓 | — | + +详细记录见 `Document/PLSR_document/问题总清单_2026-08-10.md`、`已知问题清单_2026-08-09.md`。 + +## 五、压力测试与正式收尾(⑤批)——下一步主线 + +1. **上板验证**(立即):AB 预热(7500)、硬件计数 -1(四轴 200000)、软限位回归。 +2. **四轴 100kHz AB 模式**:只测过 PULSE/DIR 100kHz;AB 高频加速段重定相干扰未实测。 +3. **双 AB 并发**:Q0/Q1 + Q2/Q3 同时 AB 运行(独立轴停止互不影响已有实现,未实测并发)。 +4. **Modbus 压力**:`HostComputer/plsr_modbus_counter_stress_test.py` 已有部分覆盖(12 次状态读取),补全并发读写压力。 +5. **掉电恢复**:Flash 存储(SFD900~1419 片内 Flash 已实现)掉电/上电恢复未实测。 +6. **性能测试**:`HostComputer/PLSR_MODBUS_PERFORMANCE_TEST.md` 已建,脚本未写。 +7. **清自测收尾**:删 `plsr_self_test.c`(及 main 中的调用)、调试快照、tmp 资料清理、git 历史大对象(1GB bin 松散对象)清理。 + +## 六、已固化的关键决策(勿回退) + +- Y 输出 OC 负逻辑(正向=低=ON);PULSE/DIR、AB、CW/CCW 三模式,CW/CCW 返回 NOT_SUPPORTED +- AB 仅允许 D=Y0/Y2(Q0/Q1、Q2/Q3),调频同时更新 A/B 不重置相位,正常完成只在完整 00 周期边界停止 +- 段间衔接仅方向变化时走 DIR_SETTLING(~2ms),否则直连 +- 每 tick `PlsrProfileSyncPulses` 校准 profile 虚拟计数到硬件实发数(段尾冻结修复) +- 地址映射固定:HSD 384B / SFD RAM 缓存 2080B / SD 192B / SM 4B,`plsr_address_map.h` +- PLSR 阶段不实现 FOLLOW 随动运算(见批次③) + +## 七、代码地图与测试体系 + +- `PLSR/Inc|Src/`:plsr_core(状态机/命令)、plsr_job(任务快照解析/校验)、plsr_path(多段执行器)、plsr_profile(速度曲线)、plsr_hal_f407(定时器/GPIO/硬件计数)、plsr_modbus_control(命令)、plsr_modbus_data(D 区数据源)、plsr_persistence(Flash)、plsr_self_test(上电自测)、plsr_position、plsr_resource、plc_device +- **Host 测试**:`PLSR/Test/run_host_tests.ps1`(1268 项,`-std=c11 -Wall -Wextra -Werror -DPLSR_HOST_TEST -lm`);**IAR 编译**:`IarBuild.exe EWARM/Modbus.ewp -build Modbus`(0 错误 0 警告基线) +- **真机测试脚本**(HostComputer/):`plsr_modbus_control_test.py`(命令)、`plsr_modbus_counter_stress_test.py`(四轴 100kHz+计数)、`plsr_modbus_frequency_test.py`(动态调频)、`plsr_modbus_soft_limit_matrix_test.py`(限位矩阵)、`modbus_timing_tester.py`(时序) +- **波形验证**:`Document/PLSR_document/波形/*.bin` + `plot_waveform.py`(时频分析:总脉冲数/段结构/频率命中/段间 gap) +- **文档**:方案设计书 V1.0、需求规格统计 V1.3、对标追踪矩阵、问题总清单 2026-08-10 + +## 八、建议的下一步行动(新对话从这里开始) + +1. `git status` + `git log --oneline -5` 确认工作区 22 个未提交文件内容(P14 硬件计数批次),**先上板验证再提交**(或与用户确认提交策略)。 +2. 上板验证三件套:硬件计数 -1(counter_stress 脚本)→ AB 预热(7500)→ 软限位回归(matrix 脚本)。 +3. 通过后提交 P14 批次,然后按第五节 2→7 推进压力测试与收尾。 diff --git a/Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md b/Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md new file mode 100644 index 0000000..816b27f --- /dev/null +++ b/Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md @@ -0,0 +1,69 @@ +# 提示词:发给 Codex 老对话(请求输出交接总结) + +> 用法:把下面【发送内容】整段复制,发给当前正在进行的 Codex 老对话。 +> 老对话回复的交接总结,将整段粘贴给新对话,作为新对话的唯一历史输入。 +> (仓库里另有一份兜底参考:`交接提示词_2026-08-10.md`,若老对话总结不完整,可一起给新对话。) + +--- + +【发送内容】 + +``` +我们即将结束本对话,把所有任务交接给一个全新的对话(新 Codex/ZCode 会话)。 +新对话没有任何本对话的历史,它只能看到两样东西: + (1) 你下面这份回复(原样粘贴给它); + (2) 仓库 F:\Xinje_Modbus_IAR\TrainCamp_yuwenhao_modbus 中的文件。 + +请【一次性、直接】输出一份完整、自包含、Markdown 格式的交接总结。 +不要提问、不要确认、不要寒暄、不要分多轮——就这一轮输出全部内容。 + +硬性要求: +1. 不假设新对话知道任何背景:所有结论、数据、文件路径、命令、编号都写全。 +2. 高密度:多用表格、编号、代码引用(文件:行号),少废话。 +3. 必须覆盖以下章节(顺序可微调,但章节都要有): + +## A. 项目概况 +- 项目一句话(做什么、对标什么硬件、当前处于什么阶段) +- 仓库路径、git 分支、最近几个提交 + +## B. 主线进度(5 批规划逐项标注 ✅ 完成 / ⚠️ 部分 / ❌ 未做) +1. Modbus 数据源正式接入(真实 D/HD/FD → S0/S1/动态调频) +2. Modbus 命令与状态接口(START/STOP/PAUSE/状态/错误码/位置/计数) +3. FOLLOW 与前馈补偿 +4. 遗留精度问题(AB 首沿、软限位、硬件计数 -1 等) +5. 压力测试与正式收尾(100kHz/双 AB/掉电/Modbus 压力/清自测) +每项给一句证据(提交号 / 测试脚本 / 文档名)。 + +## C. ⚠️ 工作区当前状态(最重要,务必写全) +- git status 所有未提交文件清单,每个文件是什么内容(哪些是待上板验证的修复、哪些是测试脚本、哪些是文档) +- 哪些代码改动【已写但未上板验证】、哪些【已验证但未提交】、哪些【既未验证也未提交】 + +## D. 未完成任务清单 +每个未完成任务给出: +- 现象(含实测数据,如 7501 vs 7500、199999 vs 200000) +- 影响/验收标准 +- 根因分析或假设 +- 代码现状(已写/未写,给出 文件:行号) +- 验证方法(用什么脚本/逻辑分析仪/命令) +- 若已写修复但未验证:修复思路一句话 + +## E. 已验证成果与固化决策(勿回退清单) +- 所有实测过的硬件特性(如 OC 负逻辑、AB 相序、段间衔接时序) +- 已修复并验证的 bug 列表 +- 明确的边界决定(如 FOLLOW 不实现、CW/CCW 不支持) +- 地址映射等固定约定 + +## F. 代码地图与测试体系 +- PLSR/ 各模块职责(core/job/path/profile/hal/modbus_control/modbus_data/persistence/self_test...) +- HostComputer/ 各测试脚本用途 +- 编译命令(IAR build、run_host_tests.ps1)与基线(当前 0 错误 0 警告?) +- 逻辑分析仪波形验证方法(bin 格式、采样率、plot_waveform.py 用法) + +## G. 下一步行动清单 +按优先级排序的具体行动(从新对话开工第一件事开始排)。 + +## H. 仅存在于对话中的信息 +凡"只在我们对话里出现过、没有落入任何文件"的关键信息(测试数据、踩坑经验、用户偏好、待确认问题),单独列一节,务必不要遗漏。 +``` + +【发送内容结束】 diff --git a/Document/PLSR_document/问题总清单_2026-08-10.md b/Document/PLSR_document/问题总清单_2026-08-10.md new file mode 100644 index 0000000..72cfa6e --- /dev/null +++ b/Document/PLSR_document/问题总清单_2026-08-10.md @@ -0,0 +1,99 @@ +# PLSR 问题总清单(2026-08-10) + +> 汇总截至今日所有未关闭的问题、待验证项与待办功能。 +> 均为上板验证/代码扫描实证,非猜测。 + +--- + +## 一、未解决 Bug(4 个主问题 + 2 个低优先级) + +### 1. AB 启动边沿(Q0/Q1 = 7501 而非 7500) + +| 项 | 内容 | +|---|---| +| 现象 | 段1 起步瞬间 A-R 与 B-R 同刻上升(0.33~0.37µs),每相各多 1 个上升沿 → 7501/7501;首周期从 11 开始而非 00→10→11 | +| 复现 | 6 版固件稳定复现(00:06 / 13:41 / 13:57 / 13:59 / 14:15),现象、位置、结构完全一致 | +| 影响 | 启动瞬间 1 个非法跳变;多数驱动器正交解码忽略,位置误差最多 1 脉冲 | +| 已排除 | 调频/重定相路径无同升(稳定段 <10µs 间隔 0 个)——问题只在首次启动路径 | +| 代码现状 | `PlsrHwBeginAbOutput`:GPIO 保持 00 → UG → CNT 初值(lead 3/4T、lag 1/2T)→ 切 PWM1 → CEN → update 分时释放引脚;`abStartupPriming` 预热机制已写(首次启动吞周期)**待上板验证** | +| 根因方向 | 两路"释放"几乎同时 → 两路第一次 update 同时 → CNT 初值 1/4T 相位差未体现;或 CC1E 先于 PWM1 切换(RM0090:切模式瞬间 OCREF 跳变)+ 落后相 CNT 写入 = CCR 触发比较事件 | +| 验证方法 | ① 两 ISR 各翻转调试 GPIO 抓先后;② `SetPwmMode1` 移到 `SetCc1e` 之前;③ 启动后读两路 CNT 确认初值 | +| 状态 | 🔴 未关闭(验收:严格 7500) | + +### 2. 正软限位边界精度(502 脉冲 vs 验收 499~501) + +| 项 | 内容 | +|---|---| +| 现象 | 目标 +10000、正软限位 +500、1000Hz、减速 100ms → 实测 Q0=502 上升沿,超停 1~2 个 | +| 波形结构 | 恒定段 460 个 @1000Hz → 减速段 42 个(84.5ms,535→134Hz 平滑)→ 停止后零边沿 | +| 偏差 | 验收"接近第 450 个脉冲开始减速"→ 实测 459 开始(晚 9 个);理论提前量 = 50 脉冲(应在 450 触发、正好停 500) | +| 影响 | 超停 1~2 个工程单位;软限位是安全功能,偏差为系统性(速度/限位配置变化时超停量随之变化) | +| 根因方向 | ① 限位判定位置源(hardwarePulses/虚拟计数/logicalPosition)与判定 tick 采样点;② 提前量(减速距离)计算与实际减速段脉冲数(42 vs 50)不一致;③ 收尾差 1 与 DONE vs 硬件计数边界可能同源 | +| 验证方法 | 调限位值/速度复测多组(+200/+1000、500Hz/2000Hz),统计触发位置偏差是否随参数线性变化 | +| 状态 | 🟡 已修复待上板:制动距离改用真实硬件输出频率,新增四轴正/负、500/2000Hz矩阵测试(验收±1脉冲) | + +### 3. CW/CCW 段尾截断窄脉冲(2.5µs / 5µs) + +| 项 | 内容 | +|---|---| +| 现象 | 段1 最后脉冲只持续 2.5µs 高(0.167797→0.167800)、段2 最后脉冲 5.0µs——最后脉冲上升沿后即拉低,未走完完整周期 | +| 影响 | 计数不受影响(Q0=300、Q1=200 精确,中断计数不依赖脉冲宽度);但部分驱动器可能把窄脉冲当噪声忽略(物理少走最后 1 个脉冲) | +| 根因方向 | CW/CCW 停止时机在"最后脉冲上升沿 + 2.5~5µs"(比 CC1 下降沿早半个周期)——停止应延迟到最后脉冲 CC1 匹配(下降沿)之后 | +| 状态 | 🟡 未关闭(验收"微秒级毛刺 0 个"严格不达标;主验收已过) | + +### 4. P14 硬件计数少 1 个脉冲(Q0/Q1 = 199999 而非 200000)【新增】 + +| 项 | 内容 | +|---|---| +| 现象 | 四轴 100kHz 压力测试:Q0/Q1(TIM9/TIM12 硬件计数)各 199999 个上升沿;Q2/Q3(软件计数回退)精确 200000 | +| 影响 | 每轴少 1 个脉冲(0.0005%),位置误差 -1 脉冲/次,**系统性**(非随机);影响硬件计数"转正" | +| 代码现状 | 硬件计数本轮新接入:TIM9 对 Q0/Q2、TIM12 对 Q1/Q3 累计;16 位溢出扩展(counterBlockPulses)+ 目标脉冲比较(CC1IF 判定,`PlsrHwOnCounterInterrupt`);本轮同时修复"启动 UG 事件被误计" | +| 根因方向 | 代码实证:`pulses = counterBlockPulses + CNT`,`CC1IF && pulses >= targetPulses` 判 DONE——**启动时计数器初值偏移 +1(CNT 从 1 开始或第一个 OC 事件被 UG 清除时序吞掉)→ pulses 提前达到 200000 → 提前 1 个停止**(实际只发 199999);注释"TIMx_OC rises at the PWM update boundary = 物理下降沿"——启动瞬间的计数边界对齐即嫌疑点 | +| 验证方法 | DONE 判定处打印硬件计数器最终值(199999 → 少计 1 个;200000 → 停止判定滞后);或对比 Q0(硬件)与 Q2(软件)启动前 3 个脉冲边沿 | +| 状态 | 🟡 已修复待上板:从计数器延迟到源OC启动UG完成后再配置/使能,消除无物理脉冲的内部首沿;验收仍为四轴均200000 | + +### 5.(低)AB 段3 反向切换 10ms 过渡期 + +| 项 | 内容 | +|---|---| +| 现象 | 段2→段3 反向切换有 ~10ms 混合相序过渡(A-R→B-R→B-F→A-F)后才进入标准 B 超前 00→01→11→10→00;无毛刺、每状态时长正常 | +| 影响 | 若要求"反向也严格从 00 起步"则未达标;实际影响很小 | +| 状态 | 🟢 低优先级,可暂缓 | + +### 6.(低)方向信号 Q5 启动瞬间 1µs 毛刺 + +| 项 | 内容 | +|---|---| +| 现象 | 方向逻辑测试(17:20)中 Q5 在方向建立瞬间出现 1µs 低→高→低毛刺(0.128780) | +| 影响 | 方向信号 1µs 抖动,一般不影响(方向在脉冲开始前已稳定 10ms) | +| 状态 | 🟢 低优先级,可暂缓 | + +--- + +## 二、待验证项 + +| 项 | 内容 | 状态 | +|---|---|---| +| AB 启动预热机制(abStartupPriming) | 代码已写(首次启动吞周期),未上板验证 | ⏳ 待测 | +| 四轴 100kHz AB 模式 | 本次只测了 PULSE/DIR 100kHz;AB 高频加速段重定相干扰未实测 | ⏳ 待测 | +| 双 AB 并发(Q0/Q1 + Q2/Q3 同时 AB) | 未实测 | ⏳ 待测 | + +--- + +## 三、未实现功能(清单剩余 2 项) + +| 项 | 现状 | 说明 | +|---|---|---| +| Modbus 剩余批次 | P13 控制窗口 ✅(已提交 fdbbe71)、P14 硬件计数 ✅(待提交) | 按 P14 说明"剩余 3 批",当前已完成 2 批 → **剩余约 2 批**(具体内容见 Codex 规划) | +| 收尾清理 | 未做 | 上电自测移除、调试快照(P4 后删除标记)、tmp 资料清理、git 历史重写(1GB bin 松散对象) | + +> **FOLLOW/前馈已关闭(不再列入待办)**:P14 批按信捷手册明确 FOLLOW/FOLLOW_AB 为独立随动指令(输出 = 输入 × 乘系数/除系数),PLSR 仅保留共用参数字段(follow 1~100、feedforward 0~100)的存储与校验,不实现随动运算。 + +--- + +## 四、优先级建议 + +1. **尽快修**:P14 硬件计数 -1(新路径转正门票,预期小改动) +2. **次优先**:AB 启动边沿(预热机制已写,上板验证即可见分晓) +3. **可并行**:Modbus 剩余批次、软限位/段尾截断按实验定位后修 +4. **暂缓**:低优先级两项(反向过渡/方向毛刺)、FOLLOW 运算(等需求明确) diff --git a/HostComputer/PLSR_BUILD_PROFILES.md b/HostComputer/PLSR_BUILD_PROFILES.md new file mode 100644 index 0000000..bde5608 --- /dev/null +++ b/HostComputer/PLSR_BUILD_PROFILES.md @@ -0,0 +1,41 @@ +# PLSR 验证与生产构建 + +构建开关集中在 `PLSR/Inc/plsr_build_config.h`。 + +当前仓库默认是验证构建,用于完成剩余板测: + +- `PLSR_ENABLE_BOARD_SELF_TEST=1` +- `PLSR_BOARD_TEST_SELECT=PLSR_BOARD_TEST_DUAL_AB` +- `PLSR_ENABLE_HW_TRACE=1` +- `APP_ENABLE_USB_CDC=1` +- `PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG=0` + +P14、P15、P17 共用一次参数准备,运动仍由上位机脚本发起,不会上电自启动。 + +P18 长稳使用独立 K4 且关闭软/硬限位,只允许在脱开机构或已确认机械安全的台架上使用。在 IAR +预处理器符号中临时覆盖: + +```text +PLSR_BOARD_TEST_SELECT=PLSR_BOARD_TEST_LONG_STRESS +``` + +完成长稳后删除该覆盖,仓库默认会回到 P17 双 AB 验证配置。 + +正式发布时,在 IAR 的预处理器符号中覆盖: + +```text +PLSR_ENABLE_BOARD_SELF_TEST=0 +PLSR_ENABLE_HW_TRACE=0 +PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG=0 +``` + +USB 不使用时再增加: + +```text +APP_ENABLE_USB_CDC=0 +``` + +关闭板测后,Modbus PLSR 控制窗口仍会初始化;这是正式控制链,不再依赖任何自测选择。破坏性持久化诊断默认始终关闭,只有专用验证固件、双重 magic、全轴空闲且双槽有效时才允许使能。 + +2026-08-10已实际将`PLSR_ENABLE_BOARD_SELF_TEST=0`(从而关闭HW trace)做过一次完整IAR链接, +结果0 errors / 0 warnings;随后已恢复仓库默认P17验证配置并再次完整构建通过。 diff --git a/HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md b/HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md new file mode 100644 index 0000000..007469a --- /dev/null +++ b/HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md @@ -0,0 +1,55 @@ +# PLSR 双 AB 100kHz 压力测试 + +本测试覆盖两组固定 AB 点对:Q0/Q1(轴 Y0)和 Q2/Q3(轴 Y2)。脚本使用 +K3 的 100kHz 零加减速配置,自动写入 S0/S1、COMMIT、START,并核对两组 +TIM9/TIM12 硬件计数租约、任务/位置/物理周期数和 DWT 实时预算。动态用例还会 +执行100kHz→50kHz→100kHz原子变频以及Q0/Q1单组PAUSE/RESUME。 + +> 当前状态(2026-08-10):安全首沿建立、双硬件计数租约和末周期快速门控代码已通过Host回归及IAR编译;尚未按本文件完成四通道真机波形验收,因此AB首次启动7501历史问题和双AB 100kHz压力项仍不得标记为关闭。 + +## 测试前提 + +- 烧录当前工作区固件后执行硬复位,使 DWT 最大值从零开始。 +- `main.c` 的板测准备路径必须执行 `PlsrHardwareCounterSelfTestPrepare()`,以生成 K3。 +- Modbus RTU 默认 COM5、9600、8E1。 +- 逻辑分析仪四通道同步连接 CH0~CH3=Q0~Q3,建议 100MS/s。 + +## 自动测试 + +完整执行独立停止、等长并发、动态变频/暂停三个用例: + +```powershell +python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 +``` + +为便于逻辑分析仪精确统计,推荐硬复位后分别采集: + +```powershell +python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case independent +python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case simultaneous +python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case dynamic +``` + +`independent` 要求 Q0/Q1 完成后计数冻结且保持低,同时 Q2/Q3 继续运行。 +`simultaneous` 使用相同的 200000 周期目标验证两组并发完成;两个 START 通过 +Modbus 串行提交,因此两组实际启动/结束时刻允许相差一次 RTU 事务时间。 +`dynamic` 使用每组300000周期,要求两组调频时保持相位和连续计数;Q0/Q1只在 +完整`00`边界暂停并保持低,Q2/Q3不受影响,随后Q0/Q1从`00`恢复正确相序。 + +脚本还检查 D1468~D1469 发布的 AB 末周期快速门控最大周期数。168MHz 下必须 +非零且小于 420 cycles(2.5us,一个 100kHz AB 周期的四分之一)。 + +## 逻辑分析仪验收 + +- 独立停止用例:CH0=CH1=100000 个上升沿;CH2=CH3=200000 个上升沿。 +- 等长用例:CH0=CH1=CH2=CH3=200000 个上升沿。 +- 动态用例:CH0=CH1=CH2=CH3=300000 个上升沿;Q0/Q1含一个暂停低电平窗口。 +- 100kHz区间完整AB周期10.000us、各相高宽5.000us;50kHz区间周期20.000us、 + 各相高宽10.000us;不得有小于1us的窄脉冲。 +- 正向必须为 `00→10→11→01→00`,反向必须为逆序。 +- 调频前后保持严格±90°且不得增加边沿;PAUSE只能在`00`边界生效,RESUME从 + `00`重新建立相序,另一组全程不得停顿或串扰。 +- 启动前为 `00`;A/B 首个上升沿间隔 2.50us(建议容差 ±0.05us),不得同时上升。 +- 末周期完整回到 `00`;停止后至少 1ms 全低且无残余边沿。 + +脚本的内部 `physical_pulses` 只用于固件一致性检查,不能替代端子波形验收。 diff --git a/HostComputer/PLSR_MODBUS_BIT_INPUT_TEST.md b/HostComputer/PLSR_MODBUS_BIT_INPUT_TEST.md new file mode 100644 index 0000000..b81bff0 --- /dev/null +++ b/HostComputer/PLSR_MODBUS_BIT_INPUT_TEST.md @@ -0,0 +1,17 @@ +# PLSR Modbus 位数据源测试 + +运行: + +```powershell +python HostComputer\plsr_modbus_bit_input_test.py --port COM5 +``` + +脚本自动完成以下检查: + +1. 使用 Modbus `0x05` 写 M123,并用 `0x01` 回读。 +2. 使用 `0x02` 读取同地址 X123,确认 X/M 是两个独立位空间。 +3. 原子写入一段 1000 Hz、500 脉冲的 S0/S1,段后等待 M123。 +4. 验证 M123=0 时轴稳定处于 WAIT,任务计数严格为 500。 +5. 将 M123 置 1,验证 WAIT 释放并进入 COMPLETED。 + +该测试关闭的是 M 位生产链。X 只提供只读映像和 FC02 协议入口;必须取得板卡“X 点号 → GPIO/输入扫描”映射后,才能验收实际 X、EXT 和正负硬限位。HM 当前也是独立映像,但尚未定义工程需要的掉电保持策略。 diff --git a/HostComputer/PLSR_MODBUS_CONTROL_TEST.md b/HostComputer/PLSR_MODBUS_CONTROL_TEST.md index eb0c868..1ea1100 100644 --- a/HostComputer/PLSR_MODBUS_CONTROL_TEST.md +++ b/HostComputer/PLSR_MODBUS_CONTROL_TEST.md @@ -1,6 +1,6 @@ # PLSR P13 Modbus 命令与状态测试 -P13 使用可配置控制窗口,当前板测选择 D1200~D1455。该范围不属于信捷固定 PLSR 地址,仅是本工程上位机测试使用的动态通信窗口。 +P13 使用可配置控制窗口,当前板测选择 D1200~D1537。D1456~D1467 是 P16 V7 分阶段性能诊断扩展,D1468~D1469是双AB末周期快速门控诊断,D1470~D1515用于持久化只读状态和受控验证请求/应答,D1516~D1537用于generation保护的只读USB CDC运行统计。该范围不属于信捷固定 PLSR 地址,仅是本工程上位机测试使用的动态通信窗口。 运行: diff --git a/HostComputer/PLSR_MODBUS_COUNTER_STRESS_TEST.md b/HostComputer/PLSR_MODBUS_COUNTER_STRESS_TEST.md new file mode 100644 index 0000000..a34d18f --- /dev/null +++ b/HostComputer/PLSR_MODBUS_COUNTER_STRESS_TEST.md @@ -0,0 +1,57 @@ +# PLSR P14 四轴高速计数压力测试 + +## 1 测试目的 + +验证四轴同时以100kHz输出时的脉冲计数、16位计数器跨界、计数资源冲突回退和Modbus并发读状态。P14按硬件连接动态分配计数资源:Q0/Q2共享TIM9,Q1/Q3共享TIM12;按Q0、Q1、Q2、Q3顺序启动时,Q0/Q1使用硬件计数,Q2/Q3使用软件回退。 + +## 2 准备 + +1. 使用验证构建编译并烧录当前工程:`PLSR_ENABLE_BOARD_SELF_TEST=1`,`PLSR_BOARD_TEST_SELECT`可选`PLSR_BOARD_TEST_HW_COUNTER`或`PLSR_BOARD_TEST_DUAL_AB`,两者都会准备P14所需K1参数。 +2. 复位开发板。 +3. 逻辑分析仪连接Q0、Q1、Q2、Q3和GND,采样率建议不低于10MS/s,采集时间至少5秒。 +4. 保持Modbus RTU参数与工程一致:默认站号1、9600 bit/s、偶校验、1停止位。 + +## 3 运行 + +```powershell +python HostComputer\plsr_modbus_counter_stress_test.py --port COM5 +``` + +脚本会自动完成四轴位置清零、S0/S1写入、COMMIT、START、运行期状态轮询和最终计数核对,不需要在Watch窗口手工修改变量。 + +当前固件同时为后续P15配置了±1000000软限位;P14每轴只运动200000脉冲,因此不会触发该限位。 + +## 4 自动判定标准 + +- 四轴均进入运行态,频率均为100000Hz。 +- Q0/Q1运行期`hardwareCounter=1`,Q2/Q3为0。 +- 四轴均进入`PLSR_STATE_COMPLETED`。 +- 每轴`logicalPosition`和`taskPulses`均精确等于200000;`physicalPulses`是上电累计值,脚本以运行前基线为准,要求本次增量精确等于200000,不能直接比较累计绝对值。 +- 四轴`error=0`且最后执行结果为`PLSR_RESULT_OK`。 + +可选Watch观察项(只需观察,不需要修改): + +- `PlsrHwCounterOwners[0]`:运行期为0,表示TIM9分配给Q0。 +- `PlsrHwCounterOwners[1]`:运行期为1,表示TIM12分配给Q1。 +- `PlsrHwAxes[0].counterBlockPulses`和`PlsrHwAxes[1].counterBlockPulses`:运行中依次跨过65536、131072、196608。 +- `PlsrHwAxes[0..3].hardwareCounterActive`:运行期依次为1、1、0、0;完成后租约释放并回到0。 + +## 5 波形判定标准 + +- Q0、Q1、Q2、Q3各200000个上升沿。 +- 每路稳定频率100kHz,周期10us,占空比约50%。 +- 无额外首沿、段尾截断、窄脉冲或停止后的残余脉冲。 +- 四轴启动时刻允许因串行发送START命令而不同;这不是同步插补测试。 + +若脚本通过而硬件计数轴的波形数量不符,应优先检查TIM9/TIM12内部触发边沿与物理输出反相关系;Host测试无法覆盖这一项。 + +## 6 已关闭的板测记录(2026-08-10) + +- P14脚本通过:四轴本次任务计数和`physicalPulses`增量均为200000。 +- 逻辑分析仪确认Q0~Q3各严格200000个物理上升沿,稳定频率100000.0Hz。 +- 周期中位数10.000us,实测范围9.990~10.010us;高电平宽度5.000us。 +- 无小于0.5us窄脉冲、无启动毛刺、无停止后残余,四路最终均为低电平。 + +上述记录关闭当前P14 PULSE/DIR计数验收,但不能替代双AB、掉电恢复或长稳并发专项验收。 +该记录也是最终AB/持久化/USB批次合入前的基线;烧录当前最终工作区后应再执行一次本脚本和四通道 +边沿统计,确认共享HAL与中断改动没有造成PULSE/DIR回退。 diff --git a/HostComputer/PLSR_MODBUS_LONG_STRESS_TEST.md b/HostComputer/PLSR_MODBUS_LONG_STRESS_TEST.md new file mode 100644 index 0000000..dd323f1 --- /dev/null +++ b/HostComputer/PLSR_MODBUS_LONG_STRESS_TEST.md @@ -0,0 +1,85 @@ +# PLSR Modbus / USB 长稳并发测试 + +本批测试用于在四轴 `PULSE/DIR` 持续运动时,同时施加 Modbus 状态轮询、32 位动态频率写入和可选 USB CDC OUT 流量。脚本不发送 `SAVE_CONFIG`,不会在运动中触发 Flash 擦写。 + +> 当前状态(2026-08-10):0x47只读诊断、USB CDC统计、USB中断优先级4及自动长稳脚本已完成代码和静态构建验证;30分钟及更长的Modbus/USB/四轴真机并发仍待执行,不能以脚本存在替代板测结论。 + +## 固件诊断接口 + +Modbus RTU 新增只读功能码 `0x47`。请求 PDU 与 `0x03` 一样包含 `start word` 和 `quantity`,完整快照为 40 WORD;请求示例为 `47 00 00 00 28`。不存在写入口,也不占用 D/HD/FD 地址。 + +主要字段如下(32 位字段均为低 WORD 在前): + +| WORD | 内容 | +|---:|---| +| 0~3 | 签名 `0x4D42`、版本 1、长度 40、运行标志 | +| 4~9 | 当前 Tick、最后有效帧 Tick、最后帧间隔 DWT 周期 | +| 10~15 | DMA 接收重启次数、重启失败次数、最后 UART ErrorCode | +| 16~19 | 最近重启 HAL 状态、拼帧长度、待处理帧长度、连接超时口径 | +| 20~39 | 10 个完整 32 位统计:RX 事件、有效帧、TX 帧、CRC 错误、忽略站号、非法功能、非法地址、非法值、丢帧、UART 错误 | + +固件内部也可调用 `ModbusSlaveGetRuntimeDiagnostics()` 获取这些诊断值。Cortex-M4 对齐的 32 位及更小字段保证单字段不撕裂;为避免阻塞运动中断,完整结构允许跨相邻 UART 事件,不应解释为事务级原子快照。USB CDC 提供 `CDC_GetRuntimeDiagnostics()`,记录 OUT 包/字节、接收重挂失败、发送请求/忙/失败/完成;USB OTG FS 中断优先级为 4,低于 PLSR 输出定时器优先级 1 和 TIM6 优先级 2。 + +同一组USB统计自动发布到控制窗口D1516~D1537,32位字段均为低WORD在前。D1516~D1517与D1536~D1537是首尾generation,读取时必须相同且为偶数;D1518为USB诊断版本1,D1519为`initialized`;D1520~D1535依次为`rxPacketCount`、`rxByteCount`、`rxRearmFailureCount`、`txRequestCount`、`txByteCount`、`txBusyCount`、`txFailureCount`、`txCompleteCount`。Host回归构建发布相同布局、版本和generation,但`initialized`及八个统计值固定为0。 + +## 基本运行 + +本脚本只能配合专用 `P18 LONG_STRESS` 验证固件运行:编译前令 `PLSR_BOARD_TEST_SELECT=PLSR_BOARD_TEST_LONG_STRESS`,再编译、烧录并硬复位。P18 为四轴准备 K4 100 kHz 平台参数,保留 Q4~Q7 的 PULSE/DIR 方向映射,并明确禁用软限位和硬限位输入。不要使用 P14/P15/P17 固件运行长稳脚本;它们的 K1 软限位会在约 1,000,000 脉冲处按设计停止。 + +P18 是无软限位的台架耐久测试配置,只能在确认机构脱开、运动范围安全或仅连接逻辑分析仪时使用。默认 COM5、100 kHz、30 分钟: + +```powershell +python HostComputer\plsr_modbus_long_stress_test.py +``` + +快速冒烟测试: + +```powershell +python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 60 +``` + +脚本固定选择 P18 的 K4,自动完成四轴位置清零、重复序号幂等检查、COMMIT/START、状态一致性轮询、`0x10` 原子动态频率切换,并在结束时对四轴执行软件立即停止。停止清理会逐轴尝试,不会因为某一轴已经停止或应答异常而跳过后续轴。不会要求在 IAR Watch 中改值。 + +证据默认写入 `HostComputer/long_stress_logs/`: + +- CSV:每个采样点的四轴状态、频率、位置/任务/物理计数和 Modbus 32 位统计; +- JSON:参数、事件、Modbus与USB设备端起止诊断快照、统计增量、USB主机发送量和最终状态。 + +## 可选压力与恢复用例 + +USB CDC OUT 与 Modbus 并发(将 COM8 替换为实际 USB 虚拟串口): + +```powershell +python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 1800 --usb-port COM8 --usb-rate 64000 +``` + +每 60 秒注入一次仅 CRC 错误的只读请求;固件必须静默丢弃,随后正常通信,`crcErrorCount` 每次至少加 1: + +```powershell +python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 600 --bad-crc-period 60 +``` + +运行 120 秒时由主机主动关闭 COM5 3 秒,再重新连接;断线期间四轴脉冲计数必须继续增长: + +```powershell +python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 600 --disconnect-at 120 --disconnect-duration 3 +``` + +三个压力项也可组合。故障注入默认关闭,避免日常回归意外中断通信。 + +## 验收准则 + +- 脚本最终输出 `长稳 PASS`,CSV/JSON 均成功生成; +- 全程轴 `error=0`、`last_result=0`,状态快照首尾 generation 相同且为偶数; +- 四轴 `physical_pulses` 单调不回退,动态频率由 FC16 一次写两个 WORD; +- 非故障注入基线下 `uart_errors`、`dropped_frames`、`restart_failures` 不增长;如增长,JSON 必须保留差值并按失败处理; +- 开启坏 CRC 时,只允许 `crc_errors` 按注入次数增长,不得出现运动错误或通信失联; +- 开启计划断线时,重连成功且断线期间四轴计数继续增长; +- 开启 USB 时,脚本自动读取D1516~D1537起止快照:结束快照必须`initialized=1`,`rxPacketCount`与`rxByteCount`必须增长,`rxRearmFailureCount`不得增长;同时保留主机`bytes_written`且要求`write_errors=0`,无需IAR Watch; +- 逻辑分析仪抽检 Q0~Q3:频率切换连续,无小于 1 us 窄脉冲、无串扰,停止后全低。 + +长稳结束后,若需要严格单任务脉冲总数,另跑 P14/P16 脚本和逻辑分析仪 200000 沿验收;本脚本以持续运行、通信恢复和计数单调性为目标。 + +## 持久化专项边界 + +HSD Backup SRAM双检查点、SFD Flash A/B槽、generation、CRC和故障回退工具已完成代码与Host故障注入覆盖;破坏性持久化诊断在正式构建中默认关闭。真实VBAT断主电恢复、Flash擦写/坏CRC回退、异常复位清除`position_valid`以及反复保存对运动无抖动的板测仍未关闭,必须在所有轴安全停止时单独执行;本长稳脚本不会也不应在运动中发起Flash保存或擦除。 diff --git a/HostComputer/PLSR_MODBUS_PERFORMANCE_TEST.md b/HostComputer/PLSR_MODBUS_PERFORMANCE_TEST.md new file mode 100644 index 0000000..6beb0a4 --- /dev/null +++ b/HostComputer/PLSR_MODBUS_PERFORMANCE_TEST.md @@ -0,0 +1,67 @@ +# PLSR P16 实时性能统计测试 + +## 1 功能 + +固件使用STM32F407的DWT周期计数器,持续记录以下路径从本次复位以来的最坏执行周期: + +- `PlsrProcess`任务自身CPU执行时间; +- `PlsrProcess`包含高优先级中断抢占的墙钟响应时间; +- TIM6 10kHz控制刷新中断; +- TIM10/11/13/14输出定时器中断; +- TIM9/TIM12硬件计数中断; +- 双AB末周期快速门控路径。 +- `PlsrProcess`六个内部阶段的自身CPU时间,用于定位超过1ms的峰值路径。 + +统计结果通过D1200~D1537控制窗口中的D1256~D1263和D1456~D1469发布。D1205~D1206给出CPU计时频率,D1207为性能统计版本V7;V7保留D1470~D1515持久化诊断并增加D1516~D1537 USB CDC只读统计,但不改变P16各计时字段。该区域属于项目诊断增强,不对应信捷固定软元件。 + +| 地址 | 内容 | +| --- | --- | +| D1256~D1257 | PlsrProcess自身CPU最大周期数(32位) | +| D1258~D1259 | PlsrProcess墙钟响应最大周期数(32位) | +| D1260~D1261 | TIM6控制ISR最大周期数(32位) | +| D1262 | 输出定时器ISR最大周期数(16位饱和) | +| D1263 | TIM9/TIM12计数ISR最大周期数(16位饱和) | +| D1456~D1457 | 脉冲合并与保护检查最大周期数(32位) | +| D1458~D1459 | 关键事件处理最大周期数(32位) | +| D1460~D1461 | 命令队列处理最大周期数(32位) | +| D1462~D1463 | 普通事件与方向批量提交最大周期数(32位) | +| D1464~D1465 | HAL tick、路径和Profile推进最大周期数(32位) | +| D1466~D1467 | 合并后的HSD Backup SRAM检查点最大周期数(32位) | +| D1468~D1469 | 双AB末周期快速门控最大周期数(32位) | + +## 2 测试方法 + +重新复位开发板后运行P14脚本: + +```powershell +python HostComputer\plsr_modbus_counter_stress_test.py --port COM5 +``` + +脚本在四轴100kHz、Modbus持续轮询完成后自动读取P16数据,显示cycles和微秒,不需要手工读取Watch。 + +## 3 自动预算 + +- `PlsrProcess`自身CPU时间必须小于1ms对应周期数。 +- `PlsrProcess`墙钟响应单独显示;超过1ms时给出抢占提示,不与函数自身WCET混算。 +- TIM6控制ISR必须小于0.1ms对应周期数。 +- 输出ISR和计数ISR必须小于100kHz的10us周期数。 +- 双AB压力测试中,末周期快速门控必须非零且小于420 cycles(168MHz下为2.5us)。P14 PULSE/DIR用例不要求该项非零。 +- P14所覆盖的五项必须都被实际执行,数值不得为0。 +- 仍须同时满足P14四轴计数精确、无毛刺和无残余输出;执行时间通过不能替代波形验收。 + +建议把脚本输出的五个总体最坏值、六个阶段值和五个流程检查点记录到测试报告。V7保留自身CPU时间和包含抢占的墙钟响应时间、六个分阶段独立最大值及合并后的HSD检查点,并保留D1468~D1469的双AB快速门控统计;阶段最大值来自不同轮次,不能直接相加作为总执行时间。 + +## 4 已关闭的P16板测记录(2026-08-10) + +硬复位清除DWT历史最大值后,四轴PULSE/DIR 100kHz、每轴200000脉冲及Modbus并发轮询实测: + +- `PlsrProcess`自身最大53748 cycles(319.929us),低于168000 cycles预算; +- `PlsrProcess`响应最大85655 cycles(509.851us); +- TIM6控制ISR最大231 cycles(1.375us); +- 输出ISR最大893 cycles(5.315us); +- TIM9/TIM12计数ISR最大883 cycles(5.256us)。 + +P14端子波形同时确认Q0~Q3各严格200000个上升沿,因此本次P16 PULSE/DIR性能闭环验收通过。D1468~D1469只属于双AB新路径,仍须随双AB 100kHz板测单独关闭。 + +该记录是本轮AB、持久化V7窗口和USB诊断落盘前的已通过基线。烧录最终工作区后仍须硬复位并重跑 +P14/P16;只有新输出继续满足预算且四轴计数/波形不回退,才可作为本批次发布回归证据。 diff --git a/HostComputer/PLSR_MODBUS_SOFT_LIMIT_MATRIX_TEST.md b/HostComputer/PLSR_MODBUS_SOFT_LIMIT_MATRIX_TEST.md new file mode 100644 index 0000000..05b101f --- /dev/null +++ b/HostComputer/PLSR_MODBUS_SOFT_LIMIT_MATRIX_TEST.md @@ -0,0 +1,41 @@ +# PLSR P15 四轴软限位精度测试 + +## 1 测试内容 + +本测试与P14复用同一固件和D1200控制窗口,不需要修改Watch变量。固件公共参数将四轴软限位配置为`-1000000~+1000000`;K1继续用于P14四轴100kHz测试,K2用于以下保护矩阵: + +| 轴 | 方向 | 初始位置 | 频率 | 理论输出 | +| --- | --- | ---: | ---: | ---: | +| Q0 | 正 | +999800 | 500Hz | 约200脉冲 | +| Q1 | 正 | +999000 | 2000Hz | 约1000脉冲 | +| Q2 | 负 | -999800 | 500Hz | 约200脉冲 | +| Q3 | 负 | -999000 | 2000Hz | 约1000脉冲 | + +每组减速时间均为100ms。脚本逐轴执行,检查正负限位、逻辑位置、任务计数、物理累计、兼容停止原因和错误复位。 + +## 2 测试步骤 + +1. 编译并烧录当前工程,复位开发板。 +2. 逻辑分析仪连接Q0、Q1、Q2、Q3和GND,采样率建议不低于1MS/s,采集至少8秒。 +3. 执行: + +```powershell +python HostComputer\plsr_modbus_soft_limit_matrix_test.py --port COM5 +``` + +脚本可在P14压力测试之后直接运行;每组开始前都会自动设置当前位置,并在结束后清除该轴错误。 + +## 3 验收标准 + +- 四轴均因对应软限位进入`PLSR_STATE_STOPPED`,不得进入正常完成状态。 +- 最终逻辑位置与`+1000000`或`-1000000`的差值不超过1脉冲。 +- Q0/Q2上升沿分别为199~201;Q1/Q3分别为999~1001。 +- 正限位错误码为`PLSR_ERROR_LIMIT_POSITIVE`,负限位为`PLSR_ERROR_LIMIT_NEGATIVE`。 +- 每次`RESET_ERROR`后轴回到IDLE且错误清零。 +- 减速段连续、无窄脉冲、截断脉冲和停止后残余输出。 + +如果脚本位置通过而逻辑分析仪脉冲数不通过,应记录对应轴的上升沿数;这表示硬件内部计数边界仍与端子物理边沿不一致。 + +## 4 已关闭的板测记录(2026-08-10) + +四轴正负方向、500/2000Hz软限位矩阵已由用户真机确认通过。修复后的保护距离使用当前硬件实际频率并补偿1ms保护采样窗口,最终位置保持在目标软限位的±1脉冲范围内。后续修改保护、计数或停止路径时仍应重跑本脚本回归。 diff --git a/HostComputer/PLSR_PERSISTENCE_BOARD_TEST.md b/HostComputer/PLSR_PERSISTENCE_BOARD_TEST.md new file mode 100644 index 0000000..80e6670 --- /dev/null +++ b/HostComputer/PLSR_PERSISTENCE_BOARD_TEST.md @@ -0,0 +1,116 @@ +# PLSR HSD/SFD 真机持久化验收 + +本测试验证 STM32F407 板上的真实 Backup SRAM/VBAT 和单 Bank Flash 行为。脚本默认使用 `COM5`,阶段之间由操作者真实断主电或硬复位,不用软件复位代替掉电。 + +## 固件与诊断窗口 + +当前控制窗口为 D1200~D1537,D1470~D1499 是只读持久化快照;新增的D1516~D1537 USB诊断块不会移动持久化地址。固件只在 HSD/SFD 的加载、保存、擦除或显式破坏性诊断时计算 CRC;1ms 的 `PlsrModbusControlPoll()` 只复制缓存结果,不扫描 Backup SRAM/Flash,因此不会重新引入 P16 热点。 + +| 地址 | 内容 | +| --- | --- | +| D1470~D1471 / D1498~D1499 | 一致性 generation(首尾相同且为偶数) | +| D1472 | 持久化诊断版本,当前 V1 | +| D1473 | 低字节 HSD valid mask,高字节 SFD valid mask | +| D1474 | 低字节 HSD newest mask,高字节 SFD newest mask | +| D1475 | bit0 HSD dirty;bit1 SFD dirty;bit2 restored position valid(四轴位置均有效时才为1);bit3 restored last busy;bit4 破坏性诊断已编译启用 | +| D1476~D1480 | HSD/SFD last load、last save、last erase 结果 | +| D1482~D1489 | HSD A/B 与 SFD A/B generation | +| D1490~D1493 | 本次上电后成功 HSD/SFD 实际保存次数 | +| D1494~D1497 | 当前选中 HSD/SFD 记录的已存 CRC32 | + +`save_count` 只在真正完成记录提交时增加。SFD 为 clean 时执行 SAVE 是 no-op,不擦 Flash,也不增加次数。 + +## 依赖与烧录 + +```powershell +py -m pip install -r HostComputer\requirements.txt + +& 'F:\IAR Systems\Embedded Workbench 8.3\common\bin\IarBuild.exe' ` + 'EWARM\Modbus.ewp' -build Modbus -log warnings +``` + +烧录后硬复位。先确认诊断可读: + +```powershell +python HostComputer\plsr_persistence_board_test.py --phase diagnostics +``` + +阶段状态默认保存在系统临时目录的 `plsr_persistence_board_state.json`;可用 `--state-file` 指定别处。 + +## 1. HSD + VBAT 正常停机恢复 + +```powershell +python HostComputer\plsr_persistence_board_test.py --phase hsd-prepare +``` + +脚本自动给四轴写入互不相同的已知位置,等待 HSD 检查点真正完成,并保存期望值。看到 PASS 后关闭主电源,保持 VBAT,等待数秒,再上主电并执行: + +```powershell +python HostComputer\plsr_persistence_board_test.py --phase hsd-verify +``` + +验收:四轴位置分别与脚本保存的期望值完全一致、四轴 `position_valid=1`、无脉冲输出、HSD load=OK、restored last busy=0。固件以四轴 `position_valid` 的保守 AND 写入全局元数据,单独校准一轴不会把其他未校准轴误判为可信。 + +## 2. 运行中掉电/复位安全恢复 + +```powershell +python HostComputer\plsr_persistence_board_test.py --phase busy-prepare +``` + +脚本启动一个约 500 秒的长任务并确认端子正在输出。看到“运行中掉电已就绪”后,直接断主电或硬复位,不要发送 STOP。重启后执行: + +```powershell +python HostComputer\plsr_persistence_board_test.py --phase busy-verify +``` + +验收:所有轴 IDLE、无自动续跑、无输出、所有轴 `position_valid=0`,诊断显示 restored last busy=1。 + +## 3. SFD Flash A/B + CRC 上电恢复 + +验证固件的 P14/P17 自测准备会在启动后写入确定的 SFD RAM 配置并置 dirty;正式固件也可由正常参数配置入口置 dirty。执行一次: + +```powershell +python HostComputer\plsr_persistence_board_test.py --phase sfd-save +``` + +脚本只发送一次 SAVE_CONFIG。若 SFD dirty,要求 generation 前进、save_count 只增加 1、CRC 非零;若 SFD 已 clean,则要求 SAVE 正确 no-op,绝不重复擦写。随后硬复位或掉电重启,再执行: + +```powershell +python HostComputer\plsr_persistence_board_test.py --phase sfd-verify +``` + +验收:启动时 SFD load=OK,选中槽的 generation 与 CRC 和保存阶段完全一致。 + +## 4. 运动中禁止 Flash 擦写 + +```powershell +python HostComputer\plsr_persistence_board_test.py --phase motion-save-busy +``` + +脚本启动长任务,只发送一次 SAVE_CONFIG,要求内核执行结果为 BUSY 且 SFD save_count 不增加,随后自动 `STOP_IMMEDIATE` 清理。该测试不要求逻辑分析仪,但应观察运动过程中没有因 Flash 擦除产生输出停顿。 + +## 5. 可选 A/B 回退破坏性测试 + +正常构建的 `PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG=0`,请求一定返回 NOT_SUPPORTED。只有专用验证构建显式改为 1 时才允许使用。固件仍会同时检查: + +- 两个固定 magic; +- 非零 sequence 及其 32 位反码; +- 固定 arm 字; +- 所有轴 idle 且无输出; +- 目标介质必须已有两个 CRC 有效槽; +- 只能失效 newest 槽,协议不接受任意 Flash/内存地址。 + +执行还需要主机侧双重显式确认: + +```powershell +python HostComputer\plsr_persistence_board_test.py ` + --phase invalidate-sfd ` + --allow-destructive ` + --confirm INVALIDATE-NEWEST-SLOT +``` + +HSD 对应用 `--phase invalidate-hsd`。失效后应只剩一个有效槽;立即重启/LOAD,确认回退到旧 generation。测试结束务必恢复默认宏并重新烧录生产固件。 + +## 记录要求 + +每个阶段保存完整终端输出,至少记录 valid mask、selected generation、CRC、save_count、last load/save 结果和 flags。掉电阶段同时记录 VBAT 是否保持、断电时长、复位方式与上电时间。不得把 Host 模拟测试替代为上述真实掉电证据。 diff --git a/HostComputer/plsr_modbus_ab_stress_test.py b/HostComputer/plsr_modbus_ab_stress_test.py new file mode 100644 index 0000000..2fb554c --- /dev/null +++ b/HostComputer/plsr_modbus_ab_stress_test.py @@ -0,0 +1,492 @@ +#!/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) diff --git a/HostComputer/plsr_modbus_bit_input_test.py b/HostComputer/plsr_modbus_bit_input_test.py new file mode 100644 index 0000000..d2db699 --- /dev/null +++ b/HostComputer/plsr_modbus_bit_input_test.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Validate the production Modbus M -> PLSR WAIT bit-data path. + +X is intentionally read-only through function 0x02. A real X/EXT/hard-limit +test is only possible after the board-specific GPIO-to-X mapping is supplied. +""" + +from __future__ import annotations + +import argparse +import struct +import time + +import serial + +from plsr_modbus_control_test import ( + CALL_COMMIT, + CALL_START, + CONTROL_BASE, + CONTROL_WINDOW_WORDS, + S0_BASE, + S1_BASE, + check_result, + read_axis_status, + send_call, +) +from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words + + +RESULT_OK = 0 +STATE_WAIT = 5 +STATE_COMPLETED = 7 +M_TEST_POINT = 123 + + +def write_coil(client: RtuClient, address: int, value: bool) -> None: + encoded = 0xFF00 if value else 0x0000 + pdu = bytes((0x05,)) + struct.pack(">HH", address, encoded) + response = client.exchange(pdu, 8) + expected = bytes((client.slave, 0x05)) + struct.pack(">HH", address, encoded) + if response[:6] != expected: + raise RuntimeError(f"FC05 回显错误: {response.hex(' ')}") + + +def read_bits(client: RtuClient, function: int, address: int, count: int) -> list[int]: + byte_count = (count + 7) // 8 + pdu = bytes((function,)) + struct.pack(">HH", address, count) + response = client.exchange(pdu, 5 + byte_count) + if response[1] != function or response[2] != byte_count: + raise RuntimeError(f"FC{function:02X} 响应格式错误: {response.hex(' ')}") + return [ + (response[3 + (index // 8)] >> (index % 8)) & 1 + for index in range(count) + ] + + +def wait_for_state(client: RtuClient, expected: int, timeout: float) -> dict[str, int]: + deadline = time.monotonic() + timeout + latest: dict[str, int] | None = None + while time.monotonic() < deadline: + latest = read_axis_status(client) + if latest["state"] == expected: + return latest + raise RuntimeError(f"等待状态 {expected} 超时,最后状态: {latest}") + + +def prepare_wait_job(client: RtuClient) -> None: + words = [0] * 20 + words[0:2] = signed_dword_words(1) # one segment + words[10:12] = signed_dword_words(1000) # 1000 Hz + words[12:14] = signed_dword_words(500) # +500 pulses + words[14] = (2 << 8) | 5 # WAIT_SIGNAL, source M + words[15:17] = signed_dword_words(M_TEST_POINT) + words[17] = 0 # constant fall-through jump + words[18:20] = signed_dword_words(0) + client.write_multiple(S0_BASE, words) + client.write_multiple(S1_BASE, [0, 0, 0, 0]) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="PLSR 真实 Modbus M 位源、WAIT 与 FC02 X 只读视图测试" + ) + parser.add_argument("--port", default="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[3] != CONTROL_WINDOW_WORDS: + raise RuntimeError( + f"控制窗口版本不匹配: firmware={header[3]}, script={CONTROL_WINDOW_WORDS}" + ) + + # FC02 must exist and must not alias writable M coils. + x_before = read_bits(client, 0x02, M_TEST_POINT, 1)[0] + write_coil(client, M_TEST_POINT, False) + if read_bits(client, 0x01, M_TEST_POINT, 1) != [0]: + raise RuntimeError("FC05 写 M=0 后 FC01 回读不一致") + if read_bits(client, 0x02, M_TEST_POINT, 1)[0] != x_before: + raise RuntimeError("X 与 M 发生别名:写 M 意外改变了 FC02 X") + + prepare_wait_job(client) + sequence = int(time.time()) & 0x7FFFFFFF + response = send_call(client, sequence, CALL_COMMIT) + check_result(response, RESULT_OK, "COMMIT") + response = send_call(client, sequence + 1, CALL_START) + check_result(response, RESULT_OK, "START") + + waiting = wait_for_state(client, STATE_WAIT, 5.0) + if waiting["task_pulses"] != 500: + raise RuntimeError(f"进入 WAIT 时任务脉冲不是 500: {waiting}") + print("M123=0:500 脉冲完成后稳定进入 WAIT") + + write_coil(client, M_TEST_POINT, True) + if read_bits(client, 0x01, M_TEST_POINT, 1) != [1]: + raise RuntimeError("FC05 写 M=1 后 FC01 回读不一致") + completed = wait_for_state(client, STATE_COMPLETED, 3.0) + if completed["task_pulses"] != 500: + raise RuntimeError(f"WAIT 释放后任务计数异常: {completed}") + print("M123 0->1:PLSR WAIT 已释放并正常 COMPLETED") + print(f"FC02 X123 只读值={x_before};写 M 不会改变 X") + print("PASS:Modbus FC05/FC01 -> M image -> PLSR readBit/WAIT 生产链通过") + print("待硬件映射后再测:实际 X 输入、EXT 上升沿及正/负硬限位。") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (RuntimeError, serial.SerialException) as error: + print(f"测试失败:{error}") + raise SystemExit(1) diff --git a/HostComputer/plsr_modbus_control_test.py b/HostComputer/plsr_modbus_control_test.py index b5931bb..805ab00 100644 --- a/HostComputer/plsr_modbus_control_test.py +++ b/HostComputer/plsr_modbus_control_test.py @@ -12,6 +12,7 @@ from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_word CONTROL_BASE = 1200 +CONTROL_WINDOW_WORDS = 338 S0_BASE = 1600 S1_BASE = 1700 CALL_REQUEST = CONTROL_BASE + 8 @@ -122,6 +123,7 @@ def read_axis_status(client: RtuClient) -> dict[str, int]: "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), } @@ -186,9 +188,15 @@ def main() -> int: ) as uart: client = RtuClient(uart, args.slave) header = client.read_holding(CONTROL_BASE, 8) - if header[:5] != [0x504C, 0x5352, 0x0100, 256, 0x0007]: + if header[:5] != [ + 0x504C, + 0x5352, + 0x0100, + CONTROL_WINDOW_WORDS, + 0x0007, + ]: raise RuntimeError(f"P13 控制窗口未就绪:{header}") - print("P13 控制窗口就绪:D1200~D1455,协议 V1.0") + print("P13 控制窗口就绪:D1200~D1537,协议 V1.0") s0 = [0] * 20 put_u32(s0, 0, 1) diff --git a/HostComputer/plsr_modbus_counter_stress_test.py b/HostComputer/plsr_modbus_counter_stress_test.py new file mode 100644 index 0000000..f6b0fa3 --- /dev/null +++ b/HostComputer/plsr_modbus_counter_stress_test.py @@ -0,0 +1,371 @@ +#!/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) diff --git a/HostComputer/plsr_modbus_long_stress_test.py b/HostComputer/plsr_modbus_long_stress_test.py new file mode 100644 index 0000000..ce09ec9 --- /dev/null +++ b/HostComputer/plsr_modbus_long_stress_test.py @@ -0,0 +1,912 @@ +#!/usr/bin/env python3 +"""Long-duration four-axis PLSR/Modbus/optional USB concurrency test. + +Requires the dedicated P18 firmware configuration (K4, soft limits disabled). +The tool creates one long PULSE/DIR segment on every axis, continuously reads +generation-protected status snapshots, alternates the live frequency using one +FC16 transaction per 32-bit value, and writes CSV plus JSON evidence. Optional +bad-CRC and planned-disconnect probes are disabled unless explicitly requested. +No persistence SAVE command is issued by this script. +""" + +from __future__ import annotations + +import argparse +import csv +import json +import math +import struct +import threading +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import serial + +from plsr_modbus_frequency_test import RtuClient, add_crc, signed_dword_words + + +CONTROL_BASE = 1200 +CONTROL_WINDOW_WORDS = 338 +PERFORMANCE_VERSION = 7 +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 +USB_DIAGNOSTICS_BASE = CONTROL_BASE + 316 +USB_DIAGNOSTICS_WORDS = 22 +USB_DIAGNOSTICS_VERSION = 1 +S0_BASES = (1600, 1800, 2000, 2200) +S1_BASES = (1700, 1900, 2100, 2300) + +RUNTIME_DIAGNOSTICS_FUNCTION = 0x47 +RUNTIME_DIAGNOSTICS_SIGNATURE = 0x4D42 +RUNTIME_DIAGNOSTICS_VERSION = 1 +RUNTIME_DIAGNOSTICS_WORDS = 40 + +RESULT_OK = 0 +RESULT_QUEUED = 1 +RESULT_INVALID_STATE = 4 +STATE_IDLE = 1 +STATE_ACCEL = 2 +STATE_RUN = 3 +STATE_DECEL = 4 +STATE_COMPLETED = 7 +STATE_STOPPED = 8 +STATE_ERROR = 9 +CALL_COMMIT = 1 +CALL_START = 2 +CMD_STOP_IMMEDIATE = 2 +CMD_SET_POSITION = 5 + +CSV_FIELDS = ( + "host_time_utc", + "elapsed_s", + "sample", + "axis", + "state", + "flags", + "error", + "last_result", + "counter_mode", + "current_frequency", + "target_frequency", + "logical_position", + "task_pulses", + "physical_pulses", + "snapshot_retries", + "modbus_valid_frames", + "modbus_tx_frames", + "modbus_crc_errors", + "modbus_dropped_frames", + "modbus_uart_errors", + "modbus_restart_failures", + "modbus_last_uart_error", +) + + +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: + value = sum(words[offset + index] << (16 * index) for index in range(4)) + if signed and value & (1 << 63): + value -= 1 << 64 + return value + + +def wait_response( + client: RtuClient, + address: int, + quantity: int, + sequence: int, + timeout: float = 3.0, +) -> list[int]: + deadline = time.monotonic() + timeout + latest: list[int] = [] + while time.monotonic() < deadline: + latest = client.read_holding(address, quantity) + if get_u32(latest, 0) == sequence: + return latest + raise RuntimeError(f"等待命令序号 {sequence} 应答超时;最后应答={latest}") + + +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, +) -> list[int]: + request = [0] * 16 + put_u32(request, 0, sequence) + request[2] = 0 # S0 is D + put_u32(request, 3, S0_BASES[axis]) + request[5] = 0 # S1 is D + put_u32(request, 6, S1_BASES[axis]) + request[8] = 0 # S2 is constant K4 (dedicated P18 long-stress setup) + put_u32(request, 10, 4) + 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, attempts: int = 4 +) -> tuple[dict[str, int], int]: + """Read one coherent generation-guarded axis status snapshot.""" + address = AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS + for retry in range(attempts): + 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 and not generation_begin & 1: + return ( + { + "generation": generation_begin, + "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), + }, + retry, + ) + raise RuntimeError(f"轴{axis}状态快照连续 {attempts} 次版本不一致") + + +def read_runtime_diagnostics(client: RtuClient) -> dict[str, Any]: + pdu = bytes((RUNTIME_DIAGNOSTICS_FUNCTION,)) + struct.pack( + ">HH", 0, RUNTIME_DIAGNOSTICS_WORDS + ) + response = client.exchange(pdu, 5 + RUNTIME_DIAGNOSTICS_WORDS * 2) + if ( + response[1] != RUNTIME_DIAGNOSTICS_FUNCTION + or response[2] != RUNTIME_DIAGNOSTICS_WORDS * 2 + ): + raise RuntimeError(f"0x47 诊断应答格式错误:{response.hex(' ')}") + words = list( + struct.unpack(f">{RUNTIME_DIAGNOSTICS_WORDS}H", response[3:-2]) + ) + if words[:3] != [ + RUNTIME_DIAGNOSTICS_SIGNATURE, + RUNTIME_DIAGNOSTICS_VERSION, + RUNTIME_DIAGNOSTICS_WORDS, + ]: + raise RuntimeError(f"Modbus 运行诊断版本不匹配:{words[:3]}") + stat_names = ( + "rx_events", + "valid_frames", + "tx_frames", + "crc_errors", + "ignored_addresses", + "illegal_functions", + "illegal_addresses", + "illegal_values", + "dropped_frames", + "uart_errors", + ) + statistics = { + name: get_u32(words, 20 + index * 2) + for index, name in enumerate(stat_names) + } + return { + "flags": words[3], + "initialized": bool(words[3] & (1 << 0)), + "connected": bool(words[3] & (1 << 2)), + "tx_busy": bool(words[3] & (1 << 3)), + "rx_restart_ok": bool(words[3] & (1 << 6)), + "current_tick": get_u32(words, 4), + "last_valid_frame_tick": get_u32(words, 6), + "last_inter_frame_gap_cycles": get_u32(words, 8), + "restart_attempts": get_u32(words, 10), + "restart_failures": get_u32(words, 12), + "last_uart_error": get_u32(words, 14), + "last_receive_start_status": words[16], + "rx_assembly_length": words[17], + "rx_frame_length": words[18], + "statistics": statistics, + } + + +def read_usb_diagnostics( + client: RtuClient, attempts: int = 4 +) -> dict[str, Any]: + """Read one coherent generation-guarded USB CDC diagnostic block.""" + counter_names = ( + "rx_packet_count", + "rx_byte_count", + "rx_rearm_failure_count", + "tx_request_count", + "tx_byte_count", + "tx_busy_count", + "tx_failure_count", + "tx_complete_count", + ) + for retry in range(attempts): + words = client.read_holding(USB_DIAGNOSTICS_BASE, USB_DIAGNOSTICS_WORDS) + generation_begin = get_u32(words, 0) + generation_end = get_u32(words, 20) + if generation_begin == generation_end and not generation_begin & 1: + if words[2] != USB_DIAGNOSTICS_VERSION: + raise RuntimeError( + "USB CDC 诊断版本不匹配:" + f"读取={words[2]},要求={USB_DIAGNOSTICS_VERSION}" + ) + return { + "generation": generation_begin, + "version": words[2], + "initialized": bool(words[3]), + "snapshot_retries": retry, + "counters": { + name: get_u32(words, 4 + index * 2) + for index, name in enumerate(counter_names) + }, + } + raise RuntimeError(f"USB CDC 诊断块连续 {attempts} 次版本不一致") + + +def inject_bad_crc(port: serial.Serial, slave: int, baud: int) -> None: + """Send a read-only request with a deliberately invalid CRC.""" + valid = add_crc(bytes((slave, 0x03)) + struct.pack(">HH", CONTROL_BASE, 1)) + malformed = valid[:-1] + bytes((valid[-1] ^ 0x01,)) + port.reset_input_buffer() + port.write(malformed) + port.flush() + # Match the firmware's Modbus RTU timing rule and leave a small host margin. + t35_seconds = 0.00175 if baud > 19_200 else (3.5 * 11.0 / baud) + time.sleep(t35_seconds + 0.005) + + +def open_modbus(args: argparse.Namespace) -> tuple[serial.Serial, RtuClient]: + port = serial.Serial( + port=args.port, + baudrate=args.baud, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_EVEN, + stopbits=serial.STOPBITS_ONE, + timeout=args.timeout, + write_timeout=args.timeout, + ) + return port, RtuClient(port, args.slave) + + +def reopen_modbus(args: argparse.Namespace) -> tuple[serial.Serial, RtuClient]: + """Reopen a planned/lost link with a bounded number of host retries.""" + last_error: BaseException | None = None + attempts = max(1, args.max_communication_errors) + for _ in range(attempts): + try: + return open_modbus(args) + except (OSError, serial.SerialException) as error: + last_error = error + time.sleep(args.reconnect_delay) + raise RuntimeError( + f"连续 {attempts} 次无法重新打开 {args.port}:{last_error}" + ) from last_error + + +class UsbOutPressure: + def __init__(self, port: str | None, bytes_per_second: int) -> None: + self.port = port + self.bytes_per_second = bytes_per_second + self.bytes_written = 0 + self.write_errors = 0 + self.last_error = "" + self._stop = threading.Event() + self._thread: threading.Thread | None = None + + def start(self) -> None: + if self.port is None: + return + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + + def stop(self) -> None: + self._stop.set() + if self._thread is not None: + self._thread.join(timeout=3.0) + + def _run(self) -> None: + payload = (b"PLSR-USB-CDC-OUT-STRESS-" * 3)[:64] + try: + with serial.Serial( + self.port, + baudrate=115200, + timeout=0.2, + write_timeout=1.0, + ) as usb: + next_send = time.monotonic() + while not self._stop.is_set(): + usb.write(payload) + usb.flush() + self.bytes_written += len(payload) + if self.bytes_per_second > 0: + next_send += len(payload) / self.bytes_per_second + delay = next_send - time.monotonic() + if delay > 0: + self._stop.wait(delay) + elif delay < -1.0: + next_send = time.monotonic() + except (OSError, serial.SerialException) as error: + self.write_errors += 1 + self.last_error = str(error) + + def summary(self) -> dict[str, Any]: + return { + "port": self.port, + "target_bytes_per_second": self.bytes_per_second, + "bytes_written": self.bytes_written, + "write_errors": self.write_errors, + "last_error": self.last_error, + } + + +def prepare_jobs(client: RtuClient, frequency: int, pulses: int) -> None: + for axis in range(4): + s0 = [0] * 20 + put_u32(s0, 0, 1) + put_u32(s0, 10, frequency) + put_u32(s0, 12, pulses) + client.write_multiple(S0_BASES[axis], s0) + client.write_multiple(S1_BASES[axis], [0] * 4) + + +def stop_all_axes(client: RtuClient, sequence: int) -> int: + send_failures: list[str] = [] + + for axis in range(4): + try: + response = send_command( + client, sequence, axis, CMD_STOP_IMMEDIATE, argument=0 + ) + if response[4] not in { + RESULT_OK, + RESULT_QUEUED, + RESULT_INVALID_STATE, + }: + send_failures.append( + f"轴{axis} STOP_IMMEDIATE 返回 {response[4]}" + ) + except (RuntimeError, serial.SerialException, OSError) as error: + # Never let one failed/already-stopped axis prevent stop attempts + # for the remaining axes. + send_failures.append(f"轴{axis} STOP_IMMEDIATE 异常:{error}") + sequence += 1 + + deadline = time.monotonic() + 8.0 + latest: dict[int, dict[str, int]] = {} + terminal_states = {STATE_IDLE, STATE_COMPLETED, STATE_STOPPED, STATE_ERROR} + while time.monotonic() < deadline: + for axis in range(4): + try: + latest[axis] = read_axis_status(client, axis)[0] + except (RuntimeError, serial.SerialException, OSError) as error: + send_failures.append(f"轴{axis}停止状态读取异常:{error}") + if len(latest) == 4 and all( + latest[axis]["state"] in terminal_states + and (latest[axis]["flags"] & (1 << 1)) == 0 + for axis in range(4) + ): + return sequence + raise RuntimeError( + "STOP_IMMEDIATE 后仍有轴未确认安全停止:" + f"status={latest};发送/读取异常={send_failures}" + ) + + +def delta32(end: int, start: int) -> int: + return (end - start) & 0xFFFFFFFF + + +def is_retryable_communication_error(error: BaseException) -> bool: + if isinstance(error, serial.SerialException): + return True + if not isinstance(error, RuntimeError): + return False + message = str(error) + return message.startswith("响应超时:") or message.startswith("响应 CRC 错误:") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="PLSR Modbus/USB 长稳并发测试") + parser.add_argument("--port", default="COM5", help="Modbus RTU 串口,默认 COM5") + parser.add_argument("--baud", type=int, default=9600) + parser.add_argument("--slave", type=int, default=1) + parser.add_argument("--timeout", type=float, default=1.5) + parser.add_argument("--duration", type=float, default=1800.0, help="运行秒数") + parser.add_argument("--frequency", type=int, default=100_000) + parser.add_argument( + "--low-frequency", + type=int, + help="动态频率低值,默认主频率的一半", + ) + parser.add_argument("--status-period", type=float, default=1.0) + parser.add_argument( + "--frequency-period", + type=float, + default=10.0, + help="动态频率切换周期;0 表示禁用", + ) + parser.add_argument( + "--bad-crc-period", + type=float, + default=0.0, + help="坏 CRC 注入周期;默认 0(禁用)", + ) + parser.add_argument( + "--disconnect-at", + type=float, + default=0.0, + help="运行到指定秒数时主动断开串口;默认 0(禁用)", + ) + parser.add_argument("--disconnect-duration", type=float, default=3.0) + parser.add_argument("--usb-port", help="可选 USB CDC 虚拟串口,例如 COM8") + parser.add_argument("--usb-rate", type=int, default=64_000, help="USB OUT B/s") + parser.add_argument("--max-communication-errors", type=int, default=5) + parser.add_argument("--reconnect-delay", type=float, default=1.0) + parser.add_argument( + "--output-dir", + type=Path, + default=Path("HostComputer/long_stress_logs"), + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + if args.baud <= 0: + raise RuntimeError("baud 必须大于 0") + if not 1 <= args.slave <= 247: + raise RuntimeError("slave 必须为 1~247") + if args.timeout <= 0: + raise RuntimeError("timeout 必须大于 0") + if args.duration <= 0 or args.status_period <= 0: + raise RuntimeError("duration 和 status-period 必须大于 0") + if args.frequency_period < 0 or args.bad_crc_period < 0: + raise RuntimeError("frequency-period 和 bad-crc-period 不得为负数") + if args.disconnect_at < 0 or args.disconnect_duration < 0: + raise RuntimeError("disconnect-at 和 disconnect-duration 不得为负数") + if args.disconnect_at > 0 and args.disconnect_duration <= 0: + raise RuntimeError("启用计划断线时 disconnect-duration 必须大于 0") + if args.usb_rate < 0: + raise RuntimeError("usb-rate 不得为负数") + if args.usb_port and args.usb_rate == 0: + raise RuntimeError("启用 USB 压力时 usb-rate 必须大于 0") + if args.max_communication_errors < 0: + raise RuntimeError("max-communication-errors 不得为负数") + if args.reconnect_delay < 0: + raise RuntimeError("reconnect-delay 不得为负数") + if not 1 <= args.frequency <= 100_000: + raise RuntimeError("frequency 必须为 1~100000Hz(K4/P18 配置上限)") + low_frequency = args.low_frequency or max(1, args.frequency // 2) + if not 1 <= low_frequency <= args.frequency: + raise RuntimeError("low-frequency 必须为 1~frequency") + if args.usb_port and args.usb_port.upper() == args.port.upper(): + raise RuntimeError("USB CDC 串口不能与 Modbus 串口相同") + + pulse_target = math.ceil( + (args.duration + max(0.0, args.disconnect_duration) + 120.0) + * args.frequency + ) + if pulse_target > 2_000_000_000: + raise RuntimeError("测试时长/频率使单段脉冲超过 20 亿;请降低时长或频率") + + args.output_dir.mkdir(parents=True, exist_ok=True) + run_id = datetime.now().strftime("%Y%m%d_%H%M%S") + csv_path = args.output_dir / f"plsr_long_stress_{run_id}.csv" + json_path = args.output_dir / f"plsr_long_stress_{run_id}.json" + events: list[dict[str, Any]] = [] + summary: dict[str, Any] = { + "started_utc": datetime.now(timezone.utc).isoformat(), + "arguments": { + key: str(value) if isinstance(value, Path) else value + for key, value in vars(args).items() + }, + "pulse_target": pulse_target, + "low_frequency": low_frequency, + "events": events, + "result": "FAIL", + } + usb_pressure = UsbOutPressure(args.usb_port, args.usb_rate) + uart: serial.Serial | None = None + client: RtuClient | None = None + sequence = 1000 + failure: BaseException | None = None + last_status: list[dict[str, int]] = [] + axes_stopped = False + + with csv_path.open("w", newline="", encoding="utf-8-sig") as csv_file: + writer = csv.DictWriter(csv_file, fieldnames=CSV_FIELDS) + writer.writeheader() + try: + uart, client = open_modbus(args) + header = client.read_holding(CONTROL_BASE, 8) + if ( + header[0:3] != [0x504C, 0x5352, 0x0100] + or header[3] != CONTROL_WINDOW_WORDS + or header[7] != PERFORMANCE_VERSION + ): + raise RuntimeError(f"PLSR 控制窗口未就绪:{header}") + diagnostics_start = read_runtime_diagnostics(client) + summary["diagnostics_start"] = diagnostics_start + usb_diagnostics_start: dict[str, Any] | None = None + if args.usb_port: + usb_diagnostics_start = read_usb_diagnostics(client) + summary["usb_device_diagnostics_start"] = usb_diagnostics_start + + prepare_jobs(client, args.frequency, pulse_target) + for axis in range(4): + response = send_command(client, sequence, axis, CMD_SET_POSITION, 0) + check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION") + if axis == 0: + replay = send_command(client, sequence, axis, CMD_SET_POSITION, 0) + if replay != response: + raise RuntimeError( + f"重复命令序号未回放同一应答:首次={response},重复={replay}" + ) + events.append({"elapsed_s": 0.0, "event": "idempotency_pass"}) + sequence += 1 + + 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 + + physical_baseline = [ + read_axis_status(client, axis)[0]["physical_pulses"] + for axis in range(4) + ] + for axis in range(4): + response = send_call(client, sequence, axis, CALL_START) + check_result(response, 3, RESULT_QUEUED, f"轴{axis} START") + sequence += 1 + + usb_pressure.start() + started = time.monotonic() + next_sample = started + next_frequency = ( + started + args.frequency_period + if args.frequency_period > 0 + else float("inf") + ) + next_bad_crc = ( + started + args.bad_crc_period + if args.bad_crc_period > 0 + else float("inf") + ) + disconnected = False + communication_errors = 0 + sample_index = 0 + dynamic_frequency = args.frequency + previous_physical = physical_baseline[:] + + while time.monotonic() - started < args.duration: + now = time.monotonic() + elapsed = now - started + if ( + args.disconnect_at > 0 + and not disconnected + and elapsed >= args.disconnect_at + ): + before_disconnect = previous_physical[:] + assert uart is not None + uart.close() + events.append( + {"elapsed_s": elapsed, "event": "planned_disconnect_start"} + ) + time.sleep(args.disconnect_duration) + uart, client = reopen_modbus(args) + disconnected = True + after_disconnect = [ + read_axis_status(client, axis)[0]["physical_pulses"] + for axis in range(4) + ] + if any( + after_disconnect[axis] <= before_disconnect[axis] + for axis in range(4) + ): + raise RuntimeError( + "计划断线期间存在轴脉冲未继续增长:" + f"before={before_disconnect}, after={after_disconnect}" + ) + previous_physical = after_disconnect + events.append( + { + "elapsed_s": time.monotonic() - started, + "event": "planned_disconnect_recovered", + "physical_pulses": after_disconnect, + } + ) + next_sample = time.monotonic() + continue + + try: + assert client is not None and uart is not None + if now >= next_frequency: + dynamic_frequency = ( + low_frequency + if dynamic_frequency == args.frequency + else args.frequency + ) + for axis in range(4): + # FC16 writes both words of the signed INT32 atomically. + client.write_multiple( + S0_BASES[axis] + 10, + signed_dword_words(dynamic_frequency), + ) + events.append( + { + "elapsed_s": elapsed, + "event": "frequency_change", + "frequency_hz": dynamic_frequency, + } + ) + next_frequency += args.frequency_period + + if now >= next_bad_crc: + before_crc = read_runtime_diagnostics(client)["statistics"][ + "crc_errors" + ] + inject_bad_crc(uart, args.slave, args.baud) + after_crc = read_runtime_diagnostics(client)["statistics"][ + "crc_errors" + ] + if delta32(after_crc, before_crc) < 1: + raise RuntimeError("注入坏 CRC 后 crcErrorCount 未增长") + events.append( + { + "elapsed_s": elapsed, + "event": "bad_crc_rejected", + "crc_count": after_crc, + } + ) + next_bad_crc += args.bad_crc_period + + if now < next_sample: + time.sleep(min(next_sample - now, 0.05)) + continue + + diagnostics = read_runtime_diagnostics(client) + if not diagnostics["connected"]: + raise RuntimeError(f"Modbus connected 标志丢失:{diagnostics}") + statuses: list[dict[str, int]] = [] + snapshot_retries: list[int] = [] + for axis in range(4): + status, retries = read_axis_status(client, axis) + statuses.append(status) + snapshot_retries.append(retries) + if status["error"] != 0 or status["last_result"] != RESULT_OK: + raise RuntimeError(f"轴{axis}进入错误状态:{status}") + if status["state"] not in {STATE_ACCEL, STATE_RUN, STATE_DECEL}: + raise RuntimeError(f"轴{axis}意外离开运行态:{status}") + if status["physical_pulses"] < previous_physical[axis]: + raise RuntimeError( + f"轴{axis}物理累计计数回退:" + f"{previous_physical[axis]} -> {status['physical_pulses']}" + ) + previous_physical[axis] = status["physical_pulses"] + + timestamp = datetime.now(timezone.utc).isoformat() + stats = diagnostics["statistics"] + for axis, status in enumerate(statuses): + writer.writerow( + { + "host_time_utc": timestamp, + "elapsed_s": f"{elapsed:.6f}", + "sample": sample_index, + "axis": axis, + "state": status["state"], + "flags": status["flags"], + "error": status["error"], + "last_result": status["last_result"], + "counter_mode": status["counter_mode"], + "current_frequency": status["current_frequency"], + "target_frequency": status["target_frequency"], + "logical_position": status["logical_position"], + "task_pulses": status["task_pulses"], + "physical_pulses": status["physical_pulses"], + "snapshot_retries": snapshot_retries[axis], + "modbus_valid_frames": stats["valid_frames"], + "modbus_tx_frames": stats["tx_frames"], + "modbus_crc_errors": stats["crc_errors"], + "modbus_dropped_frames": stats["dropped_frames"], + "modbus_uart_errors": stats["uart_errors"], + "modbus_restart_failures": diagnostics[ + "restart_failures" + ], + "modbus_last_uart_error": diagnostics[ + "last_uart_error" + ], + } + ) + csv_file.flush() + last_status = statuses + sample_index += 1 + if sample_index % 30 == 0: + print( + f"{elapsed:8.1f}s:样本 {sample_index}," + f"Q0物理累计={statuses[0]['physical_pulses']}," + f"Modbus有效帧={stats['valid_frames']}" + ) + next_sample = max( + next_sample + args.status_period, time.monotonic() + ) + communication_errors = 0 + except (RuntimeError, serial.SerialException) as error: + if not is_retryable_communication_error(error): + raise + communication_errors += 1 + events.append( + { + "elapsed_s": time.monotonic() - started, + "event": "communication_error", + "count": communication_errors, + "message": str(error), + } + ) + if communication_errors > args.max_communication_errors: + raise + if uart is not None: + uart.close() + time.sleep(args.reconnect_delay) + uart, client = reopen_modbus(args) + next_sample = time.monotonic() + + assert client is not None + sequence = stop_all_axes(client, sequence) + axes_stopped = True + usb_pressure.stop() + last_status = [read_axis_status(client, axis)[0] for axis in range(4)] + diagnostics_end = read_runtime_diagnostics(client) + summary["diagnostics_end"] = diagnostics_end + summary["diagnostics_delta"] = { + name: delta32( + diagnostics_end["statistics"][name], + diagnostics_start["statistics"][name], + ) + for name in diagnostics_end["statistics"] + } + summary["restart_failure_delta"] = delta32( + diagnostics_end["restart_failures"], + diagnostics_start["restart_failures"], + ) + usb_diagnostics_end: dict[str, Any] | None = None + usb_device_delta: dict[str, int] = {} + if args.usb_port: + assert usb_diagnostics_start is not None + usb_diagnostics_end = read_usb_diagnostics(client) + summary["usb_device_diagnostics_end"] = usb_diagnostics_end + usb_device_delta = { + name: delta32( + usb_diagnostics_end["counters"][name], + usb_diagnostics_start["counters"][name], + ) + for name in usb_diagnostics_end["counters"] + } + summary["usb_device_diagnostics_delta"] = usb_device_delta + injected_bad_crc = sum( + event.get("event") == "bad_crc_rejected" for event in events + ) + unhealthy = { + "uart_errors": summary["diagnostics_delta"]["uart_errors"], + "dropped_frames": summary["diagnostics_delta"]["dropped_frames"], + "restart_failures": summary["restart_failure_delta"], + "unexpected_crc_errors": ( + summary["diagnostics_delta"]["crc_errors"] + - injected_bad_crc + ), + "usb_write_errors": usb_pressure.write_errors, + } + if args.usb_port: + assert usb_diagnostics_start is not None + assert usb_diagnostics_end is not None + unhealthy.update( + { + "usb_target_not_initialized": not usb_diagnostics_end[ + "initialized" + ], + "usb_target_rx_packets_no_increment": ( + usb_device_delta["rx_packet_count"] == 0 + ), + "usb_target_rx_bytes_no_increment": ( + usb_device_delta["rx_byte_count"] == 0 + ), + "usb_target_rx_rearm_failures": usb_device_delta[ + "rx_rearm_failure_count" + ], + } + ) + unhealthy = {name: value for name, value in unhealthy.items() if value} + if unhealthy: + raise RuntimeError(f"长稳运行诊断出现异常增量:{unhealthy}") + summary["samples"] = sample_index + summary["final_status"] = last_status + summary["result"] = "PASS" + except (RuntimeError, serial.SerialException, OSError, KeyboardInterrupt) as error: + failure = error + summary["failure"] = str(error) + if client is not None and not axes_stopped: + try: + sequence = stop_all_axes(client, sequence) + except Exception as stop_error: # best-effort safety cleanup + summary["stop_cleanup_failure"] = str(stop_error) + finally: + usb_pressure.stop() + summary["usb_host_pressure"] = usb_pressure.summary() + summary["ended_utc"] = datetime.now(timezone.utc).isoformat() + if uart is not None and uart.is_open: + uart.close() + + json_path.write_text( + json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8" + ) + print(f"CSV 证据:{csv_path}") + print(f"JSON 汇总:{json_path}") + if failure is not None: + if isinstance(failure, KeyboardInterrupt): + raise RuntimeError("用户中止测试,已尝试停止四轴") from failure + raise RuntimeError(str(failure)) from failure + print( + f"长稳 PASS:{summary['samples']} 个四轴一致性样本;" + f"最终计数={[item['physical_pulses'] for item in last_status]}" + ) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (RuntimeError, serial.SerialException) as error: + print(f"测试失败:{error}") + raise SystemExit(1) diff --git a/HostComputer/plsr_modbus_performance_test.py b/HostComputer/plsr_modbus_performance_test.py new file mode 100644 index 0000000..dd9aa00 --- /dev/null +++ b/HostComputer/plsr_modbus_performance_test.py @@ -0,0 +1,664 @@ +#!/usr/bin/env python3 +"""PLSR P16 Modbus 性能统计自动测试(对应规格:PLSR_MODBUS_PERFORMANCE_TEST.md)。 + +本脚本在四轴 100kHz 输出与 Modbus 持续轮询的并发压力下,测量并判定 +规格中定义的各项性能指标,运行完成后自动读取 P16 DWT 统计并做预算检查: + +- 版本号一致性(任务要求的“32/64 位版本号一致性检查”): + D1205~D1206 的 32 位 CPU 计时频率非零、D1207 性能统计版本为 V7、 + 控制窗口协议版本 0x0100;P16 主统计块(32 位周期数与 16 位饱和字段) + 连续两次读取必须完全一致。 +- 单条命令响应时间:0x03/0x10 完整往返耗时,判定预算 = 波特率折算的 + 线上传输时间(8E1,每字符 11 位)+ 10ms 处理余量。 +- 连续状态读取吞吐:四轴状态块(48 字×4)持续轮询速率,要求不低于 + 波特率理论上限的 50%。 +- 重复序号幂等:运行期重发完全相同序号的命令,固件必须只回放既有应答、 + 不得重复执行(沿用 P13 控制测试的 PAUSE 重复序号用例)。 +- 并发读写压力模拟:四轴 100kHz 期间持续读取轴状态,并穿插重写相同的 + S0/S1 数据(幂等写入)与读取 P16 统计块,验证并发下计数精确。 +- P16 自动预算:PlsrProcess 自身 < 1ms、TIM6 控制 ISR < 0.1ms、输出/计数 + ISR < 10us,四项均须实际执行(数值不得为 0);墙钟响应单独显示, + 超过 1ms 只给抢占提示、不判失败。 + +与规格/现有脚本的差异说明: +- 规格“测试方法”一节引用 P14 脚本;本脚本独立实现相同的四轴 100kHz + + 持续轮询场景(规格要求),并额外测量主机侧性能指标,满足规格 + “自动预算”与“记录到测试报告”的要求。 +- 规格要求“无毛刺、无残余输出”,该两项只能由逻辑分析仪验收,本脚本 + 按规格保留为人工核对提示,与 plsr_modbus_counter_stress_test.py 口径一致。 +- P16 主统计块读取比 P14 脚本更严格:必须连续两次读取一致(规格要求 + 统计快照一致性),三次尝试仍不一致即判定失败。 +""" + +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 # D1256:P16 主统计块 +STAGE_PERFORMANCE_BASE = CONTROL_BASE + 256 # D1456:P16 分阶段统计块 +AB_GATE_PERFORMANCE_BASE = CONTROL_BASE + 268 # D1468:AB末周期快速门控 +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_PAUSED = 6 +STATE_COMPLETED = 7 + +CALL_COMMIT = 1 +CALL_START = 2 +CMD_PAUSE = 3 +CMD_RESUME = 4 +CMD_SET_POSITION = 5 + +BITS_PER_CHARACTER = 11 # Modbus RTU 8E1:每字符 11 位 +PROCESSING_BUDGET_MS = 10.0 # 单条命令响应预算中的处理余量(主机+从站开销) +THROUGHPUT_MIN_RATIO = 0.5 # 实测吞吐不低于波特率理论上限的比例 +LATENCY_SAMPLES = 20 # 每种命令的响应时间采样次数 +RUN_DEADLINE_SECONDS = 15.0 # 四轴运行完成的等待上限 + +METRICS: list[tuple[str, bool, str]] = [] + + +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 wait_status( + client: RtuClient, + axis: int, + states: set[int], + sequence: int | None = None, + timeout: float = 5.0, +) -> dict[str, int]: + deadline = time.monotonic() + timeout + latest: dict[str, int] | None = None + while time.monotonic() < deadline: + latest = read_axis_status(client, axis) + sequence_ok = sequence is None or latest["last_sequence"] == sequence + if latest["state"] in states and sequence_ok: + return latest + raise RuntimeError(f"等待轴{axis}状态 {sorted(states)} 超时,最后状态:{latest}") + + +def wait_running_output( + client: RtuClient, axis: int, sequence: int, timeout: float = 5.0 +) -> dict[str, int]: + """等待真实脉冲恢复,不能只依据 ACCEL/RUN 状态标签。""" + deadline = time.monotonic() + timeout + latest: dict[str, int] | None = None + while time.monotonic() < deadline: + latest = read_axis_status(client, axis) + pulse_active = (latest["flags"] & (1 << 1)) != 0 + if ( + latest["last_sequence"] == sequence + and latest["state"] in {STATE_ACCEL, STATE_RUN} + and pulse_active + and latest["current_frequency"] > 0 + ): + return latest + raise RuntimeError(f"等待轴{axis}实际脉冲恢复超时,最后状态:{latest}") + + +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 read_performance_block(client: RtuClient) -> list[int] | None: + """读取 P16 主统计块;连续两次读取一致才算有效(快照一致性)。""" + for _ in range(3): + first = client.read_holding(PERFORMANCE_BASE, 8) + second = client.read_holding(PERFORMANCE_BASE, 8) + if first == second: + return first + return None + + +def wire_time_ms(baud: int, request_bytes: int, response_bytes: int) -> float: + """RTU 8E1 线上传输时间:每字符 11 位。""" + return (request_bytes + response_bytes) * BITS_PER_CHARACTER * 1000.0 / baud + + +def throughput_theory(baud: int) -> float: + """四轴状态轮询(4×48字,请求 8B + 响应 101B)的波特率理论速率(轮/s)。""" + bytes_per_poll = 4 * (8 + 101) + return baud / (bytes_per_poll * BITS_PER_CHARACTER) + + +def sample_round_trips( + client: RtuClient, + baud: int, + actions: list[tuple[str, int, int, object]], + samples: int, +) -> dict[str, tuple[float, float, float, float]]: + """对每种命令采样往返耗时,返回 {名称: (最小, 中位, 最大, 预算) ms}。""" + results = {} + for name, request_bytes, response_bytes, action in actions: + collected = [] + for _ in range(samples): + started = time.perf_counter() + action() + collected.append((time.perf_counter() - started) * 1000.0) + ordered = sorted(collected) + results[name] = ( + ordered[0], + ordered[len(ordered) // 2], + ordered[-1], + wire_time_ms(baud, request_bytes, response_bytes) + PROCESSING_BUDGET_MS, + ) + return results + + +def build_s0_job() -> list[int]: + s0 = [0] * 20 + put_u32(s0, 0, 1) + put_u32(s0, 10, TEST_FREQUENCY_HZ) + put_u32(s0, 12, TEST_PULSES) + return s0 + + +def prepare_jobs(client: RtuClient) -> None: + for axis in range(4): + client.write_multiple(S0_BASES[axis], build_s0_job()) + client.write_multiple(S1_BASES[axis], [0] * 4) + + +def record_metric(name: str, passed: bool, detail: str) -> None: + """记录一项指标并实时打印 通过/失败 + 实测值。""" + METRICS.append((name, passed, detail)) + print(f" [{'通过' if passed else '失败'}] {name}:{detail}") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="PLSR P16 Modbus 性能统计自动测试" + "(PLSR_MODBUS_PERFORMANCE_TEST.md)" + ) + 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( + "--polls", + type=int, + default=10, + help="静态吞吐测试的完整四轴轮询轮数(默认 10)", + ) + 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"P16 控制窗口未就绪:{header};请烧录当前固件并复位" + ) + print("P16 控制窗口就绪:D1200~D1537,协议 V1.0") + + # ---- 指标1:版本号一致性(32位CPU时钟 + 16位性能版本 + 统计块快照) ---- + core_clock_hz = get_u32(header, 5) + performance_version = header[7] + baseline_block = read_performance_block(client) + version_ok = ( + core_clock_hz != 0 + and performance_version == PERFORMANCE_VERSION + and baseline_block is not None + ) + consistency_text = ( + "一致" if baseline_block is not None else "连续三次读取不一致" + ) + record_metric( + "版本号一致性", + version_ok, + f"协议版本={header[2]:#06x},32位CPU计时频率(D1205~D1206)=" + f"{core_clock_hz}Hz,性能统计版本(D1207)=V{performance_version}," + f"P16统计块两次读取{consistency_text}", + ) + if not version_ok: + raise RuntimeError("版本号一致性检查失败,后续指标失去判定基准") + print_process_checkpoint(client, "复位后基线") + print("P16 已就绪:四轴 PULSE/DIR,100kHz,200000脉冲/轴") + + # ---- 指标2:单条命令响应时间 ---- + s0_job = build_s0_job() + actions = [ + ( + "0x03读取控制头(8字)", + 8, + 21, + lambda: client.read_holding(CONTROL_BASE, 8), + ), + ( + "0x10写入S0(20字)", + 49, + 8, + lambda: client.write_multiple(S0_BASES[0], s0_job), + ), + ( + "0x10写入S1(4字)", + 17, + 8, + lambda: client.write_multiple(S1_BASES[0], [0] * 4), + ), + ] + latency = sample_round_trips(client, args.baud, actions, LATENCY_SAMPLES) + latency_ok = True + latency_parts = [] + for name, (minimum, median, maximum, budget) in latency.items(): + latency_ok = latency_ok and median <= budget + latency_parts.append( + f"{name} 中位{median:.1f}ms" + f"(最小{minimum:.1f}/最大{maximum:.1f},预算{budget:.1f}ms)" + ) + record_metric("单条命令响应时间", latency_ok, ";".join(latency_parts)) + + # ---- 指标3:连续状态读取吞吐(静态轮询,无运行压力) ---- + static_started = time.monotonic() + for _ in range(args.polls): + for axis in range(4): + read_axis_status(client, axis) + static_elapsed = time.monotonic() - static_started + static_polls = args.polls * 4 + static_rate = static_polls / static_elapsed + theory = throughput_theory(args.baud) + ratio = static_rate / theory + record_metric( + "连续状态读取吞吐", + ratio >= THROUGHPUT_MIN_RATIO, + f"静态四轴轮询 {static_polls} 次耗时 {static_elapsed:.3f}s," + f"速率 {static_rate:.2f} 轮/s,波特率理论上限 {theory:.2f} 轮/s" + f"(占比 {ratio:.0%})", + ) + + 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, "进入运行态") + + # ---- 指标4:重复序号幂等(轴0,运行期命令处理) ---- + pause_sequence = sequence + response = send_command(client, pause_sequence, 0, CMD_PAUSE) + check_result(response, 4, RESULT_QUEUED, f"轴0 PAUSE#{pause_sequence}") + status = wait_status(client, 0, {STATE_PAUSED}, sequence=pause_sequence) + # 重发完全相同的请求:必须只回放既有应答、不得重复执行 + replay = send_command(client, pause_sequence, 0, CMD_PAUSE) + status_again = read_axis_status(client, 0) + idem_ok = ( + replay == response + and status_again["state"] == STATE_PAUSED + and status_again["last_sequence"] == pause_sequence + ) + record_metric( + "重复序号幂等", + idem_ok, + f"重发 PAUSE#{pause_sequence}:应答与首次完全一致={replay == response}," + f"状态保持暂停,last_sequence={status_again['last_sequence']}" + f"(期望 {pause_sequence})", + ) + sequence += 1 + response = send_command(client, sequence, 0, CMD_RESUME) + check_result(response, 4, RESULT_QUEUED, f"轴0 RESUME#{sequence}") + status = wait_running_output(client, 0, sequence=sequence) + resumed_pulses = status["task_pulses"] + time.sleep(0.5) + status = read_axis_status(client, 0) + if status["task_pulses"] <= resumed_pulses: + raise RuntimeError(f"RESUME#{sequence} 后轴0脉冲计数未增长:{status}") + print( + f"轴0 PAUSE/RESUME 完成:恢复输出,当前 {status['current_frequency']}Hz" + ) + print_process_checkpoint(client, "暂停/恢复") + + # ---- 指标5:并发读写压力模拟(运行期持续轮询 + 幂等写入) ---- + loop_started = time.monotonic() + deadline = loop_started + RUN_DEADLINE_SECONDS + polls = 0 + writes = 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 + # 写压力:重写与 prepare_jobs 完全相同的 S0/S1(幂等, + # 不影响 COMMIT 后已建立的运行快照) + client.write_multiple(S0_BASES[0], s0_job) + client.write_multiple(S1_BASES[0], [0] * 4) + # 并发下验证 P16 统计块可正常读取 + client.read_holding(PERFORMANCE_BASE, 8) + writes += 2 + if all(item["state"] == STATE_COMPLETED for item in final_status): + break + else: + raise RuntimeError(f"等待四轴完成超时,最后状态:{final_status}") + run_elapsed = time.monotonic() - loop_started + + # 并发压力下四轴计数精确性核对 + count_errors: list[str] = [] + 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: + count_errors.append(f"轴{axis}: {bad}") + record_metric( + "并发读写压力", + not count_errors, + f"四轴100kHz运行期间读取状态 {polls} 次、穿插幂等写入 {writes} 次," + f"轮询吞吐 {polls / run_elapsed:.2f} 次/s;四轴计数均精确为 " + f"{TEST_PULSES}、error=0" + + (f";异常:{';'.join(count_errors)}" if count_errors else ""), + ) + + elapsed = time.monotonic() - started + print_process_checkpoint(client, "运行完成") + + # ---- 指标6:P16 统计块快照一致性(运行后,含基线单调性) ---- + block = read_performance_block(client) + if block is None: + record_metric( + "P16统计块快照一致性", False, "三次读取未得到连续一致结果" + ) + else: + regressions = [ + f"D{PERFORMANCE_BASE + offset}: {baseline_block[offset]}→{block[offset]}" + for offset in range(len(block)) + if block[offset] < baseline_block[offset] + ] + detail = "连续两次读取完全一致(32位周期数与16位饱和字段)" + if regressions: + detail += ";但相对复位后基线发生回退:" + "、".join(regressions) + record_metric("P16统计块快照一致性", not regressions, detail) + + # ---- 指标7~10:P16 自动预算 ---- + 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 + ) + cycle_values: dict[str, int] = {} + if block is not None: + cycle_values = { + "PlsrProcess自身": get_u32(block, 0), + "PlsrProcess响应": get_u32(block, 2), + "TIM6控制ISR": get_u32(block, 4), + "输出定时器ISR": block[6], + "TIM9/12计数ISR": block[7], + } + print("P16 DWT最坏执行时间:") + for name, cycles in cycle_values.items(): + microseconds = cycles * 1_000_000.0 / core_clock_hz + print(f" {name:<16} {cycles:8d} cycles {microseconds:8.3f}us") + 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)" + ) + + budgets = { + "PlsrProcess自身": (core_clock_hz // 1_000, "1ms"), + "TIM6控制ISR": (core_clock_hz // 10_000, "0.1ms"), + "输出定时器ISR": (core_clock_hz // TEST_FREQUENCY_HZ, "10us"), + "TIM9/12计数ISR": (core_clock_hz // TEST_FREQUENCY_HZ, "10us"), + } + for name, (budget, budget_text) in budgets.items(): + if block is None: + record_metric(f"P16 {name}预算", False, "统计块不可用,无法判定") + continue + cycles = cycle_values[name] + microseconds = cycles * 1_000_000.0 / core_clock_hz + passed = cycles != 0 and cycles < budget + detail = ( + f"实测 {cycles} cycles = {microseconds:.3f}us," + f"预算 < {budget} cycles({budget_text})" + ) + if cycles == 0: + detail += ";数值为 0,路径未被实际执行" + record_metric(f"P16 {name}预算", passed, detail) + + # 墙钟响应与阶段峰值超过 1ms 时仅给提示(规格:不与自身 CPU 混算) + if block is not None: + response_cycles = cycle_values["PlsrProcess响应"] + if response_cycles >= core_clock_hz // 1_000: + print( + "提示:PlsrProcess墙钟响应超过1ms,但自身CPU执行时间达标;" + "差值来自高优先级PLSR定时器中断抢占。" + ) + for index, name in enumerate(STAGE_NAMES): + cycles = get_u32(stage_performance, index * 2) + if cycles >= core_clock_hz // 1_000: + print( + f"提示:阶段“{name}”峰值超过1ms,需定位峰值路径;" + "各段最大值来自不同轮次,不能直接相加。" + ) + + # ---- 汇总 ---- + print("\n性能测试指标汇总:") + passed_count = sum(1 for _, passed, _ in METRICS if passed) + for name, passed, detail in METRICS: + print(f" [{'通过' if passed else '失败'}] {name}:{detail}") + print(f"最终判定:{passed_count}/{len(METRICS)} 项通过") + failed = [name for name, passed, _ in METRICS if not passed] + if failed: + raise RuntimeError("未通过指标:" + "、".join(failed)) + print( + f"全部 PASS:四轴均为 {TEST_PULSES} 脉冲,耗时 {elapsed:.3f}s," + f"运行期状态读取 {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) diff --git a/HostComputer/plsr_modbus_soft_limit_matrix_test.py b/HostComputer/plsr_modbus_soft_limit_matrix_test.py new file mode 100644 index 0000000..6f7b700 --- /dev/null +++ b/HostComputer/plsr_modbus_soft_limit_matrix_test.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""PLSR P15 four-axis positive/negative soft-limit precision test.""" + +from __future__ import annotations + +import argparse +import time + +import serial + +from plsr_modbus_counter_stress_test import ( + CALL_COMMIT, + CALL_START, + CMD_SET_POSITION, + CONTROL_BASE, + CONTROL_WINDOW_WORDS, + RESULT_OK, + RESULT_QUEUED, + RtuClient, + S0_BASES, + S1_BASES, + check_result, + put_u32, + read_axis_status, + send_call, + send_command, +) +from plsr_modbus_frequency_test import choose_port + + +STATE_ACCEL = 2 +STATE_RUN = 3 +STATE_STOPPED = 8 +STATE_IDLE = 1 +CMD_RESET_ERROR = 10 +ERROR_LIMIT_POSITIVE = 6 +ERROR_LIMIT_NEGATIVE = 7 +STOP_LIMIT_POSITIVE = 5 +STOP_LIMIT_NEGATIVE = 6 +SOFT_LIMIT = 1_000_000 + + +CASES = ( + # axis, start position, frequency, requested pulses, expected output pulses + (0, 999_800, 500, 10_000, 200), + (1, 999_000, 2_000, 10_000, 1_000), + (2, -999_800, 500, -10_000, 200), + (3, -999_000, 2_000, -10_000, 1_000), +) + + +def wait_command_applied( + client: RtuClient, + axis: int, + sequence: int, + expected_result: int = RESULT_OK, + 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"] != expected_result: + raise RuntimeError( + f"轴{axis}命令#{sequence}执行结果={latest['last_result']}," + f"期望={expected_result};状态={latest}" + ) + return latest + raise RuntimeError(f"等待轴{axis}命令#{sequence}执行超时,最后状态={latest}") + + +def wait_state( + client: RtuClient, axis: int, expected: set[int], timeout: float = 6.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["state"] in expected: + return latest + raise RuntimeError( + f"等待轴{axis}状态{sorted(expected)}超时,最后状态={latest}" + ) + + +def write_job( + client: RtuClient, axis: int, frequency_hz: int, signed_pulses: int +) -> None: + s0 = [0] * 20 + put_u32(s0, 0, 1) + put_u32(s0, 10, frequency_hz) + put_u32(s0, 12, signed_pulses) + client.write_multiple(S0_BASES[axis], s0) + client.write_multiple(S1_BASES[axis], [0] * 4) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="PLSR P15 四轴正负软限位边界精度测试" + ) + 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"P15控制窗口未就绪:{header};请烧录当前固件并复位" + ) + print("P15 已就绪:软限位±1000000,K2保护矩阵") + + sequence = 500 + for axis, start_position, frequency, requested, expected_pulses in CASES: + positive = requested > 0 + label = "正限位" if positive else "负限位" + expected_position = SOFT_LIMIT if positive else -SOFT_LIMIT + expected_error = ( + ERROR_LIMIT_POSITIVE if positive else ERROR_LIMIT_NEGATIVE + ) + expected_reason = ( + STOP_LIMIT_POSITIVE if positive else STOP_LIMIT_NEGATIVE + ) + + response = send_command( + client, sequence, axis, CMD_SET_POSITION, start_position + ) + check_result(response, 4, RESULT_QUEUED, f"轴{axis} SET_POSITION") + before = wait_command_applied(client, axis, sequence) + sequence += 1 + + write_job(client, axis, frequency, requested) + response = send_call( + client, sequence, axis, CALL_COMMIT, s2_set=2 + ) + check_result(response, 3, RESULT_OK, f"轴{axis} COMMIT") + sequence += 1 + start_sequence = sequence + response = send_call( + client, start_sequence, axis, CALL_START, s2_set=2 + ) + check_result(response, 3, RESULT_QUEUED, f"轴{axis} START") + sequence += 1 + + running = wait_state(client, axis, {STATE_ACCEL, STATE_RUN}) + if running["counter_mode"] != 1: + raise RuntimeError(f"轴{axis}未取得硬件计数器:{running}") + stopped = wait_state(client, axis, {STATE_STOPPED}) + physical_delta = stopped["physical_pulses"] - before["physical_pulses"] + expected_task = expected_pulses if positive else -expected_pulses + checks = { + "逻辑位置": (stopped["logical_position"], expected_position), + "任务脉冲": (stopped["task_pulses"], expected_task), + "物理脉冲增量": (physical_delta, expected_pulses), + } + bad = { + name: values + for name, values in checks.items() + if abs(values[0] - values[1]) > 1 + } + if bad: + raise RuntimeError(f"轴{axis}{label}边界超差:{bad};状态={stopped}") + if ( + stopped["error"] != expected_error + or stopped["stop_reason"] != expected_reason + ): + raise RuntimeError(f"轴{axis}{label}错误语义不正确:{stopped}") + print( + f"轴{axis} {label} {frequency:5d}Hz:" + f"输出={physical_delta},位置={stopped['logical_position']},PASS" + ) + + response = send_command(client, sequence, axis, CMD_RESET_ERROR) + check_result(response, 4, RESULT_QUEUED, f"轴{axis} RESET_ERROR") + reset = wait_command_applied(client, axis, sequence) + sequence += 1 + if reset["state"] != STATE_IDLE or reset["error"] != 0: + raise RuntimeError(f"轴{axis}错误复位不完整:{reset}") + + print("全部 PASS:四轴正/负软限位在±1脉冲窗口内停止,错误码及复位正确。") + print("请核对波形:Q0/Q2约200个上升沿,Q1/Q3约1000个上升沿。") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (RuntimeError, serial.SerialException) as error: + print(f"测试失败:{error}") + raise SystemExit(1) diff --git a/HostComputer/plsr_persistence_board_test.py b/HostComputer/plsr_persistence_board_test.py new file mode 100644 index 0000000..393157b --- /dev/null +++ b/HostComputer/plsr_persistence_board_test.py @@ -0,0 +1,520 @@ +#!/usr/bin/env python3 +"""Phase-based PLSR HSD/SFD persistence board acceptance tool. + +Normal phases never corrupt storage and issue at most one SFD SAVE per +invocation. Power removal/reset is intentionally manual so VBAT retention and +real reset behavior are tested rather than simulated by a software command. +""" + +from __future__ import annotations + +import argparse +import json +import tempfile +import time +from pathlib import Path + +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 +PERSISTENCE_BASE = CONTROL_BASE + 270 +PERSISTENCE_WORDS = 30 +PERSISTENCE_REQUEST = CONTROL_BASE + 300 +PERSISTENCE_RESPONSE = CONTROL_BASE + 308 +PERSISTENCE_VERSION = 1 +PERFORMANCE_VERSION = 7 + +S0_BASES = (1600, 1800, 2000, 2200) +S1_BASES = (1700, 1900, 2100, 2300) +KNOWN_POSITIONS = (123456, -234567, 345678, -456789) + +RESULT_OK = 0 +RESULT_QUEUED = 1 +RESULT_BUSY = 8 +RESULT_NOT_SUPPORTED = 10 +PERSISTENCE_OK = 0 +PERSISTENCE_NOT_IMPLEMENTED = 4 +STATE_IDLE = 1 +STATE_ACCEL = 2 +STATE_RUN = 3 +STATE_DECEL = 4 +STATE_STOPPED = 8 + +CALL_COMMIT = 1 +CALL_START = 2 +CMD_STOP_IMMEDIATE = 2 +CMD_SET_POSITION = 5 +CMD_SAVE_CONFIG = 8 + +DESTRUCTIVE_MAGIC_A = 0xDA7A +DESTRUCTIVE_MAGIC_B = 0x51F0 +DESTRUCTIVE_ARM = 0xA55A +DESTRUCTIVE_CONFIRM = "INVALIDATE-NEWEST-SLOT" + + +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 next_sequence() -> int: + value = int(time.time_ns() // 1_000_000) & 0x7FFFFFFF + return value if value != 0 else 1 + + +def wait_response( + client: RtuClient, address: int, count: int, sequence: int, timeout: float = 3.0 +) -> list[int]: + deadline = time.monotonic() + timeout + latest: list[int] | None = None + while time.monotonic() < deadline: + latest = client.read_holding(address, count) + if get_u32(latest, 0) == sequence: + return latest + raise RuntimeError(f"等待序号 {sequence} 应答超时,最后应答={latest}") + + +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 read_axis_status(client: RtuClient, axis: int) -> dict[str, int]: + words = client.read_holding( + AXIS_STATUS_BASE + axis * AXIS_STATUS_WORDS, 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}状态快照不一致:{generation_begin}/{generation_end}" + ) + return { + "state": words[2], + "flags": get_u32(words, 3), + "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), + "current_frequency": get_u32(words, 38), + } + + +def wait_command_applied( + client: RtuClient, axis: int, sequence: int, timeout: float = 5.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: + return latest + raise RuntimeError(f"等待命令 {sequence} 内核执行超时,最后状态={latest}") + + +def read_persistence(client: RtuClient) -> dict[str, int]: + latest: list[int] | None = None + for _ in range(5): + latest = client.read_holding(PERSISTENCE_BASE, PERSISTENCE_WORDS) + generation_begin = get_u32(latest, 0) + generation_end = get_u32(latest, 28) + if generation_begin == generation_end and not generation_begin & 1: + if latest[2] != PERSISTENCE_VERSION: + raise RuntimeError(f"持久化诊断版本错误:{latest[2]}") + hsd_newest = latest[4] & 0xFF + sfd_newest = (latest[4] >> 8) & 0xFF + hsd_generations = (get_u32(latest, 12), get_u32(latest, 14)) + sfd_generations = (get_u32(latest, 16), get_u32(latest, 18)) + return { + "generation": generation_begin, + "hsd_valid_mask": latest[3] & 0xFF, + "sfd_valid_mask": (latest[3] >> 8) & 0xFF, + "hsd_newest_mask": hsd_newest, + "sfd_newest_mask": sfd_newest, + "flags": latest[5], + "last_hsd_load": latest[6], + "last_sfd_load": latest[7], + "last_hsd_save": latest[8], + "last_sfd_save": latest[9], + "last_sfd_erase": latest[10], + "hsd_generation_a": hsd_generations[0], + "hsd_generation_b": hsd_generations[1], + "sfd_generation_a": sfd_generations[0], + "sfd_generation_b": sfd_generations[1], + "hsd_selected_generation": ( + hsd_generations[0] + if hsd_newest == 1 + else hsd_generations[1] if hsd_newest == 2 else 0 + ), + "sfd_selected_generation": ( + sfd_generations[0] + if sfd_newest == 1 + else sfd_generations[1] if sfd_newest == 2 else 0 + ), + "hsd_save_count": get_u32(latest, 20), + "sfd_save_count": get_u32(latest, 22), + "hsd_crc32": get_u32(latest, 24), + "sfd_crc32": get_u32(latest, 26), + } + raise RuntimeError(f"持久化诊断快照连续不一致:{latest}") + + +def wait_persistence_clean(client: RtuClient, timeout: float = 4.0) -> dict[str, int]: + deadline = time.monotonic() + timeout + latest: dict[str, int] | None = None + while time.monotonic() < deadline: + latest = read_persistence(client) + if not latest["flags"] & 1 and latest["hsd_valid_mask"] != 0: + return latest + raise RuntimeError(f"HSD 检查点未在期限内完成:{latest}") + + +def send_call( + client: RtuClient, sequence: int, axis: int, operation: int +) -> list[int]: + request = [0] * 16 + put_u32(request, 0, sequence) + request[2] = 0 + put_u32(request, 3, S0_BASES[axis]) + request[5] = 0 + put_u32(request, 6, S1_BASES[axis]) + request[8] = 0 + put_u32(request, 10, 1) + request[12] = axis + request[13] = 0 + request[14] = operation + client.write_multiple(CALL_REQUEST, request) + return wait_response(client, CALL_RESPONSE, 12, sequence) + + +def start_long_motion(client: RtuClient, sequence: int) -> int: + s0 = [0] * 20 + put_u32(s0, 0, 1) + put_u32(s0, 10, 100_000) + put_u32(s0, 12, 50_000_000) + client.write_multiple(S0_BASES[0], s0) + client.write_multiple(S1_BASES[0], [0] * 4) + + response = send_call(client, sequence, 0, CALL_COMMIT) + if response[3] != RESULT_OK or response[11] != 1: + raise RuntimeError(f"长任务 COMMIT 失败:{response}") + sequence += 1 + response = send_call(client, sequence, 0, CALL_START) + if response[3] != RESULT_QUEUED: + raise RuntimeError(f"长任务 START 失败:{response}") + + deadline = time.monotonic() + 5.0 + latest: dict[str, int] | None = None + while time.monotonic() < deadline: + latest = read_axis_status(client, 0) + if ( + latest["last_sequence"] == sequence + and latest["state"] in {STATE_ACCEL, STATE_RUN, STATE_DECEL} + and latest["flags"] & (1 << 1) + ): + return sequence + 1 + raise RuntimeError(f"长任务未进入真实输出态:{latest}") + + +def load_state(path: Path) -> dict[str, object]: + if not path.exists(): + raise RuntimeError(f"阶段状态文件不存在:{path}") + return json.loads(path.read_text(encoding="utf-8")) + + +def save_state(path: Path, state: dict[str, object]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") + + +def print_diagnostics(diag: dict[str, int]) -> None: + print( + "持久化诊断:" + f"HSD valid=0x{diag['hsd_valid_mask']:02X}, " + f"gen={diag['hsd_selected_generation']}, crc=0x{diag['hsd_crc32']:08X}, " + f"boot saves={diag['hsd_save_count']}; " + f"SFD valid=0x{diag['sfd_valid_mask']:02X}, " + f"gen={diag['sfd_selected_generation']}, crc=0x{diag['sfd_crc32']:08X}, " + f"boot saves={diag['sfd_save_count']}; flags=0x{diag['flags']:04X}" + ) + + +def phase_hsd_prepare(client: RtuClient, state_path: Path) -> None: + sequence = next_sequence() + for axis, position in enumerate(KNOWN_POSITIONS): + response = send_command(client, sequence, axis, CMD_SET_POSITION, position) + if response[4] != RESULT_QUEUED: + raise RuntimeError(f"轴{axis} SET_POSITION 未排队:{response}") + status = wait_command_applied(client, axis, sequence) + if status["last_result"] != RESULT_OK or status["logical_position"] != position: + raise RuntimeError(f"轴{axis}位置检查失败:{status}") + sequence += 1 + diag = wait_persistence_clean(client) + if diag["last_hsd_save"] != PERSISTENCE_OK: + raise RuntimeError(f"HSD 保存结果不是 OK:{diag}") + save_state(state_path, {"positions": list(KNOWN_POSITIONS)}) + print_diagnostics(diag) + print("HSD prepare PASS。现在关闭主电源(保持 VBAT),再执行 --phase hsd-verify。") + + +def phase_hsd_verify(client: RtuClient, state_path: Path) -> None: + expected = [int(value) for value in load_state(state_path)["positions"]] + for axis, position in enumerate(expected): + status = read_axis_status(client, axis) + if status["logical_position"] != position or not status["flags"] & (1 << 5): + raise RuntimeError(f"轴{axis} HSD 恢复失败:期望{position},实测{status}") + if status["flags"] & (1 << 1): + raise RuntimeError(f"轴{axis} 上电后意外输出:{status}") + diag = read_persistence(client) + if diag["last_hsd_load"] != PERSISTENCE_OK: + raise RuntimeError(f"HSD 上电加载结果不是 OK:{diag}") + if not diag["flags"] & (1 << 2) or diag["flags"] & (1 << 3): + raise RuntimeError(f"HSD restored flags 不符合正常停机:{diag}") + print_diagnostics(diag) + print("HSD VBAT 掉主电恢复 PASS。") + + +def phase_busy_prepare(client: RtuClient) -> None: + sequence = start_long_motion(client, next_sequence()) + diag = wait_persistence_clean(client) + print_diagnostics(diag) + print( + f"运行中掉电已就绪(下一序号{sequence})。现在直接关闭主电源或硬复位," + "不要先发 STOP;重启后执行 --phase busy-verify。" + ) + + +def phase_busy_verify(client: RtuClient) -> None: + for axis in range(4): + status = read_axis_status(client, axis) + if status["flags"] & (1 << 1): + raise RuntimeError(f"轴{axis} 重启后仍有输出:{status}") + if status["flags"] & (1 << 5): + raise RuntimeError(f"轴{axis} 运行中复位后 position_valid 未清除:{status}") + if status["state"] != STATE_IDLE: + raise RuntimeError(f"轴{axis} 重启后不是 IDLE:{status}") + diag = read_persistence(client) + if not diag["flags"] & (1 << 3) or diag["flags"] & (1 << 2): + raise RuntimeError(f"busy-reset restored flags 错误:{diag}") + print_diagnostics(diag) + print("运行中掉电/复位 PASS:未自动续跑,所有轴 position_valid=0。") + + +def phase_sfd_save(client: RtuClient, state_path: Path) -> None: + before = read_persistence(client) + sequence = next_sequence() + response = send_command(client, sequence, 0, CMD_SAVE_CONFIG) + if response[4] != RESULT_QUEUED: + raise RuntimeError(f"SAVE_CONFIG 未排队:{response}") + status = wait_command_applied(client, 0, sequence, timeout=10.0) + if status["last_result"] != RESULT_OK: + raise RuntimeError(f"SAVE_CONFIG 内核执行失败:{status}") + time.sleep(0.1) + after = read_persistence(client) + was_dirty = bool(before["flags"] & (1 << 1)) + if was_dirty: + if after["sfd_save_count"] != before["sfd_save_count"] + 1: + raise RuntimeError(f"SFD 实际保存次数不正确:before={before}, after={after}") + if after["last_sfd_save"] != PERSISTENCE_OK: + raise RuntimeError(f"SFD 保存结果不是 OK:{after}") + if after["sfd_selected_generation"] == before["sfd_selected_generation"]: + raise RuntimeError(f"SFD generation 未前进:before={before}, after={after}") + elif after["sfd_save_count"] != before["sfd_save_count"]: + raise RuntimeError("SFD clean no-op 不应擦写 Flash") + if after["sfd_valid_mask"] == 0: + raise RuntimeError(f"SFD 没有可验证的有效槽:{after}") + state = load_state(state_path) if state_path.exists() else {} + state["sfd_generation"] = after["sfd_selected_generation"] + state["sfd_crc32"] = after["sfd_crc32"] + save_state(state_path, state) + print_diagnostics(after) + if not was_dirty: + print("SFD 当前为 clean,本次 SAVE 正确地没有重复擦写;验证现有提交记录。") + print("SFD save PASS。现在硬复位/掉电重启,再执行 --phase sfd-verify。") + + +def phase_sfd_verify(client: RtuClient, state_path: Path) -> None: + state = load_state(state_path) + expected_generation = int(state["sfd_generation"]) + expected_crc = int(state["sfd_crc32"]) + diag = read_persistence(client) + if diag["last_sfd_load"] != PERSISTENCE_OK: + raise RuntimeError(f"SFD 上电加载结果不是 OK:{diag}") + if ( + diag["sfd_selected_generation"] != expected_generation + or diag["sfd_crc32"] != expected_crc + ): + raise RuntimeError( + "SFD 上电记录不一致:" + f"期望 gen={expected_generation}, crc=0x{expected_crc:08X};实测={diag}" + ) + print_diagnostics(diag) + print("SFD A/B + CRC 上电加载 PASS。") + + +def phase_motion_save_busy(client: RtuClient) -> None: + sequence = start_long_motion(client, next_sequence()) + before = read_persistence(client) + response = send_command(client, sequence, 0, CMD_SAVE_CONFIG) + if response[4] != RESULT_QUEUED: + raise RuntimeError(f"运行中 SAVE_CONFIG 未进入命令队列:{response}") + status = wait_command_applied(client, 0, sequence) + if status["last_result"] != RESULT_BUSY: + raise RuntimeError(f"运行中 SAVE_CONFIG 未返回 BUSY:{status}") + after = read_persistence(client) + if after["sfd_save_count"] != before["sfd_save_count"]: + raise RuntimeError("运行中 SAVE_CONFIG 意外擦写了 Flash") + sequence += 1 + response = send_command(client, sequence, 0, CMD_STOP_IMMEDIATE) + if response[4] != RESULT_QUEUED: + raise RuntimeError(f"清理 STOP_IMMEDIATE 未排队:{response}") + status = wait_command_applied(client, 0, sequence) + if status["last_result"] != RESULT_OK or status["state"] != STATE_STOPPED: + raise RuntimeError(f"清理停止失败:{status}") + print_diagnostics(after) + print("运行中禁止 SFD Flash 擦写 PASS:SAVE_CONFIG=BUSY,save_count 未增加。") + + +def phase_invalidate( + client: RtuClient, target: str, allow: bool, confirmation: str | None +) -> None: + if not allow or confirmation != DESTRUCTIVE_CONFIRM: + raise RuntimeError( + "破坏性诊断未授权;必须同时使用 --allow-destructive " + f"--confirm {DESTRUCTIVE_CONFIRM}" + ) + before = read_persistence(client) + if not before["flags"] & (1 << 4): + raise RuntimeError("固件未显式启用 PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG") + mask_key = "hsd_valid_mask" if target == "hsd" else "sfd_valid_mask" + if before[mask_key] != 3: + raise RuntimeError(f"必须先有两个有效槽,当前诊断={before}") + sequence = next_sequence() + inverse = (~sequence) & 0xFFFFFFFF + request = [DESTRUCTIVE_MAGIC_A, DESTRUCTIVE_MAGIC_B] + request += signed_dword_words(sequence) + request += signed_dword_words(inverse) + request += [1 if target == "hsd" else 2, DESTRUCTIVE_ARM] + client.write_multiple(PERSISTENCE_REQUEST, request) + response = wait_response(client, PERSISTENCE_RESPONSE, 8, sequence) + if response[3] == RESULT_NOT_SUPPORTED: + raise RuntimeError("破坏性诊断被固件拒绝(normal build)") + if response[3] != RESULT_OK: + raise RuntimeError(f"失效 newest {target.upper()} 槽失败:{response}") + after = read_persistence(client) + if after[mask_key] not in {1, 2}: + raise RuntimeError(f"失效后应只剩一个有效槽:{after}") + print_diagnostics(after) + print(f"受控失效 newest {target.upper()} 槽 PASS;请立即执行对应 LOAD/重启回退验证。") + + +def main() -> int: + parser = argparse.ArgumentParser(description="PLSR HSD/SFD 真机掉电与 Flash 验收") + parser.add_argument( + "--phase", + required=True, + choices=( + "diagnostics", + "hsd-prepare", + "hsd-verify", + "busy-prepare", + "busy-verify", + "sfd-save", + "sfd-verify", + "motion-save-busy", + "invalidate-hsd", + "invalidate-sfd", + ), + ) + parser.add_argument("--port", default="COM5") + parser.add_argument("--baud", type=int, default=9600) + parser.add_argument("--slave", type=int, default=1) + parser.add_argument( + "--state-file", + type=Path, + default=Path(tempfile.gettempdir()) / "plsr_persistence_board_state.json", + ) + parser.add_argument("--allow-destructive", action="store_true") + parser.add_argument("--confirm") + 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"控制窗口未就绪或固件过旧:{header}") + if header[7] != PERFORMANCE_VERSION: + raise RuntimeError(f"需要诊断版本 V{PERFORMANCE_VERSION},当前 V{header[7]}") + print(f"控制窗口 D1200~D1537 就绪;阶段状态文件:{args.state_file}") + + if args.phase == "diagnostics": + print_diagnostics(read_persistence(client)) + elif args.phase == "hsd-prepare": + phase_hsd_prepare(client, args.state_file) + elif args.phase == "hsd-verify": + phase_hsd_verify(client, args.state_file) + elif args.phase == "busy-prepare": + phase_busy_prepare(client) + elif args.phase == "busy-verify": + phase_busy_verify(client) + elif args.phase == "sfd-save": + phase_sfd_save(client, args.state_file) + elif args.phase == "sfd-verify": + phase_sfd_verify(client, args.state_file) + elif args.phase == "motion-save-busy": + phase_motion_save_busy(client) + elif args.phase == "invalidate-hsd": + phase_invalidate(client, "hsd", args.allow_destructive, args.confirm) + else: + phase_invalidate(client, "sfd", args.allow_destructive, args.confirm) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (RuntimeError, serial.SerialException, KeyError, ValueError) as error: + print(f"测试失败:{error}") + raise SystemExit(1) diff --git a/Modbus/Inc/modbus_data_store.h b/Modbus/Inc/modbus_data_store.h index a8700fa..99a1e72 100644 --- a/Modbus/Inc/modbus_data_store.h +++ b/Modbus/Inc/modbus_data_store.h @@ -16,9 +16,22 @@ typedef enum MODBUS_DATA_DEVICE_FD } MODBUS_DATA_DEVICE; +/* Logical PLC bit-device spaces. Standard Modbus coils expose M; X is + * written by the input-image producer and HM is available to retained logic. + * All consumers, including PLSR, read the same packed images here. */ +typedef enum +{ + MODBUS_BIT_DEVICE_X = 0, + MODBUS_BIT_DEVICE_M, + MODBUS_BIT_DEVICE_HM +} MODBUS_BIT_DEVICE; + #define MODBUS_DATA_D_WORD_COUNT (10000UL) #define MODBUS_DATA_HD_WORD_COUNT (10000UL) #define MODBUS_DATA_FD_WORD_COUNT (10000UL) +#define MODBUS_DATA_X_BIT_COUNT (10000UL) +#define MODBUS_DATA_M_BIT_COUNT (10000UL) +#define MODBUS_DATA_HM_BIT_COUNT (10000UL) uint8_t ModbusDataValidateWords(MODBUS_DATA_DEVICE device, uint32_t firstAddress, @@ -44,6 +57,16 @@ uint8_t ModbusDataReadLinear(uint32_t address, uint16_t *value); /* Even means stable; odd means a multi-register write is in progress. */ uint32_t ModbusDataGetWriteSequence(void); +uint8_t ModbusDataValidateBits(MODBUS_BIT_DEVICE device, + uint32_t firstAddress, + uint32_t bitCount); +uint8_t ModbusDataReadBit(MODBUS_BIT_DEVICE device, + uint32_t address, + uint8_t *value); +uint8_t ModbusDataWriteBit(MODBUS_BIT_DEVICE device, + uint32_t address, + uint8_t value); + #ifdef __cplusplus } #endif diff --git a/Modbus/Inc/modbus_rtu_slave.h b/Modbus/Inc/modbus_rtu_slave.h index 3e139df..89bc7b9 100644 --- a/Modbus/Inc/modbus_rtu_slave.h +++ b/Modbus/Inc/modbus_rtu_slave.h @@ -20,6 +20,21 @@ extern "C" #define MODBUS_SLAVE_DEFAULT_ADDRESS (1U) #define MODBUS_CONNECTION_TIMEOUT_MS (1000U) +/* Read-only vendor diagnostics. The request uses the same start/quantity + * layout as function 0x03; diagnostic words are described below. */ +#define MODBUS_RUNTIME_DIAGNOSTICS_FUNCTION (0x47U) +#define MODBUS_RUNTIME_DIAGNOSTICS_SIGNATURE (0x4D42U) +#define MODBUS_RUNTIME_DIAGNOSTICS_VERSION (1U) +#define MODBUS_RUNTIME_DIAGNOSTICS_WORDS (40U) + +#define MODBUS_RUNTIME_FLAG_INITIALIZED (1U << 0U) +#define MODBUS_RUNTIME_FLAG_VALID_FRAME_SEEN (1U << 1U) +#define MODBUS_RUNTIME_FLAG_CONNECTED (1U << 2U) +#define MODBUS_RUNTIME_FLAG_TX_BUSY (1U << 3U) +#define MODBUS_RUNTIME_FLAG_RX_FRAME_READY (1U << 4U) +#define MODBUS_RUNTIME_FLAG_RX_ASSEMBLY_INVALID (1U << 5U) +#define MODBUS_RUNTIME_FLAG_RX_RESTART_OK (1U << 6U) + /** * @brief 与 TouchWin 触摸屏联调使用的演示地址 * @@ -66,6 +81,34 @@ typedef struct uint32_t droppedFrameCount; ///< 接收槽占用或长度错误导致的丢帧数 uint32_t uartErrorCount; ///< HAL 串口错误计数 } MODBUS_SLAVE_STATS; + +/** + * @brief Read-only snapshot of the Modbus RTU runtime state. + * + * Every aligned 32-bit-or-smaller field is read without tearing on Cortex-M4. + * The getter does not mask motion or UART interrupts, so the complete structure + * may intentionally span adjacent UART events and is not a transaction-wide + * atomic snapshot. + */ +typedef struct +{ + MODBUS_SLAVE_STATS statistics; + uint32_t currentTick; + uint32_t lastValidFrameTick; + uint32_t lastInterFrameGapCycles; + uint32_t receiveRestartAttemptCount; + uint32_t receiveRestartFailureCount; + uint32_t lastUartErrorCode; + uint16_t rxAssemblyLength; + uint16_t rxFrameLength; + uint8_t lastReceiveStartStatus; + uint8_t initialized; + uint8_t hasReceivedValidFrame; + uint8_t connected; + uint8_t txBusy; + uint8_t rxFrameReady; + uint8_t rxAssemblyInvalid; +} MODBUS_SLAVE_RUNTIME_DIAGNOSTICS; /** @brief Modbus 从站通信统计数据 */ extern volatile MODBUS_SLAVE_STATS ModbusSlaveStatistics; /** @@ -140,6 +183,17 @@ uint8_t ModbusSlaveGetCoil(uint16_t address, uint8_t *state); * @retval 0 尚未收到有效请求或连接已经超时 */ uint8_t ModbusSlaveIsConnected(uint32_t timeoutMs); + +/** + * @brief Copy all RTU diagnostics with tear-free individual scalar fields. + * @param[in] timeoutMs Age used to calculate the connected flag. + * @param[out] diagnostics Destination for the complete snapshot. + * @retval 1 Snapshot returned. + * @retval 0 diagnostics is NULL. + */ +uint8_t ModbusSlaveGetRuntimeDiagnostics( + uint32_t timeoutMs, + MODBUS_SLAVE_RUNTIME_DIAGNOSTICS *diagnostics); void ModbusRetainedRegistersLoad(void); void ModbusRetainedRegistersPoll(void); diff --git a/Modbus/Src/modbus_data_store.c b/Modbus/Src/modbus_data_store.c index e850c36..e83e140 100644 --- a/Modbus/Src/modbus_data_store.c +++ b/Modbus/Src/modbus_data_store.c @@ -12,6 +12,11 @@ #define MODBUS_DATA_CCM_WORD_COUNT (29999UL) #define MODBUS_DATA_HD_SRAM_OFFSET (10000UL) #define MODBUS_DATA_LINEAR_CCM_BASE (40000UL) +#if (MODBUS_DATA_X_BIT_COUNT != MODBUS_DATA_M_BIT_COUNT) \ + || (MODBUS_DATA_X_BIT_COUNT != MODBUS_DATA_HM_BIT_COUNT) +#error "Packed X/M/HM images require equal configured capacities" +#endif +#define MODBUS_DATA_BIT_BYTES ((MODBUS_DATA_X_BIT_COUNT + 7UL) / 8UL) static uint16_t ModbusDataSram[MODBUS_DATA_SRAM_WORD_COUNT]; @@ -22,6 +27,8 @@ __root #endif static uint16_t ModbusDataCcm[MODBUS_DATA_CCM_WORD_COUNT]; +static uint8_t ModbusBitImages[3U][MODBUS_DATA_BIT_BYTES]; + static volatile uint32_t ModbusDataWriteSequence; static volatile uint32_t ModbusDataWriteFirstAddress; static volatile uint32_t ModbusDataWriteWordCount; @@ -347,3 +354,83 @@ uint32_t ModbusDataGetWriteSequence(void) { return ModbusDataWriteSequence; } + +static uint32_t ModbusDataBitCapacity(MODBUS_BIT_DEVICE device) +{ + switch (device) + { + case MODBUS_BIT_DEVICE_X: + return MODBUS_DATA_X_BIT_COUNT; + + case MODBUS_BIT_DEVICE_M: + return MODBUS_DATA_M_BIT_COUNT; + + case MODBUS_BIT_DEVICE_HM: + return MODBUS_DATA_HM_BIT_COUNT; + + default: + return 0UL; + } +} + +uint8_t ModbusDataValidateBits(MODBUS_BIT_DEVICE device, + uint32_t firstAddress, + uint32_t bitCount) +{ + uint32_t capacity = ModbusDataBitCapacity(device); + + if ((capacity == 0UL) || (bitCount == 0UL) + || (firstAddress >= capacity)) + { + return 0U; + } + return (bitCount <= (capacity - firstAddress)) ? 1U : 0U; +} + +uint8_t ModbusDataReadBit(MODBUS_BIT_DEVICE device, + uint32_t address, + uint8_t *value) +{ + uint8_t mask; + + if ((value == NULL) + || (ModbusDataValidateBits(device, address, 1UL) == 0U)) + { + return 0U; + } + mask = (uint8_t)(1U << (address & 7UL)); + MODBUS_DATA_BARRIER(); + *value = ((ModbusBitImages[(uint32_t)device][address >> 3U] & mask) != 0U) + ? 1U + : 0U; + MODBUS_DATA_BARRIER(); + return 1U; +} + +uint8_t ModbusDataWriteBit(MODBUS_BIT_DEVICE device, + uint32_t address, + uint8_t value) +{ + uint8_t *byte; + uint8_t mask; + uint32_t interruptState; + + if (ModbusDataValidateBits(device, address, 1UL) == 0U) + { + return 0U; + } + byte = &ModbusBitImages[(uint32_t)device][address >> 3U]; + mask = (uint8_t)(1U << (address & 7UL)); + interruptState = ModbusDataEnterShortCritical(); + if (value != 0U) + { + *byte |= mask; + } + else + { + *byte &= (uint8_t)(~mask); + } + MODBUS_DATA_BARRIER(); + ModbusDataExitShortCritical(interruptState); + return 1U; +} diff --git a/Modbus/Src/modbus_rtu_slave.c b/Modbus/Src/modbus_rtu_slave.c index fa6478a..f823337 100644 --- a/Modbus/Src/modbus_rtu_slave.c +++ b/Modbus/Src/modbus_rtu_slave.c @@ -51,12 +51,16 @@ static uint32_t ModbusRtuT35Cycles; // T3.5对应的CPU周期数 static uint32_t ModbusRtuCharCycles; // 一个UART字符对应的CPU周期数 static volatile uint32_t ModbusLastValidFrameTick; static volatile uint8_t ModbusHasReceivedValidFrame; +static volatile uint32_t ModbusReceiveRestartAttemptCount; +static volatile uint32_t ModbusReceiveRestartFailureCount; +static volatile uint32_t ModbusNextReceiveRetryTick; +static volatile uint32_t ModbusLastUartErrorCode; +static volatile uint8_t ModbusLastReceiveStartStatus = (uint8_t)HAL_ERROR; static volatile MODBUS_BACKUP_DATA *ModbusBackupData = (volatile MODBUS_BACKUP_DATA *)BKPSRAM_BASE; -/* Word data is owned by modbus_data_store.c. This file keeps only the - * protocol-facing coil space and RTU buffers. */ -static uint8_t ModbusCoils[(MODBUS_MAP_ITEM_COUNT + 7U) / 8U]; +/* Word and bit images are owned by modbus_data_store.c. Standard Modbus + * coils are the protocol view of the PLC M device space. */ volatile MODBUS_SLAVE_STATS ModbusSlaveStatistics; @@ -136,9 +140,10 @@ static uint8_t ModbusAddressRangeIsValid(uint16_t start, uint16_t quantity) */ static uint8_t ModbusCoilGetUnchecked(uint16_t address) { - uint8_t mask = (uint8_t)(1U << (address & 0x0007U)); + uint8_t value = 0U; - return ((ModbusCoils[address >> 3U] & mask) != 0U) ? 1U : 0U; + (void)ModbusDataReadBit(MODBUS_BIT_DEVICE_M, address, &value); + return value; } /** @@ -148,16 +153,7 @@ static uint8_t ModbusCoilGetUnchecked(uint16_t address) */ static void ModbusCoilSetUnchecked(uint16_t address, uint8_t state) { - uint8_t mask = (uint8_t)(1U << (address & 0x0007U)); - - if (state != 0U) - { - ModbusCoils[address >> 3U] |= mask; - } - else - { - ModbusCoils[address >> 3U] &= (uint8_t)(~mask); - } + (void)ModbusDataWriteBit(MODBUS_BIT_DEVICE_M, address, state); } /** @@ -256,9 +252,10 @@ static void ModbusTryFinalizeReceive(void) /* 静默达到T3.5,当前RTU帧结束,发送响应前停止接收DMA */ (void)HAL_UART_AbortReceive(ModbusUart); - __disable_irq(); + /* HAL_UART_AbortReceive has already stopped DMA/IDLE callbacks. The + * finalize routine may copy a full RTU ADU, so it must not globally mask + * the 100 kHz motion interrupts around that memcpy. */ ModbusRxAssemblyFinalize(); - __enable_irq(); /* 无效帧被丢弃后,重新启动DMA接收。 */ if (ModbusRxFrameReady == 0U) { @@ -278,14 +275,37 @@ static HAL_StatusTypeDef ModbusStartReceive(void) { HAL_StatusTypeDef status; + ModbusReceiveRestartAttemptCount++; + + if (ModbusUart == NULL) + { + status = HAL_ERROR; + ModbusReceiveRestartFailureCount++; + ModbusLastReceiveStartStatus = (uint8_t)status; + return status; + } + if (ModbusTxBusy != 0U) { - return HAL_BUSY; + status = HAL_BUSY; + ModbusReceiveRestartFailureCount++; + ModbusLastReceiveStartStatus = (uint8_t)status; + return status; } status = HAL_UARTEx_ReceiveToIdle_DMA(ModbusUart, ModbusRxDmaBuffer, sizeof(ModbusRxDmaBuffer)); + /* A duplicate start request can race with an already healthy DMA receiver. + * Treat that specific HAL_BUSY case as operational, not as a recovery + * failure; all other HAL_BUSY/HAL_ERROR results remain visible. */ + if ((status == HAL_BUSY) + && (ModbusUart->RxState == HAL_UART_STATE_BUSY_RX) + && ((ModbusUart->Instance->CR3 & USART_CR3_DMAR) != 0U)) + { + status = HAL_OK; + } + if ((status == HAL_OK) && (ModbusUart->hdmarx != NULL)) { /* @@ -295,6 +315,18 @@ static HAL_StatusTypeDef ModbusStartReceive(void) __HAL_DMA_DISABLE_IT(ModbusUart->hdmarx, DMA_IT_HT); } + if (status != HAL_OK) + { + ModbusReceiveRestartFailureCount++; + ModbusLastUartErrorCode = ModbusUart->ErrorCode; + ModbusNextReceiveRetryTick = HAL_GetTick() + 10UL; + } + else + { + ModbusNextReceiveRetryTick = 0UL; + } + ModbusLastReceiveStartStatus = (uint8_t)status; + return status; } @@ -329,13 +361,133 @@ static uint16_t ModbusBuildException(uint8_t function, uint8_t exception) } /** - * @brief 处理读线圈功能码 0x01 - * @param[in] request RTU 请求帧 - * @param[in] requestLength 请求帧长度 - * @return 待发送响应长度,异常请求返回异常响应长度 + * @brief Build a bounded, read-only runtime diagnostic response (function 0x47). + * + * The request is: function, start word (BE), quantity (BE). Multi-word + * 32-bit values use low word first, matching the PLSR diagnostic window. */ -static uint16_t ModbusProcessReadCoils(const uint8_t *request, - uint16_t requestLength) +static uint16_t ModbusProcessRuntimeDiagnostics(const uint8_t *request, + uint16_t requestLength) +{ + MODBUS_SLAVE_RUNTIME_DIAGNOSTICS diagnostics; + uint16_t words[MODBUS_RUNTIME_DIAGNOSTICS_WORDS]; + uint32_t flags = 0U; + uint32_t stats[10]; + uint16_t start; + uint16_t quantity; + uint16_t index; + uint16_t responseLength; + + if (requestLength != 8U) + { + ModbusSlaveStatistics.illegalValueCount++; + return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_VALUE); + } + + start = ModbusGetU16Be(&request[2]); + quantity = ModbusGetU16Be(&request[4]); + if ((quantity == 0U) + || (start >= MODBUS_RUNTIME_DIAGNOSTICS_WORDS) + || (quantity > (MODBUS_RUNTIME_DIAGNOSTICS_WORDS - start))) + { + ModbusSlaveStatistics.illegalAddressCount++; + return ModbusBuildException(request[1], MODBUS_EX_ILLEGAL_ADDRESS); + } + + (void)ModbusSlaveGetRuntimeDiagnostics(MODBUS_CONNECTION_TIMEOUT_MS, + &diagnostics); + if (diagnostics.initialized != 0U) + { + flags |= MODBUS_RUNTIME_FLAG_INITIALIZED; + } + if (diagnostics.hasReceivedValidFrame != 0U) + { + flags |= MODBUS_RUNTIME_FLAG_VALID_FRAME_SEEN; + } + if (diagnostics.connected != 0U) + { + flags |= MODBUS_RUNTIME_FLAG_CONNECTED; + } + if (diagnostics.txBusy != 0U) + { + flags |= MODBUS_RUNTIME_FLAG_TX_BUSY; + } + if (diagnostics.rxFrameReady != 0U) + { + flags |= MODBUS_RUNTIME_FLAG_RX_FRAME_READY; + } + if (diagnostics.rxAssemblyInvalid != 0U) + { + flags |= MODBUS_RUNTIME_FLAG_RX_ASSEMBLY_INVALID; + } + if (diagnostics.lastReceiveStartStatus == (uint8_t)HAL_OK) + { + flags |= MODBUS_RUNTIME_FLAG_RX_RESTART_OK; + } + + words[0] = MODBUS_RUNTIME_DIAGNOSTICS_SIGNATURE; + words[1] = MODBUS_RUNTIME_DIAGNOSTICS_VERSION; + words[2] = MODBUS_RUNTIME_DIAGNOSTICS_WORDS; + words[3] = (uint16_t)flags; + words[4] = (uint16_t)diagnostics.currentTick; + words[5] = (uint16_t)(diagnostics.currentTick >> 16U); + words[6] = (uint16_t)diagnostics.lastValidFrameTick; + words[7] = (uint16_t)(diagnostics.lastValidFrameTick >> 16U); + words[8] = (uint16_t)diagnostics.lastInterFrameGapCycles; + words[9] = (uint16_t)(diagnostics.lastInterFrameGapCycles >> 16U); + words[10] = (uint16_t)diagnostics.receiveRestartAttemptCount; + words[11] = (uint16_t)(diagnostics.receiveRestartAttemptCount >> 16U); + words[12] = (uint16_t)diagnostics.receiveRestartFailureCount; + words[13] = (uint16_t)(diagnostics.receiveRestartFailureCount >> 16U); + words[14] = (uint16_t)diagnostics.lastUartErrorCode; + words[15] = (uint16_t)(diagnostics.lastUartErrorCode >> 16U); + words[16] = diagnostics.lastReceiveStartStatus; + words[17] = diagnostics.rxAssemblyLength; + words[18] = diagnostics.rxFrameLength; + words[19] = MODBUS_CONNECTION_TIMEOUT_MS; + + stats[0] = diagnostics.statistics.rxEventCount; + stats[1] = diagnostics.statistics.validFrameCount; + stats[2] = diagnostics.statistics.txFrameCount; + stats[3] = diagnostics.statistics.crcErrorCount; + stats[4] = diagnostics.statistics.ignoredAddressCount; + stats[5] = diagnostics.statistics.illegalFunctionCount; + stats[6] = diagnostics.statistics.illegalAddressCount; + stats[7] = diagnostics.statistics.illegalValueCount; + stats[8] = diagnostics.statistics.droppedFrameCount; + stats[9] = diagnostics.statistics.uartErrorCount; + for (index = 0U; index < 10U; index++) + { + words[20U + (index * 2U)] = (uint16_t)stats[index]; + words[21U + (index * 2U)] = (uint16_t)(stats[index] >> 16U); + } + + ModbusTxFrame[0] = ModbusSlaveAddress; + ModbusTxFrame[1] = MODBUS_RUNTIME_DIAGNOSTICS_FUNCTION; + ModbusTxFrame[2] = (uint8_t)(quantity * 2U); + for (index = 0U; index < quantity; index++) + { + uint16_t value = words[start + index]; + ModbusTxFrame[3U + (index * 2U)] = (uint8_t)(value >> 8U); + ModbusTxFrame[4U + (index * 2U)] = (uint8_t)value; + } + responseLength = (uint16_t)(3U + (quantity * 2U)); + ModbusAppendCrc(ModbusTxFrame, responseLength); + return (uint16_t)(responseLength + 2U); +} + +/** + * @brief Process read coils (0x01) or discrete inputs (0x02). + * @param[in] request RTU request frame. + * @param[in] requestLength Request frame length. + * @param[in] functionCode Function code echoed in the response. + * @param[in] device Backing PLC bit-device image. + * @return Response ADU length, including an exception response when invalid. + */ +static uint16_t ModbusProcessReadBits(const uint8_t *request, + uint16_t requestLength, + uint8_t functionCode, + MODBUS_BIT_DEVICE device) { uint16_t start; @@ -367,13 +519,18 @@ static uint16_t ModbusProcessReadCoils(const uint8_t *request, byteCount = (uint8_t)((quantity + 7U) / 8U); ModbusTxFrame[0] = ModbusSlaveAddress; - ModbusTxFrame[1] = 0X01; + ModbusTxFrame[1] = functionCode; ModbusTxFrame[2] = byteCount; (void)memset(&ModbusTxFrame[3], 0, byteCount); for (index = 0U; index < quantity; index++) { - if (ModbusCoilGetUnchecked((uint16_t)(start + index)) != 0U) + uint8_t value = 0U; + + if ((ModbusDataReadBit(device, + (uint32_t)start + index, + &value) != 0U) + && (value != 0U)) { ModbusTxFrame[3U + (index >> 3U)] |= (uint8_t)(1U << (index & 0x0007U)); @@ -811,7 +968,18 @@ static uint16_t ModbusProcessRequest(const uint8_t *request, // 广播请求不允许读取,从站不作响应 return (isBroadcast != 0U) ? 0U - : ModbusProcessReadCoils(request, requestLength); + : ModbusProcessReadBits(request, + requestLength, + 0X01U, + MODBUS_BIT_DEVICE_M); + + case 0X02U: /* Read discrete inputs: protocol view of PLC X. */ + return (isBroadcast != 0U) + ? 0U + : ModbusProcessReadBits(request, + requestLength, + 0X02U, + MODBUS_BIT_DEVICE_X); case 0X03U: // 读保持寄存器 return (isBroadcast != 0U) @@ -833,6 +1001,12 @@ static uint16_t ModbusProcessRequest(const uint8_t *request, return ModbusProcessWriteMultipleRegisters(request, requestLength, isBroadcast); + case MODBUS_RUNTIME_DIAGNOSTICS_FUNCTION: + return (isBroadcast != 0U) + ? 0U + : ModbusProcessRuntimeDiagnostics(request, + requestLength); + case 0x48U: // 读大地址保持寄存器 return (isBroadcast != 0U) ? 0U @@ -851,6 +1025,10 @@ HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, uint8_t slaveAddress) { + if ((huart == NULL) || (slaveAddress == 0U) || (slaveAddress > 247U)) + { + return HAL_ERROR; + } ModbusUart = huart; ModbusSlaveAddress = slaveAddress; @@ -859,6 +1037,7 @@ HAL_StatusTypeDef ModbusSlaveInit(UART_HandleTypeDef *huart, ModbusRxAssemblyInvalid = 0U; ModbusRxFrameReady = 0U; ModbusTxBusy = 0U; + ModbusNextReceiveRetryTick = 0UL; ModbusRtuTimingInit(huart->Init.BaudRate); return ModbusStartReceive(); @@ -868,6 +1047,19 @@ void ModbusSlavePoll(void) { uint16_t responseLength; + /* Recover from a failed DMA rearm even when no later UART callback occurs. + * Limit retries to 100 Hz so a persistent HAL fault cannot monopolize the + * 1 ms application task. */ + if ((ModbusUart != NULL) && (ModbusTxBusy == 0U) + && (ModbusRxFrameReady == 0U) + && ((ModbusUart->RxState != HAL_UART_STATE_BUSY_RX) + || ((ModbusUart->Instance->CR3 & USART_CR3_DMAR) == 0U)) + && ((ModbusNextReceiveRetryTick == 0UL) + || ((int32_t)(HAL_GetTick() - ModbusNextReceiveRetryTick) >= 0))) + { + (void)ModbusStartReceive(); + } + /* 检查末字节后的静默时间是否已经达到T3.5 */ ModbusTryFinalizeReceive(); if ((ModbusRxFrameReady == 0U) || (ModbusTxBusy != 0U)) @@ -889,6 +1081,7 @@ void ModbusSlavePoll(void) { ModbusTxBusy = 0U; ModbusSlaveStatistics.uartErrorCount++; + ModbusLastUartErrorCode = ModbusUart->ErrorCode; (void)ModbusStartReceive(); // 重启DMA接收 } } @@ -1000,6 +1193,7 @@ void ModbusSlaveOnUartError(UART_HandleTypeDef *huart) } ModbusSlaveStatistics.uartErrorCount++; + ModbusLastUartErrorCode = huart->ErrorCode; ModbusTxBusy = 0U; ModbusRxAssemblyLength = 0U; ModbusRxAssemblyInvalid = 0U; @@ -1059,6 +1253,65 @@ uint8_t ModbusSlaveIsConnected(uint32_t timeoutMs) return ((HAL_GetTick() - ModbusLastValidFrameTick) <= timeoutMs) ? 1U : 0U; } +uint8_t ModbusSlaveGetRuntimeDiagnostics( + uint32_t timeoutMs, + MODBUS_SLAVE_RUNTIME_DIAGNOSTICS *diagnostics) +{ + if (diagnostics == NULL) + { + return 0U; + } + + /* Every source field is naturally aligned and 32-bit-or-smaller on the + * Cortex-M4. A diagnostic snapshot may span adjacent frame events, but + * must never mask the 100 kHz motion/AB fast-gate interrupts. */ + __DMB(); + diagnostics->statistics.rxEventCount = + ModbusSlaveStatistics.rxEventCount; + diagnostics->statistics.validFrameCount = + ModbusSlaveStatistics.validFrameCount; + diagnostics->statistics.txFrameCount = + ModbusSlaveStatistics.txFrameCount; + diagnostics->statistics.crcErrorCount = + ModbusSlaveStatistics.crcErrorCount; + diagnostics->statistics.ignoredAddressCount = + ModbusSlaveStatistics.ignoredAddressCount; + diagnostics->statistics.illegalFunctionCount = + ModbusSlaveStatistics.illegalFunctionCount; + diagnostics->statistics.illegalAddressCount = + ModbusSlaveStatistics.illegalAddressCount; + diagnostics->statistics.illegalValueCount = + ModbusSlaveStatistics.illegalValueCount; + diagnostics->statistics.droppedFrameCount = + ModbusSlaveStatistics.droppedFrameCount; + diagnostics->statistics.uartErrorCount = + ModbusSlaveStatistics.uartErrorCount; + diagnostics->currentTick = HAL_GetTick(); + diagnostics->lastValidFrameTick = ModbusLastValidFrameTick; + diagnostics->lastInterFrameGapCycles = ModbusLastInterFrameGapCycles; + diagnostics->receiveRestartAttemptCount = + ModbusReceiveRestartAttemptCount; + diagnostics->receiveRestartFailureCount = + ModbusReceiveRestartFailureCount; + diagnostics->lastUartErrorCode = ModbusLastUartErrorCode; + diagnostics->rxAssemblyLength = ModbusRxAssemblyLength; + diagnostics->rxFrameLength = ModbusRxFrameLength; + diagnostics->lastReceiveStartStatus = ModbusLastReceiveStartStatus; + diagnostics->initialized = (ModbusUart != NULL) ? 1U : 0U; + diagnostics->hasReceivedValidFrame = ModbusHasReceivedValidFrame; + diagnostics->connected = + ((ModbusHasReceivedValidFrame != 0U) + && ((diagnostics->currentTick - ModbusLastValidFrameTick) + <= timeoutMs)) + ? 1U + : 0U; + diagnostics->txBusy = ModbusTxBusy; + diagnostics->rxFrameReady = ModbusRxFrameReady; + diagnostics->rxAssemblyInvalid = ModbusRxAssemblyInvalid; + __DMB(); + return 1U; +} + // 上电恢复函数 void ModbusRetainedRegistersLoad(void) { diff --git a/PLSR/Inc/plsr_build_config.h b/PLSR/Inc/plsr_build_config.h new file mode 100644 index 0000000..eebf6a6 --- /dev/null +++ b/PLSR/Inc/plsr_build_config.h @@ -0,0 +1,37 @@ +#ifndef PLSR_BUILD_CONFIG_H +#define PLSR_BUILD_CONFIG_H + +/* Validation is the repository default while the remaining board acceptance + * suite is being executed. A production image must override this symbol to + * zero in the IAR configuration; the Modbus control plane remains available. */ +#ifndef PLSR_ENABLE_BOARD_SELF_TEST +#define PLSR_ENABLE_BOARD_SELF_TEST (1U) +#endif + +#ifndef PLSR_ENABLE_HW_TRACE +#define PLSR_ENABLE_HW_TRACE PLSR_ENABLE_BOARD_SELF_TEST +#endif + +#ifndef PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG +#define PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG (0U) +#endif + +#ifndef APP_ENABLE_USB_CDC +#define APP_ENABLE_USB_CDC (1U) +#endif + +#define PLSR_BOARD_TEST_NONE (0U) +#define PLSR_BOARD_TEST_CW_CCW (9U) +#define PLSR_BOARD_TEST_FAST_REFRESH (10U) +#define PLSR_BOARD_TEST_DYNAMIC_FREQ (11U) +#define PLSR_BOARD_TEST_MODBUS_DATA (12U) +#define PLSR_BOARD_TEST_MODBUS_CONTROL (13U) +#define PLSR_BOARD_TEST_HW_COUNTER (14U) +#define PLSR_BOARD_TEST_DUAL_AB (17U) +#define PLSR_BOARD_TEST_LONG_STRESS (18U) + +#ifndef PLSR_BOARD_TEST_SELECT +#define PLSR_BOARD_TEST_SELECT PLSR_BOARD_TEST_DUAL_AB +#endif + +#endif diff --git a/PLSR/Inc/plsr_core.h b/PLSR/Inc/plsr_core.h index 8f8bf6d..5bf44f5 100644 --- a/PLSR/Inc/plsr_core.h +++ b/PLSR/Inc/plsr_core.h @@ -9,11 +9,26 @@ extern "C" { #endif +#define PLSR_PROCESS_STAGE_COUNT (6U) + +typedef enum +{ + PLSR_PROCESS_STAGE_ACCOUNT_PROTECTION = 0, + PLSR_PROCESS_STAGE_CRITICAL_EVENTS, + PLSR_PROCESS_STAGE_COMMANDS, + PLSR_PROCESS_STAGE_NORMAL_EVENTS, + PLSR_PROCESS_STAGE_TICK_PATH_PROFILE, + PLSR_PROCESS_STAGE_HSD_CHECKPOINT +} PLSR_PROCESS_STAGE; + PLSR_RESULT PlsrInit(void); void PlsrTask(void *argument); void PlsrProcess(void); void PlsrControlTick100us(void); void PlsrSetControlTickHook(void (*hook)(void)); +uint32_t PlsrGetMaxProcessCycles(void); +uint32_t PlsrGetMaxProcessResponseCycles(void); +uint32_t PlsrGetMaxProcessStageCycles(uint8_t stage); PLSR_RESULT PlsrPostCall(const PLSR_CALL *call); /* Side-effect-free COMMIT validation. It parses the complete S0/S1/S2/D diff --git a/PLSR/Inc/plsr_hal_f407.h b/PLSR/Inc/plsr_hal_f407.h index 6947bf0..0b42361 100644 --- a/PLSR/Inc/plsr_hal_f407.h +++ b/PLSR/Inc/plsr_hal_f407.h @@ -49,6 +49,18 @@ uint32_t PlsrHwGetTimerClockHz(uint8_t axis); uint32_t PlsrHwGetCurrentFrequencyHz(uint8_t axis); int64_t PlsrHwGetEmittedPulses(uint8_t axis); uint8_t PlsrHwIsAbStartupPriming(uint8_t axis); +uint8_t PlsrHwUsesHardwareCounter(uint8_t axis); +uint32_t PlsrHwGetMaxOutputIsrCycles(void); +uint32_t PlsrHwGetMaxCounterIsrCycles(void); +uint32_t PlsrHwGetMaxControlIsrCycles(void); +/* Fast gate from the final AB 00 boundary; budget is one 100kHz quarter + * period (420 cycles at 168MHz). */ +uint32_t PlsrHwGetMaxAbGateCycles(void); +/* Atomically sample the free-running CPU counter and the accumulated time + * spent inside PLSR timer ISRs. PlsrProcess uses the pair to distinguish its + * own CPU cost from wall-clock response time under high-rate preemption. */ +void PlsrHwGetCycleSnapshot(uint32_t *cycleCount, + uint64_t *plsrIsrCycles); /* 每 1ms tick 推进 HAL 状态机(DIR 延时等)。 */ void PlsrHwTick(uint8_t axis); @@ -73,8 +85,12 @@ uint8_t PlsrHwTestGetAbPhaseA(uint8_t axis); uint8_t PlsrHwTestGetAbPhaseB(uint8_t axis); uint8_t PlsrHwTestGetAbQuarter(uint8_t axis); void PlsrHwTestTriggerUpdate(uint8_t axis); +void PlsrHwTestTriggerUpdateAndCompare(uint8_t axis); void PlsrHwTestTriggerCompare(uint8_t axis); void PlsrHwTestAdvanceAbQuarter(uint8_t axis); +uint32_t PlsrHwTestGetAbFullGateCount(void); +void PlsrHwTestSignalDualAbFinalBoundary(uint8_t firstAxis); +void PlsrHwTestSignalDualAbStaggeredFinalBoundary(uint8_t firstAxis); #endif #ifdef __cplusplus diff --git a/PLSR/Inc/plsr_modbus_control.h b/PLSR/Inc/plsr_modbus_control.h index c97275d..45036e0 100644 --- a/PLSR/Inc/plsr_modbus_control.h +++ b/PLSR/Inc/plsr_modbus_control.h @@ -9,18 +9,35 @@ extern "C" { #endif #define PLSR_MODBUS_PROTOCOL_VERSION (0x0100U) -#define PLSR_MODBUS_WINDOW_WORDS (256UL) +#define PLSR_MODBUS_WINDOW_WORDS (338UL) #define PLSR_MODBUS_CALL_REQUEST_OFFSET (8UL) #define PLSR_MODBUS_CALL_RESPONSE_OFFSET (24UL) #define PLSR_MODBUS_COMMAND_REQUEST_OFFSET (40UL) #define PLSR_MODBUS_COMMAND_RESPONSE_OFFSET (48UL) +#define PLSR_MODBUS_PERFORMANCE_OFFSET (56UL) +#define PLSR_MODBUS_PERFORMANCE_WORDS (8UL) #define PLSR_MODBUS_AXIS_STATUS_OFFSET (64UL) #define PLSR_MODBUS_AXIS_STATUS_WORDS (48UL) +#define PLSR_MODBUS_STAGE_PERFORMANCE_OFFSET (256UL) +#define PLSR_MODBUS_STAGE_PERFORMANCE_WORDS (12UL) +#define PLSR_MODBUS_AB_GATE_PERFORMANCE_OFFSET (268UL) +#define PLSR_MODBUS_AB_GATE_PERFORMANCE_WORDS (2UL) +#define PLSR_MODBUS_PERSISTENCE_OFFSET (270UL) +#define PLSR_MODBUS_PERSISTENCE_WORDS (30UL) +#define PLSR_MODBUS_PERSISTENCE_REQUEST_OFFSET (300UL) +#define PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS (8UL) +#define PLSR_MODBUS_PERSISTENCE_RESPONSE_OFFSET (308UL) +#define PLSR_MODBUS_PERSISTENCE_RESPONSE_WORDS (8UL) +#define PLSR_MODBUS_USB_DIAGNOSTICS_OFFSET (316UL) +#define PLSR_MODBUS_USB_DIAGNOSTICS_WORDS (22UL) #define PLSR_MODBUS_CALL_NONE (0U) #define PLSR_MODBUS_CALL_COMMIT (1U) #define PLSR_MODBUS_CALL_START (2U) +#define PLSR_MODBUS_PERFORMANCE_VERSION (7U) +#define PLSR_MODBUS_PERSISTENCE_VERSION (1U) +#define PLSR_MODBUS_USB_DIAGNOSTICS_VERSION (1U) PLSR_RESULT PlsrModbusControlInit(uint16_t baseAddress); void PlsrModbusControlPoll(void); diff --git a/PLSR/Inc/plsr_modbus_data.h b/PLSR/Inc/plsr_modbus_data.h index f32d60d..892a6b1 100644 --- a/PLSR/Inc/plsr_modbus_data.h +++ b/PLSR/Inc/plsr_modbus_data.h @@ -7,8 +7,9 @@ extern "C" { #endif -/* Build a PLSR data source backed by the Modbus register store. D, HD and FD - * are separate logical spaces; standard Modbus holding registers expose D. */ +/* Build a PLSR data source backed by the shared PLC data store. D/HD/FD and + * X/M/HM are separate logical spaces; standard Modbus holding registers expose + * D and standard Modbus coils expose M. */ void PlsrModbusDataSourceInit(PLSR_DATA_SOURCE *source); #ifdef __cplusplus diff --git a/PLSR/Inc/plsr_persistence.h b/PLSR/Inc/plsr_persistence.h index b849f02..82e1f78 100644 --- a/PLSR/Inc/plsr_persistence.h +++ b/PLSR/Inc/plsr_persistence.h @@ -36,6 +36,35 @@ typedef enum PLSR_PERSISTENCE_PROGRAM_FAILED } PLSR_PERSISTENCE_RESULT; +/* Cached persistence health snapshot. Slot CRCs are evaluated only by + * load/save/erase/diagnostic operations; reading this structure never scans + * Backup SRAM or Flash and is therefore safe in the 1 ms Modbus poll path. */ +typedef struct +{ + uint8_t hsdValidMask; + uint8_t sfdValidMask; + uint8_t hsdNewestMask; + uint8_t sfdNewestMask; + uint8_t destructiveDiagnosticEnabled; + PLSR_PERSISTENCE_RESULT lastHsdLoadResult; + PLSR_PERSISTENCE_RESULT lastSfdLoadResult; + PLSR_PERSISTENCE_RESULT lastHsdSaveResult; + PLSR_PERSISTENCE_RESULT lastSfdSaveResult; + PLSR_PERSISTENCE_RESULT lastSfdEraseResult; + uint32_t hsdGeneration[2]; + uint32_t sfdGeneration[2]; + uint32_t hsdSaveCount; + uint32_t sfdSaveCount; + uint32_t selectedHsdCrc32; + uint32_t selectedSfdCrc32; +} PLSR_PERSISTENCE_DIAGNOSTICS; + +typedef enum +{ + PLSR_PERSISTENCE_DIAG_TARGET_HSD = 1, + PLSR_PERSISTENCE_DIAG_TARGET_SFD = 2 +} PLSR_PERSISTENCE_DIAG_TARGET; + PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadHsd(PLSR_HSD_DATA *data); PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveHsd(const PLSR_HSD_DATA *data); void PlsrPersistenceResetHsd(void); @@ -44,6 +73,16 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data); PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data); PLSR_PERSISTENCE_RESULT PlsrPersistenceEraseSfd(void); +void PlsrPersistenceGetDiagnostics(PLSR_PERSISTENCE_DIAGNOSTICS *diagnostics); + +/* Deliberately unavailable in normal builds. A validation build may enable + * it with PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG=1; the Modbus control layer + * additionally requires a double-magic, sequence/inverse handshake and all + * motion axes idle. This function only invalidates the newest committed slot + * and never accepts a memory address. */ +PLSR_PERSISTENCE_RESULT PlsrPersistenceDiagnosticInvalidateNewest( + PLSR_PERSISTENCE_DIAG_TARGET target); + #ifdef PLSR_HOST_TEST typedef enum { @@ -58,6 +97,8 @@ void PlsrPersistenceTestResetStorage(void); void PlsrPersistenceTestCorruptNewestHsd(void); void PlsrPersistenceTestCorruptNewestSfd(void); void PlsrPersistenceTestSetSfdFault(PLSR_TEST_SFD_FAULT fault); +uint32_t PlsrPersistenceTestGetHsdSaveCount(void); +uint32_t PlsrPersistenceTestCrc32(const uint8_t *data, uint32_t length); #endif #ifdef __cplusplus diff --git a/PLSR/Inc/plsr_self_test.h b/PLSR/Inc/plsr_self_test.h index de766b8..53fa143 100644 --- a/PLSR/Inc/plsr_self_test.h +++ b/PLSR/Inc/plsr_self_test.h @@ -38,6 +38,10 @@ PLSR_RESULT PlsrDynamicFrequencySelfTestQueue(void); PLSR_RESULT PlsrModbusDataSelfTestQueue(void); /* P13 prepares deterministic SFD K1 data; motion is commanded via Modbus. */ PLSR_RESULT PlsrModbusControlSelfTestPrepare(void); +/* P14/P15/P17: P/D counter, soft-limit and dual-AB Modbus stress setup. */ +PLSR_RESULT PlsrHardwareCounterSelfTestPrepare(void); +/* P18: K4 four-axis 100kHz P/D long-stress setup with soft limits disabled. */ +PLSR_RESULT PlsrLongStressSelfTestPrepare(void); void PlsrSelfTestControlTick100us(void); /* P11 board-test control. Write this signed dword from IAR Watch while the diff --git a/PLSR/Inc/plsr_types.h b/PLSR/Inc/plsr_types.h index b7bbd4b..a962fdb 100644 --- a/PLSR/Inc/plsr_types.h +++ b/PLSR/Inc/plsr_types.h @@ -179,6 +179,7 @@ typedef struct uint8_t wait; uint8_t directionPositive; uint8_t highResourceMask; + uint8_t hardwareCounter; uint8_t directionPoint; uint8_t positionValid; uint8_t jobValid; diff --git a/PLSR/Src/plsr_core.c b/PLSR/Src/plsr_core.c index 258ec74..359eba2 100644 --- a/PLSR/Src/plsr_core.c +++ b/PLSR/Src/plsr_core.c @@ -85,6 +85,12 @@ static uint8_t PlsrInitialized; static PLSR_JOB_SNAPSHOT PlsrJobScratch; static PLSR_JOB_SNAPSHOT PlsrValidationScratch; static void (* volatile PlsrControlTickHook)(void); +static volatile uint32_t PlsrMaxProcessCycles; +static volatile uint32_t PlsrMaxProcessResponseCycles; +static volatile uint32_t + PlsrMaxProcessStageCycles[PLSR_PROCESS_STAGE_COUNT]; +static uint8_t PlsrDeferHsdCheckpoint; +static uint8_t PlsrHsdCheckpointPending; static void PlsrStopSegmentHardware(uint8_t axis, PLSR_AXIS *axisObject); static PLSR_RESULT PlsrStartSegmentHardware(uint8_t axis, @@ -94,6 +100,63 @@ static void PlsrAccountHardwarePulses(uint8_t axis, static void PlsrPublishSegmentEvent(uint8_t axis, PLSR_AXIS *axisObject, PLSR_STOP_REASON reason); +static uint8_t PlsrAnyAxisBusy(void); +static uint8_t PlsrAllAxesPositionValid(void); + +#ifndef PLSR_HOST_TEST +static void PlsrUpdateProcessStageMax(uint8_t stage, + uint32_t started, + uint64_t startedIsrCycles, + uint32_t finished, + uint64_t finishedIsrCycles) +{ + uint32_t responseCycles = finished - started; + uint64_t preemptedCycles = finishedIsrCycles - startedIsrCycles; + uint32_t processCycles = + (preemptedCycles < (uint64_t)responseCycles) + ? responseCycles - (uint32_t)preemptedCycles + : 0UL; + + if ((stage < PLSR_PROCESS_STAGE_COUNT) + && (processCycles > PlsrMaxProcessStageCycles[stage])) + { + PlsrMaxProcessStageCycles[stage] = processCycles; + } +} +#endif + +static void PlsrFlushHsdCheckpoint(void) +{ + if (PlsrHsdCheckpointPending != 0U) + { + (void)PlcDeviceSetHsdCheckpointMeta( + PlsrAllAxesPositionValid(), + PlsrAnyAxisBusy()); + if (PlcDeviceCheckpointHsd() == PLC_DEVICE_OK) + { + PlsrHsdCheckpointPending = 0U; + } + } +} + +static void PlsrCheckpointHsd(void) +{ + PlsrHsdCheckpointPending = 1U; + if (PlsrDeferHsdCheckpoint == 0U) + { + PlsrFlushHsdCheckpoint(); + } +} + +static void PlsrCheckpointHsdImmediate(void) +{ + uint8_t deferHsdCheckpoint = PlsrDeferHsdCheckpoint; + + PlsrHsdCheckpointPending = 1U; + PlsrDeferHsdCheckpoint = 0U; + PlsrFlushHsdCheckpoint(); + PlsrDeferHsdCheckpoint = deferHsdCheckpoint; +} static uint32_t PlsrCoreEnterCritical(void) { @@ -485,11 +548,15 @@ static PLSR_RESULT PlsrReadLimitInput(const PLSR_JOB_SNAPSHOT *job, return PLSR_RESULT_OK; } -static int64_t PlsrGetBrakingDistance(const PLSR_AXIS *axisObject) +static int64_t PlsrGetBrakingDistance(uint8_t axis, + const PLSR_AXIS *axisObject) { uint64_t frequencyHz; + uint64_t hardwareFrequencyHz; uint64_t denominator; uint64_t numerator; + uint64_t brakingPulses; + uint64_t samplingPulses; uint64_t frequencyQ32; uint32_t decelSlopeHzPerMs; uint32_t interruptState; @@ -508,9 +575,30 @@ static int64_t PlsrGetBrakingDistance(const PLSR_AXIS *axisObject) return 0; } frequencyHz = frequencyQ32 >> 32U; + hardwareFrequencyHz = PlsrHwGetCurrentFrequencyHz(axis); + if (hardwareFrequencyHz > frequencyHz) + { + /* Protection must follow the frequency already present at the output, + * not an earlier/lower profile value waiting behind timer preload. */ + frequencyHz = hardwareFrequencyHz; + } numerator = frequencyHz * frequencyHz; denominator = UINT64_C(2000) * decelSlopeHzPerMs; - return (int64_t)((numerator + denominator - 1UL) / denominator); + brakingPulses = (numerator + denominator - 1UL) / denominator; + /* The 1ms protection task can observe several new pulses per pass. The + * inclusive position comparison already covers one of them; reserve only + * the additional pulses so <=1kHz behavior is unchanged while higher + * frequencies cannot cross the limit by a complete sampling window. */ + samplingPulses = (frequencyHz + UINT64_C(999)) / UINT64_C(1000); + if (samplingPulses > 0UL) + { + samplingPulses--; + } + if (brakingPulses > (uint64_t)INT64_MAX - samplingPulses) + { + return INT64_MAX; + } + return (int64_t)(brakingPulses + samplingPulses); } static PLSR_RESULT PlsrUpdateLimitState(PLSR_AXIS *axisObject, @@ -541,7 +629,8 @@ static PLSR_RESULT PlsrUpdateLimitState(PLSR_AXIS *axisObject, { if (includeBrakingDistance != 0U) { - brakingDistance = PlsrGetBrakingDistance(axisObject); + brakingDistance = PlsrGetBrakingDistance(job->dAxis, + axisObject); } if (axisObject->logicalPosition >= job->limits.positiveSoftLimitPulses) @@ -800,9 +889,8 @@ PLSR_RESULT PlsrStateTransition(uint8_t axis, PlsrPathTerminate(&axisObject->path); PlsrStopSegmentHardware(axis, axisObject); PlsrResourceRelease(&axisObject->lease); - /* 运动已结束:记录 lastBusy=0,保证掉电后恢复时位置仍可信。 */ - (void)PlcDeviceSetHsdCheckpointMeta(axisObject->positionValid, 0U); - (void)PlcDeviceCheckpointHsd(); + /* 检查点写入前按全部轴的最终状态统一计算 lastBusy。 */ + PlsrCheckpointHsd(); } PlsrPublishAxis(axis); return PLSR_RESULT_OK; @@ -1108,11 +1196,27 @@ static uint8_t PlsrAnyAxisBusy(void) return 0U; } +static uint8_t PlsrAllAxesPositionValid(void) +{ + uint8_t axis; + + for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) + { + if ((PlsrAxes[axis].positionValid == 0U) + || (PlsrAxes[axis].positionOverflow != 0U)) + { + return 0U; + } + } + return 1U; +} + static PLSR_RESULT PlsrStartAxis(PLSR_AXIS *axisObject, const PLSR_START_REQUEST *start) { PLSR_RESOURCE_REQUEST resourceRequest; PLSR_RESULT result; + uint8_t axisWasBusy = PlsrAnyAxisBusy(); if ((axisObject->state != PLSR_STATE_IDLE) && (axisObject->state != PLSR_STATE_COMPLETED) @@ -1165,9 +1269,12 @@ static PLSR_RESULT PlsrStartAxis(PLSR_AXIS *axisObject, } else { - /* 运动开始:掉电恢复时据此判定"断电时在运动中"。 */ - (void)PlcDeviceSetHsdCheckpointMeta(axisObject->positionValid, 1U); - (void)PlcDeviceCheckpointHsd(); + /* 全局空闲到运行的边沿立即落盘,保持原有掉电安全窗口;后续 + * 并发轴已由同一个全局 lastBusy=1 检查点覆盖。 */ + if (axisWasBusy == 0U) + { + PlsrCheckpointHsdImmediate(); + } } return result; } @@ -1178,6 +1285,7 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject, PLSR_RESOURCE_REQUEST resourceRequest; PLSR_PARSE_CONTEXT parseContext; PLSR_RESULT result; + uint8_t axisWasBusy = PlsrAnyAxisBusy(); if ((axisObject->state != PLSR_STATE_IDLE) && (axisObject->state != PLSR_STATE_COMPLETED) @@ -1308,6 +1416,12 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject, } else { + /* 先持久化全局 0->1 busy 边沿,再允许硬件输出启动;后续并发 + * 轴由同一个 lastBusy=1 检查点覆盖。 */ + if (axisWasBusy == 0U) + { + PlsrCheckpointHsdImmediate(); + } /* 启动当前段硬件输出与速度曲线。 */ /* 零脉冲跳转链耗尽本轮预算时没有实际运动段,等待下一次 * PlsrPathTick 找到非零段后再启动硬件。 */ @@ -1346,9 +1460,6 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject, } return result; } - /* 运动开始:掉电恢复时据此判定"断电时在运动中"。 */ - (void)PlcDeviceSetHsdCheckpointMeta(axisObject->positionValid, 1U); - (void)PlcDeviceCheckpointHsd(); } return result; } @@ -1618,8 +1729,7 @@ static PLSR_RESULT PlsrExecuteCommand(const PLSR_COMMAND_SLOT *slot) axisObject->positionValid = 1U; axisObject->positionOverflow = 0U; PlsrPublishPosition(slot->command.axis, axisObject); - (void)PlcDeviceSetHsdCheckpointMeta(1U, 0U); - (void)PlcDeviceCheckpointHsd(); + PlsrCheckpointHsd(); result = PLSR_RESULT_OK; } break; @@ -1635,8 +1745,7 @@ static PLSR_RESULT PlsrExecuteCommand(const PLSR_COMMAND_SLOT *slot) axisObject->positionValid = 1U; axisObject->positionOverflow = 0U; PlsrPublishPosition(slot->command.axis, axisObject); - (void)PlcDeviceSetHsdCheckpointMeta(1U, 0U); - (void)PlcDeviceCheckpointHsd(); + PlsrCheckpointHsd(); result = PLSR_RESULT_OK; } break; @@ -1649,10 +1758,7 @@ static PLSR_RESULT PlsrExecuteCommand(const PLSR_COMMAND_SLOT *slot) else { axisObject->totalPulses = 0; - (void)PlcDeviceSetHsdCheckpointMeta( - axisObject->positionValid, - 0U); - (void)PlcDeviceCheckpointHsd(); + PlsrCheckpointHsd(); result = PLSR_RESULT_OK; } break; @@ -2336,6 +2442,13 @@ PLSR_RESULT PlsrInit(void) (void)memset(PlsrAxes, 0, sizeof(PlsrAxes)); (void)memset(PlsrCommandQueue, 0, sizeof(PlsrCommandQueue)); PlsrNextTicket = 0UL; + PlsrMaxProcessCycles = 0UL; + PlsrMaxProcessResponseCycles = 0UL; + (void)memset((void *)PlsrMaxProcessStageCycles, + 0, + sizeof(PlsrMaxProcessStageCycles)); + PlsrDeferHsdCheckpoint = 0U; + PlsrHsdCheckpointPending = 0U; PlsrResourceInit(); (void)PlsrHwInit(); PlsrControlTickHook = NULL; @@ -2526,6 +2639,12 @@ void PlsrProcess(void) { PLSR_COMMAND_SLOT slot; uint32_t events; +#ifndef PLSR_HOST_TEST + uint32_t started; + uint64_t startedIsrCycles; + uint32_t stageStarted; + uint64_t stageStartedIsrCycles; +#endif uint8_t processedCommands = 0U; uint8_t criticalAxes = 0U; uint8_t axis; @@ -2534,6 +2653,12 @@ void PlsrProcess(void) { return; } +#ifndef PLSR_HOST_TEST + PlsrHwGetCycleSnapshot(&started, &startedIsrCycles); + stageStarted = started; + stageStartedIsrCycles = startedIsrCycles; +#endif + PlsrDeferHsdCheckpoint = 1U; /* 先合并 ISR 已完成的实际脉冲,确保段完成、STOP或新命令不会在 * HAL 计数清零前丢失最后一批位置增量。 */ @@ -2542,6 +2667,21 @@ void PlsrProcess(void) PlsrAccountHardwarePulses(axis, &PlsrAxes[axis]); PlsrMonitorAxisProtection(axis); } +#ifndef PLSR_HOST_TEST + { + uint32_t finished; + uint64_t finishedIsrCycles; + + PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); + PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_ACCOUNT_PROTECTION, + stageStarted, + stageStartedIsrCycles, + finished, + finishedIsrCycles); + stageStarted = finished; + stageStartedIsrCycles = finishedIsrCycles; + } +#endif for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) { @@ -2552,6 +2692,21 @@ void PlsrProcess(void) PlsrProcessCriticalEvents(axis, events); } } +#ifndef PLSR_HOST_TEST + { + uint32_t finished; + uint64_t finishedIsrCycles; + + PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); + PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_CRITICAL_EVENTS, + stageStarted, + stageStartedIsrCycles, + finished, + finishedIsrCycles); + stageStarted = finished; + stageStartedIsrCycles = finishedIsrCycles; + } +#endif /* Apply related multi-axis DIR changes after all commands and segment * events, keeping cross-port GPIO writes in one short commit window. */ @@ -2575,6 +2730,21 @@ void PlsrProcess(void) } processedCommands++; } +#ifndef PLSR_HOST_TEST + { + uint32_t finished; + uint64_t finishedIsrCycles; + + PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); + PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_COMMANDS, + stageStarted, + stageStartedIsrCycles, + finished, + finishedIsrCycles); + stageStarted = finished; + stageStartedIsrCycles = finishedIsrCycles; + } +#endif for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) { @@ -2588,6 +2758,21 @@ void PlsrProcess(void) } PlsrHwEndDirectionBatch(); +#ifndef PLSR_HOST_TEST + { + uint32_t finished; + uint64_t finishedIsrCycles; + + PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); + PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_NORMAL_EVENTS, + stageStarted, + stageStartedIsrCycles, + finished, + finishedIsrCycles); + stageStarted = finished; + stageStartedIsrCycles = finishedIsrCycles; + } +#endif /* 1ms tick:路径执行器推进(WAIT/ACT 计时、信号/EXT 轮询、跳转链) * + 速度曲线推进(P2) + HAL 状态机(DIR 延时)。 */ @@ -2597,7 +2782,11 @@ void PlsrProcess(void) PLSR_PATH_ACTION action; PlsrHwTick(axis); - PlsrAccountHardwarePulses(axis, axisObject); + /* Pulses were merged at the beginning of this pass. Merging again + * here republishes HSD/SD runtime data for the few pulses emitted + * while PlsrProcess itself was running and nearly doubles the + * four-axis cost. Those pulses are safely merged at the beginning + * of the next pass or by the terminal event path. */ /* 首次 AB 内部预热周期不属于用户运动,速度曲线也必须冻结; * 否则低速起步时会在隐藏周期内提前爬升十余个刷新步。 */ if (PlsrHwIsAbStartupPriming(axis) != 0U) @@ -2623,6 +2812,68 @@ void PlsrProcess(void) PlsrStepProfileAxis(axis); } } +#ifndef PLSR_HOST_TEST + { + uint32_t tickFinished; + uint32_t finished; + uint32_t responseCycles; + uint32_t processCycles; + uint64_t tickFinishedIsrCycles; + uint64_t finishedIsrCycles; + uint64_t preemptedCycles; + + PlsrHwGetCycleSnapshot(&tickFinished, &tickFinishedIsrCycles); + PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_TICK_PATH_PROFILE, + stageStarted, + stageStartedIsrCycles, + tickFinished, + tickFinishedIsrCycles); + PlsrDeferHsdCheckpoint = 0U; + PlsrFlushHsdCheckpoint(); + PlsrHwGetCycleSnapshot(&finished, &finishedIsrCycles); + PlsrUpdateProcessStageMax(PLSR_PROCESS_STAGE_HSD_CHECKPOINT, + tickFinished, + tickFinishedIsrCycles, + finished, + finishedIsrCycles); + responseCycles = finished - started; + preemptedCycles = finishedIsrCycles - startedIsrCycles; + processCycles = (preemptedCycles < (uint64_t)responseCycles) + ? responseCycles - (uint32_t)preemptedCycles + : 0UL; + + if (processCycles > PlsrMaxProcessCycles) + { + PlsrMaxProcessCycles = processCycles; + } + if (responseCycles > PlsrMaxProcessResponseCycles) + { + PlsrMaxProcessResponseCycles = responseCycles; + } + } +#else + PlsrDeferHsdCheckpoint = 0U; + PlsrFlushHsdCheckpoint(); +#endif +} + +uint32_t PlsrGetMaxProcessCycles(void) +{ + return PlsrMaxProcessCycles; +} + +uint32_t PlsrGetMaxProcessResponseCycles(void) +{ + return PlsrMaxProcessResponseCycles; +} + +uint32_t PlsrGetMaxProcessStageCycles(uint8_t stage) +{ + if (stage >= PLSR_PROCESS_STAGE_COUNT) + { + return 0UL; + } + return PlsrMaxProcessStageCycles[stage]; } void PlsrTask(void *argument) @@ -2673,6 +2924,7 @@ PLSR_RESULT PlsrGetStatus(uint8_t axis, PLSR_STATUS *status) status->wait = (axisObject->state == PLSR_STATE_WAIT) ? 1U : 0U; status->directionPositive = axisObject->directionPositive; status->highResourceMask = axisObject->lease.highMask; + status->hardwareCounter = PlsrHwUsesHardwareCounter(axis); status->directionPoint = (axisObject->lease.valid != 0U) ? axisObject->lease.directionPoint : PLSR_DIRECTION_POINT_NONE; diff --git a/PLSR/Src/plsr_hal_f407.c b/PLSR/Src/plsr_hal_f407.c index 23a51c8..46dc058 100644 --- a/PLSR/Src/plsr_hal_f407.c +++ b/PLSR/Src/plsr_hal_f407.c @@ -1,5 +1,6 @@ #include "plsr_hal_f407.h" #include "plsr_address_map.h" +#include "plsr_build_config.h" #include "plsr_core.h" #include "plsr_job.h" #include @@ -15,6 +16,9 @@ #define PLSR_HW_OUTPUT_POINT_COUNT (21U) #define PLSR_HW_DBG_SNAPSHOT_COUNT (160U) #define PLSR_HW_AB_QUARTER_COUNT (4U) +#define PLSR_HW_COUNTER_COUNT (2U) +#define PLSR_HW_COUNTER_NONE (0xFFU) +#define PLSR_HW_COUNTER_BLOCK_PULSES (UINT64_C(65536)) typedef struct { @@ -153,7 +157,6 @@ typedef struct uint8_t abQuarter; uint8_t abCountAxis; uint8_t abStartupPriming; - uint8_t abOutputPrimed; uint16_t abActiveBasePsc; uint16_t abActivePairPsc; uint16_t abActiveArr; @@ -161,17 +164,94 @@ typedef struct uint16_t abPendingPairPsc; uint16_t abPendingArr; uint8_t abFrequencyPending; + uint8_t abStopArmed; + uint8_t abFastGated; + uint8_t abPausePending; + uint8_t abPauseGated; + uint8_t abCompletionDeferred; + uint8_t counterSourceAxis; + uint32_t abCounterBoundaryCnt; uint8_t cwActiveAxis; uint8_t cwStopPending; + uint8_t counterIndex; + uint8_t hardwareCounterActive; + uint8_t hardwareCounterConfigured; + uint64_t counterBlockPulses; } PLSR_HW_AXIS_STATE; static PLSR_HW_AXIS_STATE PlsrHwAxes[PLSR_HW_AXIS_COUNT]; static uint8_t PlsrHwDirectionBatchActive; +static uint8_t PlsrHwCounterOwners[PLSR_HW_COUNTER_COUNT]; +static volatile uint32_t PlsrHwMaxOutputIsrCycles; +static volatile uint32_t PlsrHwMaxCounterIsrCycles; +static volatile uint32_t PlsrHwMaxControlIsrCycles; +static volatile uint32_t PlsrHwMaxAbGateCycles; +#ifndef PLSR_HOST_TEST +static volatile uint8_t PlsrHwAbGateMeasurePending; +#endif +static volatile uint64_t PlsrHwTotalIsrCycles; +static volatile uint32_t PlsrHwIsrBusyStarted; +static volatile uint8_t PlsrHwIsrNesting; + +#ifndef PLSR_HOST_TEST +static TIM_TypeDef * const PlsrHwCounters[PLSR_HW_COUNTER_COUNT] = +{ + TIM9, TIM12 +}; +#endif + +static void PlsrHwCounterBegin(uint8_t axis); +static void PlsrHwCounterSuspend(uint8_t axis); +static void PlsrHwFinishDeferredAbWork(uint8_t axis); + +#ifdef PLSR_HOST_TEST +static uint8_t PlsrHwTestLateAbFlagAxis = PLSR_HW_COUNTER_NONE; +static uint32_t PlsrHwTestAbFullGateCount; +#endif + +#ifndef PLSR_HOST_TEST +static uint32_t PlsrHwCycleBegin(void) +{ + uint32_t started = DWT->CYCCNT; + + if (PlsrHwIsrNesting == 0U) + { + PlsrHwIsrBusyStarted = started; + } + PlsrHwIsrNesting++; + return started; +} + +static void PlsrHwRecordMaxCycles(volatile uint32_t *maximum, + uint32_t started) +{ + uint32_t finished = DWT->CYCCNT; + uint32_t elapsed = finished - started; + + if (elapsed > *maximum) + { + *maximum = elapsed; + } + if (PlsrHwIsrNesting > 0U) + { + PlsrHwIsrNesting--; + if (PlsrHwIsrNesting == 0U) + { + uint32_t busyStarted = PlsrHwIsrBusyStarted; + uint64_t totalCycles = PlsrHwTotalIsrCycles; + + /* Count a nested TIM6/high-speed interrupt window once. */ + totalCycles += finished - busyStarted; + PlsrHwTotalIsrCycles = totalCycles; + } + } +} +#endif /* 调试快照:当前上板自测只记录 Q0 的 160 ms,避免四轴 * PlsrHwTick 互相混入,同时控制临时 RAM 占用。reason=0 表示段启动, * reason=3 表示 1 ms HAL tick,reason=4 表示 AB 在 00 边界换频重定相。 */ -#ifndef PLSR_HOST_TEST +#if !defined(PLSR_HOST_TEST) && (PLSR_ENABLE_HW_TRACE != 0U) typedef struct { uint8_t reason; /* 0=PwmBegin(UG后) 3=PlsrHwTick(每1ms) */ @@ -241,6 +321,15 @@ static void PlsrHwTimerSetCcr(uint8_t axis, uint32_t value) #endif } +static uint32_t PlsrHwTimerGetCcr(uint8_t axis) +{ +#ifdef PLSR_HOST_TEST + return PlsrHwTimers[axis].ccr1; +#else + return PlsrHwAxisMap[axis].timer->CCR1; +#endif +} + static void PlsrHwTimerSetCnt(uint8_t axis, uint32_t value) { #ifdef PLSR_HOST_TEST @@ -312,15 +401,6 @@ static void PlsrHwTimerSetForcedInactive(uint8_t axis) #endif } -static void PlsrHwTimerSetFrozen(uint8_t axis) -{ -#ifdef PLSR_HOST_TEST - PlsrHwTimers[axis].ccmr1 = 0x0008UL; -#else - PlsrHwAxisMap[axis].timer->CCMR1 = TIM_CCMR1_OC1PE; -#endif -} - static void PlsrHwTimerSetUie(uint8_t axis, uint32_t value) { #ifdef PLSR_HOST_TEST @@ -570,19 +650,42 @@ static void PlsrHwConfigurePwm(uint8_t axis, uint32_t frequencyHz) /* 首次启动输出:加载影子寄存器后使能更新中断、通道与计数。 */ static void PlsrHwPwmBegin(uint8_t axis) { + /* Stop the slave before changing the source OCREF phase. This is also + * required when a paused hardware-counted segment is resumed. */ + PlsrHwCounterSuspend(axis); PlsrHwTimerSetUg(axis); + if (PlsrHwAxes[axis].hardwareCounterActive != 0U) + { + /* PWM1 is inactive when CNT >= CCR1. Arm the ITR slave from that + * known-low OCREF phase, then expose the first complete terminal high + * half-cycle through CC1E. Starting at CNT=0 leaves OCREF high while + * SMS is enabled; TIM9/TIM12 count that internal startup level as one + * event even though no complete terminal pulse has occurred. */ + PlsrHwTimerSetCnt(axis, PlsrHwTimerGetCcr(axis)); + } /* UG 只用于加载影子寄存器,不是物理脉冲,不得计数。 */ PlsrHwTimerClearUif(axis); PlsrHwTimerClearCc1if(axis); + /* The slave trigger was selected while OCREF was forced low. Enable its + * external-clock mode only after the source PWM and startup UG are stable. */ + PlsrHwCounterBegin(axis); PlsrHwDbgCapture(axis, 0U); PlsrHwTimerSetCc1ie(axis, 0UL); - PlsrHwTimerSetUie(axis, 1UL); + /* TIM9/TIM12 count OC events in hardware. Only the two fallback axes + * retain a per-period output-timer interrupt. */ + PlsrHwTimerSetUie(axis, + (PlsrHwAxes[axis].hardwareCounterActive != 0U) + ? 0UL + : 1UL); PlsrHwTimerSetCc1e(axis, 1UL); PlsrHwTimerSetCen(axis, 1UL); } static void PlsrHwStopPwmTimer(uint8_t axis) { + /* Freeze the ITR slave before changing OCREF/CC1E so a stop or pause + * transition cannot be mistaken for a physical pulse boundary. */ + PlsrHwCounterSuspend(axis); PlsrHwTimerSetCc1e(axis, 0UL); PlsrHwTimerSetUie(axis, 0UL); PlsrHwTimerSetCc1ie(axis, 0UL); @@ -729,6 +832,12 @@ static uint8_t PlsrHwQueueAbFrequency(uint8_t axis, uint32_t frequencyHz) { /* 量化后的分频参数未变化时取消旧请求,避免匀速段每 1ms 重定相。 */ state->abFrequencyPending = 0U; + if ((state->hardwareCounterActive != 0U) + && (state->abStopArmed == 0U) + && (state->abPausePending == 0U)) + { + PlsrHwTimerSetCc1ie(state->abCountAxis, 0UL); + } } else { @@ -736,6 +845,13 @@ static uint8_t PlsrHwQueueAbFrequency(uint8_t axis, uint32_t frequencyHz) state->abPendingPairPsc = pairPsc; state->abPendingArr = arr; state->abFrequencyPending = 1U; + if (state->hardwareCounterActive != 0U) + { + /* Hardware counting keeps the per-cycle CC interrupt disabled. + * Wake it as a one-shot to apply this request at the next 00. */ + PlsrHwTimerClearCc1if(state->abCountAxis); + PlsrHwTimerSetCc1ie(state->abCountAxis, 1UL); + } } #ifndef PLSR_HOST_TEST __DMB(); @@ -767,17 +883,34 @@ static void PlsrHwBeginAbOutput(uint8_t axis, uint8_t debugReason) #else periodTicks = PlsrHwAxisMap[axis].timer->ARR + 1UL; #endif - leadStart = (periodTicks * 3UL) / 4UL; - /* 落后相从 CCR 精确起步;切回 PWM 后清 CC1IF,最后才开 CC1IE。 */ - lagStart = periodTicks / 2UL; + leadStart = (periodTicks * 3UL) / 4UL + 1UL; + if (leadStart >= periodTicks) + { + leadStart = periodTicks - 1UL; + } + /* Both counters must be strictly beyond CCR while GPIO is handed back to + * AF. CNT==CCR can leave the compare/OCREF state implementation-defined + * at the mux boundary and previously exposed one simultaneous A/B edge. */ + lagStart = periodTicks / 2UL + 1UL; + if (lagStart >= periodTicks) + { + lagStart = periodTicks - 1UL; + } state->abCountAxis = lagAxis; + state->counterSourceAxis = (axis == 0U) ? axis : pairAxis; + state->abCounterBoundaryCnt = + (state->counterSourceAxis == leadAxis) + ? leadStart - 1UL + : periodTicks / 2UL; state->abQuarter = 0U; - state->abStartupPriming = (state->abOutputPrimed == 0U) ? 1U : 0U; + state->abStartupPriming = 0U; + state->abFastGated = 0U; #ifndef PLSR_HOST_TEST interruptState = __get_PRIMASK(); __disable_irq(); __DMB(); + PlsrHwCounterSuspend(axis); PlsrHwHoldPulsePinLow(axis); PlsrHwHoldPulsePinLow(pairAxis); #endif @@ -791,8 +924,6 @@ static void PlsrHwBeginAbOutput(uint8_t axis, uint8_t debugReason) PlsrHwTimerSetCc1ie(pairAxis, 0UL); PlsrHwTimerSetForcedInactive(axis); PlsrHwTimerSetForcedInactive(pairAxis); - PlsrHwTimerSetFrozen(axis); - PlsrHwTimerSetFrozen(pairAxis); PlsrHwTimerSetUg(axis); PlsrHwTimerSetUg(pairAxis); PlsrHwTimerClearUif(axis); @@ -801,37 +932,50 @@ static void PlsrHwBeginAbOutput(uint8_t axis, uint8_t debugReason) PlsrHwTimerClearCc1if(pairAxis); PlsrHwTimerSetCnt(leadAxis, leadStart); PlsrHwTimerSetCnt(lagAxis, lagStart); -#ifdef PLSR_HOST_TEST + /* Enable the forced-inactive channels while GPIO still owns the pins. + * AF handoff and the later PWM1 selection therefore preserve the same 00 + * electrical level at every mux point. */ PlsrHwTimerSetCc1e(axis, 1UL); PlsrHwTimerSetCc1e(pairAxis, 1UL); +#ifndef PLSR_HOST_TEST + __DMB(); + /* CC1E is enabled, but forced-inactive drives the same idle level as + * the GPIO hold. Hand the pins to AF now, before either timer can run. */ + PlsrHwReleasePulsePin(axis); + PlsrHwReleasePulsePin(pairAxis); +#endif +#ifdef PLSR_HOST_TEST PlsrHwTimerSetPwmMode1(axis); PlsrHwTimerSetPwmMode1(pairAxis); PlsrHwTimerClearCc1if(axis); PlsrHwTimerClearCc1if(pairAxis); + PlsrHwCounterBegin(axis); PlsrHwTimerSetCen(axis, 1UL); PlsrHwTimerSetCen(pairAxis, 1UL); PlsrHwTimerClearCc1if(axis); PlsrHwTimerClearCc1if(pairAxis); - PlsrHwTimerSetCc1ie(lagAxis, 1UL); + PlsrHwTimerSetCc1ie( + lagAxis, + ((state->hardwareCounterActive != 0U) + && (state->abStopArmed == 0U)) + ? 0UL + : 1UL); #else - PlsrHwTimerSetCc1e(axis, 1UL); - PlsrHwTimerSetCc1e(pairAxis, 1UL); PlsrHwTimerSetPwmMode1(axis); PlsrHwTimerSetPwmMode1(pairAxis); PlsrHwTimerClearCc1if(axis); PlsrHwTimerClearCc1if(pairAxis); - if (state->abStartupPriming == 0U) - { - /* 完成过首次预热后,段间/调频重定相均从已验证的 00 边界 - * 直接交还 AF,不额外吞掉用户周期。 */ - PlsrHwReleasePulsePin(axis); - PlsrHwReleasePulsePin(pairAxis); - } + PlsrHwCounterBegin(axis); PlsrHwTimerSetCen(axis, 1UL); PlsrHwTimerSetCen(pairAxis, 1UL); PlsrHwTimerClearCc1if(axis); PlsrHwTimerClearCc1if(pairAxis); - PlsrHwTimerSetCc1ie(lagAxis, 1UL); + PlsrHwTimerSetCc1ie( + lagAxis, + ((state->hardwareCounterActive != 0U) + && (state->abStopArmed == 0U)) + ? 0UL + : 1UL); __DMB(); if (interruptState == 0UL) { @@ -1012,6 +1156,353 @@ static void PlsrHwStopActiveOutput(uint8_t axis, } } +static uint8_t PlsrHwCounterIndexForAxis(uint8_t axis) +{ + return (uint8_t)(axis & 1U); +} + +static uint8_t PlsrHwCounterTryAcquire(uint8_t axis, + PLSR_OUTPUT_MODE outputMode) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + uint8_t counterIndex; + uint8_t acquired = 0U; +#ifndef PLSR_HOST_TEST + uint32_t interruptState; +#endif + + state->counterIndex = PLSR_HW_COUNTER_NONE; + state->hardwareCounterActive = 0U; + state->hardwareCounterConfigured = 0U; + state->counterBlockPulses = 0UL; + if ((outputMode != PLSR_OUTPUT_PULSE_DIR) + && (outputMode != PLSR_OUTPUT_AB)) + { + return 0U; + } + if (outputMode == PLSR_OUTPUT_AB) + { + /* One counter per fixed AB pair: Q0/Q1 -> TIM9, Q2/Q3 -> TIM12. + * Very short jobs retain the existing per-cycle ISR because a + * target-1 guard compare cannot be armed at raw count zero. */ + if ((PlsrHwIsAbBaseAxis(axis) == 0U) || (state->targetPulses < 2)) + { + return 0U; + } + counterIndex = (uint8_t)(axis >> 1U); + } + else + { + counterIndex = PlsrHwCounterIndexForAxis(axis); + } +#ifndef PLSR_HOST_TEST + interruptState = __get_PRIMASK(); + __disable_irq(); + __DMB(); +#endif + if (PlsrHwCounterOwners[counterIndex] == PLSR_HW_COUNTER_NONE) + { + PlsrHwCounterOwners[counterIndex] = axis; + state->counterIndex = counterIndex; + state->hardwareCounterActive = 1U; + acquired = 1U; + } +#ifndef PLSR_HOST_TEST + __DMB(); + if (interruptState == 0UL) + { + __enable_irq(); + } +#endif + return acquired; +} + +static void PlsrHwCounterRelease(uint8_t axis) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; +#ifndef PLSR_HOST_TEST + uint32_t interruptState = __get_PRIMASK(); + + __disable_irq(); + __DMB(); +#endif + + if (state->counterIndex < PLSR_HW_COUNTER_COUNT) + { +#ifndef PLSR_HOST_TEST + TIM_TypeDef *counter = PlsrHwCounters[state->counterIndex]; + + counter->CR1 = 0UL; + counter->DIER = 0UL; + counter->SMCR = 0UL; + counter->SR = 0UL; +#endif + if (PlsrHwCounterOwners[state->counterIndex] == axis) + { + PlsrHwCounterOwners[state->counterIndex] = + PLSR_HW_COUNTER_NONE; + } + } + state->counterIndex = PLSR_HW_COUNTER_NONE; + state->hardwareCounterActive = 0U; + state->hardwareCounterConfigured = 0U; + state->counterBlockPulses = 0UL; +#ifndef PLSR_HOST_TEST + __DMB(); + if (interruptState == 0UL) + { + __enable_irq(); + } +#endif +} + +static void PlsrHwCounterConfigure(uint8_t axis) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + + state->counterBlockPulses = 0UL; +#ifndef PLSR_HOST_TEST + if (state->counterIndex < PLSR_HW_COUNTER_COUNT) + { + TIM_TypeDef *counter = PlsrHwCounters[state->counterIndex]; + uint32_t triggerSelection = ((axis & 2U) == 0U) + ? TIM_SMCR_TS_1 + : (TIM_SMCR_TS_1 | TIM_SMCR_TS_0); + + uint64_t comparePulses = (uint64_t)state->targetPulses; + + /* PULSE/DIR owns one source timer and can force it inactive here. AB + * owns a pair; its atomic startup routine establishes both OCREF lows + * immediately before CounterBegin instead. */ + if (state->outputMode == PLSR_OUTPUT_PULSE_DIR) + { + PlsrHwTimerSetCen(axis, 0UL); + PlsrHwTimerSetCc1e(axis, 0UL); + PlsrHwTimerSetForcedInactive(axis); + PlsrHwTimerSetUg(axis); + PlsrHwTimerClearUif(axis); + PlsrHwTimerClearCc1if(axis); + } + else + { + /* Wake the lag-CC1 one complete cycle before the target boundary. + * It verifies raw>=target at 00, avoiding ISR-latency overshoot. */ + comparePulses--; + } + counter->CR1 = 0UL; + counter->DIER = 0UL; + counter->SMCR = 0UL; + counter->PSC = 0UL; + counter->ARR = 0xFFFFUL; + counter->CCR1 = (uint32_t)(comparePulses & UINT64_C(0xFFFF)); + counter->CNT = 0UL; + counter->EGR = TIM_EGR_UG; + counter->SR = 0UL; + counter->SMCR = triggerSelection; + counter->DIER = TIM_DIER_UIE | TIM_DIER_CC1IE; + } +#endif +} + +static void PlsrHwCounterBegin(uint8_t axis) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + + if (state->hardwareCounterActive == 0U) + { + return; + } + if (state->hardwareCounterConfigured == 0U) + { + return; + } +#ifndef PLSR_HOST_TEST + if (state->counterIndex < PLSR_HW_COUNTER_COUNT) + { + TIM_TypeDef *counter = PlsrHwCounters[state->counterIndex]; + + counter->SMCR |= TIM_SMCR_SMS_2 | TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0; + counter->CR1 |= TIM_CR1_CEN; + } +#endif +} + +static void PlsrHwCounterSuspend(uint8_t axis) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + + if ((state->hardwareCounterActive == 0U) + || (state->hardwareCounterConfigured == 0U)) + { + return; + } +#ifndef PLSR_HOST_TEST + if (state->counterIndex < PLSR_HW_COUNTER_COUNT) + { + TIM_TypeDef *counter = PlsrHwCounters[state->counterIndex]; + + counter->CR1 &= ~TIM_CR1_CEN; + counter->SMCR &= ~(TIM_SMCR_SMS_2 | TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0); + } +#endif +} + +static void PlsrHwCounterRebase(uint8_t axis, uint64_t pulses) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + + if ((state->hardwareCounterActive == 0U) + || (state->hardwareCounterConfigured == 0U)) + { + return; + } +#ifdef PLSR_HOST_TEST + state->counterBlockPulses = pulses; +#else + state->counterBlockPulses = + pulses & ~(PLSR_HW_COUNTER_BLOCK_PULSES - UINT64_C(1)); + if (state->counterIndex < PLSR_HW_COUNTER_COUNT) + { + TIM_TypeDef *counter = PlsrHwCounters[state->counterIndex]; + + counter->CNT = (uint16_t)pulses; + counter->SR = 0UL; + } +#endif +} + +static uint64_t PlsrHwCounterRawSnapshot(uint8_t axis) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + uint64_t pulses = state->counterBlockPulses; + +#ifndef PLSR_HOST_TEST + if (state->counterIndex < PLSR_HW_COUNTER_COUNT) + { + TIM_TypeDef *counter = PlsrHwCounters[state->counterIndex]; + + pulses += (uint16_t)counter->CNT; + /* Cover the short window after wrap and before the block ISR. */ + if ((counter->SR & TIM_SR_UIF) != 0UL) + { + pulses += PLSR_HW_COUNTER_BLOCK_PULSES; + } + } +#else + pulses = (state->outputMode == PLSR_OUTPUT_AB) + ? state->counterBlockPulses + : (uint64_t)state->emittedPulses; +#endif + return pulses; +} + +static uint64_t PlsrHwCounterSnapshot(uint8_t axis) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + uint64_t pulses = PlsrHwCounterRawSnapshot(axis); + +#ifndef PLSR_HOST_TEST + if ((state->outputMode == PLSR_OUTPUT_AB) && (pulses > 0UL) + && (state->counterSourceAxis < PLSR_HW_AXIS_COUNT)) + { + uint64_t verifiedPulses; + uint32_t sourceCnt; + uint8_t attempt; + + /* The ITR source rises inside an AB cycle, before the following 00 + * boundary. A stable raw/CNT/raw snapshot identifies that interval + * and publishes only complete four-state cycles. */ + for (attempt = 0U; attempt < 2U; attempt++) + { + pulses = PlsrHwCounterRawSnapshot(axis); + sourceCnt = PlsrHwAxisMap[state->counterSourceAxis].timer->CNT; + verifiedPulses = PlsrHwCounterRawSnapshot(axis); + if (pulses == verifiedPulses) + { + if ((sourceCnt < state->abCounterBoundaryCnt) + && (pulses > 0UL)) + { + pulses--; + } + break; + } + pulses = verifiedPulses; + } + } +#else + if ((state->outputMode == PLSR_OUTPUT_AB) && (pulses > 0UL)) + { + uint8_t pairAxis = PlsrHwGetPairedAxis(axis); + uint8_t leadAxis = (state->directionPositive != 0U) + ? axis + : pairAxis; + uint8_t sourceQuarter = (state->counterSourceAxis == leadAxis) + ? 1U + : 2U; + + if ((state->abQuarter >= sourceQuarter) + && (state->abQuarter != 0U)) + { + pulses--; + } + } +#endif + if (pulses > (uint64_t)state->targetPulses) + { + pulses = (uint64_t)state->targetPulses; + } + return pulses; +} + +/* AB terminal and pause IRQs only freeze the two phase timers at a verified + * 00 boundary. GPIO handoff, counter release/rebase and event publication + * are deliberately deferred to PlsrHwTick so an equal-priority second AB + * boundary can be serviced before its next quarter-period transition. */ +static void PlsrHwFinishDeferredAbWork(uint8_t axis) +{ + PLSR_HW_AXIS_STATE *state; + + if ((axis >= PLSR_HW_AXIS_COUNT) || (PlsrHwIsAbBaseAxis(axis) == 0U)) + { + return; + } + state = &PlsrHwAxes[axis]; + if (state->abPauseGated != 0U) + { + uint64_t completedPulses = + (state->hardwareCounterActive != 0U) + ? PlsrHwCounterSnapshot(axis) + : (uint64_t)state->emittedPulses; + + state->emittedPulses = (int64_t)completedPulses; + PlsrHwStopActiveOutput(axis, state->outputMode); + if (state->hardwareCounterActive != 0U) + { + PlsrHwCounterRebase(axis, completedPulses); + } + state->abQuarter = 0U; + state->abFrequencyPending = 0U; + state->abPauseGated = 0U; + state->abStopArmed = + (completedPulses + >= (uint64_t)(state->targetPulses - 1)) + ? 1U + : 0U; + } + if (state->abCompletionDeferred != 0U) + { + state->emittedPulses = state->targetPulses; + PlsrHwStopActiveOutput(axis, state->outputMode); + PlsrHwCounterRelease(axis); + state->abQuarter = 0U; + state->abFrequencyPending = 0U; + state->abStopArmed = 0U; + state->abFastGated = 0U; + state->abCompletionDeferred = 0U; + (void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE); + } +} + uint8_t PlsrHwResolveDirectionPoint(uint8_t pointNumber) { /* 与资源层一致的合法输出点掩码(Q0~Q7、Q10~Q17、Q20)。 */ @@ -1037,13 +1528,35 @@ uint8_t PlsrHwResolveDirectionPoint(uint8_t pointNumber) PLSR_RESULT PlsrHwInit(void) { uint8_t axis; + uint8_t counterIndex; (void)memset(PlsrHwAxes, 0, sizeof(PlsrHwAxes)); PlsrHwDirectionBatchActive = 0U; + PlsrHwMaxOutputIsrCycles = 0UL; + PlsrHwMaxCounterIsrCycles = 0UL; + PlsrHwMaxControlIsrCycles = 0UL; + PlsrHwMaxAbGateCycles = 0UL; +#ifndef PLSR_HOST_TEST + PlsrHwAbGateMeasurePending = 0U; +#endif + PlsrHwTotalIsrCycles = 0UL; + PlsrHwIsrBusyStarted = 0UL; + PlsrHwIsrNesting = 0U; +#ifdef PLSR_HOST_TEST + PlsrHwTestLateAbFlagAxis = PLSR_HW_COUNTER_NONE; + PlsrHwTestAbFullGateCount = 0UL; +#endif + for (counterIndex = 0U; + counterIndex < PLSR_HW_COUNTER_COUNT; + counterIndex++) + { + PlsrHwCounterOwners[counterIndex] = PLSR_HW_COUNTER_NONE; + } for (axis = 0U; axis < PLSR_HW_AXIS_COUNT; axis++) { PlsrHwAxes[axis].state = PLSR_HW_STATE_IDLE; PlsrHwAxes[axis].directionPoint = PLSR_HW_DIR_POINT_NONE; + PlsrHwAxes[axis].counterIndex = PLSR_HW_COUNTER_NONE; PlsrHwAxes[axis].configuredDirectionPoint = PLSR_HW_DIR_POINT_NONE; #ifdef PLSR_HOST_TEST @@ -1063,6 +1576,10 @@ PLSR_RESULT PlsrHwInit(void) uint16_t tim6Psc; uint16_t tim6Arr; + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CYCCNT = 0UL; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; + /* 1. 输出点 GPIO 时钟(DIR 点按需配置时使用)。 */ __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOI_CLK_ENABLE(); @@ -1080,6 +1597,8 @@ PLSR_RESULT PlsrHwInit(void) __HAL_RCC_TIM13_CLK_ENABLE(); __HAL_RCC_TIM14_CLK_ENABLE(); __HAL_RCC_TIM6_CLK_ENABLE(); + __HAL_RCC_TIM9_CLK_ENABLE(); + __HAL_RCC_TIM12_CLK_ENABLE(); /* Independent 10kHz control clock for S2 refreshCode=2. */ tim6ClockHz = HAL_RCC_GetPCLK1Freq(); @@ -1126,6 +1645,10 @@ PLSR_RESULT PlsrHwInit(void) HAL_NVIC_EnableIRQ(TIM1_TRG_COM_TIM11_IRQn); HAL_NVIC_SetPriority(TIM8_TRG_COM_TIM14_IRQn, 1U, 0U); HAL_NVIC_EnableIRQ(TIM8_TRG_COM_TIM14_IRQn); + HAL_NVIC_SetPriority(TIM1_BRK_TIM9_IRQn, 1U, 0U); + HAL_NVIC_EnableIRQ(TIM1_BRK_TIM9_IRQn); + HAL_NVIC_SetPriority(TIM8_BRK_TIM12_IRQn, 1U, 0U); + HAL_NVIC_EnableIRQ(TIM8_BRK_TIM12_IRQn); } #endif return PLSR_RESULT_OK; @@ -1155,10 +1678,18 @@ PLSR_RESULT PlsrHwStartPulse(uint8_t axis, const PLSR_HW_START_PARAMS *params) return PLSR_RESULT_INVALID_AXIS; } state = &PlsrHwAxes[axis]; - if (state->state == PLSR_HW_STATE_RUNNING) + if ((state->state == PLSR_HW_STATE_RUNNING) + || (state->abCompletionDeferred != 0U) + || (state->abPauseGated != 0U)) { return PLSR_RESULT_BUSY; } + /* A caller may replace a prepared-but-not-started segment. Return its + * counter lease first, otherwise the paired axis would fall back forever. */ + if (state->hardwareCounterActive != 0U) + { + PlsrHwCounterRelease(axis); + } /* 方向延时只在方向发生变化时生效(首次启动/换向/换方向点): * 段间同向衔接不再等待 10ms,直接进入 PWM 待启动。 */ @@ -1187,6 +1718,11 @@ PLSR_RESULT PlsrHwStartPulse(uint8_t axis, const PLSR_HW_START_PARAMS *params) : 0U; state->abQuarter = 0U; state->abFrequencyPending = 0U; + state->abStopArmed = 0U; + state->abFastGated = 0U; + state->abPausePending = 0U; + state->abPauseGated = 0U; + state->abCompletionDeferred = 0U; if (params->outputMode == PLSR_OUTPUT_PULSE_DIR) { PlsrHwSetDirLevel(axis, @@ -1205,6 +1741,12 @@ PLSR_RESULT PlsrHwStartPulse(uint8_t axis, const PLSR_HW_START_PARAMS *params) ? axis : PlsrHwGetPairedAxis(axis); } + (void)PlsrHwCounterTryAcquire(axis, params->outputMode); + if (state->hardwareCounterActive != 0U) + { + PlsrHwCounterConfigure(axis); + state->hardwareCounterConfigured = 1U; + } state->state = (state->directionDelayRemainingMs > 0U) ? PLSR_HW_STATE_DIR_SETTLING : PLSR_HW_STATE_PWM_PENDING; @@ -1227,6 +1769,14 @@ PLSR_RESULT PlsrHwSetFrequency(uint8_t axis, uint32_t frequencyHz) * boundary; no later profile write may move that boundary. */ return PLSR_RESULT_OK; } + if ((state->state == PLSR_HW_STATE_RUNNING) + && (frequencyHz == state->currentFrequencyHz)) + { + /* Cruise ticks commonly request the same frequency for every axis. + * Recomputing PSC/ARR performs two 64-bit divisions and rewrites the + * same preload registers without changing the waveform. */ + return PLSR_RESULT_OK; + } state->currentFrequencyHz = frequencyHz; if (state->state == PLSR_HW_STATE_RUNNING) { @@ -1249,7 +1799,34 @@ PLSR_RESULT PlsrHwSetFrequency(uint8_t axis, uint32_t frequencyHz) } else { - PlsrHwStopActiveOutput(axis, state->outputMode); + if (state->outputMode == PLSR_OUTPUT_AB) + { +#ifndef PLSR_HOST_TEST + uint32_t interruptState = __get_PRIMASK(); + + __disable_irq(); + __DMB(); +#endif + /* PAUSE is a controlled AB stop. Keep both timers running + * until the lag compare reaches the next real 00 boundary; + * forcing GPIO low here would discard and later re-emit an + * already-started cycle, adding one terminal edge. */ + state->abFrequencyPending = 0U; + state->abPausePending = 1U; + PlsrHwTimerClearCc1if(state->abCountAxis); + PlsrHwTimerSetCc1ie(state->abCountAxis, 1UL); +#ifndef PLSR_HOST_TEST + __DMB(); + if (interruptState == 0UL) + { + __enable_irq(); + } +#endif + } + else + { + PlsrHwStopActiveOutput(axis, state->outputMode); + } } } else if ((state->state == PLSR_HW_STATE_PWM_PENDING) @@ -1274,14 +1851,16 @@ PLSR_RESULT PlsrHwResumePulse(uint8_t axis) state = &PlsrHwAxes[axis]; if ((state->state != PLSR_HW_STATE_RUNNING) || (state->currentFrequencyHz != 0UL) - || (state->emittedPulses >= state->targetPulses)) + || (state->abPausePending != 0U) + || (state->abPauseGated != 0U) + || (PlsrHwGetEmittedPulses(axis) >= state->targetPulses)) { return PLSR_RESULT_INVALID_STATE; } - /* PlsrHwSetFrequency(0) stopped the physical timer but deliberately kept - * the segment counters. PWM_PENDING makes the next non-zero control-tick - * update take the normal clean-start path without resetting those counts. */ + /* The pause boundary worker stopped the physical timers at 00 and kept + * the completed-cycle count. PWM_PENDING makes the next non-zero control + * tick use the clean-start path without resetting that count. */ state->state = PLSR_HW_STATE_PWM_PENDING; return PLSR_RESULT_OK; } @@ -1297,9 +1876,20 @@ PLSR_RESULT PlsrHwStopPulse(uint8_t axis) state = &PlsrHwAxes[axis]; if (state->state != PLSR_HW_STATE_IDLE) { + if (state->hardwareCounterActive != 0U) + { + state->emittedPulses = + (int64_t)PlsrHwCounterSnapshot(axis); + } PlsrHwStopActiveOutput(axis, state->outputMode); + PlsrHwCounterRelease(axis); state->abQuarter = 0U; state->abFrequencyPending = 0U; + state->abStopArmed = 0U; + state->abFastGated = 0U; + state->abPausePending = 0U; + state->abPauseGated = 0U; + state->abCompletionDeferred = 0U; state->state = PLSR_HW_STATE_IDLE; } return PLSR_RESULT_OK; @@ -1352,14 +1942,18 @@ int64_t PlsrHwGetEmittedPulses(uint8_t axis) return 0; } #ifdef PLSR_HOST_TEST - emittedPulses = PlsrHwAxes[axis].emittedPulses; + emittedPulses = (PlsrHwAxes[axis].hardwareCounterActive != 0U) + ? (int64_t)PlsrHwCounterSnapshot(axis) + : PlsrHwAxes[axis].emittedPulses; #else { uint32_t interruptState = __get_PRIMASK(); __disable_irq(); __DMB(); - emittedPulses = PlsrHwAxes[axis].emittedPulses; + emittedPulses = (PlsrHwAxes[axis].hardwareCounterActive != 0U) + ? (int64_t)PlsrHwCounterSnapshot(axis) + : PlsrHwAxes[axis].emittedPulses; __DMB(); if (interruptState == 0UL) { @@ -1379,6 +1973,68 @@ uint8_t PlsrHwIsAbStartupPriming(uint8_t axis) return PlsrHwAxes[axis].abStartupPriming; } +uint8_t PlsrHwUsesHardwareCounter(uint8_t axis) +{ + if (axis >= PLSR_HW_AXIS_COUNT) + { + return 0U; + } + return PlsrHwAxes[axis].hardwareCounterActive; +} + +uint32_t PlsrHwGetMaxOutputIsrCycles(void) +{ + return PlsrHwMaxOutputIsrCycles; +} + +uint32_t PlsrHwGetMaxCounterIsrCycles(void) +{ + return PlsrHwMaxCounterIsrCycles; +} + +uint32_t PlsrHwGetMaxControlIsrCycles(void) +{ + return PlsrHwMaxControlIsrCycles; +} + +uint32_t PlsrHwGetMaxAbGateCycles(void) +{ + return PlsrHwMaxAbGateCycles; +} + +void PlsrHwGetCycleSnapshot(uint32_t *cycleCount, + uint64_t *plsrIsrCycles) +{ +#ifdef PLSR_HOST_TEST + if (cycleCount != NULL) + { + *cycleCount = 0UL; + } + if (plsrIsrCycles != NULL) + { + *plsrIsrCycles = 0UL; + } +#else + uint32_t interruptState = __get_PRIMASK(); + + __disable_irq(); + __DMB(); + if (cycleCount != NULL) + { + *cycleCount = DWT->CYCCNT; + } + if (plsrIsrCycles != NULL) + { + *plsrIsrCycles = PlsrHwTotalIsrCycles; + } + __DMB(); + if (interruptState == 0UL) + { + __enable_irq(); + } +#endif +} + void PlsrHwTick(uint8_t axis) { PLSR_HW_AXIS_STATE *state; @@ -1388,6 +2044,11 @@ void PlsrHwTick(uint8_t axis) return; } state = &PlsrHwAxes[axis]; + if ((state->abPauseGated != 0U) + || (state->abCompletionDeferred != 0U)) + { + PlsrHwFinishDeferredAbWork(axis); + } /* 调试:每 tick 记录定时器实况(CNT 演化定位第一周期压缩)。 */ PlsrHwDbgCapture(axis, 3U); switch (state->state) @@ -1419,6 +2080,93 @@ void PlsrHwTick(uint8_t axis) } } +static void PlsrHwFastGateAbPair(uint8_t axis) +{ + uint8_t pairAxis = PlsrHwGetPairedAxis(axis); + + PlsrHwTimerSetCen(axis, 0UL); + PlsrHwTimerSetCen(pairAxis, 0UL); + PlsrHwCounterSuspend(axis); +} + +static void PlsrHwDeferAbCompletion(uint8_t axis) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + + state->emittedPulses = state->targetPulses; + state->abQuarter = 0U; + state->abFrequencyPending = 0U; + state->abStopArmed = 0U; + state->abPausePending = 0U; + state->abPauseGated = 0U; + state->abCompletionDeferred = 1U; + state->state = PLSR_HW_STATE_DONE; +} + +static void PlsrHwRecordFullAbGateTime(void) +{ +#ifndef PLSR_HOST_TEST + /* The wrapper records the endpoint after the common ISR accounting, just + * before exception return. Measuring here would omit that equal-priority + * blocking tail and could understate the 2.5us near-simultaneous window. */ + PlsrHwAbGateMeasurePending = 1U; +#else + PlsrHwTestAbFullGateCount++; +#endif +} + +#ifndef PLSR_HOST_TEST +static void PlsrHwFinishAbGateMeasurement(uint32_t started) +{ + if (PlsrHwAbGateMeasurePending != 0U) + { + uint32_t finished; + uint32_t elapsed; + + PlsrHwAbGateMeasurePending = 0U; + finished = DWT->CYCCNT; + elapsed = finished - started; + if (elapsed > PlsrHwMaxAbGateCycles) + { + PlsrHwMaxAbGateCycles = elapsed; + } + } +} +#endif + +static uint8_t PlsrHwGateArmedAbOutputs(void) +{ + static const uint8_t baseAxes[2] = {0U, 2U}; + uint8_t gated = 0U; + uint8_t index; + + /* Both AB pairs use equal-priority IRQs. Scan and gate every pair before + * doing any event publication so two simultaneous 100kHz completions + * cannot make the second pair run an extra quarter while its IRQ waits. */ + for (index = 0U; index < 2U; index++) + { + uint8_t axis = baseAxes[index]; + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + + if ((state->state == PLSR_HW_STATE_RUNNING) + && (state->outputMode == PLSR_OUTPUT_AB) + && (state->abStopArmed != 0U) + && (state->abFastGated == 0U) + && (PlsrHwTimerHasCc1if(state->abCountAxis) != 0U) + && (PlsrHwCounterRawSnapshot(axis) + >= (uint64_t)state->targetPulses)) + { + /* This entry is the verified 00 boundary. Stop both counters + * first, but keep CC1E driving the frozen 00 until the slower path + * hands the pins to GPIO. */ + PlsrHwFastGateAbPair(axis); + state->abFastGated = 1U; + gated = 1U; + } + } + return gated; +} + /* 输出定时器中断入口:PULSE/DIR 在 update 计数;AB 在落后相 * CC1 下降沿(四状态回到 00)计一个完整正交周期。 */ void PlsrHwOnTimerUpdate(uint8_t axis) @@ -1426,11 +2174,30 @@ void PlsrHwOnTimerUpdate(uint8_t axis) PLSR_HW_AXIS_STATE *state; uint8_t ownerAxis; uint8_t hasCc1; + uint8_t abGated; if (axis >= PLSR_HW_AXIS_COUNT) { return; } + abGated = PlsrHwGateArmedAbOutputs(); +#ifdef PLSR_HOST_TEST + /* Model the second equal-priority lag flag arriving after the first scan + * but before any completion bookkeeping. */ + if (PlsrHwTestLateAbFlagAxis < PLSR_HW_AXIS_COUNT) + { + PlsrHwTimers[PlsrHwTestLateAbFlagAxis].sr |= + PLSR_HW_TIMER_CC1_BIT; + PlsrHwTestLateAbFlagAxis = PLSR_HW_COUNTER_NONE; + abGated = 1U; + } +#endif + /* Completion cleanup is deferred. This second scan closes the injected + * arrival window; a still-later flag gets CPU back before its next jump. */ + if (abGated != 0U) + { + (void)PlsrHwGateArmedAbOutputs(); + } /* CC1IF 无论当前状态如何都必须先清除;否则启动窗口中的杂散 * compare 标志会让共享 IRQ 持续重入,主线程无法完成 CEN 配置。 */ @@ -1452,27 +2219,77 @@ void PlsrHwOnTimerUpdate(uint8_t axis) { return; } - if (state->abStartupPriming != 0U) + if (state->abFastGated != 0U) { - /* F407 首次切换 OC 模式时 OC1REF 初态不可直接作为物理AB相。 - * GPIO 保持 00 隐藏首个内部周期;落后相下降沿是真实 00 - * 边界,此时再交还 AF,且该隐藏周期绝不能计入 emitted。 */ - state->abStartupPriming = 0U; - state->abOutputPrimed = 1U; -#ifndef PLSR_HOST_TEST - PlsrHwReleasePulsePin(ownerAxis); - PlsrHwReleasePulsePin(PlsrHwGetPairedAxis(ownerAxis)); -#endif + PlsrHwDeferAbCompletion(ownerAxis); + PlsrHwRecordFullAbGateTime(); + return; + } + if (state->abPausePending != 0U) + { + uint8_t targetReached; + + targetReached = + (state->hardwareCounterActive != 0U) + ? ((PlsrHwCounterRawSnapshot(ownerAxis) + >= (uint64_t)state->targetPulses) + ? 1U + : 0U) + : (((uint64_t)state->emittedPulses + 1UL + >= (uint64_t)state->targetPulses) + ? 1U + : 0U); + PlsrHwFastGateAbPair(ownerAxis); + if (targetReached != 0U) + { + PlsrHwDeferAbCompletion(ownerAxis); + } + else + { + if (state->hardwareCounterActive == 0U) + { + state->emittedPulses++; + } + state->abQuarter = 0U; + state->abFrequencyPending = 0U; + state->abPausePending = 0U; + state->abPauseGated = 1U; + } + PlsrHwRecordFullAbGateTime(); + return; + } + if (state->hardwareCounterActive != 0U) + { + /* One-shot 00 interrupt for a queued frequency change or for the + * target guard. Counting itself remains entirely in TIM9/12. */ + if (state->abFrequencyPending != 0U) + { + uint16_t basePsc = state->abPendingBasePsc; + uint16_t pairPsc = state->abPendingPairPsc; + uint16_t arr = state->abPendingArr; + uint8_t pairAxis = PlsrHwGetPairedAxis(ownerAxis); + + /* This CC1 is 00. Gate first; divider calculation and timer + * reloading are intentionally outside the 2.5us edge window. */ + PlsrHwTimerSetCen(ownerAxis, 0UL); + PlsrHwTimerSetCen(pairAxis, 0UL); + PlsrHwCounterSuspend(ownerAxis); + state->abFrequencyPending = 0U; + PlsrHwLoadAbPwm(ownerAxis, basePsc, pairPsc, arr); + PlsrHwBeginAbOutput(ownerAxis, 4U); + } + else if (state->abStopArmed == 0U) + { + PlsrHwTimerSetCc1ie(state->abCountAxis, 0UL); + } return; } state->emittedPulses++; if (state->emittedPulses >= state->targetPulses) { - PlsrHwStopActiveOutput(ownerAxis, state->outputMode); - state->abQuarter = 0U; - state->abFrequencyPending = 0U; - state->state = PLSR_HW_STATE_DONE; - (void)PlsrPostEvent(ownerAxis, PLSR_EVENT_SEGMENT_COMPLETE); + PlsrHwFastGateAbPair(ownerAxis); + PlsrHwDeferAbCompletion(ownerAxis); + PlsrHwRecordFullAbGateTime(); } else if (state->abFrequencyPending != 0U) { @@ -1525,13 +2342,16 @@ void PlsrHwOnTimerUpdate(uint8_t axis) return; } - if (hasCc1 != 0U) + state = &PlsrHwAxes[axis]; + if ((hasCc1 != 0U) + && !((state->state == PLSR_HW_STATE_RUNNING) + && (state->outputMode == PLSR_OUTPUT_PULSE_DIR))) { - /* 非运行态/非 AB 模式的 CC1 仅作为杂散标志消费。 */ + /* 非运行态或其他模式的 CC1 仅作为杂散标志消费。PULSE/DIR + * 的 CC1IE 关闭,但半周期比较仍会置 CC1IF;更新 IRQ 必须 + * 在同一次入口继续消费 UIF,不能留下 UIF 再触发第二次 ISR。 */ return; } - - state = &PlsrHwAxes[axis]; if (PlsrHwTimerHasUif(axis) == 0U) { return; @@ -1545,6 +2365,14 @@ void PlsrHwOnTimerUpdate(uint8_t axis) { return; } +#ifndef PLSR_HOST_TEST + if (state->hardwareCounterActive != 0U) + { + /* A hardware-counted axis has UIE disabled. Ignore any stale update + * flag rather than counting the same OC event in software as well. */ + return; + } +#endif state->emittedPulses++; if (state->emittedPulses >= state->targetPulses) @@ -1552,6 +2380,7 @@ void PlsrHwOnTimerUpdate(uint8_t axis) /* 更新时刻 = 周期结束:关通道即完整下降沿后停止,无额外脉冲。 */ PlsrHwStopActiveOutput(axis, state->outputMode); state->state = PLSR_HW_STATE_DONE; + PlsrHwCounterRelease(axis); (void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE); } } @@ -1648,9 +2477,17 @@ uint8_t PlsrHwTestGetAbQuarter(uint8_t axis) return PlsrHwAxes[axis].abQuarter; } +uint32_t PlsrHwTestGetAbFullGateCount(void) +{ + return PlsrHwTestAbFullGateCount; +} + void PlsrHwTestAdvanceAbQuarter(uint8_t axis) { PLSR_HW_AXIS_STATE *state; + uint8_t countAxis; + uint8_t leadAxis; + uint8_t sourceQuarter; if ((axis >= PLSR_HW_AXIS_COUNT) || (PlsrHwIsAbBaseAxis(axis) == 0U)) { @@ -1663,14 +2500,108 @@ void PlsrHwTestAdvanceAbQuarter(uint8_t axis) return; } + countAxis = state->abCountAxis; + leadAxis = (state->directionPositive != 0U) + ? axis + : PlsrHwGetPairedAxis(axis); + sourceQuarter = (state->counterSourceAxis == leadAxis) ? 1U : 2U; state->abQuarter = (uint8_t)((state->abQuarter + 1U) % PLSR_HW_AB_QUARTER_COUNT); + if ((state->hardwareCounterActive != 0U) + && (state->abQuarter == sourceQuarter)) + { + state->counterBlockPulses++; + if ((state->abStopArmed == 0U) + && (state->counterBlockPulses + >= (uint64_t)(state->targetPulses - 1))) + { + /* Host model of the target-1 TIM9/TIM12 compare. */ + state->abStopArmed = 1U; + PlsrHwTimerClearCc1if(countAxis); + PlsrHwTimerSetCc1ie(countAxis, 1UL); + } + } if (state->abQuarter == 0U) { /* 模拟目标板落后相 CC1 下降沿中断,复用生产计数路径。 */ + /* Model the lag CC IRQ only while it is enabled. This detects a + * regression that accidentally restores one interrupt per AB cycle. */ + if ((PlsrHwTimers[countAxis].dier & PLSR_HW_TIMER_CC1_BIT) != 0UL) + { + PlsrHwTimers[countAxis].sr |= PLSR_HW_TIMER_CC1_BIT; + PlsrHwOnTimerUpdate(countAxis); + } + } +} + +void PlsrHwTestSignalDualAbFinalBoundary(uint8_t firstAxis) +{ + static const uint8_t baseAxes[2] = {0U, 2U}; + uint8_t index; + uint8_t firstCountAxis; + + if ((firstAxis != 0U) && (firstAxis != 2U)) + { + return; + } + for (index = 0U; index < 2U; index++) + { + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[baseAxes[index]]; + + if ((state->state != PLSR_HW_STATE_RUNNING) + || (state->outputMode != PLSR_OUTPUT_AB) + || (state->hardwareCounterActive == 0U)) + { + return; + } + } + for (index = 0U; index < 2U; index++) + { + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[baseAxes[index]]; + + state->counterBlockPulses = (uint64_t)state->targetPulses; + state->abQuarter = 0U; + state->abStopArmed = 1U; + PlsrHwTimerSetCc1ie(state->abCountAxis, 1UL); PlsrHwTimers[state->abCountAxis].sr |= PLSR_HW_TIMER_CC1_BIT; - PlsrHwOnTimerUpdate(state->abCountAxis); } + firstCountAxis = PlsrHwAxes[firstAxis].abCountAxis; + PlsrHwOnTimerUpdate(firstCountAxis); +} + +void PlsrHwTestSignalDualAbStaggeredFinalBoundary(uint8_t firstAxis) +{ + static const uint8_t baseAxes[2] = {0U, 2U}; + uint8_t firstCountAxis; + uint8_t secondAxis; + uint8_t secondCountAxis; + uint8_t index; + + if ((firstAxis != 0U) && (firstAxis != 2U)) + { + return; + } + secondAxis = (firstAxis == 0U) ? 2U : 0U; + for (index = 0U; index < 2U; index++) + { + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[baseAxes[index]]; + + if ((state->state != PLSR_HW_STATE_RUNNING) + || (state->outputMode != PLSR_OUTPUT_AB) + || (state->hardwareCounterActive == 0U)) + { + return; + } + state->counterBlockPulses = (uint64_t)state->targetPulses; + state->abQuarter = 0U; + state->abStopArmed = 1U; + PlsrHwTimerSetCc1ie(state->abCountAxis, 1UL); + } + firstCountAxis = PlsrHwAxes[firstAxis].abCountAxis; + secondCountAxis = PlsrHwAxes[secondAxis].abCountAxis; + PlsrHwTimers[firstCountAxis].sr |= PLSR_HW_TIMER_CC1_BIT; + PlsrHwTestLateAbFlagAxis = secondCountAxis; + PlsrHwOnTimerUpdate(firstCountAxis); } void PlsrHwTestTriggerUpdate(uint8_t axis) @@ -1682,6 +2613,16 @@ void PlsrHwTestTriggerUpdate(uint8_t axis) PlsrHwOnTimerUpdate(axis); } +void PlsrHwTestTriggerUpdateAndCompare(uint8_t axis) +{ + if (axis < PLSR_HW_AXIS_COUNT) + { + PlsrHwTimers[axis].sr |= PLSR_HW_TIMER_UPDATE_BIT + | PLSR_HW_TIMER_CC1_BIT; + } + PlsrHwOnTimerUpdate(axis); +} + void PlsrHwTestTriggerCompare(uint8_t axis) { if (axis < PLSR_HW_AXIS_COUNT) @@ -1693,32 +2634,128 @@ void PlsrHwTestTriggerCompare(uint8_t axis) #endif #ifndef PLSR_HOST_TEST +static void PlsrHwOnCounterInterrupt(uint8_t counterIndex) +{ + TIM_TypeDef *counter; + PLSR_HW_AXIS_STATE *state; + uint32_t flags; + uint64_t pulses; + uint8_t axis; + + if (counterIndex >= PLSR_HW_COUNTER_COUNT) + { + return; + } + counter = PlsrHwCounters[counterIndex]; + flags = counter->SR & (TIM_SR_UIF | TIM_SR_CC1IF); + counter->SR = ~(TIM_SR_UIF | TIM_SR_CC1IF); + axis = PlsrHwCounterOwners[counterIndex]; + if ((flags == 0UL) || (axis >= PLSR_HW_AXIS_COUNT)) + { + return; + } + state = &PlsrHwAxes[axis]; + if ((state->hardwareCounterActive == 0U) + || (state->counterIndex != counterIndex) + || (state->state != PLSR_HW_STATE_RUNNING)) + { + return; + } + if ((flags & TIM_SR_UIF) != 0UL) + { + state->counterBlockPulses += PLSR_HW_COUNTER_BLOCK_PULSES; + } + pulses = state->counterBlockPulses + (uint16_t)counter->CNT; + if (state->outputMode == PLSR_OUTPUT_AB) + { + uint64_t guard = (uint64_t)state->targetPulses - 1UL; + + if (((flags & TIM_SR_CC1IF) != 0UL) && (pulses >= guard)) + { + /* Wake the lag-CC1 one cycle early. It remains enabled until a + * 00 boundary observes raw>=target and fast-gates both phases. */ + state->abStopArmed = 1U; + PlsrHwTimerClearCc1if(state->abCountAxis); + PlsrHwTimerSetCc1ie(state->abCountAxis, 1UL); + } + return; + } + if (((flags & TIM_SR_CC1IF) != 0UL) + && (pulses >= (uint64_t)state->targetPulses)) + { + /* TIMx_OC rises at the PWM update boundary. On this board that is + * the physical falling edge, so the target pulse is already complete + * and both the counter and PWM may be stopped without truncation. */ + state->emittedPulses = state->targetPulses; + PlsrHwStopActiveOutput(axis, state->outputMode); + state->state = PLSR_HW_STATE_DONE; + PlsrHwCounterRelease(axis); + (void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE); + } +} + void TIM1_UP_TIM10_IRQHandler(void) { + uint32_t started = PlsrHwCycleBegin(); + PlsrHwOnTimerUpdate(0U); + PlsrHwRecordMaxCycles(&PlsrHwMaxOutputIsrCycles, started); + PlsrHwFinishAbGateMeasurement(started); } void TIM8_UP_TIM13_IRQHandler(void) { + uint32_t started = PlsrHwCycleBegin(); + PlsrHwOnTimerUpdate(1U); + PlsrHwRecordMaxCycles(&PlsrHwMaxOutputIsrCycles, started); + PlsrHwFinishAbGateMeasurement(started); } void TIM1_TRG_COM_TIM11_IRQHandler(void) { + uint32_t started = PlsrHwCycleBegin(); + PlsrHwOnTimerUpdate(2U); + PlsrHwRecordMaxCycles(&PlsrHwMaxOutputIsrCycles, started); + PlsrHwFinishAbGateMeasurement(started); } void TIM8_TRG_COM_TIM14_IRQHandler(void) { + uint32_t started = PlsrHwCycleBegin(); + PlsrHwOnTimerUpdate(3U); + PlsrHwRecordMaxCycles(&PlsrHwMaxOutputIsrCycles, started); + PlsrHwFinishAbGateMeasurement(started); +} + +void TIM1_BRK_TIM9_IRQHandler(void) +{ + uint32_t started = PlsrHwCycleBegin(); + + PlsrHwOnCounterInterrupt(0U); + PlsrHwRecordMaxCycles(&PlsrHwMaxCounterIsrCycles, started); +} + +void TIM8_BRK_TIM12_IRQHandler(void) +{ + uint32_t started = PlsrHwCycleBegin(); + + PlsrHwOnCounterInterrupt(1U); + PlsrHwRecordMaxCycles(&PlsrHwMaxCounterIsrCycles, started); } void TIM6_DAC_IRQHandler(void) { + uint32_t started = PlsrHwCycleBegin(); + if ((TIM6->SR & TIM_SR_UIF) != 0UL) { TIM6->SR &= ~TIM_SR_UIF; PlsrControlTick100us(); } + PlsrHwRecordMaxCycles(&PlsrHwMaxControlIsrCycles, started); } + #endif diff --git a/PLSR/Src/plsr_modbus_control.c b/PLSR/Src/plsr_modbus_control.c index 8a8b581..bd00527 100644 --- a/PLSR/Src/plsr_modbus_control.c +++ b/PLSR/Src/plsr_modbus_control.c @@ -1,12 +1,20 @@ #include "plsr_modbus_control.h" #include "modbus_data_store.h" +#include "plc_device.h" #include "plsr_address_map.h" #include "plsr_core.h" +#include "plsr_hal_f407.h" #include "plsr_job.h" #include "plsr_modbus_data.h" +#include "plsr_persistence.h" #include #include +#ifndef PLSR_HOST_TEST +#include "stm32f4xx.h" +#include "usbd_cdc_if.h" +#endif + #define PLSR_MODBUS_MAGIC_LOW (0x504CU) #define PLSR_MODBUS_MAGIC_HIGH (0x5352U) #define PLSR_MODBUS_CAPABILITIES (0x0007U) @@ -19,6 +27,11 @@ #define PLSR_MODBUS_S1_WORDS (4UL) #define PLSR_MODBUS_HASH_OFFSET (2166136261UL) #define PLSR_MODBUS_HASH_PRIME (16777619UL) +#define PLSR_MODBUS_PERSIST_MAGIC_A (0xDA7AU) +#define PLSR_MODBUS_PERSIST_MAGIC_B (0x51F0U) +#define PLSR_MODBUS_PERSIST_ARM (0xA55AU) +#define PLSR_MODBUS_PERSIST_INVALIDATE_HSD (1U) +#define PLSR_MODBUS_PERSIST_INVALIDATE_SFD (2U) typedef struct { @@ -32,6 +45,9 @@ static uint8_t PlsrModbusEnabled; static uint32_t PlsrModbusLastCallRequestSequence; static uint32_t PlsrModbusLastCommandRequestSequence; static uint32_t PlsrModbusStatusGeneration[PLSR_AXIS_COUNT]; +static uint32_t PlsrModbusPersistenceGeneration; +static uint32_t PlsrModbusUsbDiagnosticsGeneration; +static uint32_t PlsrModbusLastPersistenceRequestSequence; static PLSR_MODBUS_COMMITTED_CALL PlsrModbusCommitted[PLSR_AXIS_COUNT]; static uint16_t PlsrModbusStatusWords[PLSR_AXIS_COUNT] [PLSR_MODBUS_AXIS_STATUS_WORDS]; @@ -436,6 +452,164 @@ static void PlsrModbusHandleCommandRequest(void) result); } +static uint8_t PlsrModbusPersistenceAllAxesIdle(void) +{ + PLSR_STATUS status; + uint8_t axis; + + for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) + { + if ((PlsrGetStatus(axis, &status) != PLSR_RESULT_OK) + || (status.busy != 0U) + || (status.pulseActive != 0U)) + { + return 0U; + } + } + return 1U; +} + +static uint32_t PlsrModbusEnterPersistenceDiagnosticCritical(void) +{ +#ifdef PLSR_HOST_TEST + return 0UL; +#else + uint32_t interruptState = __get_PRIMASK(); + + __disable_irq(); + __DMB(); + return interruptState; +#endif +} + +static void PlsrModbusExitPersistenceDiagnosticCritical( + uint32_t interruptState) +{ +#ifdef PLSR_HOST_TEST + (void)interruptState; +#else + __DMB(); + if (interruptState == 0UL) + { + __enable_irq(); + } +#endif +} + +static PLSR_RESULT PlsrModbusMapPersistenceResult( + PLSR_PERSISTENCE_RESULT persistenceResult) +{ + if (persistenceResult == PLSR_PERSISTENCE_OK) + { + return PLSR_RESULT_OK; + } + if (persistenceResult == PLSR_PERSISTENCE_NOT_IMPLEMENTED) + { + return PLSR_RESULT_NOT_SUPPORTED; + } + if (persistenceResult == PLSR_PERSISTENCE_INVALID_ARGUMENT) + { + return PLSR_RESULT_INVALID_ARGUMENT; + } + return PLSR_RESULT_PERSISTENCE_ERROR; +} + +static void PlsrModbusPublishPersistenceResponse(uint32_t sequence, + uint16_t operation, + PLSR_RESULT result) +{ + PLSR_PERSISTENCE_DIAGNOSTICS diagnostics; + uint16_t response[PLSR_MODBUS_PERSISTENCE_RESPONSE_WORDS] = {0U}; + + PlsrPersistenceGetDiagnostics(&diagnostics); + PlsrModbusPutU32(response, 0UL, sequence); + response[2UL] = operation; + response[3UL] = (uint16_t)result; + response[4UL] = (uint16_t)diagnostics.hsdValidMask + | ((uint16_t)diagnostics.sfdValidMask << 8U); + response[5UL] = (uint16_t)diagnostics.hsdNewestMask + | ((uint16_t)diagnostics.sfdNewestMask << 8U); + response[6UL] = diagnostics.destructiveDiagnosticEnabled; + (void)ModbusDataWriteWords( + MODBUS_DATA_DEVICE_D, + (uint32_t)PlsrModbusBaseAddress + + PLSR_MODBUS_PERSISTENCE_RESPONSE_OFFSET, + response, + PLSR_MODBUS_PERSISTENCE_RESPONSE_WORDS); +} + +static void PlsrModbusHandlePersistenceRequest(void) +{ + uint16_t request[PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS]; + const uint16_t clearRequest[PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS] = + {0U}; + PLSR_PERSISTENCE_RESULT persistenceResult; + PLSR_PERSISTENCE_DIAG_TARGET target; + PLSR_RESULT result; + uint32_t sequence; + uint32_t inverseSequence; + uint32_t interruptState; + uint16_t operation; + + if (PlsrModbusReadWords(PLSR_MODBUS_PERSISTENCE_REQUEST_OFFSET, + request, + PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS) == 0U) + { + return; + } + if ((request[0UL] != PLSR_MODBUS_PERSIST_MAGIC_A) + || (request[1UL] != PLSR_MODBUS_PERSIST_MAGIC_B)) + { + return; + } + sequence = PlsrModbusGetU32(request, 2UL); + inverseSequence = PlsrModbusGetU32(request, 4UL); + operation = request[6UL]; + if ((sequence != 0UL) + && (sequence == PlsrModbusLastPersistenceRequestSequence)) + { + return; + } + if (sequence != 0UL) + { + PlsrModbusLastPersistenceRequestSequence = sequence; + } + + result = PLSR_RESULT_INVALID_ARGUMENT; + if ((sequence != 0UL) + && (inverseSequence == ~sequence) + && (request[7UL] == PLSR_MODBUS_PERSIST_ARM) + && ((operation == PLSR_MODBUS_PERSIST_INVALIDATE_HSD) + || (operation == PLSR_MODBUS_PERSIST_INVALIDATE_SFD))) + { + /* Keep the idle check and the optional one-word invalidation in one + * scheduling exclusion window. This diagnostic is disabled in + * normal builds; when enabled, no START can race the Flash write. */ + interruptState = PlsrModbusEnterPersistenceDiagnosticCritical(); + if (PlsrModbusPersistenceAllAxesIdle() == 0U) + { + result = PLSR_RESULT_BUSY; + } + else + { + target = (operation == PLSR_MODBUS_PERSIST_INVALIDATE_HSD) + ? PLSR_PERSISTENCE_DIAG_TARGET_HSD + : PLSR_PERSISTENCE_DIAG_TARGET_SFD; + persistenceResult = + PlsrPersistenceDiagnosticInvalidateNewest(target); + result = PlsrModbusMapPersistenceResult(persistenceResult); + } + PlsrModbusExitPersistenceDiagnosticCritical(interruptState); + } + PlsrModbusPublishPersistenceResponse(sequence, operation, result); + (void)ModbusDataWriteWords( + MODBUS_DATA_DEVICE_D, + (uint32_t)PlsrModbusBaseAddress + + PLSR_MODBUS_PERSISTENCE_REQUEST_OFFSET, + clearRequest, + PLSR_MODBUS_PERSISTENCE_REQUEST_WORDS); +} + static void PlsrModbusPublishAxisStatus(uint8_t axis) { PLSR_STATUS status; @@ -488,6 +662,7 @@ static void PlsrModbusPublishAxisStatus(uint8_t axis) words[34UL] = status.currentSegment; words[35UL] = status.directionPoint; words[36UL] = status.highResourceMask; + words[37UL] = status.hardwareCounter; PlsrModbusPutU32(words, 38UL, status.currentFrequencyHz); PlsrModbusPutU32(words, 40UL, status.targetFrequencyHz); PlsrModbusPutU32(words, 42UL, status.liveFrequencyRejectCount); @@ -501,6 +676,144 @@ static void PlsrModbusPublishAxisStatus(uint8_t axis) PLSR_MODBUS_AXIS_STATUS_WORDS); } +static void PlsrModbusPublishPerformance(void) +{ + uint16_t words[PLSR_MODBUS_PERFORMANCE_WORDS]; + uint16_t stageWords[PLSR_MODBUS_STAGE_PERFORMANCE_WORDS]; + uint16_t abGateWords[PLSR_MODBUS_AB_GATE_PERFORMANCE_WORDS]; + uint32_t outputCycles = PlsrHwGetMaxOutputIsrCycles(); + uint32_t counterCycles = PlsrHwGetMaxCounterIsrCycles(); + uint8_t stage; + + PlsrModbusPutU32(words, 0UL, PlsrGetMaxProcessCycles()); + PlsrModbusPutU32(words, 2UL, PlsrGetMaxProcessResponseCycles()); + PlsrModbusPutU32(words, 4UL, PlsrHwGetMaxControlIsrCycles()); + words[6UL] = (uint16_t)((outputCycles > UINT16_MAX) + ? UINT16_MAX + : outputCycles); + words[7UL] = (uint16_t)((counterCycles > UINT16_MAX) + ? UINT16_MAX + : counterCycles); + (void)ModbusDataWriteWords( + MODBUS_DATA_DEVICE_D, + (uint32_t)PlsrModbusBaseAddress + PLSR_MODBUS_PERFORMANCE_OFFSET, + words, + PLSR_MODBUS_PERFORMANCE_WORDS); + for (stage = 0U; stage < PLSR_PROCESS_STAGE_COUNT; stage++) + { + PlsrModbusPutU32(stageWords, + (uint32_t)stage * 2UL, + PlsrGetMaxProcessStageCycles(stage)); + } + (void)ModbusDataWriteWords( + MODBUS_DATA_DEVICE_D, + (uint32_t)PlsrModbusBaseAddress + + PLSR_MODBUS_STAGE_PERFORMANCE_OFFSET, + stageWords, + PLSR_MODBUS_STAGE_PERFORMANCE_WORDS); + PlsrModbusPutU32(abGateWords, 0UL, PlsrHwGetMaxAbGateCycles()); + (void)ModbusDataWriteWords( + MODBUS_DATA_DEVICE_D, + (uint32_t)PlsrModbusBaseAddress + + PLSR_MODBUS_AB_GATE_PERFORMANCE_OFFSET, + abGateWords, + PLSR_MODBUS_AB_GATE_PERFORMANCE_WORDS); +} + +static void PlsrModbusPublishPersistence(void) +{ + PLSR_PERSISTENCE_DIAGNOSTICS diagnostics; + uint16_t words[PLSR_MODBUS_PERSISTENCE_WORDS] = {0U}; + uint32_t flags = 0UL; + uint32_t generation = PlsrModbusPersistenceGeneration + 2UL; + + if (generation == 0UL) + { + generation = 2UL; + } + PlsrModbusPersistenceGeneration = generation; + PlsrPersistenceGetDiagnostics(&diagnostics); + if (PlcDeviceIsHsdDirty() != 0U) flags |= (1UL << 0U); + if (PlcDeviceIsSfdDirty() != 0U) flags |= (1UL << 1U); + if (PlcDeviceGetRestoredHsdPositionValid() != 0U) + { + flags |= (1UL << 2U); + } + if (PlcDeviceGetRestoredHsdLastBusy() != 0U) + { + flags |= (1UL << 3U); + } + if (diagnostics.destructiveDiagnosticEnabled != 0U) + { + flags |= (1UL << 4U); + } + + PlsrModbusPutU32(words, 0UL, generation); + words[2UL] = PLSR_MODBUS_PERSISTENCE_VERSION; + words[3UL] = (uint16_t)diagnostics.hsdValidMask + | ((uint16_t)diagnostics.sfdValidMask << 8U); + words[4UL] = (uint16_t)diagnostics.hsdNewestMask + | ((uint16_t)diagnostics.sfdNewestMask << 8U); + words[5UL] = (uint16_t)flags; + words[6UL] = (uint16_t)diagnostics.lastHsdLoadResult; + words[7UL] = (uint16_t)diagnostics.lastSfdLoadResult; + words[8UL] = (uint16_t)diagnostics.lastHsdSaveResult; + words[9UL] = (uint16_t)diagnostics.lastSfdSaveResult; + words[10UL] = (uint16_t)diagnostics.lastSfdEraseResult; + PlsrModbusPutU32(words, 12UL, diagnostics.hsdGeneration[0]); + PlsrModbusPutU32(words, 14UL, diagnostics.hsdGeneration[1]); + PlsrModbusPutU32(words, 16UL, diagnostics.sfdGeneration[0]); + PlsrModbusPutU32(words, 18UL, diagnostics.sfdGeneration[1]); + PlsrModbusPutU32(words, 20UL, diagnostics.hsdSaveCount); + PlsrModbusPutU32(words, 22UL, diagnostics.sfdSaveCount); + PlsrModbusPutU32(words, 24UL, diagnostics.selectedHsdCrc32); + PlsrModbusPutU32(words, 26UL, diagnostics.selectedSfdCrc32); + PlsrModbusPutU32(words, 28UL, generation); + (void)ModbusDataWriteWords( + MODBUS_DATA_DEVICE_D, + (uint32_t)PlsrModbusBaseAddress + PLSR_MODBUS_PERSISTENCE_OFFSET, + words, + PLSR_MODBUS_PERSISTENCE_WORDS); +} + +static void PlsrModbusPublishUsbDiagnostics(void) +{ + uint16_t words[PLSR_MODBUS_USB_DIAGNOSTICS_WORDS] = {0U}; + uint32_t generation = PlsrModbusUsbDiagnosticsGeneration + 2UL; + +#ifndef PLSR_HOST_TEST + USB_CDC_RUNTIME_DIAGNOSTICS diagnostics; + + (void)memset(&diagnostics, 0, sizeof(diagnostics)); + (void)CDC_GetRuntimeDiagnostics(&diagnostics); +#endif + if (generation == 0UL) + { + generation = 2UL; + } + PlsrModbusUsbDiagnosticsGeneration = generation; + PlsrModbusPutU32(words, 0UL, generation); + words[2UL] = PLSR_MODBUS_USB_DIAGNOSTICS_VERSION; +#ifndef PLSR_HOST_TEST + words[3UL] = diagnostics.initialized; + PlsrModbusPutU32(words, 4UL, diagnostics.rxPacketCount); + PlsrModbusPutU32(words, 6UL, diagnostics.rxByteCount); + PlsrModbusPutU32(words, 8UL, diagnostics.rxRearmFailureCount); + PlsrModbusPutU32(words, 10UL, diagnostics.txRequestCount); + PlsrModbusPutU32(words, 12UL, diagnostics.txByteCount); + PlsrModbusPutU32(words, 14UL, diagnostics.txBusyCount); + PlsrModbusPutU32(words, 16UL, diagnostics.txFailureCount); + PlsrModbusPutU32(words, 18UL, diagnostics.txCompleteCount); +#endif + PlsrModbusPutU32(words, 20UL, generation); + (void)ModbusDataWriteWords( + MODBUS_DATA_DEVICE_D, + (uint32_t)PlsrModbusBaseAddress + + PLSR_MODBUS_USB_DIAGNOSTICS_OFFSET, + words, + PLSR_MODBUS_USB_DIAGNOSTICS_WORDS); +} + PLSR_RESULT PlsrModbusControlInit(uint16_t baseAddress) { uint16_t header[8] = {0U}; @@ -515,6 +828,9 @@ PLSR_RESULT PlsrModbusControlInit(uint16_t baseAddress) PlsrModbusEnabled = 0U; PlsrModbusLastCallRequestSequence = 0UL; PlsrModbusLastCommandRequestSequence = 0UL; + PlsrModbusLastPersistenceRequestSequence = 0UL; + PlsrModbusPersistenceGeneration = 0UL; + PlsrModbusUsbDiagnosticsGeneration = 0UL; (void)memset(PlsrModbusCommitted, 0, sizeof(PlsrModbusCommitted)); (void)memset(PlsrModbusStatusGeneration, 0, @@ -531,6 +847,8 @@ PLSR_RESULT PlsrModbusControlInit(uint16_t baseAddress) header[2UL] = PLSR_MODBUS_PROTOCOL_VERSION; header[3UL] = (uint16_t)PLSR_MODBUS_WINDOW_WORDS; header[4UL] = PLSR_MODBUS_CAPABILITIES; + PlsrModbusPutU32(header, 5UL, PlsrHwGetTimerClockHz(0U)); + header[7UL] = PLSR_MODBUS_PERFORMANCE_VERSION; if (ModbusDataWriteWords(MODBUS_DATA_DEVICE_D, baseAddress, header, @@ -553,10 +871,14 @@ void PlsrModbusControlPoll(void) } PlsrModbusHandleCallRequest(); PlsrModbusHandleCommandRequest(); + PlsrModbusHandlePersistenceRequest(); for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) { PlsrModbusPublishAxisStatus(axis); } + PlsrModbusPublishPerformance(); + PlsrModbusPublishPersistence(); + PlsrModbusPublishUsbDiagnostics(); } uint8_t PlsrModbusControlIsEnabled(void) diff --git a/PLSR/Src/plsr_modbus_data.c b/PLSR/Src/plsr_modbus_data.c index 650aa7d..cfdf558 100644 --- a/PLSR/Src/plsr_modbus_data.c +++ b/PLSR/Src/plsr_modbus_data.c @@ -58,6 +58,33 @@ static uint8_t PlsrModbusReadDword(void *context, return ModbusDataReadDword(modbusDevice, address, value); } +static uint8_t PlsrModbusBitDevice(PLSR_DEVICE_TYPE device, + MODBUS_BIT_DEVICE *modbusDevice) +{ + if ((modbusDevice == NULL) || (device < PLSR_DEVICE_X) + || (device > PLSR_DEVICE_HM)) + { + return 0U; + } + *modbusDevice = (MODBUS_BIT_DEVICE)(device - PLSR_DEVICE_X); + return 1U; +} + +static uint8_t PlsrModbusReadBit(void *context, + PLSR_DEVICE_TYPE device, + uint32_t address, + uint8_t *value) +{ + MODBUS_BIT_DEVICE modbusDevice; + + (void)context; + if (PlsrModbusBitDevice(device, &modbusDevice) == 0U) + { + return 0U; + } + return ModbusDataReadBit(modbusDevice, address, value); +} + void PlsrModbusDataSourceInit(PLSR_DATA_SOURCE *source) { if (source == NULL) @@ -68,5 +95,5 @@ void PlsrModbusDataSourceInit(PLSR_DATA_SOURCE *source) source->validateWords = PlsrModbusValidateWords; source->readWord = PlsrModbusReadWord; source->readDword = PlsrModbusReadDword; - source->readBit = NULL; + source->readBit = PlsrModbusReadBit; } diff --git a/PLSR/Src/plsr_persistence.c b/PLSR/Src/plsr_persistence.c index 42ead20..ccf8ab9 100644 --- a/PLSR/Src/plsr_persistence.c +++ b/PLSR/Src/plsr_persistence.c @@ -1,4 +1,5 @@ #include "plsr_persistence.h" +#include "plsr_build_config.h" #include #include @@ -23,6 +24,14 @@ #define PLSR_HSD_PARAMETER_SET_STRIDE (20U) #define PLSR_HSD_DEFAULT_BASE (460U) +static const uint32_t PlsrPersistenceCrc32Nibble[16] = +{ + 0x00000000UL, 0x1DB71064UL, 0x3B6E20C8UL, 0x26D930ACUL, + 0x76DC4190UL, 0x6B6B51F4UL, 0x4DB26158UL, 0x5005713CUL, + 0xEDB88320UL, 0xF00F9344UL, 0xD6D6A3E8UL, 0xCB61B38CUL, + 0x9B64C2B0UL, 0x86D3D2D4UL, 0xA00AE278UL, 0xBDBDF21CUL +}; + typedef struct { uint32_t magic; @@ -55,33 +64,53 @@ typedef struct static PLSR_HSD_BACKUP_RECORD PlsrHostBackupSlots[2]; static PLSR_SFD_FLASH_RECORD PlsrHostSfdSlots[2]; static PLSR_TEST_SFD_FAULT PlsrHostSfdFault; +static uint32_t PlsrHostHsdSaveCount; #else #include "stm32f4xx.h" #include "stm32f4xx_hal.h" #include "stm32f4xx_hal_flash_ex.h" #endif +static PLSR_PERSISTENCE_DIAGNOSTICS PlsrPersistenceDiagnostics; +static uint8_t PlsrPersistenceDiagnosticsInitialized; + +static void PlsrPersistenceEnsureDiagnostics(void) +{ + if (PlsrPersistenceDiagnosticsInitialized == 0U) + { + (void)memset(&PlsrPersistenceDiagnostics, + 0, + sizeof(PlsrPersistenceDiagnostics)); + PlsrPersistenceDiagnostics.lastHsdLoadResult = + PLSR_PERSISTENCE_NOT_IMPLEMENTED; + PlsrPersistenceDiagnostics.lastSfdLoadResult = + PLSR_PERSISTENCE_NOT_IMPLEMENTED; + PlsrPersistenceDiagnostics.lastHsdSaveResult = + PLSR_PERSISTENCE_NOT_IMPLEMENTED; + PlsrPersistenceDiagnostics.lastSfdSaveResult = + PLSR_PERSISTENCE_NOT_IMPLEMENTED; + PlsrPersistenceDiagnostics.lastSfdEraseResult = + PLSR_PERSISTENCE_NOT_IMPLEMENTED; +#if PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG != 0U + PlsrPersistenceDiagnostics.destructiveDiagnosticEnabled = 1U; +#endif + PlsrPersistenceDiagnosticsInitialized = 1U; + } +} + static uint32_t PlsrPersistenceCrc32Update(uint32_t crc, const volatile uint8_t *data, uint32_t length) { uint32_t index; - uint8_t bit; for (index = 0U; index < length; index++) { crc ^= data[index]; - for (bit = 0U; bit < 8U; bit++) - { - if ((crc & 1UL) != 0UL) - { - crc = (crc >> 1U) ^ 0xEDB88320UL; - } - else - { - crc >>= 1U; - } - } + crc = (crc >> 4U) + ^ PlsrPersistenceCrc32Nibble[crc & 0x0FUL]; + crc = (crc >> 4U) + ^ PlsrPersistenceCrc32Nibble[crc & 0x0FUL]; } return crc; @@ -161,6 +190,106 @@ static uint8_t PlsrPersistenceSfdRecordIsValid( return (expectedCrc == record->crc32) ? 1U : 0U; } +static uint8_t PlsrPersistenceNewestMask(uint8_t validA, + uint8_t validB, + uint32_t generationA, + uint32_t generationB) +{ + if ((validA != 0U) && (validB != 0U)) + { + return (PlsrPersistenceGenerationIsNewer(generationB, generationA) + != 0U) + ? 2U + : 1U; + } + if (validA != 0U) + { + return 1U; + } + return (validB != 0U) ? 2U : 0U; +} + +static void PlsrPersistenceUpdateHsdDiagnostics( + const volatile PLSR_HSD_BACKUP_RECORD *slotA, + const volatile PLSR_HSD_BACKUP_RECORD *slotB, + uint8_t validA, + uint8_t validB) +{ + uint8_t newestMask; + + PlsrPersistenceEnsureDiagnostics(); + PlsrPersistenceDiagnostics.hsdValidMask = + (uint8_t)((validA != 0U ? 1U : 0U) | (validB != 0U ? 2U : 0U)); + PlsrPersistenceDiagnostics.hsdGeneration[0] = + (validA != 0U) ? slotA->generation : 0UL; + PlsrPersistenceDiagnostics.hsdGeneration[1] = + (validB != 0U) ? slotB->generation : 0UL; + newestMask = PlsrPersistenceNewestMask( + validA, + validB, + PlsrPersistenceDiagnostics.hsdGeneration[0], + PlsrPersistenceDiagnostics.hsdGeneration[1]); + PlsrPersistenceDiagnostics.hsdNewestMask = newestMask; + PlsrPersistenceDiagnostics.selectedHsdCrc32 = + (newestMask == 1U) ? slotA->crc32 + : (newestMask == 2U) ? slotB->crc32 + : 0UL; +} + +static void PlsrPersistenceUpdateSfdDiagnostics( + const volatile PLSR_SFD_FLASH_RECORD *slotA, + const volatile PLSR_SFD_FLASH_RECORD *slotB, + uint8_t validA, + uint8_t validB) +{ + uint8_t newestMask; + + PlsrPersistenceEnsureDiagnostics(); + PlsrPersistenceDiagnostics.sfdValidMask = + (uint8_t)((validA != 0U ? 1U : 0U) | (validB != 0U ? 2U : 0U)); + PlsrPersistenceDiagnostics.sfdGeneration[0] = + (validA != 0U) ? slotA->generation : 0UL; + PlsrPersistenceDiagnostics.sfdGeneration[1] = + (validB != 0U) ? slotB->generation : 0UL; + newestMask = PlsrPersistenceNewestMask( + validA, + validB, + PlsrPersistenceDiagnostics.sfdGeneration[0], + PlsrPersistenceDiagnostics.sfdGeneration[1]); + PlsrPersistenceDiagnostics.sfdNewestMask = newestMask; + PlsrPersistenceDiagnostics.selectedSfdCrc32 = + (newestMask == 1U) ? slotA->crc32 + : (newestMask == 2U) ? slotB->crc32 + : 0UL; +} + +static void PlsrPersistenceRefreshSfdDiagnostics(void) +{ + volatile PLSR_SFD_FLASH_RECORD *slotA = + PlsrPersistenceGetSfdSlot(0U); + volatile PLSR_SFD_FLASH_RECORD *slotB = + PlsrPersistenceGetSfdSlot(1U); + + PlsrPersistenceUpdateSfdDiagnostics( + slotA, + slotB, + PlsrPersistenceSfdRecordIsValid(slotA), + PlsrPersistenceSfdRecordIsValid(slotB)); +} + +static PLSR_PERSISTENCE_RESULT PlsrPersistenceCompleteSfdSave( + PLSR_PERSISTENCE_RESULT result) +{ + PlsrPersistenceEnsureDiagnostics(); + PlsrPersistenceDiagnostics.lastSfdSaveResult = result; + PlsrPersistenceRefreshSfdDiagnostics(); + if (result == PLSR_PERSISTENCE_OK) + { + PlsrPersistenceDiagnostics.sfdSaveCount++; + } + return result; +} + static void PlsrPersistenceCopySfdFromVolatile( PLSR_SFD_DATA *destination, const volatile PLSR_SFD_DATA *source) @@ -443,8 +572,11 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadHsd(PLSR_HSD_DATA *data) uint8_t validA; uint8_t validB; + PlsrPersistenceEnsureDiagnostics(); if (data == NULL) { + PlsrPersistenceDiagnostics.lastHsdLoadResult = + PLSR_PERSISTENCE_INVALID_ARGUMENT; return PLSR_PERSISTENCE_INVALID_ARGUMENT; } @@ -452,10 +584,13 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadHsd(PLSR_HSD_DATA *data) slotB = PlsrPersistenceGetHsdSlot(1U); validA = PlsrPersistenceHsdRecordIsValid(slotA); validB = PlsrPersistenceHsdRecordIsValid(slotB); + PlsrPersistenceUpdateHsdDiagnostics(slotA, slotB, validA, validB); if ((validA == 0U) && (validB == 0U)) { PlsrPersistenceApplyHsdDefaults(data); + PlsrPersistenceDiagnostics.lastHsdLoadResult = + PLSR_PERSISTENCE_DEFAULTED; return PLSR_PERSISTENCE_DEFAULTED; } @@ -475,6 +610,7 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadHsd(PLSR_HSD_DATA *data) } PlsrPersistenceCopyHsdFromVolatile(data, &selected->data); + PlsrPersistenceDiagnostics.lastHsdLoadResult = PLSR_PERSISTENCE_OK; return PLSR_PERSISTENCE_OK; } @@ -489,13 +625,23 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveHsd(const PLSR_HSD_DATA *data) uint32_t generationB; uint8_t validA; uint8_t validB; + uint8_t targetSlot; + uint8_t targetValid; + PlsrPersistenceEnsureDiagnostics(); if (data == NULL) { + PlsrPersistenceDiagnostics.lastHsdSaveResult = + PLSR_PERSISTENCE_INVALID_ARGUMENT; return PLSR_PERSISTENCE_INVALID_ARGUMENT; } +#ifdef PLSR_HOST_TEST + PlsrHostHsdSaveCount++; +#endif if (sizeof(PLSR_HSD_BACKUP_RECORD) > PLSR_BACKUP_SLOT_STRIDE) { + PlsrPersistenceDiagnostics.lastHsdSaveResult = + PLSR_PERSISTENCE_VERIFY_FAILED; return PLSR_PERSISTENCE_VERIFY_FAILED; } @@ -514,26 +660,31 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveHsd(const PLSR_HSD_DATA *data) { newestGeneration = generationB; target = slotA; + targetSlot = 0U; } else { newestGeneration = generationA; target = slotB; + targetSlot = 1U; } } else if (validA != 0U) { newestGeneration = slotA->generation; target = slotB; + targetSlot = 1U; } else if (validB != 0U) { newestGeneration = slotB->generation; target = slotA; + targetSlot = 0U; } else { target = slotA; + targetSlot = 0U; } (void)memset(&record, 0, sizeof(record)); @@ -547,18 +698,39 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveHsd(const PLSR_HSD_DATA *data) (uint32_t)offsetof(PLSR_HSD_BACKUP_RECORD, crc32)); PlsrPersistenceWriteRecord(target, &record); - return (PlsrPersistenceHsdRecordIsValid(target) != 0U) - ? PLSR_PERSISTENCE_OK - : PLSR_PERSISTENCE_VERIFY_FAILED; + targetValid = PlsrPersistenceHsdRecordIsValid(target); + if (targetSlot == 0U) + { + validA = targetValid; + } + else + { + validB = targetValid; + } + PlsrPersistenceUpdateHsdDiagnostics(slotA, slotB, validA, validB); + PlsrPersistenceDiagnostics.lastHsdSaveResult = + (targetValid != 0U) ? PLSR_PERSISTENCE_OK + : PLSR_PERSISTENCE_VERIFY_FAILED; + if (targetValid != 0U) + { + PlsrPersistenceDiagnostics.hsdSaveCount++; + } + return PlsrPersistenceDiagnostics.lastHsdSaveResult; } void PlsrPersistenceResetHsd(void) { - PlsrPersistenceGetHsdSlot(0U)->magic = 0UL; - PlsrPersistenceGetHsdSlot(1U)->magic = 0UL; + volatile PLSR_HSD_BACKUP_RECORD *slotA = + PlsrPersistenceGetHsdSlot(0U); + volatile PLSR_HSD_BACKUP_RECORD *slotB = + PlsrPersistenceGetHsdSlot(1U); + + slotA->magic = 0UL; + slotB->magic = 0UL; #ifndef PLSR_HOST_TEST __DMB(); #endif + PlsrPersistenceUpdateHsdDiagnostics(slotA, slotB, 0U, 0U); } PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data) @@ -571,8 +743,11 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data) uint8_t validA; uint8_t validB; + PlsrPersistenceEnsureDiagnostics(); if (data == NULL) { + PlsrPersistenceDiagnostics.lastSfdLoadResult = + PLSR_PERSISTENCE_INVALID_ARGUMENT; return PLSR_PERSISTENCE_INVALID_ARGUMENT; } @@ -580,10 +755,13 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data) slotB = PlsrPersistenceGetSfdSlot(1U); validA = PlsrPersistenceSfdRecordIsValid(slotA); validB = PlsrPersistenceSfdRecordIsValid(slotB); + PlsrPersistenceUpdateSfdDiagnostics(slotA, slotB, validA, validB); if ((validA == 0U) && (validB == 0U)) { PlsrPersistenceApplySfdDefaults(data); + PlsrPersistenceDiagnostics.lastSfdLoadResult = + PLSR_PERSISTENCE_DEFAULTED; return PLSR_PERSISTENCE_DEFAULTED; } @@ -603,6 +781,7 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceLoadSfd(PLSR_SFD_DATA *data) } PlsrPersistenceCopySfdFromVolatile(data, &selected->data); + PlsrPersistenceDiagnostics.lastSfdLoadResult = PLSR_PERSISTENCE_OK; return PLSR_PERSISTENCE_OK; } @@ -628,12 +807,17 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) uint8_t validA; uint8_t validB; + PlsrPersistenceEnsureDiagnostics(); if (data == NULL) { + PlsrPersistenceDiagnostics.lastSfdSaveResult = + PLSR_PERSISTENCE_INVALID_ARGUMENT; return PLSR_PERSISTENCE_INVALID_ARGUMENT; } if (sizeof(PLSR_SFD_FLASH_RECORD) > PLSR_SFD_FLASH_SECTOR_SIZE) { + PlsrPersistenceDiagnostics.lastSfdSaveResult = + PLSR_PERSISTENCE_VERIFY_FAILED; return PLSR_PERSISTENCE_VERIFY_FAILED; } @@ -702,13 +886,14 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) result = PlsrPersistenceBeginSfdOperation(); if (result != PLSR_PERSISTENCE_OK) { - return result; + return PlsrPersistenceCompleteSfdSave(result); } result = PlsrPersistenceEraseSfdSlot(targetSlot); if (result != PLSR_PERSISTENCE_OK) { - return PlsrPersistenceEndSfdOperation(result); + result = PlsrPersistenceEndSfdOperation(result); + return PlsrPersistenceCompleteSfdSave(result); } /* The valid magic is committed last so an interrupted write stays invalid. */ @@ -718,7 +903,8 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) headerWords[index]); if (result != PLSR_PERSISTENCE_OK) { - return PlsrPersistenceEndSfdOperation(result); + result = PlsrPersistenceEndSfdOperation(result); + return PlsrPersistenceCompleteSfdSave(result); } } @@ -729,7 +915,8 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) dataWords[index]); if (result != PLSR_PERSISTENCE_OK) { - return PlsrPersistenceEndSfdOperation(result); + result = PlsrPersistenceEndSfdOperation(result); + return PlsrPersistenceCompleteSfdSave(result); } } @@ -738,7 +925,8 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) crc32); if (result != PLSR_PERSISTENCE_OK) { - return PlsrPersistenceEndSfdOperation(result); + result = PlsrPersistenceEndSfdOperation(result); + return PlsrPersistenceCompleteSfdSave(result); } #ifdef PLSR_HOST_TEST @@ -754,38 +942,44 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceSaveSfd(const PLSR_SFD_DATA *data) crc32) == 0U) { - return PlsrPersistenceEndSfdOperation( + result = PlsrPersistenceEndSfdOperation( PLSR_PERSISTENCE_VERIFY_FAILED); + return PlsrPersistenceCompleteSfdSave(result); } #ifdef PLSR_HOST_TEST if (PlsrHostSfdFault == PLSR_TEST_SFD_FAULT_BEFORE_COMMIT) { PlsrHostSfdFault = PLSR_TEST_SFD_FAULT_NONE; - return PlsrPersistenceEndSfdOperation( + result = PlsrPersistenceEndSfdOperation( PLSR_PERSISTENCE_PROGRAM_FAILED); + return PlsrPersistenceCompleteSfdSave(result); } #endif result = PlsrPersistenceProgramSfdWord(&targetWords[0], headerWords[0]); if (result != PLSR_PERSISTENCE_OK) { - return PlsrPersistenceEndSfdOperation(result); + result = PlsrPersistenceEndSfdOperation(result); + return PlsrPersistenceCompleteSfdSave(result); } result = (PlsrPersistenceSfdRecordIsValid(target) != 0U) ? PLSR_PERSISTENCE_OK : PLSR_PERSISTENCE_VERIFY_FAILED; - return PlsrPersistenceEndSfdOperation(result); + result = PlsrPersistenceEndSfdOperation(result); + return PlsrPersistenceCompleteSfdSave(result); } PLSR_PERSISTENCE_RESULT PlsrPersistenceEraseSfd(void) { PLSR_PERSISTENCE_RESULT result; + PlsrPersistenceEnsureDiagnostics(); result = PlsrPersistenceBeginSfdOperation(); if (result != PLSR_PERSISTENCE_OK) { + PlsrPersistenceDiagnostics.lastSfdEraseResult = result; return result; } @@ -795,7 +989,103 @@ PLSR_PERSISTENCE_RESULT PlsrPersistenceEraseSfd(void) result = PlsrPersistenceEraseSfdSlot(1U); } - return PlsrPersistenceEndSfdOperation(result); + result = PlsrPersistenceEndSfdOperation(result); + PlsrPersistenceDiagnostics.lastSfdEraseResult = result; + PlsrPersistenceRefreshSfdDiagnostics(); + return result; +} + +void PlsrPersistenceGetDiagnostics( + PLSR_PERSISTENCE_DIAGNOSTICS *diagnostics) +{ + if (diagnostics == NULL) + { + return; + } + PlsrPersistenceEnsureDiagnostics(); + *diagnostics = PlsrPersistenceDiagnostics; +} + +PLSR_PERSISTENCE_RESULT PlsrPersistenceDiagnosticInvalidateNewest( + PLSR_PERSISTENCE_DIAG_TARGET target) +{ +#if PLSR_ENABLE_DESTRUCTIVE_PERSISTENCE_DIAG != 0U + uint32_t generationA; + uint32_t generationB; + uint8_t validA; + uint8_t validB; + + PlsrPersistenceEnsureDiagnostics(); + if (target == PLSR_PERSISTENCE_DIAG_TARGET_HSD) + { + volatile PLSR_HSD_BACKUP_RECORD *slotA = + PlsrPersistenceGetHsdSlot(0U); + volatile PLSR_HSD_BACKUP_RECORD *slotB = + PlsrPersistenceGetHsdSlot(1U); + volatile PLSR_HSD_BACKUP_RECORD *newest; + + validA = PlsrPersistenceHsdRecordIsValid(slotA); + validB = PlsrPersistenceHsdRecordIsValid(slotB); + PlsrPersistenceUpdateHsdDiagnostics(slotA, slotB, validA, validB); + if ((validA == 0U) || (validB == 0U)) + { + return PLSR_PERSISTENCE_VERIFY_FAILED; + } + generationA = slotA->generation; + generationB = slotB->generation; + newest = (PlsrPersistenceGenerationIsNewer(generationB, generationA) + != 0U) + ? slotB + : slotA; + newest->magic = 0UL; +#ifndef PLSR_HOST_TEST + __DMB(); +#endif + PlsrPersistenceUpdateHsdDiagnostics( + slotA, + slotB, + (newest == slotA) ? 0U : 1U, + (newest == slotB) ? 0U : 1U); + return PLSR_PERSISTENCE_OK; + } + if (target == PLSR_PERSISTENCE_DIAG_TARGET_SFD) + { + volatile PLSR_SFD_FLASH_RECORD *slotA = + PlsrPersistenceGetSfdSlot(0U); + volatile PLSR_SFD_FLASH_RECORD *slotB = + PlsrPersistenceGetSfdSlot(1U); + volatile PLSR_SFD_FLASH_RECORD *newest; + PLSR_PERSISTENCE_RESULT result; + + validA = PlsrPersistenceSfdRecordIsValid(slotA); + validB = PlsrPersistenceSfdRecordIsValid(slotB); + PlsrPersistenceUpdateSfdDiagnostics(slotA, slotB, validA, validB); + if ((validA == 0U) || (validB == 0U)) + { + return PLSR_PERSISTENCE_VERIFY_FAILED; + } + generationA = slotA->generation; + generationB = slotB->generation; + newest = (PlsrPersistenceGenerationIsNewer(generationB, generationA) + != 0U) + ? slotB + : slotA; + result = PlsrPersistenceBeginSfdOperation(); + if (result == PLSR_PERSISTENCE_OK) + { + result = PlsrPersistenceProgramSfdWord( + (volatile uint32_t *)&newest->magic, + 0UL); + result = PlsrPersistenceEndSfdOperation(result); + } + PlsrPersistenceRefreshSfdDiagnostics(); + return result; + } + return PLSR_PERSISTENCE_INVALID_ARGUMENT; +#else + (void)target; + return PLSR_PERSISTENCE_NOT_IMPLEMENTED; +#endif } #ifdef PLSR_HOST_TEST @@ -804,6 +1094,29 @@ void PlsrPersistenceTestResetStorage(void) (void)memset(PlsrHostBackupSlots, 0, sizeof(PlsrHostBackupSlots)); (void)memset(PlsrHostSfdSlots, 0xFF, sizeof(PlsrHostSfdSlots)); PlsrHostSfdFault = PLSR_TEST_SFD_FAULT_NONE; + PlsrHostHsdSaveCount = 0UL; + PlsrPersistenceDiagnosticsInitialized = 0U; + PlsrPersistenceEnsureDiagnostics(); + PlsrPersistenceUpdateHsdDiagnostics( + PlsrPersistenceGetHsdSlot(0U), + PlsrPersistenceGetHsdSlot(1U), + 0U, + 0U); + PlsrPersistenceRefreshSfdDiagnostics(); +} + +uint32_t PlsrPersistenceTestGetHsdSaveCount(void) +{ + return PlsrHostHsdSaveCount; +} + +uint32_t PlsrPersistenceTestCrc32(const uint8_t *data, uint32_t length) +{ + if ((data == NULL) && (length != 0UL)) + { + return 0UL; + } + return PlsrPersistenceCrc32(data, length); } void PlsrPersistenceTestCorruptNewestHsd(void) diff --git a/PLSR/Src/plsr_self_test.c b/PLSR/Src/plsr_self_test.c index 764f292..1712689 100644 --- a/PLSR/Src/plsr_self_test.c +++ b/PLSR/Src/plsr_self_test.c @@ -1,4 +1,5 @@ #include "plsr_self_test.h" +#include "plsr_build_config.h" #include "plc_device.h" #include "modbus_data_store.h" #include "plsr_core.h" @@ -6,6 +7,8 @@ #include "plsr_modbus_data.h" #include +#if PLSR_ENABLE_BOARD_SELF_TEST != 0U + /* 上电自测(验证后可删除): * - AB 模式使用 Q0(A)/Q1(B),其余用出厂默认参数(K1) * - 任务:3 段完整 AB 周期(H00 完成,顺序衔接): @@ -964,3 +967,143 @@ PLSR_RESULT PlsrModbusControlSelfTestPrepare(void) SelfTestWriteSfdDword((uint16_t)(setBase + 18U), 200UL); return PLSR_RESULT_OK; } + +PLSR_RESULT PlsrHardwareCounterSelfTestPrepare(void) +{ + uint16_t commonBase; + uint16_t setBase; + uint16_t limitSetBase; + uint16_t abSetBase; + uint32_t limitFrequencyHz; + uint8_t axis; + + for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) + { + commonBase = (uint16_t)(900U + + (uint16_t)axis + * SELF_TEST_SFD_AXIS_STRIDE); + setBase = (uint16_t)(commonBase + SELF_TEST_SFD_SET_OFFSET); + limitSetBase = (uint16_t)(setBase + 20U); + abSetBase = (uint16_t)(setBase + 40U); + + /* PULSE/DIR, pulse-unit soft limits, one direction point per axis. */ + (void)PlcDeviceWriteSfd(commonBase, (1U << 2U)); + SelfTestWriteSfdDword((uint16_t)(commonBase + 2U), 1UL); + SelfTestWriteSfdDword((uint16_t)(commonBase + 4U), 1UL); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 6U), + (uint16_t)(SELF_TEST_DIR_POINT + axis)); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 7U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 8U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 9U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 12U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 15U), 0xFFFFU); + SelfTestWriteSfdDword((uint16_t)(commonBase + 30U), 1000000UL); + SelfTestWriteSfdDword((uint16_t)(commonBase + 32U), + (uint32_t)(int32_t)-1000000); + + /* Exact 100kHz plateau. 200000-pulse S0 jobs cross the 16-bit + * counter boundary three times while avoiding profile ramp effects. */ + SelfTestWriteSfdDword(setBase, 100000UL); + (void)PlcDeviceWriteSfd((uint16_t)(setBase + 2U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(setBase + 3U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(setBase + 4U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(setBase + 5U), 0U); + SelfTestWriteSfdDword((uint16_t)(setBase + 6U), 100000UL); + SelfTestWriteSfdDword((uint16_t)(setBase + 8U), 100000UL); + SelfTestWriteSfdDword((uint16_t)(setBase + 10U), 0UL); + (void)PlcDeviceWriteSfd((uint16_t)(setBase + 12U), 50U); + (void)PlcDeviceWriteSfd((uint16_t)(setBase + 13U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(setBase + 14U), 0U); + SelfTestWriteSfdDword((uint16_t)(setBase + 16U), 2000UL); + SelfTestWriteSfdDword((uint16_t)(setBase + 18U), 200UL); + + /* K2: P15 limit matrix. Even axes use 500Hz, odd axes 2000Hz; + * start at target speed and decelerate for 100ms at a soft limit. */ + limitFrequencyHz = ((axis & 1U) == 0U) ? 500UL : 2000UL; + SelfTestWriteSfdDword(limitSetBase, limitFrequencyHz); + (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 2U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 3U), 100U); + (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 4U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 5U), 0U); + SelfTestWriteSfdDword((uint16_t)(limitSetBase + 6U), + limitFrequencyHz); + SelfTestWriteSfdDword((uint16_t)(limitSetBase + 8U), + limitFrequencyHz); + SelfTestWriteSfdDword((uint16_t)(limitSetBase + 10U), 0UL); + (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 12U), 50U); + (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 13U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(limitSetBase + 14U), 0U); + SelfTestWriteSfdDword((uint16_t)(limitSetBase + 16U), 2000UL); + SelfTestWriteSfdDword((uint16_t)(limitSetBase + 18U), 200UL); + + /* K3: dual-AB 100kHz plateau. TIM9/TIM12 count complete-cycle + * source events so the output timers only interrupt for rephase and + * the guarded final 00 boundary. */ + SelfTestWriteSfdDword(abSetBase, 100000UL); + (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 2U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 3U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 4U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 5U), 0U); + SelfTestWriteSfdDword((uint16_t)(abSetBase + 6U), 100000UL); + SelfTestWriteSfdDword((uint16_t)(abSetBase + 8U), 100000UL); + SelfTestWriteSfdDword((uint16_t)(abSetBase + 10U), 0UL); + (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 12U), 50U); + (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 13U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(abSetBase + 14U), 0U); + SelfTestWriteSfdDword((uint16_t)(abSetBase + 16U), 2000UL); + SelfTestWriteSfdDword((uint16_t)(abSetBase + 18U), 200UL); + } + return PLSR_RESULT_OK; +} + +PLSR_RESULT PlsrLongStressSelfTestPrepare(void) +{ + uint16_t commonBase; + uint16_t longSetBase; + uint8_t axis; + + for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) + { + commonBase = (uint16_t)(900U + + (uint16_t)axis + * SELF_TEST_SFD_AXIS_STRIDE); + longSetBase = (uint16_t)(commonBase + + SELF_TEST_SFD_SET_OFFSET + + 60U); /* K4 occupies the final 20 words. */ + + /* P18 is a bench-only PULSE/DIR endurance fixture. Keep the proven + * Q4..Q7 direction mapping and disable both hard-input assignments and + * soft limits so the deliberately long positive jobs cannot stop at + * the P14/P15 +/-1000000-pulse validation boundary. */ + (void)PlcDeviceWriteSfd(commonBase, 0U); + SelfTestWriteSfdDword((uint16_t)(commonBase + 2U), 1UL); + SelfTestWriteSfdDword((uint16_t)(commonBase + 4U), 1UL); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 6U), + (uint16_t)(SELF_TEST_DIR_POINT + axis)); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 7U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 8U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 9U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 12U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(commonBase + 15U), 0xFFFFU); + SelfTestWriteSfdDword((uint16_t)(commonBase + 30U), 0UL); + SelfTestWriteSfdDword((uint16_t)(commonBase + 32U), 0UL); + + /* K4: exact 100kHz plateau, no acceleration/deceleration ramp. */ + SelfTestWriteSfdDword(longSetBase, 100000UL); + (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 2U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 3U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 4U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 5U), 0U); + SelfTestWriteSfdDword((uint16_t)(longSetBase + 6U), 100000UL); + SelfTestWriteSfdDword((uint16_t)(longSetBase + 8U), 100000UL); + SelfTestWriteSfdDword((uint16_t)(longSetBase + 10U), 0UL); + (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 12U), 50U); + (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 13U), 0U); + (void)PlcDeviceWriteSfd((uint16_t)(longSetBase + 14U), 0U); + SelfTestWriteSfdDword((uint16_t)(longSetBase + 16U), 2000UL); + SelfTestWriteSfdDword((uint16_t)(longSetBase + 18U), 200UL); + } + return PLSR_RESULT_OK; +} + +#endif diff --git a/PLSR/Test/test_plc_device.c b/PLSR/Test/test_plc_device.c index 40403a5..672f4ad 100644 --- a/PLSR/Test/test_plc_device.c +++ b/PLSR/Test/test_plc_device.c @@ -57,11 +57,18 @@ static void TestAddressMap(void) static void TestHsdAndPersistence(void) { + static const uint8_t crcVector[] = "123456789"; uint16_t value16; int32_t value32; const int32_t firstDword = INT32_C(0x12345678); const int32_t secondDword = -INT32_C(0x1020304); + TEST_CHECK(PlsrPersistenceTestCrc32(crcVector, + sizeof(crcVector) - 1UL) + == 0xCBF43926UL); + TEST_CHECK(PlsrPersistenceTestCrc32(NULL, 0UL) == 0UL); + TEST_CHECK(PlsrPersistenceTestCrc32(NULL, 1UL) == 0UL); + PlsrPersistenceTestResetStorage(); TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); TEST_CHECK(PlcDeviceGetLastHsdLoadResult() == PLSR_PERSISTENCE_DEFAULTED); diff --git a/PLSR/Test/test_plsr_core.c b/PLSR/Test/test_plsr_core.c index d59d3f3..5bd5073 100644 --- a/PLSR/Test/test_plsr_core.c +++ b/PLSR/Test/test_plsr_core.c @@ -349,12 +349,29 @@ static void TestPositionCheckpointing(void) PLSR_STATUS status; int32_t hsdPosition; int32_t hsdEquivalent; + uint8_t axis; - /* 1. SET_POSITION 后立即写入 HSD 检查点。 */ + /* 1. 全局position-valid必须保守聚合:只建立轴0坐标时,重启 + * 后不得把其他三条未校准轴一起判为可信。 */ TestReset(); TEST_CHECK(TestPostCommand(1U, 0U, PLSR_CMD_SET_POSITION, 500) == PLSR_RESULT_QUEUED); PlsrProcess(); + TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); + TEST_CHECK(PlcDeviceGetRestoredHsdPositionValid() == 0U); + + /* 2. 同一轮的四轴 SET_POSITION 合并为一个完整 HSD 检查点。 */ + TestReset(); + for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) + { + TEST_CHECK(TestPostCommand((uint32_t)axis + 1UL, + axis, + PLSR_CMD_SET_POSITION, + (axis == 0U) ? 500 : 0) + == PLSR_RESULT_QUEUED); + } + PlsrProcess(); + TEST_CHECK(PlsrPersistenceTestGetHsdSaveCount() == 1UL); status = TestGetStatus(0U); TEST_CHECK(status.lastCommandResult == PLSR_RESULT_OK); TEST_CHECK(status.positionValid != 0U); @@ -364,14 +381,14 @@ static void TestPositionCheckpointing(void) TEST_CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK); TEST_CHECK(hsdEquivalent == 500); - /* 2. 模拟重启:位置与 positionValid 应恢复到上次正常停机的检查点。 */ + /* 3. 模拟重启:位置与 positionValid 应恢复到上次正常停机的检查点。 */ TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); TEST_CHECK(PlsrInit() == PLSR_RESULT_OK); status = TestGetStatus(0U); TEST_CHECK(status.positionValid != 0U); TEST_CHECK(status.logicalPosition == 500); - /* 3. 运动中掉电(lastBusy=1):恢复后位置不可信。 */ + /* 4. 运动中掉电(lastBusy=1):恢复后位置不可信。 */ TEST_CHECK(PlcDeviceSetHsdCheckpointMeta(1U, 1U) == PLC_DEVICE_OK); TEST_CHECK(PlcDeviceCheckpointHsd() == PLC_DEVICE_OK); TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); @@ -380,8 +397,18 @@ static void TestPositionCheckpointing(void) TEST_CHECK(status.positionValid == 0U); TEST_CHECK(status.logicalPosition == 500); - /* 4. 64位位置超出 INT32 范围:保留内部值、HSD保持最近合法值。 */ - TEST_CHECK(TestPostCommand(2U, + /* 5. 先重新建立四轴有效坐标,再验证64位位置超出INT32范围: + * 保留内部值、HSD保持最近合法值,但持久化有效位必须清除。 */ + for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++) + { + TEST_CHECK(TestPostCommand(10UL + (uint32_t)axis, + axis, + PLSR_CMD_SET_POSITION, + (axis == 0U) ? 500 : 0) + == PLSR_RESULT_QUEUED); + } + PlsrProcess(); + TEST_CHECK(TestPostCommand(20U, 0U, PLSR_CMD_SET_POSITION, INT64_C(0x100000000)) @@ -396,8 +423,17 @@ static void TestPositionCheckpointing(void) TEST_CHECK(PlcDeviceReadHsdDword(2U, &hsdEquivalent) == PLC_DEVICE_OK); TEST_CHECK(hsdEquivalent == 500); - /* 5. 清零位置重新建立有效坐标,并解除兼容发布溢出锁存。 */ - TEST_CHECK(TestPostCommand(3U, 0U, PLSR_CMD_CLEAR_POSITION, 0) + /* HSD只能保存32位位置;发生发布溢出后,旧的合法HSD值不得在 + * 重启时冒充当前可信坐标。 */ + TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); + TEST_CHECK(PlcDeviceGetRestoredHsdPositionValid() == 0U); + TEST_CHECK(PlsrInit() == PLSR_RESULT_OK); + status = TestGetStatus(0U); + TEST_CHECK(status.positionValid == 0U); + TEST_CHECK(status.logicalPosition == 500); + + /* 6. 清零位置重新建立有效坐标,并解除兼容发布溢出锁存。 */ + TEST_CHECK(TestPostCommand(21U, 0U, PLSR_CMD_CLEAR_POSITION, 0) == PLSR_RESULT_QUEUED); PlsrProcess(); status = TestGetStatus(0U); @@ -410,6 +446,28 @@ static void TestPositionCheckpointing(void) TEST_CHECK(hsdEquivalent == 0); } +static void TestMultiAxisBusyCheckpoint(void) +{ + /* 轴0结束时轴1仍在运动,持久化lastBusy必须保持1。 */ + TestReset(); + TEST_CHECK(TestPostStart(100U, + 0U, + PLSR_OUTPUT_PULSE_DIR, + 4U, + 1U) == PLSR_RESULT_QUEUED); + TEST_CHECK(TestPostStart(101U, + 1U, + PLSR_OUTPUT_PULSE_DIR, + 5U, + 1U) == PLSR_RESULT_QUEUED); + PlsrProcess(); + TEST_CHECK(PlsrPostEvent(0U, PLSR_EVENT_JOB_COMPLETE) + == PLSR_RESULT_OK); + PlsrProcess(); + TEST_CHECK(PlcDeviceInit() == PLC_DEVICE_OK); + TEST_CHECK(PlcDeviceGetRestoredHsdLastBusy() != 0U); +} + int main(void) { TestResourceManager(); @@ -417,6 +475,7 @@ int main(void) TestLimitWaitAndPairs(); TestQueueAndInputValidation(); TestPositionCheckpointing(); + TestMultiAxisBusyCheckpoint(); (void)printf("PASS: %u PLSR core checks\n", TestCount); return EXIT_SUCCESS; diff --git a/PLSR/Test/test_plsr_hal.c b/PLSR/Test/test_plsr_hal.c index b381de6..4cbf358 100644 --- a/PLSR/Test/test_plsr_hal.c +++ b/PLSR/Test/test_plsr_hal.c @@ -123,20 +123,6 @@ static void TestResetEnvironment(void) CHECK(PlsrInit() == PLSR_RESULT_OK); } -static void TestCompleteFirstAbPrime(uint8_t axis) -{ - int quarter; - - CHECK(PlsrHwIsAbStartupPriming(axis) != 0U); - for (quarter = 0; quarter < 4; quarter++) - { - PlsrHwTestAdvanceAbQuarter(axis); - } - CHECK(PlsrHwIsAbStartupPriming(axis) == 0U); - CHECK(PlsrHwGetEmittedPulses(axis) == 0); - CHECK(PlsrHwTestGetAbQuarter(axis) == 0U); -} - static PLSR_CALL TestMakeCall(TEST_MEMORY *memory) { PLSR_CALL call; @@ -300,6 +286,55 @@ static void TestDirectionBatch(void) CHECK(PlsrHwTestGetDirLevel(1U) == 1U); } +static void TestHardwareCounterLeases(void) +{ + PLSR_HW_START_PARAMS params; + + (void)PlsrHwInit(); + (void)memset(¶ms, 0, sizeof(params)); + params.frequencyHz = 100000UL; + params.targetPulses = 10; + params.outputMode = PLSR_OUTPUT_PULSE_DIR; + params.directionPoint = 4U; + params.directionPositive = 1U; + + CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); + CHECK(PlsrHwUsesHardwareCounter(0U) == 1U); + /* Replacing a prepared segment must release and reacquire the same lease + * instead of orphaning TIM9 under the old preparation. */ + CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); + CHECK(PlsrHwUsesHardwareCounter(0U) == 1U); + CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); + /* A hardware-counted PWM starts in the inactive half-cycle. TIM9/TIM12 + * must see a low ITR level when external-clock mode is armed, otherwise + * the startup OCREF level is counted before a terminal pulse exists. */ + CHECK(PlsrHwTestGetCnt(0U) == PlsrHwTestGetCcr(0U)); + params.directionPoint = 5U; + CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); + CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); + CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); + /* PWM compare and update flags coexist at the period boundary. A + * software fallback axis must consume both in one ISR and count once. */ + PlsrHwTestTriggerUpdateAndCompare(2U); + CHECK(PlsrHwGetEmittedPulses(2U) == 1); + PlsrHwTestTriggerCompare(2U); + CHECK(PlsrHwGetEmittedPulses(2U) == 1); + + CHECK(PlsrHwStopPulse(0U) == PLSR_RESULT_OK); + CHECK(PlsrHwStopPulse(2U) == PLSR_RESULT_OK); + CHECK(PlsrHwUsesHardwareCounter(0U) == 0U); + params.directionPoint = 5U; + CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); + CHECK(PlsrHwUsesHardwareCounter(2U) == 1U); + CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); + while (PlsrHwGetState(2U) != PLSR_HW_STATE_DONE) + { + PlsrHwTestTriggerUpdate(2U); + } + CHECK(PlsrHwGetEmittedPulses(2U) == 10); + CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); +} + static void TestCwCcwSequence(void) { PLSR_HW_START_PARAMS params; @@ -622,9 +657,9 @@ static void TestAbPhaseAndCounting(void) == (PlsrHwTestGetArr(0U) + 1UL) / 2UL); /* 两相从精确 00 边界起步;CC1IF 会在开中断前再次清除。 */ CHECK(PlsrHwTestGetCnt(0U) - == ((PlsrHwTestGetArr(0U) + 1UL) * 3UL) / 4UL); - CHECK(PlsrHwTestGetCnt(1U) == PlsrHwTestGetCcr(1U)); - TestCompleteFirstAbPrime(0U); + == ((PlsrHwTestGetArr(0U) + 1UL) * 3UL) / 4UL + 1UL); + CHECK(PlsrHwTestGetCnt(1U) == PlsrHwTestGetCcr(1U) + 1UL); + CHECK(PlsrHwIsAbStartupPriming(0U) == 0U); /* 任一物理 timer update 不能直接计作完整 AB 周期。 */ PlsrHwTestTriggerUpdate(0U); @@ -680,8 +715,8 @@ static void TestAbPhaseAndCounting(void) == 2UL * (PlsrHwTestGetPsc(1U) + 1UL)); CHECK(PlsrHwTestGetArr(0U) == PlsrHwTestGetArr(1U)); newPeriod = PlsrHwTestGetArr(0U) + 1UL; - CHECK(PlsrHwTestGetCnt(0U) == (newPeriod * 3UL) / 4UL); - CHECK(PlsrHwTestGetCnt(1U) == newPeriod / 2UL); + CHECK(PlsrHwTestGetCnt(0U) == (newPeriod * 3UL) / 4UL + 1UL); + CHECK(PlsrHwTestGetCnt(1U) == newPeriod / 2UL + 1UL); for (quarter = 0; quarter < 4; quarter++) { @@ -689,6 +724,7 @@ static void TestAbPhaseAndCounting(void) } CHECK(PlsrHwGetEmittedPulses(0U) == 4); CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); + PlsrHwTick(0U); CHECK(PlsrHwTestGetPwmEnabled(0U) == 0U); CHECK(PlsrHwTestGetPwmEnabled(1U) == 0U); CHECK(PlsrHwTestGetAbPhaseA(0U) == 0U); @@ -707,6 +743,7 @@ static void TestAbPhaseAndCounting(void) } CHECK(PlsrHwGetEmittedPulses(0U) == 1); CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); + PlsrHwTick(0U); /* 紧急停止即使发生在周期中间,也必须回到安全 00。 */ params.targetPulses = 10; @@ -742,14 +779,12 @@ static void TestTwoAbAxesIndependent(void) CHECK(PlsrHwTestGetPwmEnabled(2U) != 0U); CHECK(PlsrHwTestGetPwmEnabled(3U) != 0U); - TestCompleteFirstAbPrime(0U); - TestCompleteFirstAbPrime(2U); - for (quarter = 0; quarter < 4; quarter++) { PlsrHwTestAdvanceAbQuarter(0U); } CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); + PlsrHwTick(0U); CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_RUNNING); CHECK(PlsrHwGetEmittedPulses(2U) == 0); CHECK(PlsrHwTestGetPwmEnabled(0U) == 0U); @@ -763,11 +798,164 @@ static void TestTwoAbAxesIndependent(void) } CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_DONE); CHECK(PlsrHwGetEmittedPulses(2U) == 1); + PlsrHwTick(2U); +} + +static void TestDualAbHardwareCountersAndResume(void) +{ + PLSR_HW_START_PARAMS params; + int quarter; + int cycle; + + (void)PlsrHwInit(); + (void)memset(¶ms, 0, sizeof(params)); + params.frequencyHz = 100000UL; + params.targetPulses = 6; + params.outputMode = PLSR_OUTPUT_AB; + params.directionPoint = PLSR_HW_DIR_POINT_NONE; + params.directionPositive = 1U; + + CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); + CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); + CHECK(PlsrHwUsesHardwareCounter(0U) == 1U); + CHECK(PlsrHwUsesHardwareCounter(2U) == 1U); + CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); + CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); + + for (cycle = 0; cycle < 2; cycle++) + { + for (quarter = 0; quarter < 4; quarter++) + { + PlsrHwTestAdvanceAbQuarter(0U); + } + } + for (quarter = 0; quarter < 4; quarter++) + { + PlsrHwTestAdvanceAbQuarter(2U); + } + CHECK(PlsrHwGetEmittedPulses(0U) == 2); + CHECK(PlsrHwGetEmittedPulses(2U) == 1); + + /* Request PAUSE after the source edge but before the following 00. The + * pair must finish this already-started cycle instead of forcing GPIO 00 + * and re-emitting its source edge after RESUME. */ + PlsrHwTestAdvanceAbQuarter(0U); + CHECK(PlsrHwGetEmittedPulses(0U) == 2); + CHECK(PlsrHwSetFrequency(0U, 0UL) == PLSR_RESULT_OK); + CHECK(PlsrHwTestGetAbQuarter(0U) == 1U); + CHECK((PlsrHwTestGetCr1(0U) & 1UL) != 0UL); + CHECK((PlsrHwTestGetCr1(1U) & 1UL) != 0UL); + for (quarter = 1; quarter < 4; quarter++) + { + PlsrHwTestAdvanceAbQuarter(0U); + } + CHECK(PlsrHwGetEmittedPulses(0U) == 3); + CHECK((PlsrHwTestGetCr1(0U) & 1UL) == 0UL); + CHECK((PlsrHwTestGetCr1(1U) & 1UL) == 0UL); + CHECK(PlsrHwResumePulse(0U) == PLSR_RESULT_INVALID_STATE); + PlsrHwTick(0U); + CHECK(PlsrHwGetEmittedPulses(0U) == 3); + CHECK(PlsrHwResumePulse(0U) == PLSR_RESULT_OK); + CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); + + for (cycle = 0; cycle < 3; cycle++) + { + for (quarter = 0; quarter < 4; quarter++) + { + PlsrHwTestAdvanceAbQuarter(0U); + } + } + CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); + CHECK(PlsrHwGetEmittedPulses(0U) == 6); + PlsrHwTick(0U); + CHECK(PlsrHwUsesHardwareCounter(0U) == 0U); + CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_RUNNING); + + for (cycle = 1; cycle < 6; cycle++) + { + for (quarter = 0; quarter < 4; quarter++) + { + PlsrHwTestAdvanceAbQuarter(2U); + } + } + CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_DONE); + CHECK(PlsrHwGetEmittedPulses(2U) == 6); + PlsrHwTick(2U); + CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); +} + +static void TestDualAbSimultaneousFastGate(void) +{ + PLSR_HW_START_PARAMS params; + + (void)PlsrHwInit(); + (void)memset(¶ms, 0, sizeof(params)); + params.frequencyHz = 100000UL; + params.targetPulses = 200000; + params.outputMode = PLSR_OUTPUT_AB; + params.directionPoint = PLSR_HW_DIR_POINT_NONE; + params.directionPositive = 1U; + + CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); + params.directionPositive = 0U; + CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); + CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); + CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); + CHECK(PlsrHwUsesHardwareCounter(0U) == 1U); + CHECK(PlsrHwUsesHardwareCounter(2U) == 1U); + + /* Both lag flags become pending before the first equal-priority handler. + * Its entry scan must freeze all four timers before publishing axis 0. */ + PlsrHwTestSignalDualAbFinalBoundary(0U); + CHECK(PlsrHwTestGetAbFullGateCount() == 1UL); + CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); + CHECK(PlsrHwGetEmittedPulses(0U) == 200000); + CHECK((PlsrHwTestGetCr1(2U) & 1UL) == 0UL); + CHECK((PlsrHwTestGetCr1(3U) & 1UL) == 0UL); + CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_RUNNING); + + /* The lag axis for negative Q2/Q3 is Q2. */ + PlsrHwTestTriggerCompare(2U); + CHECK(PlsrHwTestGetAbFullGateCount() == 2UL); + CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_DONE); + CHECK(PlsrHwGetEmittedPulses(2U) == 200000); + PlsrHwTick(0U); + PlsrHwTick(2U); + CHECK(PlsrHwUsesHardwareCounter(0U) == 0U); + CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); + + /* Repeat with pair 2's flag injected after the first entry scan. The + * second scan must gate it before pair 0 performs deferred GPIO/counter + * cleanup, and both pairs must still be frozen at a real 00 boundary. */ + (void)PlsrHwInit(); + params.directionPositive = 1U; + CHECK(PlsrHwStartPulse(0U, ¶ms) == PLSR_RESULT_OK); + params.directionPositive = 0U; + CHECK(PlsrHwStartPulse(2U, ¶ms) == PLSR_RESULT_OK); + CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); + CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK); + + PlsrHwTestSignalDualAbStaggeredFinalBoundary(0U); + CHECK(PlsrHwTestGetAbFullGateCount() == 1UL); + CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); + CHECK(PlsrHwGetEmittedPulses(0U) == 200000); + CHECK((PlsrHwTestGetCr1(2U) & 1UL) == 0UL); + CHECK((PlsrHwTestGetCr1(3U) & 1UL) == 0UL); + CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_RUNNING); + PlsrHwTestTriggerCompare(2U); + CHECK(PlsrHwTestGetAbFullGateCount() == 2UL); + CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_DONE); + CHECK(PlsrHwGetEmittedPulses(2U) == 200000); + PlsrHwTick(0U); + PlsrHwTick(2U); + CHECK(PlsrHwUsesHardwareCounter(0U) == 0U); + CHECK(PlsrHwUsesHardwareCounter(2U) == 0U); } static void TestAbFrequencyLimits(void) { PLSR_HW_START_PARAMS params; + uint32_t oldArr; int quarter; (void)PlsrHwInit(); @@ -785,13 +973,17 @@ static void TestAbFrequencyLimits(void) == 2UL * (PlsrHwTestGetPsc(1U) + 1UL)); CHECK(PlsrHwTestGetArr(0U) <= 65535UL); - TestCompleteFirstAbPrime(0U); - + oldArr = PlsrHwTestGetArr(0U); CHECK(PlsrHwSetFrequency(0U, 100000UL) == PLSR_RESULT_OK); + CHECK(PlsrHwTestGetArr(0U) == oldArr); for (quarter = 0; quarter < 4; quarter++) { PlsrHwTestAdvanceAbQuarter(0U); } + /* This request is 99 cycles away from target-1: it must be applied by + * the frequency one-shot CCIE, not accidentally by the final guard. */ + CHECK(PlsrHwTestGetArr(0U) != oldArr); + CHECK(PlsrHwTestGetArr(0U) == 839UL); CHECK(PlsrHwTestGetArr(0U) == PlsrHwTestGetArr(1U)); CHECK((PlsrHwTestGetPsc(0U) + 1UL) == 2UL * (PlsrHwTestGetPsc(1U) + 1UL)); @@ -969,8 +1161,6 @@ static void TestEndToEndAbSegment(void) CHECK(PlsrHwTestGetPwmEnabled(0U) != 0U); CHECK(PlsrHwTestGetPwmEnabled(1U) != 0U); - TestCompleteFirstAbPrime(0U); - /* 负脉冲选择反向相序,完整两个周期后由同一事件链结束任务。 */ PlsrHwTestAdvanceAbQuarter(0U); CHECK(PlsrHwTestGetAbPhaseA(0U) == 0U); @@ -982,6 +1172,7 @@ static void TestEndToEndAbSegment(void) CHECK(PlsrHwGetEmittedPulses(0U) == 2); CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); PlsrProcess(); + PlsrProcess(); status = TestGetStatus(); CHECK(status.state == PLSR_STATE_COMPLETED); CHECK(status.done != 0U); @@ -1211,6 +1402,7 @@ static void TestSoftLimitAndSegmentEvent(void) int32_t errorCode; int pulse; int tick; + uint32_t pulsePhase = 0UL; TestResetEnvironment(); CHECK(PlcDeviceWriteSfd(900U, (1U << 2U)) == PLC_DEVICE_OK); @@ -1303,6 +1495,97 @@ static void TestSoftLimitAndSegmentEvent(void) CHECK(PlcDeviceReadEvent(6000U, &eventRecord) == PLC_DEVICE_OK); CHECK(eventRecord.count == 1UL); CHECK(eventRecord.lastReason == PLSR_STOP_REASON_LIMIT_POSITIVE); + + /* Board-equivalent time simulation for the +500 precision case. Each + * loop represents 1 ms and emits complete hardware periods according to + * the frequency that was active during that interval. */ + TestResetEnvironment(); + CHECK(PlcDeviceWriteSfd(900U, (1U << 2U)) == PLC_DEVICE_OK); + CHECK(PlcDeviceWriteSfd(907U, 0U) == PLC_DEVICE_OK); + TestWriteSfdDword(930U, 500UL); + TestWriteSfdDword(932U, (uint32_t)(int32_t)-500); + TestWriteSfdDword(950U, 1000UL); + CHECK(PlcDeviceWriteSfd(952U, 100U) == PLC_DEVICE_OK); + CHECK(PlcDeviceWriteSfd(953U, 100U) == PLC_DEVICE_OK); + TestWriteSfdDword(958U, 1000UL); + TestWriteSfdDword(960U, 0UL); + command.sequence = 45UL; + command.opcode = PLSR_CMD_SET_POSITION; + command.argument = 0; + CHECK(PlsrPostCommand(&command) == PLSR_RESULT_QUEUED); + PlsrProcess(); + (void)memset(&memory, 0, sizeof(memory)); + TestWriteDword(&memory, PLSR_DEVICE_D, TEST_S0_BASE, 1); + TestSetSegment(&memory, 1U, 1000U, 10000); + call = TestMakeCall(&memory); + call.sequence = 46UL; + CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED); + PlsrProcess(); + for (tick = 0; tick < 1000; tick++) + { + pulsePhase += PlsrHwGetCurrentFrequencyHz(0U); + while ((pulsePhase >= 1000UL) + && (PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING)) + { + pulsePhase -= 1000UL; + PlsrHwTestTriggerUpdate(0U); + } + PlsrProcess(); + status = TestGetStatus(); + if (status.state == PLSR_STATE_STOPPED) + { + break; + } + } + CHECK(status.state == PLSR_STATE_STOPPED); + CHECK(status.stopReason == PLSR_STOP_REASON_LIMIT_POSITIVE); + CHECK(status.logicalPosition >= 499); + CHECK(status.logicalPosition <= 501); + + /* Negative/high-speed companion case for the P15 board matrix. */ + TestResetEnvironment(); + pulsePhase = 0UL; + CHECK(PlcDeviceWriteSfd(900U, (1U << 2U)) == PLC_DEVICE_OK); + CHECK(PlcDeviceWriteSfd(907U, 0U) == PLC_DEVICE_OK); + TestWriteSfdDword(930U, 1000UL); + TestWriteSfdDword(932U, (uint32_t)(int32_t)-1000); + TestWriteSfdDword(950U, 2000UL); + CHECK(PlcDeviceWriteSfd(952U, 100U) == PLC_DEVICE_OK); + CHECK(PlcDeviceWriteSfd(953U, 100U) == PLC_DEVICE_OK); + TestWriteSfdDword(958U, 2000UL); + TestWriteSfdDword(960U, 0UL); + command.sequence = 47UL; + command.opcode = PLSR_CMD_SET_POSITION; + command.argument = 0; + CHECK(PlsrPostCommand(&command) == PLSR_RESULT_QUEUED); + PlsrProcess(); + (void)memset(&memory, 0, sizeof(memory)); + TestWriteDword(&memory, PLSR_DEVICE_D, TEST_S0_BASE, 1); + TestSetSegment(&memory, 1U, 2000U, -10000); + call = TestMakeCall(&memory); + call.sequence = 48UL; + CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED); + PlsrProcess(); + for (tick = 0; tick < 1000; tick++) + { + pulsePhase += PlsrHwGetCurrentFrequencyHz(0U); + while ((pulsePhase >= 1000UL) + && (PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING)) + { + pulsePhase -= 1000UL; + PlsrHwTestTriggerUpdate(0U); + } + PlsrProcess(); + status = TestGetStatus(); + if (status.state == PLSR_STATE_STOPPED) + { + break; + } + } + CHECK(status.state == PLSR_STATE_STOPPED); + CHECK(status.stopReason == PLSR_STOP_REASON_LIMIT_NEGATIVE); + CHECK(status.logicalPosition >= -1001); + CHECK(status.logicalPosition <= -999); } static void TestHardLimitAndEmergencyLatch(void) @@ -1543,6 +1826,7 @@ static void TestFourAxisSelfTest(void) CHECK(PlsrHwGetState(axis) == PLSR_HW_STATE_RUNNING); CHECK(PlsrHwGetCurrentFrequencyHz(axis) == expectedFrequency[axis]); + CHECK(status.hardwareCounter == ((axis < 2U) ? 1U : 0U)); } /* A 0.25ms base slot produces 1/2/3/4kHz update ratios while all four @@ -2019,6 +2303,10 @@ static void TestModbusControlProtocol(void) uint16_t callResponse[12]; uint16_t commandRequest[8] = {0U}; uint16_t commandResponse[8]; + uint16_t controlHeader[8]; + uint16_t performanceWords[8]; + uint16_t stagePerformanceWords[PLSR_MODBUS_STAGE_PERFORMANCE_WORDS]; + uint16_t usbDiagnosticsWords[PLSR_MODBUS_USB_DIAGNOSTICS_WORDS]; uint16_t axisStatus[48]; uint16_t pulseWords[2]; uint32_t generationBegin; @@ -2032,6 +2320,54 @@ static void TestModbusControlProtocol(void) CHECK(PlsrModbusControlInit((uint16_t)controlBase) == PLSR_RESULT_OK); CHECK(PlsrModbusControlIsEnabled() == 1U); CHECK(PlsrModbusControlGetBaseAddress() == controlBase); + for (tick = 0; tick < 8; tick++) + { + CHECK(ModbusDataReadWord(MODBUS_DATA_DEVICE_D, + controlBase + (uint32_t)tick, + &controlHeader[tick]) == 1U); + CHECK(ModbusDataReadWord( + MODBUS_DATA_DEVICE_D, + controlBase + PLSR_MODBUS_PERFORMANCE_OFFSET + + (uint32_t)tick, + &performanceWords[tick]) == 1U); + } + CHECK(controlHeader[5] == (uint16_t)(168000000UL & 0xFFFFUL)); + CHECK(controlHeader[6] == (uint16_t)(168000000UL >> 16U)); + CHECK(controlHeader[7] == PLSR_MODBUS_PERFORMANCE_VERSION); + CHECK(controlHeader[3] == (uint16_t)PLSR_MODBUS_WINDOW_WORDS); + for (tick = 0; tick < 8; tick++) + { + CHECK(performanceWords[tick] == 0U); + } + for (tick = 0; tick < (int)PLSR_MODBUS_STAGE_PERFORMANCE_WORDS; tick++) + { + CHECK(ModbusDataReadWord( + MODBUS_DATA_DEVICE_D, + controlBase + PLSR_MODBUS_STAGE_PERFORMANCE_OFFSET + + (uint32_t)tick, + &stagePerformanceWords[tick]) == 1U); + CHECK(stagePerformanceWords[tick] == 0U); + } + for (tick = 0; tick < (int)PLSR_MODBUS_USB_DIAGNOSTICS_WORDS; tick++) + { + CHECK(ModbusDataReadWord( + MODBUS_DATA_DEVICE_D, + controlBase + PLSR_MODBUS_USB_DIAGNOSTICS_OFFSET + + (uint32_t)tick, + &usbDiagnosticsWords[tick]) == 1U); + } + generationBegin = (uint32_t)usbDiagnosticsWords[0] + | ((uint32_t)usbDiagnosticsWords[1] << 16U); + generationEnd = (uint32_t)usbDiagnosticsWords[20] + | ((uint32_t)usbDiagnosticsWords[21] << 16U); + CHECK(generationBegin == generationEnd); + CHECK((generationBegin & 1UL) == 0UL); + CHECK(usbDiagnosticsWords[2] == + PLSR_MODBUS_USB_DIAGNOSTICS_VERSION); + for (tick = 3; tick < 20; tick++) + { + CHECK(usbDiagnosticsWords[tick] == 0U); + } s0Words[0] = 1U; s0Words[10] = 2000U; @@ -2152,6 +2488,7 @@ static void TestModbusControlProtocol(void) CHECK(axisStatus[10] == 4U); CHECK(axisStatus[11] == 0U); CHECK(axisStatus[8] == PLSR_RESULT_OK); + CHECK(axisStatus[37] == 1U); commandRequest[0] = 10U; commandRequest[2] = PLSR_CMD_PAUSE; @@ -2281,6 +2618,7 @@ int main(void) TestMapping(); TestDirDelaySequence(); TestDirectionBatch(); + TestHardwareCounterLeases(); TestCwCcwSequence(); TestFastRefreshControlTick(); TestDynamicFrequencyRetarget(); @@ -2288,6 +2626,8 @@ int main(void) TestPulseCounting(); TestAbPhaseAndCounting(); TestTwoAbAxesIndependent(); + TestDualAbHardwareCountersAndResume(); + TestDualAbSimultaneousFastGate(); TestAbFrequencyLimits(); TestStopAndInvalidArgs(); TestEndToEndTwoSegments(); diff --git a/PLSR/Test/test_plsr_modbus_data.c b/PLSR/Test/test_plsr_modbus_data.c index afc0998..ce0c717 100644 --- a/PLSR/Test/test_plsr_modbus_data.c +++ b/PLSR/Test/test_plsr_modbus_data.c @@ -21,6 +21,7 @@ int main(void) PLSR_DATA_SOURCE source; uint16_t words[4] = {0x5678U, 0x1234U, 0xFFFEU, 0xFFFFU}; uint16_t word; + uint8_t bit; int32_t dword; uint32_t sequence; @@ -57,12 +58,32 @@ int main(void) CHECK(ModbusDataReadLinear(39999UL, &word) == 0U); CHECK(ModbusDataReadLinear(69999UL, &word) == 0U); + CHECK(ModbusDataValidateBits(MODBUS_BIT_DEVICE_X, 9999UL, 1UL) == 1U); + CHECK(ModbusDataValidateBits(MODBUS_BIT_DEVICE_M, 9999UL, 2UL) == 0U); + CHECK(ModbusDataValidateBits(MODBUS_BIT_DEVICE_HM, 0UL, 0UL) == 0U); + CHECK(ModbusDataValidateBits((MODBUS_BIT_DEVICE)3, 0UL, 1UL) == 0U); + CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_X, 17UL, 1U) == 1U); + CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_M, 17UL, 0U) == 1U); + CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_HM, 17UL, 1U) == 1U); + CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_X, 18UL, 1U) == 1U); + CHECK(ModbusDataWriteBit(MODBUS_BIT_DEVICE_X, 17UL, 0U) == 1U); + CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_X, 17UL, &bit) == 1U); + CHECK(bit == 0U); + CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_X, 18UL, &bit) == 1U); + CHECK(bit == 1U); + CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_M, 17UL, &bit) == 1U); + CHECK(bit == 0U); + CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_HM, 17UL, &bit) == 1U); + CHECK(bit == 1U); + CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_X, 10000UL, &bit) == 0U); + CHECK(ModbusDataReadBit(MODBUS_BIT_DEVICE_X, 0UL, NULL) == 0U); + PlsrModbusDataSourceInit(&source); CHECK(source.context == NULL); CHECK(source.validateWords != NULL); CHECK(source.readWord != NULL); CHECK(source.readDword != NULL); - CHECK(source.readBit == NULL); + CHECK(source.readBit != NULL); CHECK(source.validateWords(source.context, PLSR_DEVICE_D, 1000UL, 4UL) == 1U); CHECK(source.validateWords(source.context, PLSR_DEVICE_X, 0UL, 1UL) @@ -77,6 +98,14 @@ int main(void) CHECK(source.readWord(source.context, PLSR_DEVICE_FD, 5UL, &word) == 1U); CHECK(word == 0x5AA5U); CHECK(source.readWord(source.context, PLSR_DEVICE_M, 5UL, &word) == 0U); + CHECK(source.readBit(source.context, PLSR_DEVICE_X, 17UL, &bit) == 1U); + CHECK(bit == 0U); + CHECK(source.readBit(source.context, PLSR_DEVICE_M, 17UL, &bit) == 1U); + CHECK(bit == 0U); + CHECK(source.readBit(source.context, PLSR_DEVICE_HM, 17UL, &bit) == 1U); + CHECK(bit == 1U); + CHECK(source.readBit(source.context, PLSR_DEVICE_D, 17UL, &bit) == 0U); + CHECK(source.readBit(source.context, PLSR_DEVICE_X, 10000UL, &bit) == 0U); PlsrModbusDataSourceInit(NULL); (void)printf("PASS: %u Modbus data-source checks\n", TestChecks); diff --git a/USB_DEVICE/App/usbd_cdc_if.c b/USB_DEVICE/App/usbd_cdc_if.c index 440c082..6c62997 100644 --- a/USB_DEVICE/App/usbd_cdc_if.c +++ b/USB_DEVICE/App/usbd_cdc_if.c @@ -95,6 +95,8 @@ uint8_t UserTxBufferFS[APP_TX_DATA_SIZE]; /* USER CODE BEGIN PRIVATE_VARIABLES */ +static volatile USB_CDC_RUNTIME_DIAGNOSTICS CdcRuntimeDiagnostics; + /* USER CODE END PRIVATE_VARIABLES */ /** @@ -155,6 +157,7 @@ static int8_t CDC_Init_FS(void) /* Set Application Buffers */ USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0); USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS); + CdcRuntimeDiagnostics.initialized = 1U; return (USBD_OK); /* USER CODE END 3 */ } @@ -166,6 +169,7 @@ static int8_t CDC_Init_FS(void) static int8_t CDC_DeInit_FS(void) { /* USER CODE BEGIN 4 */ + CdcRuntimeDiagnostics.initialized = 0U; return (USBD_OK); /* USER CODE END 4 */ } @@ -261,9 +265,25 @@ static int8_t CDC_Control_FS(uint8_t cmd, uint8_t* pbuf, uint16_t length) static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) { /* USER CODE BEGIN 6 */ - USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); - USBD_CDC_ReceivePacket(&hUsbDeviceFS); - return (USBD_OK); + uint8_t result; + + if ((Buf == NULL) || (Len == NULL)) + { + CdcRuntimeDiagnostics.rxRearmFailureCount++; + return (USBD_FAIL); + } + CdcRuntimeDiagnostics.rxPacketCount++; + CdcRuntimeDiagnostics.rxByteCount += *Len; + result = USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); + if (result == USBD_OK) + { + result = USBD_CDC_ReceivePacket(&hUsbDeviceFS); + } + if (result != USBD_OK) + { + CdcRuntimeDiagnostics.rxRearmFailureCount++; + } + return (int8_t)result; /* USER CODE END 6 */ } @@ -283,11 +303,33 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len) uint8_t result = USBD_OK; /* USER CODE BEGIN 7 */ USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData; - if (hcdc->TxState != 0){ + CdcRuntimeDiagnostics.txRequestCount++; + if ((Buf == NULL) || (Len == 0U) || (hcdc == NULL)) + { + CdcRuntimeDiagnostics.txFailureCount++; + return USBD_FAIL; + } + if (hcdc->TxState != 0U){ + CdcRuntimeDiagnostics.txBusyCount++; return USBD_BUSY; } - USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); - result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); + result = USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len); + if (result == USBD_OK) + { + result = USBD_CDC_TransmitPacket(&hUsbDeviceFS); + } + if (result == USBD_OK) + { + CdcRuntimeDiagnostics.txByteCount += Len; + } + else if (result == USBD_BUSY) + { + CdcRuntimeDiagnostics.txBusyCount++; + } + else + { + CdcRuntimeDiagnostics.txFailureCount++; + } /* USER CODE END 7 */ return result; } @@ -311,12 +353,37 @@ static int8_t CDC_TransmitCplt_FS(uint8_t *Buf, uint32_t *Len, uint8_t epnum) UNUSED(Buf); UNUSED(Len); UNUSED(epnum); + CdcRuntimeDiagnostics.txCompleteCount++; /* USER CODE END 13 */ return result; } /* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */ +uint8_t CDC_GetRuntimeDiagnostics(USB_CDC_RUNTIME_DIAGNOSTICS *diagnostics) +{ + if (diagnostics == NULL) + { + return 0U; + } + + /* Aligned word reads are atomic on Cortex-M4. Do not mask the motion + * interrupts merely to make unrelated USB counters mutually consistent. */ + __DMB(); + diagnostics->rxPacketCount = CdcRuntimeDiagnostics.rxPacketCount; + diagnostics->rxByteCount = CdcRuntimeDiagnostics.rxByteCount; + diagnostics->rxRearmFailureCount = + CdcRuntimeDiagnostics.rxRearmFailureCount; + diagnostics->txRequestCount = CdcRuntimeDiagnostics.txRequestCount; + diagnostics->txByteCount = CdcRuntimeDiagnostics.txByteCount; + diagnostics->txBusyCount = CdcRuntimeDiagnostics.txBusyCount; + diagnostics->txFailureCount = CdcRuntimeDiagnostics.txFailureCount; + diagnostics->txCompleteCount = CdcRuntimeDiagnostics.txCompleteCount; + diagnostics->initialized = CdcRuntimeDiagnostics.initialized; + __DMB(); + return 1U; +} + /* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */ /** diff --git a/USB_DEVICE/App/usbd_cdc_if.h b/USB_DEVICE/App/usbd_cdc_if.h index e3daa46..559e7c9 100644 --- a/USB_DEVICE/App/usbd_cdc_if.h +++ b/USB_DEVICE/App/usbd_cdc_if.h @@ -32,6 +32,8 @@ /* USER CODE BEGIN INCLUDE */ +#include + /* USER CODE END INCLUDE */ /** @addtogroup STM32_USB_OTG_DEVICE_LIBRARY @@ -66,6 +68,20 @@ /* USER CODE BEGIN EXPORTED_TYPES */ +/** Read-only USB CDC activity snapshot used by production diagnostics. */ +typedef struct +{ + uint32_t rxPacketCount; + uint32_t rxByteCount; + uint32_t rxRearmFailureCount; + uint32_t txRequestCount; + uint32_t txByteCount; + uint32_t txBusyCount; + uint32_t txFailureCount; + uint32_t txCompleteCount; + uint8_t initialized; +} USB_CDC_RUNTIME_DIAGNOSTICS; + /* USER CODE END EXPORTED_TYPES */ /** @@ -110,6 +126,8 @@ uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len); /* USER CODE BEGIN EXPORTED_FUNCTIONS */ +uint8_t CDC_GetRuntimeDiagnostics(USB_CDC_RUNTIME_DIAGNOSTICS *diagnostics); + /* USER CODE END EXPORTED_FUNCTIONS */ /** diff --git a/USB_DEVICE/Target/usbd_conf.c b/USB_DEVICE/Target/usbd_conf.c index f36ce5d..4df1573 100644 --- a/USB_DEVICE/Target/usbd_conf.c +++ b/USB_DEVICE/Target/usbd_conf.c @@ -91,7 +91,10 @@ void HAL_PCD_MspInit(PCD_HandleTypeDef* pcdHandle) __HAL_RCC_USB_OTG_FS_CLK_ENABLE(); /* Peripheral interrupt init */ - HAL_NVIC_SetPriority(OTG_FS_IRQn, 0, 0); + /* Motion timers run at priorities 1/2. USB is intentionally lower so a + burst of CDC traffic cannot delay pulse generation or the 10 kHz + control ISR; it remains above the USART/DMA communication IRQs (5). */ + HAL_NVIC_SetPriority(OTG_FS_IRQn, 4, 0); HAL_NVIC_EnableIRQ(OTG_FS_IRQn); /* USER CODE BEGIN USB_OTG_FS_MspInit 1 */