Переглянути джерело

P14/P15/P16 批次合并:硬件计数、软限位高频采样、性能遥测与真机测试体系

P14 硬件计数与四轴 100kHz 压力:
- TIM9/TIM12 硬件计数接入(Q0/Q2、Q1/Q3 共享,16 位溢出扩展 counterBlockPulses,目标脉冲 CC1IF 判定)
- 计数资源租约与软件回退(Q2/Q3 走源 UIF),counter_stress 脚本 + 规格文档

P15 软限位边界:
- 制动距离改用真实硬件输出频率,2000Hz 高频采样提前 1 脉冲进入减速
- 四轴正/负 500/2000Hz 矩阵测试(验收 ±1 脉冲),soft_limit_matrix 脚本

P16 性能遥测:
- PlsrProcess/TIM6/输出 ISR/计数 ISR 最坏执行时间遥测(DWT cycles + 分段峰值)
- performance_test 脚本 + 规格文档(全部预算 PASS)

其余:
- Modbus 命令扩展:SAVE/LOAD_CONFIG 持久化命令、错误码 5/6、RESET_ERROR
- Flash 双槽 CRC+代数持久化(SFD900~1419,SAVE/LOAD 已接命令口)
- CW/CCW 段尾停止机制(cwStopPending,物理下降沿停止)
- AB 启动预热机制(abStartupPriming,首次启动 ISR 内释放引脚)
- USB CDC 运行时诊断(Rx 重装失败计数等)
- 上电自测改由 plsr_build_config.h 宏开关控制
- 新增真机测试脚本/文档:AB_STRESS、BIT_INPUT、LONG_STRESS、PERSISTENCE_BOARD
- 交接提示词与问题总清单(2026-08-10)
master
ywh 1 місяць тому
джерело
коміт
f96f713e09
50 змінених файлів з 7515 додано та 211 видалено
  1. +5
    -0
      .gitignore
  2. +30
    -11
      Core/Src/main.c
  3. +19
    -2
      Document/PLSR_document/PLSR信捷对标追踪矩阵.md
  4. +43
    -11
      Document/PLSR_document/PLSR方案设计书_V1.0.md
  5. +27
    -6
      Document/PLSR_document/PLSR需求规格统计_V1.3.md
  6. +80
    -0
      Document/PLSR_document/交接提示词_2026-08-10.md
  7. +69
    -0
      Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md
  8. +99
    -0
      Document/PLSR_document/问题总清单_2026-08-10.md
  9. +41
    -0
      HostComputer/PLSR_BUILD_PROFILES.md
  10. +55
    -0
      HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md
  11. +17
    -0
      HostComputer/PLSR_MODBUS_BIT_INPUT_TEST.md
  12. +1
    -1
      HostComputer/PLSR_MODBUS_CONTROL_TEST.md
  13. +57
    -0
      HostComputer/PLSR_MODBUS_COUNTER_STRESS_TEST.md
  14. +85
    -0
      HostComputer/PLSR_MODBUS_LONG_STRESS_TEST.md
  15. +67
    -0
      HostComputer/PLSR_MODBUS_PERFORMANCE_TEST.md
  16. +41
    -0
      HostComputer/PLSR_MODBUS_SOFT_LIMIT_MATRIX_TEST.md
  17. +116
    -0
      HostComputer/PLSR_PERSISTENCE_BOARD_TEST.md
  18. +492
    -0
      HostComputer/plsr_modbus_ab_stress_test.py
  19. +143
    -0
      HostComputer/plsr_modbus_bit_input_test.py
  20. +10
    -2
      HostComputer/plsr_modbus_control_test.py
  21. +371
    -0
      HostComputer/plsr_modbus_counter_stress_test.py
  22. +912
    -0
      HostComputer/plsr_modbus_long_stress_test.py
  23. +664
    -0
      HostComputer/plsr_modbus_performance_test.py
  24. +207
    -0
      HostComputer/plsr_modbus_soft_limit_matrix_test.py
  25. +520
    -0
      HostComputer/plsr_persistence_board_test.py
  26. +23
    -0
      Modbus/Inc/modbus_data_store.h
  27. +54
    -0
      Modbus/Inc/modbus_rtu_slave.h
  28. +87
    -0
      Modbus/Src/modbus_data_store.c
  29. +280
    -27
      Modbus/Src/modbus_rtu_slave.c
  30. +37
    -0
      PLSR/Inc/plsr_build_config.h
  31. +15
    -0
      PLSR/Inc/plsr_core.h
  32. +16
    -0
      PLSR/Inc/plsr_hal_f407.h
  33. +18
    -1
      PLSR/Inc/plsr_modbus_control.h
  34. +3
    -2
      PLSR/Inc/plsr_modbus_data.h
  35. +41
    -0
      PLSR/Inc/plsr_persistence.h
  36. +4
    -0
      PLSR/Inc/plsr_self_test.h
  37. +1
    -0
      PLSR/Inc/plsr_types.h
  38. +273
    -21
      PLSR/Src/plsr_core.c
  39. +1095
    -58
      PLSR/Src/plsr_hal_f407.c
  40. +322
    -0
      PLSR/Src/plsr_modbus_control.c
  41. +28
    -1
      PLSR/Src/plsr_modbus_data.c
  42. +340
    -27
      PLSR/Src/plsr_persistence.c
  43. +143
    -0
      PLSR/Src/plsr_self_test.c
  44. +7
    -0
      PLSR/Test/test_plc_device.c
  45. +66
    -7
      PLSR/Test/test_plsr_core.c
  46. +366
    -26
      PLSR/Test/test_plsr_hal.c
  47. +30
    -1
      PLSR/Test/test_plsr_modbus_data.c
  48. +73
    -6
      USB_DEVICE/App/usbd_cdc_if.c
  49. +18
    -0
      USB_DEVICE/App/usbd_cdc_if.h
  50. +4
    -1
      USB_DEVICE/Target/usbd_conf.c

+ 5
- 0
.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


+ 30
- 11
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();


+ 19
- 2
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、异常复位和反复擦写板测 |

因此截至本状态日仍不得宣称“严格功能对标全部完成”。

+ 43
- 11
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 信捷兼容错误码


+ 27
- 6
Document/PLSR_document/PLSR需求规格统计_V1.3.md Переглянути файл

@@ -1,7 +1,7 @@
# PLSR 需求规格书

> 文档版本:V1.3 Rev.B(正式需求规格基线)<br>
> 更新日期:2026-08-05<br>
> 更新日期:2026-08-10<br>
> 目标平台:XDM-60T4-E<br>
> 软件平台:STM32F407 + uC/OS-II<br>
> 对标基线:信捷 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)


+ 80
- 0
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 推进压力测试与收尾。

+ 69
- 0
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. 仅存在于对话中的信息
凡"只在我们对话里出现过、没有落入任何文件"的关键信息(测试数据、踩坑经验、用户偏好、待确认问题),单独列一节,务必不要遗漏。
```

【发送内容结束】

+ 99
- 0
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 运算(等需求明确)

+ 41
- 0
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验证配置并再次完整构建通过。

+ 55
- 0
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` 只用于固件一致性检查,不能替代端子波形验收。

+ 17
- 0
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 当前也是独立映像,但尚未定义工程需要的掉电保持策略。

+ 1
- 1
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 地址,仅是本工程上位机测试使用的动态通信窗口。

运行:



+ 57
- 0
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回退。

+ 85
- 0
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保存或擦除。

+ 67
- 0
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;只有新输出继续满足预算且四轴计数/波形不回退,才可作为本批次发布回归证据。

+ 41
- 0
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脉冲范围内。后续修改保护、计数或停止路径时仍应重跑本脚本回归。

+ 116
- 0
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 模拟测试替代为上述真实掉电证据。

+ 492
- 0
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)

+ 143
- 0
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)

+ 10
- 2
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)


+ 371
- 0
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)

+ 912
- 0
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)

+ 664
- 0
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)

+ 207
- 0
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)

+ 520
- 0
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)

+ 23
- 0
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


+ 54
- 0
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);



+ 87
- 0
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;
}

+ 280
- 27
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)
{


+ 37
- 0
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

+ 15
- 0
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


+ 16
- 0
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


+ 18
- 1
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);


+ 3
- 2
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


+ 41
- 0
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


+ 4
- 0
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


+ 1
- 0
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;


+ 273
- 21
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;


+ 1095
- 58
PLSR/Src/plsr_hal_f407.c
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 322
- 0
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 <stddef.h>
#include <string.h>

#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)


+ 28
- 1
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;
}

+ 340
- 27
PLSR/Src/plsr_persistence.c Переглянути файл

@@ -1,4 +1,5 @@
#include "plsr_persistence.h"
#include "plsr_build_config.h"
#include <stddef.h>
#include <string.h>

@@ -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)


+ 143
- 0
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 <string.h>

#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

+ 7
- 0
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);


+ 66
- 7
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;


+ 366
- 26
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(&params, 0, sizeof(params));
params.frequencyHz = 100000UL;
params.targetPulses = 10;
params.outputMode = PLSR_OUTPUT_PULSE_DIR;
params.directionPoint = 4U;
params.directionPositive = 1U;

CHECK(PlsrHwStartPulse(0U, &params) == 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, &params) == 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, &params) == 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, &params) == 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(&params, 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, &params) == PLSR_RESULT_OK);
CHECK(PlsrHwStartPulse(2U, &params) == 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(&params, 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, &params) == PLSR_RESULT_OK);
params.directionPositive = 0U;
CHECK(PlsrHwStartPulse(2U, &params) == 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, &params) == PLSR_RESULT_OK);
params.directionPositive = 0U;
CHECK(PlsrHwStartPulse(2U, &params) == 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();


+ 30
- 1
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);


+ 73
- 6
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 */

/**


+ 18
- 0
USB_DEVICE/App/usbd_cdc_if.h Переглянути файл

@@ -32,6 +32,8 @@

/* USER CODE BEGIN INCLUDE */

#include <stdint.h>

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

/**


+ 4
- 1
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 */



Завантаження…
Відмінити
Зберегти