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 */