Przeglądaj źródła

feat(plsr): 接入XDM物理输入并闭环P/D末脉冲精度

- 接入XDM-60T4-E真实X输入扫描和X/M/HM独立位数据源
- 新增P19 Y到X端子回环自测,覆盖WAIT、EXT及正负硬限位
- 将ACT/EXT路径动作延迟到PULSE/DIR自然下降沿后执行
- 修复100kHz目标停止时的末脉冲截断和额外脉冲
- 优化TIM11/TIM14软件计数回退的成对UIF快照处理
- 完善长稳脚本的RTU T3.5坏CRC注入和实时进度显示
- 补充XDM硬件资料、IO映射、方案说明和完整测试报告

Tests:
- Host: 8 suites, 4932 checks PASS
- IAR Modbus: 0 errors, 0 warnings
- P14/P16: Y0~Y3各严格200000脉冲,实时预算全部通过
- P12/P13/P15: PASS
- Dual AB independent/simultaneous/dynamic: PASS
- P19 WAIT/EXT/正负硬限位及电气扫描: PASS
- HSD/SFD正常持久化板测: PASS
- P18 60秒、1800秒、坏CRC、断线重连及组合压力: PASS

Remaining:
- USB CDC并发长稳暂缓
- 目标板破坏性CRC回退待测
- HM掉电保持介质和语义待确定
master
ywh 1 miesiąc temu
rodzic
commit
4261f3693f
20 zmienionych plików z 1562 dodań i 86 usunięć
  1. +1
    -0
      .gitignore
  2. +15
    -0
      Core/Src/main.c
  3. +2
    -2
      Document/PLSR_document/PLSR信捷对标追踪矩阵.md
  4. +9
    -4
      Document/PLSR_document/PLSR方案设计书_V1.0.md
  5. +125
    -55
      Document/PLSR_document/PLSR测试报告_2026-08-10.md
  6. +144
    -0
      Document/PLSR_document/XDM-60T4-E资料索引与IO映射.md
  7. BIN
      Document/PLSR_document/信捷XD_XL系列PLC硬件手册_PD01_20260410_V1.6.pdf
  8. BIN
      Document/PLSR_document/信捷XD系列PLC随机手册_S121041_20250508_V2.2.pdf
  9. +26
    -10
      HostComputer/plsr_modbus_long_stress_test.py
  10. +316
    -0
      HostComputer/plsr_modbus_physical_input_test.py
  11. +2
    -1
      PLSR/Inc/plsr_build_config.h
  12. +9
    -0
      PLSR/Inc/plsr_hal_f407.h
  13. +9
    -0
      PLSR/Inc/plsr_modbus_data.h
  14. +3
    -0
      PLSR/Inc/plsr_self_test.h
  15. +56
    -1
      PLSR/Src/plsr_core.c
  16. +421
    -13
      PLSR/Src/plsr_hal_f407.c
  17. +150
    -0
      PLSR/Src/plsr_modbus_data.c
  18. +146
    -0
      PLSR/Src/plsr_self_test.c
  19. +67
    -0
      PLSR/Test/test_plsr_hal.c
  20. +61
    -0
      PLSR/Test/test_plsr_path.c

+ 1
- 0
.gitignore Wyświetl plik

@@ -37,3 +37,4 @@ Document/PLSR_document/截图/
Document/PLSR_document/波形/
Document/PLSR_document/~$需求规格书.docx
tmp/
.plsr_audit_pages/

+ 15
- 0
Core/Src/main.c Wyświetl plik

@@ -28,6 +28,7 @@
#include "plsr_core.h"
#include "plsr_build_config.h"
#include "plsr_modbus_control.h"
#include "plsr_modbus_data.h"
#include "plsr_self_test.h"
#include "stdio.h"
/* USER CODE END Includes */
@@ -109,6 +110,11 @@ static void AppTaskStart(void *pArg)
{
Error_Handler();
}
#elif PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_PHYSICAL_INPUT
if (PlsrPhysicalInputSelfTestPrepare() != PLSR_RESULT_OK)
{
Error_Handler();
}
#endif
#endif
/* Production command/status service; never couple it to a board fixture. */
@@ -144,6 +150,11 @@ static void AppTaskStart(void *pArg)
/*
* 每1ms轮询一次,
*/
PlsrModbusInputPoll();
#if (PLSR_ENABLE_BOARD_SELF_TEST != 0U) \
&& (PLSR_BOARD_TEST_SELECT == PLSR_BOARD_TEST_PHYSICAL_INPUT)
PlsrPhysicalInputSelfTestPoll();
#endif
ModbusSlavePoll();
PlsrModbusControlPoll();
// ModbusRetainedRegistersPoll();
@@ -212,6 +223,10 @@ int main(void)
{
Error_Handler();
}
if (PlsrModbusInputInit() != PLSR_RESULT_OK)
{
Error_Handler();
}
/* USER CODE BEGIN 2 */
INT8U osError;


+ 2
- 2
Document/PLSR_document/PLSR信捷对标追踪矩阵.md Wyświetl plik

@@ -128,8 +128,8 @@
| 四轴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 | `independent`与`simultaneous`四通道真机已通过:严格100000/200000及四路200000边沿、首沿2.50us±0.05us、正反相序、末周期00;D1468分别382/392 cycles | 仅`dynamic`的50kHz调频、00边界PAUSE及RESUME仍待真机验证,整项尚未关闭 |
| X/M/HM位设备 | 三套独立10000位映像、FC02读取X、标准线圈访问M、PLSR `readBit`已实现;P19真实回环已覆盖X0~X3的WAIT、EXT及正负硬限位 | X0~X3板测关闭;其余映射点按需抽测,HM保持未关闭 |
| 双AB 100kHz | `independent`、`simultaneous`、`dynamic`三用例四通道真机已通过:严格计数、首沿约2.50us、正反相序、100k↔50k调频、00边界PAUSE/RESUME及末周期完整 | 已板测关闭 |
| 通信长稳 | USB IRQ优先级4、Modbus 0x47、D1516~D1537 USB设备端统计、P18/K4夹具和自动长稳脚本已完成 | 待30分钟及更长Modbus/USB/四轴板测 |
| 持久化 | HSD双检查点、SFD Flash A/B+CRC、保守全轴有效位及分阶段掉电/故障工具已完成 | 待VBAT断电、坏CRC、异常复位和反复擦写板测 |



+ 9
- 4
Document/PLSR_document/PLSR方案设计书_V1.0.md Wyświetl plik

@@ -382,6 +382,9 @@ PlsrResult PlsrParser_ReadLiveFrequency(

快照保存变量来源类型和编号,Wait/ACT数值、Wait信号和动态跳转值在实际求值时读取,以保留信捷
动态变量语义。跳转值0表示顺序执行,1~N表示指定段,自跳表示有意无限循环。
PULSE/DIR运行中由ACT/EXT提前结束时,控制面只冻结profile并保存待执行路径动作;HAL在下一次自然
update(端子完整下降沿)锁存硬件计数并停止PWM,再用段完成事件推进路径。不得在1 ms路径tick中
直接关闭CC1E,以免截断已经产生上升沿的末脉冲。

#### 3.3.2 模块输入输出接口说明

@@ -833,9 +836,11 @@ PLSR内核通过统一设备接口访问逻辑软元件,Modbus只负责字节
本设计书不新增PLSR全局通信地址区,也不恢复已删除的Modbus历史功能。

Modbus数据存储提供相互隔离的X、M、HM三套10000位映像。FC01/FC05/FC0F访问M,FC02只读访问X;
`PlsrModbusDataSourceInit()`把PLSR `readBit`连接到对应映像。该层只解决数据命名空间和协议一致性:
当前工程尚无可据以冻结的X端子GPIO映射,HM映像也尚未接入经板测的保持介质,因此硬限位、WAIT、
EXT的真实X输入链路和HM掉电保持不得宣称关闭。
`PlsrModbusDataSourceInit()`把PLSR `readBit`连接到对应映像。依据《实现PLSR指令.pdf》已建立
X0~X12、X14~X17、X20 的 GPIO 输入扫描映射;X13/PA14 因承担 SWDCLK,在调试构建中保留不用。
P19 初测由 Y4~Y7 回接 X0~X3;为同步观察 Y0 脉冲与四路触发,当前验证夹具改为 Y1~Y4
回接 X0~X3。真实 WAIT、EXT 和正/负硬限位链路已关闭。HM 映像尚未接入经板测的
保持介质,因此 HM 掉电保持不得宣称关闭。

#### 3.8.2 模块输入输出接口说明

@@ -1299,7 +1304,7 @@ Flash操作或通信发送,并使用DWT周期计数器测量最坏执行时间
`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保持介质仍待硬件定义。上述新批次落盘后必须重跑
但最终板测未关闭项;真实 X0~X3 GPIO 链已由 P19 回环板测关闭,HM保持介质仍待硬件定义。上述新批次落盘后必须重跑
P14/P15/P16回归,不能用修改前的通过记录替代发布回归。

### 5.2 错误输出信息


+ 125
- 55
Document/PLSR_document/PLSR测试报告_2026-08-10.md Wyświetl plik

@@ -2,8 +2,8 @@

| 项目 | 内容 |
| --- | --- |
| 报告版本 | V1.0 |
| 状态日期 | 2026-08-10 |
| 报告版本 | V1.1 |
| 状态日期 | 2026-08-11 |
| 代码基线 | `master` / HEAD `fdbbe71aeacfe2524271aa4d53792cb5831211dc` + 当前未提交工作区 |
| 目标平台 | STM32F407IG、uC/OS-II、Modbus RTU |
| 开发环境 | IAR EWARM 8.3,构建配置 `Modbus` |
@@ -13,7 +13,7 @@

## 1. 报告口径

本报告汇总截至 2026-08-10 已执行的 Host 自动测试、IAR 构建、Modbus 上位机测试、
本报告汇总截至 2026-08-11 已执行的 Host 自动测试、IAR 构建、Modbus 上位机测试、
STM32F407 真机测试及逻辑分析仪结果。状态定义如下:

- **PASS**:自动检查和该用例要求的真机/波形验收均已通过。
@@ -31,18 +31,19 @@ STM32F407 真机测试及逻辑分析仪结果。状态定义如下:

| 测试域 | 当前结论 |
| --- | --- |
| Host 自动回归 | **PASS**,8 组共 4881 项 |
| Host 自动回归 | **PASS**,8 组共 4932 项 |
| IAR 全量构建 | **PASS**,0 errors / 0 warnings |
| 四轴 PULSE/DIR、P12、P13、P15 | **PASS**;P14 作为最终发布门禁仍建议重采一次四通道波形 |
| 四轴 PULSE/DIR、P12、P13、P14/P16、P15 | **PASS**;P14四通道最终波形均严格200000沿 |
| 双 AB 100 kHz | **PASS**,independent、simultaneous、dynamic 三用例均通过自动检查和四通道波形 |
| M/WAIT 位链 | **PASS**;仅关闭 Modbus M 位到 PLSR WAIT 的生产链 |
| HSD/SFD 正常持久化 | **PASS**,含 VBAT 掉主电、运行中掉电、SFD 上电加载、运动中拒绝 Flash 保存 |
| P18 长稳 | **60 秒 Modbus 冒烟 PASS**;30 分钟 USB 并发 **DEFERRED** |
| 真实 X/EXT/硬限位、HM 保持 | **BLOCKED**,缺少物理 X→GPIO 映射和 HM 保持介质约定 |
| P18 长稳 | **PASS**:60 秒、30 分钟、坏 CRC、断线重连及组合压力均已通过;USB 并发 **DEFERRED** |
| 真实 X/EXT/硬限位 | **PASS(X0~X3)**;真实端子回环已关闭电气扫描、WAIT、EXT、正/负硬限位 |
| HM 保持 | **BLOCKED**,尚缺经确认的掉电保持介质与恢复语义 |
| A/B 槽坏 CRC 真机回退 | **PENDING**,正常固件禁用破坏性诊断;Host 已覆盖 |

因此,主体运动、Modbus 控制、双 AB、性能和正常持久化链路已有充分通过证据;
但在真实 X/HM、受控坏 CRC、30 分钟 USB 并发及最终发布回归关闭前,不得宣称
但在 HM 保持、受控坏 CRC、30 分钟 USB 并发及最终发布回归关闭前,不得宣称
“严格功能对标和全部交付验收完成”。

## 3. 测试环境与固件配置
@@ -57,12 +58,12 @@ STM32F407 真机测试及逻辑分析仪结果。状态定义如下:
| `PLSR_BOARD_TEST_DUAL_AB`(P17) | P14/P15 共用准备、双 AB、位链及持久化板测 |
| `PLSR_BOARD_TEST_LONG_STRESS`(P18) | 四轴长稳专用 K4;关闭软/硬限位,仅限安全台架 |

报告生成时工作区默认选择为 P18。P18 不得用于带机构的常规运动验收;切换测试固件后均需
报告更新完成时工作区默认选择已恢复为 P17。P18 不得用于带机构的常规运动验收;切换测试固件后均需
重新编译、烧录并硬复位。DWT 最大值测试必须在硬复位后进行。

### 3.2 当前静态基线

2026-08-10 在当前工作区重新执行:
2026-08-11 在当前工作区重新执行:

```powershell
powershell -ExecutionPolicy Bypass -File PLSR\Test\run_host_tests.ps1
@@ -79,11 +80,11 @@ Host 结果:
| plc | 202 | PASS |
| core | 214 | PASS |
| job | 307 | PASS |
| path | 227 | PASS |
| path | 251 | PASS |
| profile | 336 | PASS |
| HAL | 3494 | PASS |
| HAL | 3521 | PASS |
| Modbus data source | 68 | PASS |
| **合计** | **4881** | **PASS** |
| **合计** | **4932** | **PASS** |

IAR 结果:`Total number of errors: 0`,`Total number of warnings: 0`。

@@ -173,22 +174,21 @@ python HostComputer\plsr_modbus_counter_stress_test.py --port COM5

### 7.2 P16 性能结果

最终已记录的 PULSE/DIR 通过样本包括
2026-08-11 最终工作区烧录、硬复位后的通过样本如下

| 指标 | 实测最大值 | 预算 | 结果 |
| --- | ---: | ---: | --- |
| PlsrProcess 自身 | 53748 cycles / 319.929 µs | <168000 cycles | PASS |
| PlsrProcess 响应 | 85655 cycles / 509.851 µs | 观测项 | PASS |
| TIM6 控制 ISR | 231 cycles / 1.375 µs | <16800 cycles | PASS |
| 输出定时器 ISR | 893 cycles / 5.315 µs | <1680 cycles | PASS |
| TIM9/12 计数 ISR | 883 cycles / 5.256 µs | <1680 cycles | PASS |
| PlsrProcess 自身 | 64641 cycles / 384.768 µs | <168000 cycles | PASS |
| PlsrProcess 响应 | 115902 cycles / 689.893 µs | 观测项 | PASS |
| TIM6 控制 ISR | 233 cycles / 1.387 µs | <16800 cycles | PASS |
| 输出定时器 ISR | 1456 cycles / 8.667 µs | <1680 cycles | PASS |
| TIM9/12 计数 ISR | 431 cycles / 2.565 µs | <1680 cycles | PASS |

后续 P17 回归也得到 PlsrProcess 自身 68175 cycles、输出 ISR 1105 cycles、计数 ISR
961 cycles,均在预算内。响应墙钟偶尔超过 1 ms 时,自身 CPU 时间仍达标,差值来自高优先级
定时器中断抢占
脚本内部四轴均为 200000;用户最终逻辑分析仪报告确认 Y0~Y3 各严格 200000 个完整脉冲,
100 kHz、无小于 1 µs 窄脉冲,首尾完整,启动前和停止后均保持低电平。历史上的硬件计数少 1、
软件回退多脉冲和末沿截断问题均以该最终样本关闭

**结论:PASS(发布前回归)。** 最终 AB 边界修复修改了共享 HAL;发布前应再次烧录最终工作区,
硬复位后重跑脚本并重新统计 Q0~Q3 各 200000 个端子上升沿。
**结论:PASS。**

## 8. P15 四轴软限位矩阵

@@ -299,8 +299,8 @@ python HostComputer\plsr_modbus_bit_input_test.py --port COM5

第一次脚本曾把成功的 START `QUEUED=1` 错当作失败;修正上位机期望值后通过,固件无需为此改变。

**结论:M/WAIT 生产链 PASS。** 实际 X 输入、EXT 上升沿和正/负硬限位仍需板卡
“X 点号→GPIO/输入扫描”映射;HM 独立映像尚未定义掉电保持介质,因此这些项目为 BLOCKED。
**结论:M/WAIT 生产链 PASS。** 后续 P19 已完成 X0~X3 真实端子回环,详见第 16 节;
HM 独立映像仍未定义掉电保持介质,因此 HM 保持继续为 BLOCKED。

## 11. HSD/SFD 持久化板测

@@ -357,31 +357,22 @@ python HostComputer\plsr_persistence_board_test.py --port COM5 --phase <phase>

## 12. P18 Modbus 长稳

执行命令:

```powershell
python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 60
```

证据:
P18 使用关闭软/硬限位的 K4 长任务,只允许在脱开机构或纯逻辑分析仪台架上运行。脚本已改为
每秒原地显示已运行时间、完成比例、样本数、四轴累计和 Modbus 有效帧,便于长时间观察。

- `HostComputer/long_stress_logs/plsr_long_stress_20260810_191803.csv`
- `HostComputer/long_stress_logs/plsr_long_stress_20260810_191803.json`

结果摘要:
| 用例 | 命令摘要 | 结果与证据 |
| --- | --- | --- |
| 60 秒冒烟 | `--duration 60` | **PASS**,60 个一致性样本,最终计数 `[4542201,4537414,4532663,4527973]`;`plsr_long_stress_20260811_151740.csv/.json` |
| 30 分钟 Modbus/四轴 | `--duration 1800` | **PASS**,1800 个一致性样本,Modbus 有效帧 12038,最终计数 `[166276382,166263223,166251804,166242746]`;`plsr_long_stress_20260811_152958.csv/.json` |
| 坏 CRC 注入 | `--duration 40 --frequency-period 0 --bad-crc-period 10` | **PASS**,40 个一致性样本;`plsr_long_stress_20260811_160526.csv/.json` |
| 串口断开/重连 | 60 秒、断开 3 秒 | **PASS**,57 个有效样本,运动连续并正确清理;`plsr_long_stress_20260811_161100.csv/.json` |
| 组合压力 | 70 秒动态调频、坏 CRC、35 秒处断开 3 秒 | **PASS**,67 个有效样本;`plsr_long_stress_20260811_161218.csv/.json` |

| 项目 | 结果 |
| --- | --- |
| 一致性样本 | 60 |
| 动态频率 | 每 10 s 在 100 kHz/50 kHz 间切换 |
| Modbus 有效帧增量 | 375 |
| TX 帧增量 | 375 |
| CRC/非法功能/非法地址/非法值/丢帧/UART 错误 | 全部 0 |
| DMA 接收重启失败增量 | 0 |
| 四轴最终计数 | `[4541617, 4536532, 4531732, 4526921]` |
| 结束状态 | 四轴软件停止,任务计数=物理计数=逻辑位置 |
坏 CRC 初版注入曾因未等待 RTU T3.5 而造成正常响应超时;脚本现于坏帧前后按波特率留出静默时间,
修复后独立及组合用例均通过。四轴显示的是上电累计物理计数,且四轴 START、通信读取、动态调频
和 STOP 的时刻并不完全相同,因此累计绝对值不要求相等;单任务严格计数由 P14 验证。

**结论:60 秒 Modbus 长稳冒烟 PASS。**
**结论:不含 USB 的 P18 长稳、异常帧和断线恢复均 PASS。**

USB 线未接,系统只枚举到 FTDI Modbus `COM5`,因此以下 30 分钟 USB CDC 并发测试按用户决定暂缓:

@@ -390,7 +381,7 @@ python HostComputer\plsr_modbus_long_stress_test.py `
--port COM5 --duration 1800 --usb-port COMx --usb-rate 64000
```

状态:**DEFERRED**。60 秒测试不能替代 30 分钟及更长 USB/Modbus/四轴并发验收。
状态:**DEFERRED**。已通过的 30 分钟 Modbus/四轴长稳不能替代 USB CDC 并发验收。

## 13. 已确认修复且不得回退的问题

@@ -407,26 +398,105 @@ python HostComputer\plsr_modbus_long_stress_test.py `
11. P14 将上电累计 `physicalPulses` 当成本任务计数。
12. P16 `PlsrProcess` 超过 1 ms;HSD 检查点 CRC 热点已优化并合并。
13. SFD 擦写期间上位机一次读超时被误判为 SAVE 失败;现只重试读取。
14. P19 EXT 在1 ms路径tick同步停PWM,导致最后高电平223 µs而非250 µs;现改为冻结profile并由下一自然update/物理下降沿非阻塞停机,再推进路径。

## 14. 未关闭项与后续测试

| 优先级 | 项目 | 当前状态 | 关闭条件 |
| ---: | --- | --- | --- |
| 1 | 最终工作区 P14/P16 发布回归 | PENDING | P17 硬复位后脚本 PASS;Q0~Q3 各严格 200000 端子上升沿 |
| 1 | 最终工作区 P14/P16 发布回归 | PASS | P17 脚本和四通道波形均通过;Q0~Q3 各严格 200000 端子上升沿 |
| 2 | 30 分钟 USB CDC + Modbus + 四轴并发 | DEFERRED | 枚举第二 USB CDC COM,运行 1800 s;设备/主机 USB 统计和 Modbus 诊断无错误 |
| 3 | SFD/HSD newest 槽坏 CRC 回退 | PENDING | 专用验证固件启用破坏性诊断、双槽有效、只失效 newest,重启回退旧 generation |
| 4 | 真实 X、EXT、正负硬限位 | BLOCKED | 提供板卡 X 点到 GPIO/扫描表,完成真实输入板测 |
| 4 | 真实 X、EXT、正负硬限位 | PASS(X0~X3) | P19 真实端子回环:X0/X1 硬限位、X2 EXT、X3 WAIT 均通过;其余映射点按需补充抽测 |
| 5 | HM 掉电保持 | BLOCKED | 明确 HM 存储介质、容量、恢复语义并完成掉电测试 |
| 6 | 更长时间长稳 | PENDING | 在 30 分钟通过后按交付要求延长,并保存 CSV/JSON 与必要波形窗口 |
| 6 | 更长时间纯 Modbus/四轴长稳 | 30 分钟 PASS | 已保存 1800 秒 CSV/JSON;如交付要求高于 30 分钟,再按目标时长延长 |
| 7 | 正式发布清理 | PENDING | 关闭板测启动和调试快照,保留正式控制链;生产构建完整回归 |

## 15. 最终判定

截至本报告日期:

- P12、P13、P15、双 AB 三用例、M/WAIT、正常 HSD/SFD 持久化和 P18 60 秒冒烟均已通过。
- P14/P16 已有通过基线,但最终发布前仍应针对当前完整工作区再做一次 PULSE/DIR 四通道回归。
- USB 30 分钟并发已明确暂缓;真实 X/HM 和破坏性 CRC 回退尚未关闭。
- P12、P13、P14/P16、P15、双 AB 三用例、M/WAIT、X0~X3 真实输入和正常 HSD/SFD 持久化均已通过。
- P14/P16 当前完整工作区发布回归已通过,Q0~Q3 逻辑分析仪均严格 200000 个上升沿。
- P18 已通过 60 秒冒烟、30 分钟四轴/Modbus、坏 CRC、断线重连及组合压力测试。
- USB 30 分钟并发已明确暂缓;HM 保持和破坏性 CRC 回退尚未关闭。

项目可以进入“剩余专项验收和正式发布清理”阶段,但尚不满足“全部真机测试关闭”或
“严格对标全部完成”的声明条件。

## 16. P19 XDM-60T4-E 真实 X 输入回环板测(2026-08-11)

本节为 2026-08-11 的新增实测结论,优先于前文关于“真实 X 缺少映射”的旧状态。

### 16.1 固件与接线

- 验证固件:`PLSR_BOARD_TEST_SELECT=PLSR_BOARD_TEST_PHYSICAL_INPUT`(P19)。
- 初次自动验收接线:Y4→X0、Y5→X1、Y6→X2、Y7→X3。
- 为使用 MCU 侧 Y0~Y4 五通道同步采集,当前 P19 改为:Y1→X0、Y2→X1、Y3→X2、Y4→X3;Y0 保留为 Q0 脉冲。
- 当前输出公共端 COM0~COM4 接 0 V;输入公共端接 0 V。
- P19 通过 D1540~D1546 的心跳租约驱动 Y1~Y4,租约超时自动关闭全部验证输出。
- X 端扫描采用板卡实际电平语义:空闲为 0,端子有效为 1。

### 16.2 自动测试

执行命令:

```powershell
python HostComputer\plsr_modbus_physical_input_test.py --port COM5 --case all
```

| 用例 | 实测结果 | 判定 |
| --- | --- | --- |
| 电气扫描 | 0、1、2、4、8、3、C、F、0 九种 Y1~Y4 输出组合均与 X0~X3 raw/logical mask 完全一致 | PASS |
| WAIT | Y0严格500个完整脉冲后进入WAIT;Y4→X3释放且不再输出 | PASS(脚本+波形) |
| EXT | Y3→X2触发后1.96 ms停止,514沿;修复后最后250 µs高电平完整 | PASS(脚本+波形) |
| 正硬限位 | Y1→X0后3.79 ms进入减速、49.21 ms停止,566沿,error=6、stop_reason=5 | PASS(脚本+波形) |
| 负硬限位 | Y2→X1后3.79 ms进入减速、49.20 ms停止,568沿,error=7、stop_reason=6 | PASS(脚本+波形) |

Y1~Y4 改版后的五项独立脚本及Y0~Y4同步波形均通过。脚本结束时验证输出自动关闭,轴状态回到IDLE。

2026-08-11 最终自动组合回归再次通过:电气 mask 全部一致,WAIT=500 脉冲,EXT=516 脉冲,
正限位=564 脉冲,负限位=-566 脉冲。自动用例与上述独立波形用例的触发脉冲数存在少量通信时序差,
不改变 EXT 提前切段、硬限位受控停止、错误码和完整末脉冲的验收结论。

### 16.3 波形结论

- WAIT:Y0上升/下降沿各500,主频2000 Hz、周期500.00 µs;末脉冲完整,WAIT及X3释放后全低。
- EXT:初测发现末高电平223 µs截断;边界停机修复后复测通过,触发到停止约1.96 ms、总计514沿、无窄脉冲或残余。
- 正硬限位:触发后约3.79 ms进入50 ms受控减速,47个减速脉冲,49.21 ms停止;末脉冲2083.4 µs完整。
- 负硬限位:与正向对称,47个减速脉冲,49.20 ms停止;末脉冲2083.4 µs完整。
- 正/负限位减速周期由约520.8 µs单调增至3472.2 µs;停止后至少1 ms全低,所有用例均无小于1 µs窄脉冲。

### 16.4 映射范围与限制

依据《实现PLSR指令.pdf》建立 X0~X12、X14~X17、X20 的 MCU GPIO 扫描映射;X13/PA14
是 SWDCLK,调试构建中不扫描。当前真机回环覆盖 X0~X3,其他已映射端子尚未逐点抽测;X21~X43
在现有资料中未取得可靠 MCU 管脚表,读取时返回数据访问错误,不做猜测映射。

Y→X 回环覆盖了 MCU输出、开集电极端子、公共端、实体输入端子、GPIO扫描和 PLSR `readBit` 的完整
电气链路,但不等同于机械限位开关、现场布线失效和安全回路认证。

## 17. 2026-08-11 最终执行清单

除注明“波形沿用已验收证据”的项目外,下表均在最终工作区重新构建相应固件后执行;切换固件时
均重新编译、烧录并硬复位。报告更新后默认固件已恢复为 P17。

| 固件 | 测试项与命令 | 最终结果 |
| --- | --- | --- |
| Host | `powershell -ExecutionPolicy Bypass -File PLSR\Test\run_host_tests.ps1` | **PASS**,8 组 4932 项 |
| IAR | `IarBuild.exe EWARM\Modbus.ewp -build Modbus -log warnings` | **PASS**,0 errors / 0 warnings |
| 无需上板 | `python HostComputer\modbus_timing_tester.py --self-test` | **PASS**,9600/19200/115200 bit/s 的 T1.2/T1.5/T3.5 自测通过 |
| P17 | `python HostComputer\plsr_modbus_counter_stress_test.py --port COM5` | **PASS**,四轴内部与端子均严格 200000;P16/ISR 均在预算内 |
| P17 | `python HostComputer\plsr_modbus_soft_limit_matrix_test.py --port COM5` | **PASS**,四轴正/负、500/2000 Hz 均在 ±1 脉冲窗口内 |
| P12 | `python HostComputer\plsr_modbus_frequency_test.py --port COM5` | **PASS**,1000→4000→500→默认1000→钳位/保持5000→2000;波形沿用已验收证据 |
| P13 | `python HostComputer\plsr_modbus_control_test.py --port COM5` | **PASS**,COMMIT 指纹、START、PAUSE 幂等、RESUME、STOP_DECEL;最终自动样本 task/physical=3530 |
| P17 | `python HostComputer\plsr_modbus_bit_input_test.py --port COM5` | **PASS**,FC05/FC01→M123→WAIT,X/M 空间隔离 |
| P17 | `plsr_modbus_ab_stress_test.py --case independent` | **PASS**,100000/200000 周期;gate=366 cycles |
| P17 | `plsr_modbus_ab_stress_test.py --case simultaneous` | **PASS**,两组各200000周期;gate=377 cycles |
| P17 | `plsr_modbus_ab_stress_test.py --case dynamic` | **PASS**,两组各600000周期,100k→50k→100k、PAUSE/RESUME;gate=365 cycles |
| P17 | 持久化 diagnostics、hsd-prepare/verify、busy-prepare/verify、sfd-save/verify、motion-save-busy | **PASS**;最终只读诊断 HSD/SFD valid 均为 `0x03`,generation=1105/2 |
| P18 | 60 秒、1800 秒、坏 CRC、断线重连和组合长稳 | **PASS**,证据文件见第 12 节 |
| P19 | `python HostComputer\plsr_modbus_physical_input_test.py --port COM5 --case all` | **PASS**,电气扫描、WAIT、EXT、正/负硬限位均通过;波形沿用逐项验收证据 |

仍未执行或未关闭:USB CDC + Modbus + 四轴 30 分钟并发(用户暂缓)、目标板破坏 newest 槽后的
CRC 回退,以及 HM 掉电保持。三项均不得写成已通过。

+ 144
- 0
Document/PLSR_document/XDM-60T4-E资料索引与IO映射.md Wyświetl plik

@@ -0,0 +1,144 @@
# XDM-60T4-E 资料索引与 I/O 映射

资料核对日期:2026-08-11
设备型号:信捷 XDM-60T4-E
项目 MCU:STM32F407IG,外部晶振 12 MHz

## 1. 官网资料

以下资料均来自无锡信捷电气股份有限公司官网或其 `cdn.xinje.com` 下载站点。

| 资料 | 本地文件 | 版本/日期 | 用途 |
|---|---|---|---|
| XD/XL 系列可编程序控制器用户手册【硬件篇】 | `信捷XD_XL系列PLC硬件手册_PD01_20260410_V1.6.pdf` | PD01,V1.6,2026-04-10 | 型号、端子排列、输入/输出规格和接线 |
| XD 系列可编程控制器随机手册 | `信捷XD系列PLC随机手册_S121041_20250508_V2.2.pdf` | S121041,V2.2,2025-05-08 | 安装、端子与 NPN/PNP 接线速查 |
| XD/XL 系列可编程控制器用户手册【定位控制篇】 | `信捷XD_XL系列PLC定位控制手册_PD02_20260510_V1.3.pdf` | PD02,V1.3,2026-05-10 | PLSR、限位端子、脉冲输出和运动控制;仓库原有文件 |
| 项目原始硬件附件 | `实现PLSR指令.pdf` | 项目附件 | XDM-60T4-E 的 STM32F407IG 引脚映射 |

官网页面:

- [XDM 系列常规型运动控制器](https://www.xinje.com/web/productInfo/index?indexGroup=0&seriesId=81)
- [XDM 系列规格参数](https://www.xinje.com/web/specs/index?indexGroup=0&seriesId=81)
- [硬件篇 PDF](https://cdn.xinje.com/XD%E3%80%81XL%E7%B3%BB%E5%88%97%E5%8F%AF%E7%BC%96%E7%A8%8B%E5%BA%8F%E6%8E%A7%E5%88%B6%E5%99%A8%E7%94%A8%E6%88%B7%E6%89%8B%E5%86%8C%EF%BC%88%E7%A1%AC%E4%BB%B6%E7%AF%87%EF%BC%89%EF%BC%88PD%2001%2020260410%201.6%EF%BC%89.pdf)
- [XD 系列随机手册 PDF](https://cdn.xinje.com/XD%E7%B3%BB%E5%88%97%E5%8F%AF%E7%BC%96%E7%A8%8B%E6%8E%A7%E5%88%B6%E5%99%A8%E9%9A%8F%E6%9C%BA%E6%89%8B%E5%86%8C%EF%BC%88S121041%202.2%EF%BC%89-2025.5.8.pdf)
- [定位控制篇 PDF](https://cdn.xinje.com/XD%E3%80%81XL%E7%B3%BB%E5%88%97%E5%8F%AF%E7%BC%96%E7%A8%8B%E6%8E%A7%E5%88%B6%E5%99%A8%E7%94%A8%E6%88%B7%E6%89%8B%E5%86%8C%E3%80%90%E5%AE%9A%E4%BD%8D%E6%8E%A7%E5%88%B6%E7%AF%87%E3%80%91.pdf)

文件校验值:

| 文件 | 字节数 | SHA-256 |
|---|---:|---|
| 硬件篇 V1.6 | 15,867,975 | `A048625B9FBF2726EFDF4FF2D50BFD849A5635D9EAAC3098AC19BB740FCC5226` |
| XD 系列随机手册 V2.2 | 1,806,056 | `7FA6F69BC62314584CDEA0B46EA3BDBCB7FC93A68B3B9EDB36E116E59321DE36` |
| 定位控制篇 V1.3 | 17,637,955 | `FACDE5482825F5D7D6B299EE65D7C70121A7F8ACB9B8C763CCBE60CE5F2520D9` |
| 实现 PLSR 指令附件 | 275,500 | `4E10C588340910D053BAB739AF46D23E20804B149506BEA9DA257C7E2828A45F` |

## 2. XDM-60T4-E 关键规格

依据官网规格页和硬件篇:

- 60 点 I/O:36 点 DC24V 输入、24 点晶体管输出。
- 型号没有 `P` 前缀,因此是 NPN 输入;`T4` 表示晶体管输出和 4 路高速脉冲输出。
- `E` 表示 PLC 本体 AC220V 供电;输入信号仍为 DC24V。
- 高速脉冲输出点为 Y0~Y3,OC 输出,标称最大频率 100 kHz。
- 普通 NPN 输入由无电压接点或 NPN 开集电极晶体管驱动;输入电流约 7 mA/DC24V,可靠 ON 电流不低于 4.5 mA。
- 普通输入经过光耦和数字滤波,手册给出的内部响应滞后约 6 ms。因此用于 WAIT、EXT、硬限位的自发自收信号应保持至少 20 ms,不应使用 100 kHz 脉冲直接驱动普通 X 输入。

重点页码(PDF 文件页码/手册印刷页码):

- 硬件篇 PDF 第 25 页/印刷第 13 页:XDM-60T4-E 型号、36 入/24 出、NPN 输入。
- 硬件篇 PDF 第 62 页/印刷第 50 页:XDM-60T4/60T4L 的 36 入/24 出端子排列。
- 硬件篇 PDF 第 91~93 页/印刷第 79~81 页:AC 电源型 XD 的 NPN 输入回路与接线。
- 硬件篇 PDF 第 105~106 页/印刷第 93~94 页:晶体管输出及高速脉冲输出规格。
- 定位控制篇:PLSR 正/负极限端子和外部信号的功能语义。

## 3. 项目 I/Q 与信捷 X/Y 的命名关系

《实现PLSR指令.pdf》使用 `I` 表示实际输入点、`Q` 表示实际输出点;信捷手册使用 `X` 和 `Y`。本项目按下列等价关系使用:

- `I0` 等价于信捷端子/软元件 `X0`。
- `Q0` 等价于信捷端子/软元件 `Y0`。
- 地址按八进制样式编号,因此 `I10/Q10` 紧接 `I7/Q7`,不是十进制第 10 点。

## 4. XDM-60T4-E 输入与 MCU 引脚

来源:《实现PLSR指令.pdf》第 3 页。

| 信捷 X | 项目 I | STM32F407IG |
|---|---|---|
| X0 | I0 | PH10 |
| X1 | I1 | PH11 |
| X2 | I2 | PG8 |
| X3 | I3 | PB4 |
| X4 | I4 | PB5 |
| X5 | I5 | PG12 |
| X6 | I6 | PB6 |
| X7 | I7 | PB7 |
| X10 | I10 | PF10 |
| X11 | I11 | PA15 |
| X12 | I12 | PB3 |
| X13 | I13 | PA14 |
| X14 | I14 | PI4 |
| X15 | I15 | PI5 |
| X16 | I16 | PI6 |
| X17 | I17 | PI7 |
| X20 | I20 | PE2 |

附件没有给出 X21~X43 的 MCU 引脚,不能自行推定;若后续需要这些点,必须补充原理图或板卡网表。

## 5. XDM-60T4-E 输出与 MCU 引脚

来源:《实现PLSR指令.pdf》第 3~4 页。

| 信捷 Y | 项目 Q | STM32F407IG |
|---|---|---|
| Y0 | Q0 | PF6 |
| Y1 | Q1 | PF8 |
| Y2 | Q2 | PF7 |
| Y3 | Q3 | PF9 |
| Y4 | Q4 | PI8 |
| Y5 | Q5 | PE6 |
| Y6 | Q6 | PE5 |
| Y7 | Q7 | PE4 |
| Y10 | Q10 | PG7 |
| Y11 | Q11 | PG6 |
| Y12 | Q12 | PH9 |
| Y13 | Q13 | PH8 |
| Y14 | Q14 | PH7 |
| Y15 | Q15 | PH6 |
| Y16 | Q16 | PF11 |
| Y17 | Q17 | PB0 |
| Y20 | Q20 | PH5 |

附件没有给出 Y21~Y27 的 MCU 引脚,不能自行推定。

通讯引脚:USART1_TX=PA9,USART1_RX=PA10。

## 6. Y→X 自发自收接线结论

XDM-60T4-E 的普通晶体管输出为 NPN 下拉输出,普通输入也是 NPN 输入,所以可以用空闲 Y 点驱动 X 点做自动回环测试。

推荐回环:

| 测试用途 | 输出 | 输入 | MCU 输出 | MCU 输入 |
|---|---|---|---|---|
| 正向硬限位 | Y1/Q1 | X0/I0 | PF8 | PH10 |
| 负向硬限位 | Y2/Q2 | X1/I1 | PF7 | PH11 |
| EXT | Y3/Q3 | X2/I2 | PF9 | PG8 |
| WAIT | Y4/Q4 | X3/I3 | PI8 | PB4 |

接线原则:

1. P19 只运行轴0的Y0脉冲;Y1~Y4由验证租约独占,便于逻辑分析仪在MCU侧同步采Y0~Y4。该接法不得用于P14/P17/P18或正式多轴固件。
2. 端子线连接 `Y1→X0`、`Y2→X1`、`Y3→X2`、`Y4→X3`。
3. 按 XDM-60T4-E 端子分组,Y1、Y2、Y3、Y4 分别使用输出 COM1、COM2、COM3、COM4;四个输出公共端必须接0V,并与输入COM/0V使用同一参考。接线前断电,并以机身端子标签和硬件篇端子图再次核对。
4. 不要把 24V 直接接入 MCU 引脚;所有接线只在 PLC 的 X/Y/COM/24V/0V 端子侧完成。
5. 测试固件应先保持所有 Y 为 OFF,再逐点输出至少 20 ms 的 ON/OFF 电平,并确认 X LED 与软件位同步;验证极性后才运行硬限位、WAIT、EXT 自动用例。
6. 回环测试只能验证端子电气链、GPIO 扫描和 PLSR 逻辑,不能替代机械限位开关、断线检测、外部急停或安全回路验收。

## 7. 对当前代码的直接约束

- 真实 X 扫描应读取 PH10、PH11、PG8、PB4 等附件明确给出的输入 GPIO,并发布到独立 X 位映像;不得把 X 与 Modbus M 线圈别名。
- GPIO 输入电平需要按照板级 NPN/光耦电路确认有效极性,不能仅依据 STM32 引脚电平名称猜测。
- 输入去抖/滤波应尊重板上约 6 ms 的硬件响应;WAIT/EXT/硬限位测试信号保持时间建议不低于 20 ms。
- EXT 是边沿语义时,测试程序必须先确认 X 为 OFF,再产生一次明确的 OFF→ON;不能在上电时让测试 Y 默认为 ON。
- X21~X43 和 Y21~Y27 的 MCU 映射未由项目附件提供,当前实现不得虚构这些映射。

BIN
Document/PLSR_document/信捷XD_XL系列PLC硬件手册_PD01_20260410_V1.6.pdf Wyświetl plik


BIN
Document/PLSR_document/信捷XD系列PLC随机手册_S121041_20250508_V2.2.pdf Wyświetl plik


+ 26
- 10
HostComputer/plsr_modbus_long_stress_test.py Wyświetl plik

@@ -298,12 +298,18 @@ 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,))
t35_seconds = 0.00175 if baud > 19_200 else (3.5 * 11.0 / baud)
# This raw write bypasses RtuClient's normal transaction pacing. Leave a
# complete inter-frame gap before it so it cannot be assembled with the
# preceding diagnostic response.
time.sleep(t35_seconds + 0.005)
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)
# A CRC failure intentionally has no response. Allow both T3.5 frame
# finalization and several 1 ms application polls before issuing the next
# valid request, even under four-axis 100 kHz interrupt pressure.
time.sleep(max(t35_seconds + 0.020, 0.050))


def open_modbus(args: argparse.Namespace) -> tuple[serial.Serial, RtuClient]:
@@ -563,6 +569,7 @@ def main() -> int:
failure: BaseException | None = None
last_status: list[dict[str, int]] = []
axes_stopped = False
sample_index = 0

with csv_path.open("w", newline="", encoding="utf-8-sig") as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=CSV_FIELDS)
@@ -627,7 +634,6 @@ def main() -> int:
)
disconnected = False
communication_errors = 0
sample_index = 0
dynamic_frequency = args.frequency
previous_physical = physical_baseline[:]

@@ -773,12 +779,18 @@ def main() -> int:
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']}"
)
physical_counts = ",".join(
str(item["physical_pulses"]) for item in statuses
)
progress = min(elapsed / args.duration * 100.0, 100.0)
print(
f"\r已运行 {elapsed:7.1f}/{args.duration:.0f}s "
f"({progress:5.1f}%);样本={sample_index};"
f"四轴累计=[{physical_counts}];"
f"Modbus有效帧={stats['valid_frames']}",
end="",
flush=True,
)
next_sample = max(
next_sample + args.status_period, time.monotonic()
)
@@ -803,6 +815,8 @@ def main() -> int:
uart, client = reopen_modbus(args)
next_sample = time.monotonic()

if sample_index > 0:
print()
assert client is not None
sequence = stop_all_axes(client, sequence)
axes_stopped = True
@@ -874,6 +888,8 @@ def main() -> int:
summary["final_status"] = last_status
summary["result"] = "PASS"
except (RuntimeError, serial.SerialException, OSError, KeyboardInterrupt) as error:
if sample_index > 0:
print()
failure = error
summary["failure"] = str(error)
if client is not None and not axes_stopped:


+ 316
- 0
HostComputer/plsr_modbus_physical_input_test.py Wyświetl plik

@@ -0,0 +1,316 @@
#!/usr/bin/env python3
"""P19 XDM-60T4-E Y1..Y4 -> X0..X3 physical loopback tests."""

from __future__ import annotations

import argparse
import struct
import time

import serial

from plsr_modbus_control_test import (
CALL_COMMIT,
CALL_START,
CMD_STOP_DECEL,
CONTROL_BASE,
CONTROL_WINDOW_WORDS,
RESULT_OK,
RESULT_QUEUED,
S0_BASE,
S1_BASE,
check_result,
read_axis_status,
send_call,
send_command,
)
from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words


LOOPBACK_BASE = 1540
LOOPBACK_SIGNATURE = 0x5019
STATE_IDLE = 1
STATE_ACCEL = 2
STATE_RUN = 3
STATE_WAIT = 5
STATE_COMPLETED = 7
STATE_STOPPED = 8
CMD_RESET_ERROR = 10
ERROR_LIMIT_POSITIVE = 6
ERROR_LIMIT_NEGATIVE = 7
STOP_LIMIT_POSITIVE = 5
STOP_LIMIT_NEGATIVE = 6


def read_bits(client: RtuClient, address: int, count: int) -> list[int]:
byte_count = (count + 7) // 8
pdu = bytes((0x02,)) + struct.pack(">HH", address, count)
response = client.exchange(pdu, 5 + byte_count)
if response[1] != 0x02 or response[2] != byte_count:
raise RuntimeError(f"FC02 响应格式错误:{response.hex(' ')}")
return [
(response[3 + index // 8] >> (index % 8)) & 1
for index in range(count)
]


class LoopbackLease:
def __init__(self, client: RtuClient) -> None:
self.client = client
self.sequence = int(time.time() * 1000) & 0xFFFF
self.mask = 0
self.last_write = 0.0

def set(self, mask: int) -> None:
self.mask = mask & 0x0F
self.sequence = (self.sequence + 1) & 0xFFFF
self.client.write_multiple(LOOPBACK_BASE, [self.mask, self.sequence])
self.last_write = time.monotonic()

def heartbeat(self) -> None:
if time.monotonic() - self.last_write >= 0.20:
self.set(self.mask)

def diagnostics(self) -> dict[str, int]:
words = self.client.read_holding(LOOPBACK_BASE + 2, 5)
return {
"applied": words[0],
"raw": words[1],
"logical": words[2],
"signature": words[3],
"flags": words[4],
}


def wait_until(
lease: LoopbackLease,
predicate,
timeout: float,
label: str,
) -> dict[str, int]:
deadline = time.monotonic() + timeout
latest: dict[str, int] | None = None
while time.monotonic() < deadline:
lease.heartbeat()
latest = read_axis_status(lease.client)
if predicate(latest):
return latest
raise RuntimeError(f"{label}超时,最后状态={latest}")


def prepare_job(
client: RtuClient,
pulses: int,
wait_condition: int = 0,
wait_source: int = 0,
wait_point: int = 0,
) -> None:
words = [0] * 20
words[0:2] = signed_dword_words(1)
words[10:12] = signed_dword_words(2000)
words[12:14] = signed_dword_words(pulses)
words[14] = ((wait_condition & 0xFF) << 8) | (wait_source & 0xFF)
words[15:17] = signed_dword_words(wait_point)
words[17] = 0
words[18:20] = signed_dword_words(0)
client.write_multiple(S0_BASE, words)
client.write_multiple(S1_BASE, [0, 0, 0, 0])


def start_job(client: RtuClient, sequence: int) -> int:
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_QUEUED, "START")
return sequence + 1


def reset_limit_error(lease: LoopbackLease, sequence: int) -> None:
lease.set(0)
time.sleep(0.050)
response = send_command(lease.client, sequence, CMD_RESET_ERROR)
check_result(response, RESULT_QUEUED, "RESET_ERROR")
status = wait_until(
lease,
lambda value: value["state"] == STATE_IDLE
and value["last_sequence"] == sequence,
2.0,
"RESET_ERROR",
)
if status["error"] != 0:
raise RuntimeError(f"RESET_ERROR 后错误未清除:{status}")


def test_electrical(lease: LoopbackLease) -> None:
patterns = (0x0, 0x1, 0x2, 0x4, 0x8, 0x3, 0xC, 0xF, 0x0)
for pattern in patterns:
lease.set(pattern)
time.sleep(0.050)
diagnostics = lease.diagnostics()
x_bits = read_bits(lease.client, 0, 4)
observed = sum(bit << index for index, bit in enumerate(x_bits))
if diagnostics["signature"] != LOOPBACK_SIGNATURE:
raise RuntimeError(f"不是 P19 固件:{diagnostics}")
if (diagnostics["flags"] & 0x0013) != 0x0013:
raise RuntimeError(f"P19 输入/租约未就绪:{diagnostics}")
if diagnostics["applied"] != pattern or observed != pattern:
raise RuntimeError(
f"回环不一致:写Y掩码=0x{pattern:X}, FC02 X=0x{observed:X}, "
f"诊断={diagnostics}。请检查对应 Y→X 和 COM 接线。"
)
print(
f"Y1~Y4=0x{pattern:X} -> X0~X3=0x{observed:X} PASS "
f"(MCU raw=0x{diagnostics['raw'] & 0xF:X})"
)
print("电气回环 PASS:Y1→X0、Y2→X1、Y3→X2、Y4→X3")


def test_wait(lease: LoopbackLease, sequence: int) -> None:
lease.set(0)
prepare_job(lease.client, 500, wait_condition=2, wait_source=4, wait_point=3)
start_sequence = start_job(lease.client, sequence)
waiting = wait_until(
lease,
lambda value: value["state"] == STATE_WAIT,
3.0,
"进入 X3 WAIT",
)
if waiting["task_pulses"] != 500:
raise RuntimeError(f"WAIT 前脉冲数不是500:{waiting}")
lease.set(0x8)
completed = wait_until(
lease,
lambda value: value["state"] == STATE_COMPLETED,
2.0,
"X3 释放 WAIT",
)
if completed["task_pulses"] != 500 or completed["error"] != 0:
raise RuntimeError(f"WAIT 完成状态异常:{completed}")
lease.set(0)
print(f"X3/WAIT PASS:500脉冲后等待,Y4→X3释放,序号={start_sequence}")


def test_ext(lease: LoopbackLease, sequence: int) -> None:
lease.set(0)
prepare_job(lease.client, 50000, wait_condition=4, wait_source=4, wait_point=2)
start_job(lease.client, sequence)
running = wait_until(
lease,
lambda value: value["state"] in {STATE_ACCEL, STATE_RUN}
and abs(value["task_pulses"]) >= 200,
3.0,
"EXT 触发前运行",
)
lease.set(0x4)
completed = wait_until(
lease,
lambda value: value["state"] == STATE_COMPLETED,
2.0,
"X2 EXT 上升沿切段",
)
lease.set(0)
if not (running["task_pulses"] <= completed["task_pulses"] < 50000):
raise RuntimeError(f"EXT 未提前结束任务:{completed}")
if completed["error"] != 0:
raise RuntimeError(f"EXT 完成存在错误:{completed}")
print(f"X2/EXT PASS:Y3→X2 上升沿在 {completed['task_pulses']} 脉冲提前结束")


def test_limit(
lease: LoopbackLease,
sequence: int,
positive: bool,
) -> None:
lease.set(0)
pulses = 50000 if positive else -50000
bit_mask = 0x1 if positive else 0x2
expected_error = ERROR_LIMIT_POSITIVE if positive else ERROR_LIMIT_NEGATIVE
expected_reason = STOP_LIMIT_POSITIVE if positive else STOP_LIMIT_NEGATIVE
flag_mask = 1 << (9 if positive else 10)
label = "正硬限位 X0" if positive else "负硬限位 X1"
prepare_job(lease.client, pulses)
start_job(lease.client, sequence)
wait_until(
lease,
lambda value: value["state"] in {STATE_ACCEL, STATE_RUN}
and abs(value["task_pulses"]) >= 200,
3.0,
f"{label}触发前运行",
)
lease.set(bit_mask)
stopped = wait_until(
lease,
lambda value: value["state"] == STATE_STOPPED,
3.0,
f"{label}停止",
)
if (
stopped["error"] != expected_error
or stopped["stop_reason"] != expected_reason
or (stopped["flags"] & flag_mask) == 0
or abs(stopped["task_pulses"]) >= 50000
):
raise RuntimeError(f"{label}状态不符合预期:{stopped}")
reset_limit_error(lease, sequence + 100)
print(
f"{label} PASS:提前停止于 {stopped['task_pulses']} 脉冲,"
f"error={expected_error}, stop_reason={expected_reason}"
)


def main() -> int:
parser = argparse.ArgumentParser(
description="P19 XDM-60T4-E Y1~Y4→X0~X3真实输入、WAIT/EXT/硬限位测试"
)
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(
"--case",
choices=("electrical", "wait", "ext", "positive-limit", "negative-limit", "all"),
default="electrical",
)
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"控制窗口不匹配:{header}")
lease = LoopbackLease(client)
sequence = int(time.time()) & 0x7FFFFFFF
try:
cases = (
("electrical", lambda: test_electrical(lease)),
("wait", lambda: test_wait(lease, sequence + 10)),
("ext", lambda: test_ext(lease, sequence + 20)),
("positive-limit", lambda: test_limit(lease, sequence + 30, True)),
("negative-limit", lambda: test_limit(lease, sequence + 40, False)),
)
for case_name, operation in cases:
if args.case in {case_name, "all"}:
print(f"\n开始 P19 用例:{case_name}")
operation()
print("\n全部所选 P19 自动检查 PASS")
finally:
try:
lease.set(0)
except (RuntimeError, serial.SerialException):
pass
return 0


if __name__ == "__main__":
try:
raise SystemExit(main())
except (RuntimeError, serial.SerialException) as error:
print(f"测试失败:{error}")
raise SystemExit(1)

+ 2
- 1
PLSR/Inc/plsr_build_config.h Wyświetl plik

@@ -29,9 +29,10 @@
#define PLSR_BOARD_TEST_HW_COUNTER (14U)
#define PLSR_BOARD_TEST_DUAL_AB (17U)
#define PLSR_BOARD_TEST_LONG_STRESS (18U)
#define PLSR_BOARD_TEST_PHYSICAL_INPUT (19U)

#ifndef PLSR_BOARD_TEST_SELECT
#define PLSR_BOARD_TEST_SELECT PLSR_BOARD_TEST_LONG_STRESS
#define PLSR_BOARD_TEST_SELECT PLSR_BOARD_TEST_DUAL_AB
#endif

#endif

+ 9
- 0
PLSR/Inc/plsr_hal_f407.h Wyświetl plik

@@ -37,6 +37,9 @@ PLSR_RESULT PlsrHwStartPulse(uint8_t axis, const PLSR_HW_START_PARAMS *params);
PLSR_RESULT PlsrHwSetFrequency(uint8_t axis, uint32_t frequencyHz);
/* Re-arm a PAUSE-stopped timer without clearing its emitted/target counters. */
PLSR_RESULT PlsrHwResumePulse(uint8_t axis);
/* Complete the in-flight PULSE/DIR period, then stop and post
* PLSR_EVENT_SEGMENT_COMPLETE. Used by asynchronous ACT/EXT path exits. */
PLSR_RESULT PlsrHwStopPulseAtBoundary(uint8_t axis);
PLSR_RESULT PlsrHwStopPulse(uint8_t axis);

/* Batch related multi-axis DIR changes into one short GPIO commit window. */
@@ -71,6 +74,11 @@ void PlsrHwOnTimerUpdate(uint8_t axis);
/* 输出点(Y0~Y20)引脚映射表:DIR 点解析用。 */
uint8_t PlsrHwResolveDirectionPoint(uint8_t pointNumber);

/* Validation-only slow digital output control. It is deliberately restricted
* to Y1..Y4 and is used by the P19 physical-input loopback fixture. */
PLSR_RESULT PlsrHwSetValidationOutput(uint8_t pointNumber, uint8_t terminalOn);
void PlsrHwValidationOutputsOff(void);

#ifdef PLSR_HOST_TEST
/* 模拟寄存器访问与中断触发(测试用)。 */
uint32_t PlsrHwTestGetArr(uint8_t axis);
@@ -89,6 +97,7 @@ uint8_t PlsrHwTestGetAbQuarter(uint8_t axis);
* the slave raw counter. */
void PlsrHwTestSetAbQuarterWithoutCounter(uint8_t axis, uint8_t quarter);
void PlsrHwTestTriggerUpdate(uint8_t axis);
void PlsrHwTestSetUpdatePending(uint8_t axis);
void PlsrHwTestTriggerUpdateAndCompare(uint8_t axis);
void PlsrHwTestTriggerCompare(uint8_t axis);
void PlsrHwTestAdvanceAbQuarter(uint8_t axis);


+ 9
- 0
PLSR/Inc/plsr_modbus_data.h Wyświetl plik

@@ -12,6 +12,15 @@ extern "C" {
* D and standard Modbus coils expose M. */
void PlsrModbusDataSourceInit(PLSR_DATA_SOURCE *source);

/* XDM-60T4-E physical input producer. The input image is sampled before the
* Modbus/PLSR control polls, so FC02 and motion protection consume the same
* debounced board state. */
PLSR_RESULT PlsrModbusInputInit(void);
void PlsrModbusInputPoll(void);
uint32_t PlsrModbusInputGetRawMask(void);
uint32_t PlsrModbusInputGetLogicalMask(void);
uint32_t PlsrModbusInputGetMappedMask(void);

#ifdef __cplusplus
}
#endif


+ 3
- 0
PLSR/Inc/plsr_self_test.h Wyświetl plik

@@ -42,6 +42,9 @@ PLSR_RESULT PlsrModbusControlSelfTestPrepare(void);
PLSR_RESULT PlsrHardwareCounterSelfTestPrepare(void);
/* P18: K4 four-axis 100kHz P/D long-stress setup with soft limits disabled. */
PLSR_RESULT PlsrLongStressSelfTestPrepare(void);
/* P19: Y1..Y4 -> X0..X3 physical input loopback and X-based motion tests. */
PLSR_RESULT PlsrPhysicalInputSelfTestPrepare(void);
void PlsrPhysicalInputSelfTestPoll(void);
void PlsrSelfTestControlTick100us(void);

/* P11 board-test control. Write this signed dword from IAR Watch while the


+ 56
- 1
PLSR/Src/plsr_core.c Wyświetl plik

@@ -65,6 +65,7 @@ typedef struct
uint8_t lastUserDirectionValid;
uint8_t lastUserDirectionPositive;
uint8_t runtimeSpeedClamped;
PLSR_PATH_ACTION pendingBoundaryAction;
} PLSR_AXIS;

typedef struct
@@ -868,6 +869,7 @@ PLSR_RESULT PlsrStateTransition(uint8_t axis,
axisObject->state = PLSR_STATE_ERROR;
axisObject->pendingTerminal = PLSR_STATE_UNINITIALIZED;
axisObject->immediateStopPending = 0U;
axisObject->pendingBoundaryAction = PLSR_PATH_ACTION_NONE;
PlsrStopSegmentHardware(axis, axisObject);
axisObject->backlashActive = 0U;
axisObject->backlashBypassOnce = 0U;
@@ -886,6 +888,7 @@ PLSR_RESULT PlsrStateTransition(uint8_t axis,
{
axisObject->pendingTerminal = PLSR_STATE_UNINITIALIZED;
axisObject->immediateStopPending = 0U;
axisObject->pendingBoundaryAction = PLSR_PATH_ACTION_NONE;
PlsrPathTerminate(&axisObject->path);
PlsrStopSegmentHardware(axis, axisObject);
PlsrResourceRelease(&axisObject->lease);
@@ -1247,6 +1250,7 @@ static PLSR_RESULT PlsrStartAxis(PLSR_AXIS *axisObject,
axisObject->pendingTerminal = PLSR_STATE_UNINITIALIZED;
axisObject->pauseReturnState = PLSR_STATE_UNINITIALIZED;
axisObject->immediateStopPending = 0U;
axisObject->pendingBoundaryAction = PLSR_PATH_ACTION_NONE;
axisObject->done = 0U;
axisObject->jobValid = 0U;
axisObject->taskPulses = 0;
@@ -1380,6 +1384,7 @@ static PLSR_RESULT PlsrStartCall(PLSR_AXIS *axisObject,
axisObject->pendingTerminal = PLSR_STATE_UNINITIALIZED;
axisObject->pauseReturnState = PLSR_STATE_UNINITIALIZED;
axisObject->immediateStopPending = 0U;
axisObject->pendingBoundaryAction = PLSR_PATH_ACTION_NONE;
axisObject->done = 0U;
axisObject->taskPulses = 0;
axisObject->segmentAccountedPulses = 0;
@@ -2358,6 +2363,18 @@ static void PlsrProcessNormalEvents(uint8_t axis, uint32_t events)
PLSR_TRANSITION_WAIT_COMPLETE);
}

if (((events & PLSR_EVENT_SEGMENT_COMPLETE) != 0UL)
&& (axisObject->pendingBoundaryAction != PLSR_PATH_ACTION_NONE))
{
PLSR_PATH_ACTION action = axisObject->pendingBoundaryAction;

axisObject->pendingBoundaryAction = PLSR_PATH_ACTION_NONE;
/* HAL has stopped at the physical falling/update boundary. Only now
* may the already-evaluated ACT/EXT path action advance the job. */
PlsrApplyPathAction(axis, action);
return;
}

if (((events & PLSR_EVENT_SEGMENT_COMPLETE) != 0UL)
&& ((axisObject->state == PLSR_STATE_ACCEL)
|| (axisObject->state == PLSR_STATE_RUN)
@@ -2797,6 +2814,12 @@ void PlsrProcess(void)
{
continue;
}
if (axisObject->pendingBoundaryAction != PLSR_PATH_ACTION_NONE)
{
/* ACT/EXT has already advanced the immutable path context, but
* hardware still owns the final in-flight P/D period. */
continue;
}
action = (axisObject->backlashActive != 0U)
? PLSR_PATH_ACTION_NONE
: PlsrPathTick(&axisObject->path,
@@ -2804,7 +2827,39 @@ void PlsrProcess(void)
axisObject->logicalPosition);
if (action != PLSR_PATH_ACTION_NONE)
{
PlsrApplyPathAction(axis, action);
if (((action == PLSR_PATH_ACTION_NEXT_SEGMENT)
|| (action == PLSR_PATH_ACTION_JOB_COMPLETE))
&& (axisObject->outputMode == PLSR_OUTPUT_PULSE_DIR)
&& ((axisObject->state == PLSR_STATE_ACCEL)
|| (axisObject->state == PLSR_STATE_RUN)
|| (axisObject->state == PLSR_STATE_DECEL))
&& (PlsrHwIsPulseActive(axis) != 0U))
{
PLSR_RESULT stopResult;

/* A running ACT/EXT exit is asynchronous. Freeze profile
* writes and let HAL stop at the next physical falling edge;
* the completion event applies this saved action. */
PlsrSetProfileActive(axisObject, 0U);
axisObject->profileWasAccel = 0U;
axisObject->pendingBoundaryAction = action;
stopResult = PlsrHwStopPulseAtBoundary(axis);
if (stopResult != PLSR_RESULT_OK)
{
axisObject->pendingBoundaryAction =
PLSR_PATH_ACTION_NONE;
axisObject->error = PLSR_ERROR_TIMER_FAULT;
PlsrSetStopReason(axisObject, PLSR_STOP_REASON_FAULT);
axisObject->done = 0U;
(void)PlsrStateTransition(axis,
PLSR_STATE_ERROR,
PLSR_TRANSITION_FAULT);
}
}
else
{
PlsrApplyPathAction(axis, action);
}
}

if (axisObject->job.s2.refreshCode != 2U)


+ 421
- 13
PLSR/Src/plsr_hal_f407.c Wyświetl plik

@@ -210,6 +210,8 @@ typedef struct
uint32_t abCounterBoundaryCnt;
uint8_t cwActiveAxis;
uint8_t cwStopPending;
uint8_t pulseBoundaryStopPending;
uint8_t pulseTargetStopArmed;
uint8_t counterIndex;
uint8_t hardwareCounterActive;
uint8_t hardwareCounterConfigured;
@@ -224,6 +226,7 @@ static volatile uint32_t PlsrHwMaxOutputIsrCycles;
static volatile uint32_t PlsrHwMaxCounterIsrCycles;
static volatile uint32_t PlsrHwMaxControlIsrCycles;
static volatile uint32_t PlsrHwMaxAbGateCycles;
static uint8_t PlsrHwValidationOutputMask;
#ifndef PLSR_HOST_TEST
static volatile uint8_t PlsrHwAbGateMeasurePending;
#endif
@@ -740,6 +743,12 @@ static void PlsrHwPwmBegin(uint8_t axis)
? 0UL
: 1UL);
PlsrHwTimerSetCc1e(axis, 1UL);
#ifndef PLSR_HOST_TEST
/* A previous stop may leave GPIO owning the pin at terminal-low. The
* timer is fully configured and still stopped here; hand it back to AF
* before CEN so the first transition is a deliberate full pulse. */
PlsrHwReleasePulsePin(axis);
#endif
PlsrHwTimerSetCen(axis, 1UL);
}

@@ -766,6 +775,28 @@ static uint8_t PlsrHwGetPairedAxis(uint8_t axis)
return (uint8_t)(axis + 1U);
}

static void PlsrHwStopPulseDirOutput(uint8_t axis)
{
#ifndef PLSR_HOST_TEST
uint32_t interruptState = __get_PRIMASK();

__disable_irq();
__DMB();
/* At a natural update boundary the terminal is already low. GPIO-high
* represents that same level through the board's inverting sink stage,
* so taking ownership before CC1E is cleared cannot create a short pulse. */
PlsrHwHoldPulsePinLow(axis);
#endif
PlsrHwStopPwmTimer(axis);
#ifndef PLSR_HOST_TEST
__DMB();
if (interruptState == 0UL)
{
__enable_irq();
}
#endif
}

static void PlsrHwArmAbBoundaryInterrupts(uint8_t axis)
{
uint8_t pairAxis = PlsrHwGetPairedAxis(axis);
@@ -1368,7 +1399,7 @@ static void PlsrHwStopActiveOutput(uint8_t axis,
}
else
{
PlsrHwStopPwmTimer(axis);
PlsrHwStopPulseDirOutput(axis);
}
}

@@ -1519,6 +1550,13 @@ static void PlsrHwCounterConfigure(uint8_t axis)
PlsrHwTimerSetUg(axis);
PlsrHwTimerClearUif(axis);
PlsrHwTimerClearCc1if(axis);
if (comparePulses > 1UL)
{
/* Wake one pulse before the target. The output timer can
* then stop on the target pulse's compare/falling edge rather
* than truncating the high level in this counter ISR. */
comparePulses--;
}
}
else
{
@@ -1564,6 +1602,26 @@ static void PlsrHwCounterBegin(uint8_t axis)
#endif
}

static void PlsrHwArmPulseTargetTail(uint8_t axis)
{
PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis];

if (state->pulseTargetStopArmed != 0U)
{
return;
}
state->pulseTargetStopArmed = 1U;
/* ARR/CCR are both preloaded and become active only at the target update.
* The in-flight target pulse therefore retains its old validated width;
* afterwards CNT<CCR remains terminal-low for the entire long period.
* Extending ARR alone is insufficient because the old short CCR would
* start one more physical pulse before a delayed completion ISR runs. */
PlsrHwTimerSetArr(axis, 0xFFFFUL);
PlsrHwTimerSetCcr(axis, 0xFFFFUL);
PlsrHwTimerClearUif(axis);
PlsrHwTimerSetUie(axis, 1UL);
}

static void PlsrHwCounterSuspend(uint8_t axis)
{
PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis];
@@ -1778,6 +1836,90 @@ uint8_t PlsrHwResolveDirectionPoint(uint8_t pointNumber)
return 1U;
}

PLSR_RESULT PlsrHwSetValidationOutput(uint8_t pointNumber,
uint8_t terminalOn)
{
uint8_t axis;

if ((pointNumber < 1U) || (pointNumber > 4U))
{
return PLSR_RESULT_INVALID_RESOURCE;
}
for (axis = 0U; axis < PLSR_HW_AXIS_COUNT; axis++)
{
if ((PlsrHwAxes[axis].directionPoint == pointNumber)
&& (PlsrHwAxes[axis].state != PLSR_HW_STATE_IDLE)
&& (PlsrHwAxes[axis].state != PLSR_HW_STATE_DONE))
{
return PLSR_RESULT_RESOURCE_CONFLICT;
}
if ((pointNumber < PLSR_HW_AXIS_COUNT)
&& (PlsrHwAxes[axis].state != PLSR_HW_STATE_IDLE)
&& (PlsrHwAxes[axis].state != PLSR_HW_STATE_DONE)
&& ((axis == pointNumber)
|| (((PlsrHwAxes[axis].outputMode == PLSR_OUTPUT_AB)
|| (PlsrHwAxes[axis].outputMode
== PLSR_OUTPUT_CW_CCW))
&& ((axis & 0xFEU) == (pointNumber & 0xFEU)))))
{
return PLSR_RESULT_RESOURCE_CONFLICT;
}
}
#ifdef PLSR_HOST_TEST
if (terminalOn != 0U)
{
PlsrHwValidationOutputMask |=
(uint8_t)(1U << (pointNumber - 1U));
}
else
{
PlsrHwValidationOutputMask &=
(uint8_t)(~(uint8_t)(1U << (pointNumber - 1U)));
}
#else
{
const PLSR_HW_OUTPUT_PIN *pin = &PlsrHwOutputPins[pointNumber];
GPIO_InitTypeDef gpio;
uint8_t mask = (uint8_t)(1U << (pointNumber - 1U));

/* Board output ON is MCU-low. Establish OFF before changing MODER
* so enabling the validation fixture cannot create a terminal pulse. */
if ((PlsrHwValidationOutputMask & mask) == 0U)
{
pin->port->BSRR = (uint32_t)pin->pin;
gpio.Pin = pin->pin;
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
gpio.Alternate = 0U;
HAL_GPIO_Init(pin->port, &gpio);
}
pin->port->BSRR = (terminalOn != 0U)
? ((uint32_t)pin->pin << 16U)
: (uint32_t)pin->pin;
if (terminalOn != 0U)
{
PlsrHwValidationOutputMask |= mask;
}
else
{
PlsrHwValidationOutputMask &= (uint8_t)(~mask);
}
}
#endif
return PLSR_RESULT_OK;
}

void PlsrHwValidationOutputsOff(void)
{
uint8_t point;

for (point = 1U; point <= 4U; point++)
{
(void)PlsrHwSetValidationOutput(point, 0U);
}
}

PLSR_RESULT PlsrHwInit(void)
{
uint8_t axis;
@@ -1789,6 +1931,7 @@ PLSR_RESULT PlsrHwInit(void)
PlsrHwMaxCounterIsrCycles = 0UL;
PlsrHwMaxControlIsrCycles = 0UL;
PlsrHwMaxAbGateCycles = 0UL;
PlsrHwValidationOutputMask = 0U;
#ifndef PLSR_HOST_TEST
PlsrHwAbGateMeasurePending = 0U;
#endif
@@ -1977,6 +2120,8 @@ PLSR_RESULT PlsrHwStartPulse(uint8_t axis, const PLSR_HW_START_PARAMS *params)
state->abPausePending = 0U;
state->abPauseGated = 0U;
state->abCompletionDeferred = 0U;
state->pulseBoundaryStopPending = 0U;
state->pulseTargetStopArmed = 0U;
if (params->outputMode == PLSR_OUTPUT_PULSE_DIR)
{
PlsrHwSetDirLevel(axis,
@@ -2016,6 +2161,13 @@ PLSR_RESULT PlsrHwSetFrequency(uint8_t axis, uint32_t frequencyHz)
return PLSR_RESULT_INVALID_ARGUMENT;
}
state = &PlsrHwAxes[axis];
if ((state->pulseBoundaryStopPending != 0U)
|| (state->pulseTargetStopArmed != 0U))
{
/* ACT/EXT already froze the requested waveform. Do not allow the
* 100us profile ISR to move ARR/CCR before the natural tail edge. */
return PLSR_RESULT_OK;
}
if ((state->outputMode == PLSR_OUTPUT_CW_CCW)
&& (state->cwStopPending != 0U))
{
@@ -2089,6 +2241,11 @@ PLSR_RESULT PlsrHwSetFrequency(uint8_t axis, uint32_t frequencyHz)
/* 必须先发布 RUNNING,避免启用 timer IRQ 后观察到 PWM_PENDING。 */
state->state = PLSR_HW_STATE_RUNNING;
PlsrHwBeginActiveOutput(axis, state->outputMode);
if ((state->outputMode == PLSR_OUTPUT_PULSE_DIR)
&& (state->targetPulses == 1))
{
PlsrHwArmPulseTargetTail(axis);
}
}
return PLSR_RESULT_OK;
}
@@ -2118,6 +2275,47 @@ PLSR_RESULT PlsrHwResumePulse(uint8_t axis)
return PLSR_RESULT_OK;
}

PLSR_RESULT PlsrHwStopPulseAtBoundary(uint8_t axis)
{
PLSR_HW_AXIS_STATE *state;

if (axis >= PLSR_HW_AXIS_COUNT)
{
return PLSR_RESULT_INVALID_ARGUMENT;
}
state = &PlsrHwAxes[axis];
if ((state->state != PLSR_HW_STATE_RUNNING)
|| (state->outputMode != PLSR_OUTPUT_PULSE_DIR))
{
return PLSR_RESULT_INVALID_STATE;
}
if (state->pulseBoundaryStopPending != 0U)
{
return PLSR_RESULT_OK;
}
#ifndef PLSR_HOST_TEST
{
uint32_t interruptState = __get_PRIMASK();

__disable_irq();
__DMB();
#endif
state->pulseBoundaryStopPending = 1U;
/* An update is the completed terminal-pulse boundary. Hardware-counted
* axes normally keep UIE disabled, so explicitly arm one update here. */
PlsrHwTimerClearUif(axis);
PlsrHwTimerSetUie(axis, 1UL);
#ifndef PLSR_HOST_TEST
__DMB();
if (interruptState == 0UL)
{
__enable_irq();
}
}
#endif
return PLSR_RESULT_OK;
}

PLSR_RESULT PlsrHwStopPulse(uint8_t axis)
{
PLSR_HW_AXIS_STATE *state;
@@ -2143,6 +2341,8 @@ PLSR_RESULT PlsrHwStopPulse(uint8_t axis)
state->abPausePending = 0U;
state->abPauseGated = 0U;
state->abCompletionDeferred = 0U;
state->pulseBoundaryStopPending = 0U;
state->pulseTargetStopArmed = 0U;
state->state = PLSR_HW_STATE_IDLE;
}
return PLSR_RESULT_OK;
@@ -2481,6 +2681,166 @@ static uint8_t PlsrHwGateArmedAbOutputs(uint8_t preferredAxis)

/* 输出定时器中断入口:PULSE/DIR 在 update 计数;AB 在落后相
* CC1 下降沿(四状态回到 00)计一个完整正交周期。 */
static uint8_t PlsrHwPulseDirFastPathAllowed(uint8_t axis)
{
PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis];

if ((state->state != PLSR_HW_STATE_RUNNING)
|| (state->outputMode != PLSR_OUTPUT_PULSE_DIR))
{
return 0U;
}
/* An AB target boundary may arrive while another output IRQ is active.
* Preserve the cross-pair scan in that short safety-critical window. */
if (
#ifndef PLSR_HOST_TEST
(PlsrHwAbGateMeasurePending != 0U) ||
#endif
((PlsrHwAxes[0U].state == PLSR_HW_STATE_RUNNING)
&& (PlsrHwAxes[0U].outputMode == PLSR_OUTPUT_AB)
&& (PlsrHwAxes[0U].abStopArmed != 0U))
|| ((PlsrHwAxes[2U].state == PLSR_HW_STATE_RUNNING)
&& (PlsrHwAxes[2U].outputMode == PLSR_OUTPUT_AB)
&& (PlsrHwAxes[2U].abStopArmed != 0U)))
{
return 0U;
}
return 1U;
}

static void PlsrHwAccountPulseDirUpdate(uint8_t axis)
{
PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis];

if ((state->state != PLSR_HW_STATE_RUNNING)
|| (state->outputMode != PLSR_OUTPUT_PULSE_DIR))
{
return;
}
if (state->pulseBoundaryStopPending != 0U)
{
#ifndef PLSR_HOST_TEST
if (state->hardwareCounterActive != 0U)
{
state->emittedPulses = (int64_t)PlsrHwCounterSnapshot(axis);
}
else
#endif
{
state->emittedPulses++;
}
state->pulseBoundaryStopPending = 0U;
state->pulseTargetStopArmed = 0U;
PlsrHwStopActiveOutput(axis, state->outputMode);
state->state = PLSR_HW_STATE_DONE;
PlsrHwCounterRelease(axis);
(void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE);
return;
}
#ifndef PLSR_HOST_TEST
if (state->hardwareCounterActive != 0U)
{
uint64_t pulses = PlsrHwCounterSnapshot(axis);

if ((state->pulseTargetStopArmed != 0U)
&& (pulses >= (uint64_t)state->targetPulses))
{
state->emittedPulses = state->targetPulses;
state->pulseTargetStopArmed = 0U;
PlsrHwStopActiveOutput(axis, state->outputMode);
state->state = PLSR_HW_STATE_DONE;
PlsrHwCounterRelease(axis);
(void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE);
}
return;
}
#endif

state->emittedPulses++;
if (state->emittedPulses >= state->targetPulses)
{
state->emittedPulses = state->targetPulses;
state->pulseTargetStopArmed = 0U;
PlsrHwStopActiveOutput(axis, state->outputMode);
state->state = PLSR_HW_STATE_DONE;
PlsrHwCounterRelease(axis);
(void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE);
}
else if ((state->targetPulses > 1)
&& (state->emittedPulses >= (state->targetPulses - 1)))
{
PlsrHwArmPulseTargetTail(axis);
}
}

static void PlsrHwHandlePulseDirUpdate(uint8_t axis)
{
if (PlsrHwTimerHasCc1if(axis) != 0U)
{
/* CC1IE is disabled for PULSE/DIR, but the compare flag itself still
* latches. Consume it so it cannot survive a later mode change. */
PlsrHwTimerClearCc1if(axis);
}
if (PlsrHwTimerHasUif(axis) == 0U)
{
return;
}
PlsrHwTimerClearUif(axis);
PlsrHwAccountPulseDirUpdate(axis);
}

static void PlsrHwHandlePulseDirFastGroup(uint8_t interruptAxis)
{
#ifdef PLSR_HOST_TEST
uint8_t peerAxis;

PlsrHwHandlePulseDirUpdate(interruptAxis);
if (interruptAxis < 2U)
{
return;
}
peerAxis = (uint8_t)(interruptAxis ^ 1U);
/* Host models the same single snapshot used by the target fast path. */
if ((PlsrHwPulseDirFastPathAllowed(peerAxis) != 0U)
&& (PlsrHwTimerHasUif(peerAxis) != 0U))
{
PlsrHwHandlePulseDirUpdate(peerAxis);
}
#else
if (interruptAxis < 2U)
{
PlsrHwHandlePulseDirUpdate(interruptAxis);
return;
}
{
uint32_t tim11Flags = TIM11->SR;
uint32_t tim14Flags = TIM14->SR;

/* Clear exactly the snapshotted update/compare flags in one APB write
* per timer. Writing 1 to all other timer flags preserves an event
* that arrives after the snapshot. */
if ((tim11Flags & (TIM_SR_UIF | TIM_SR_CC1IF)) != 0UL)
{
TIM11->SR = ~(tim11Flags & (TIM_SR_UIF | TIM_SR_CC1IF));
}
if ((tim14Flags & (TIM_SR_UIF | TIM_SR_CC1IF)) != 0UL)
{
TIM14->SR = ~(tim14Flags & (TIM_SR_UIF | TIM_SR_CC1IF));
}
if (((tim11Flags & TIM_SR_UIF) != 0UL)
&& (PlsrHwPulseDirFastPathAllowed(2U) != 0U))
{
PlsrHwAccountPulseDirUpdate(2U);
}
if (((tim14Flags & TIM_SR_UIF) != 0UL)
&& (PlsrHwPulseDirFastPathAllowed(3U) != 0U))
{
PlsrHwAccountPulseDirUpdate(3U);
}
}
#endif
}

void PlsrHwOnTimerUpdate(uint8_t axis)
{
PLSR_HW_AXIS_STATE *state;
@@ -2492,6 +2852,11 @@ void PlsrHwOnTimerUpdate(uint8_t axis)
{
return;
}
if (PlsrHwPulseDirFastPathAllowed(axis) != 0U)
{
PlsrHwHandlePulseDirFastGroup(axis);
return;
}
ownerAxis = (uint8_t)(axis & 0xFEU);
abGated = PlsrHwGateArmedAbOutputs(ownerAxis);
#ifdef PLSR_HOST_TEST
@@ -2692,11 +3057,41 @@ void PlsrHwOnTimerUpdate(uint8_t axis)
{
return;
}
if (state->pulseBoundaryStopPending != 0U)
{
#ifndef PLSR_HOST_TEST
if (state->hardwareCounterActive != 0U)
{
state->emittedPulses = (int64_t)PlsrHwCounterSnapshot(axis);
}
else
#endif
{
state->emittedPulses++;
}
state->pulseBoundaryStopPending = 0U;
state->pulseTargetStopArmed = 0U;
PlsrHwStopActiveOutput(axis, state->outputMode);
state->state = PLSR_HW_STATE_DONE;
PlsrHwCounterRelease(axis);
(void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE);
return;
}
#ifndef PLSR_HOST_TEST
if (state->hardwareCounterActive != 0U)
{
/* A hardware-counted axis has UIE disabled. Ignore any stale update
* flag rather than counting the same OC event in software as well. */
uint64_t pulses = PlsrHwCounterSnapshot(axis);

if ((state->pulseTargetStopArmed != 0U)
&& (pulses >= (uint64_t)state->targetPulses))
{
state->emittedPulses = state->targetPulses;
state->pulseTargetStopArmed = 0U;
PlsrHwStopActiveOutput(axis, state->outputMode);
state->state = PLSR_HW_STATE_DONE;
PlsrHwCounterRelease(axis);
(void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE);
}
return;
}
#endif
@@ -2704,12 +3099,18 @@ void PlsrHwOnTimerUpdate(uint8_t axis)
state->emittedPulses++;
if (state->emittedPulses >= state->targetPulses)
{
/* 更新时刻 = 周期结束:关通道即完整下降沿后停止,无额外脉冲。 */
state->emittedPulses = state->targetPulses;
state->pulseTargetStopArmed = 0U;
PlsrHwStopActiveOutput(axis, state->outputMode);
state->state = PLSR_HW_STATE_DONE;
PlsrHwCounterRelease(axis);
(void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE);
}
else if ((state->targetPulses > 1)
&& (state->emittedPulses >= (state->targetPulses - 1)))
{
PlsrHwArmPulseTargetTail(axis);
}
}

#ifdef PLSR_HOST_TEST
@@ -2960,15 +3361,26 @@ void PlsrHwTestTriggerUpdate(uint8_t axis)
{
if (axis < PLSR_HW_AXIS_COUNT)
{
PlsrHwTimers[axis].cnt = 0UL;
PlsrHwTimers[axis].sr |= PLSR_HW_TIMER_UPDATE_BIT;
}
PlsrHwOnTimerUpdate(axis);
}

void PlsrHwTestSetUpdatePending(uint8_t axis)
{
if (axis < PLSR_HW_AXIS_COUNT)
{
PlsrHwTimers[axis].cnt = 0UL;
PlsrHwTimers[axis].sr |= PLSR_HW_TIMER_UPDATE_BIT;
}
}

void PlsrHwTestTriggerUpdateAndCompare(uint8_t axis)
{
if (axis < PLSR_HW_AXIS_COUNT)
{
PlsrHwTimers[axis].cnt = PlsrHwTimers[axis].ccr1;
PlsrHwTimers[axis].sr |= PLSR_HW_TIMER_UPDATE_BIT
| PLSR_HW_TIMER_CC1_BIT;
}
@@ -2979,6 +3391,7 @@ void PlsrHwTestTriggerCompare(uint8_t axis)
{
if (axis < PLSR_HW_AXIS_COUNT)
{
PlsrHwTimers[axis].cnt = PlsrHwTimers[axis].ccr1;
PlsrHwTimers[axis].sr |= PLSR_HW_TIMER_CC1_BIT;
}
PlsrHwOnTimerUpdate(axis);
@@ -3033,16 +3446,11 @@ static void PlsrHwOnCounterInterrupt(uint8_t counterIndex)
return;
}
if (((flags & TIM_SR_CC1IF) != 0UL)
&& (pulses >= (uint64_t)state->targetPulses))
&& (pulses >= ((uint64_t)state->targetPulses - 1UL)))
{
/* TIMx_OC rises at the PWM update boundary. On this board that is
* the physical falling edge, so the target pulse is already complete
* and both the counter and PWM may be stopped without truncation. */
state->emittedPulses = state->targetPulses;
PlsrHwStopActiveOutput(axis, state->outputMode);
state->state = PLSR_HW_STATE_DONE;
PlsrHwCounterRelease(axis);
(void)PlsrPostEvent(axis, PLSR_EVENT_SEGMENT_COMPLETE);
/* Do not gate on the counter source edge: it starts the terminal high
* level. Pre-arm the output compare so the target high completes. */
PlsrHwArmPulseTargetTail(axis);
}
}



+ 150
- 0
PLSR/Src/plsr_modbus_data.c Wyświetl plik

@@ -2,6 +2,57 @@
#include "modbus_data_store.h"
#include <stddef.h>

#ifndef PLSR_HOST_TEST
#include "stm32f4xx_hal.h"

typedef struct
{
GPIO_TypeDef *port;
uint16_t pin;
uint8_t point;
uint8_t activeLow;
} PLSR_MODBUS_INPUT_PIN;

/* Project attachment "实现PLSR指令.pdf", XDM-60T4-E page 3.
* X13=PA14 is intentionally omitted because PA14 is SWDCLK while board
* validation/debug is active. X21..X43 have no supplied MCU mapping. */
static const PLSR_MODBUS_INPUT_PIN PlsrModbusInputPins[] =
{
{GPIOH, GPIO_PIN_10, 0U, 0U}, /* X0 */
{GPIOH, GPIO_PIN_11, 1U, 0U}, /* X1 */
{GPIOG, GPIO_PIN_8, 2U, 0U}, /* X2 */
{GPIOB, GPIO_PIN_4, 3U, 0U}, /* X3 */
{GPIOB, GPIO_PIN_5, 4U, 0U}, /* X4 */
{GPIOG, GPIO_PIN_12, 5U, 0U}, /* X5 */
{GPIOB, GPIO_PIN_6, 6U, 0U}, /* X6 */
{GPIOB, GPIO_PIN_7, 7U, 0U}, /* X7 */
{GPIOF, GPIO_PIN_10, 10U, 0U}, /* X10 */
{GPIOA, GPIO_PIN_15, 11U, 0U}, /* X11 */
{GPIOB, GPIO_PIN_3, 12U, 0U}, /* X12 */
{GPIOI, GPIO_PIN_4, 14U, 0U}, /* X14 */
{GPIOI, GPIO_PIN_5, 15U, 0U}, /* X15 */
{GPIOI, GPIO_PIN_6, 16U, 0U}, /* X16 */
{GPIOI, GPIO_PIN_7, 17U, 0U}, /* X17 */
{GPIOE, GPIO_PIN_2, 20U, 0U} /* X20 */
};

#define PLSR_MODBUS_INPUT_PIN_COUNT \
((uint32_t)(sizeof(PlsrModbusInputPins) \
/ sizeof(PlsrModbusInputPins[0])))

static uint32_t PlsrModbusInputRawMask;
static uint32_t PlsrModbusInputLogicalMask;
static uint32_t PlsrModbusInputMappedMask;

static uint8_t PlsrModbusInputIsMapped(uint32_t point)
{
return ((point < 32UL)
&& ((PlsrModbusInputMappedMask & (1UL << point)) != 0UL))
? 1U
: 0U;
}
#endif

static uint8_t PlsrModbusDevice(PLSR_DEVICE_TYPE device,
MODBUS_DATA_DEVICE *modbusDevice)
{
@@ -82,6 +133,13 @@ static uint8_t PlsrModbusReadBit(void *context,
{
return 0U;
}
#ifndef PLSR_HOST_TEST
if ((device == PLSR_DEVICE_X)
&& (PlsrModbusInputIsMapped(address) == 0U))
{
return 0U;
}
#endif
return ModbusDataReadBit(modbusDevice, address, value);
}

@@ -97,3 +155,95 @@ void PlsrModbusDataSourceInit(PLSR_DATA_SOURCE *source)
source->readDword = PlsrModbusReadDword;
source->readBit = PlsrModbusReadBit;
}

PLSR_RESULT PlsrModbusInputInit(void)
{
#ifdef PLSR_HOST_TEST
return PLSR_RESULT_OK;
#else
GPIO_InitTypeDef gpio;
uint32_t index;

__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOI_CLK_ENABLE();

gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
gpio.Alternate = 0U;
PlsrModbusInputMappedMask = 0UL;
for (index = 0UL; index < PLSR_MODBUS_INPUT_PIN_COUNT; index++)
{
gpio.Pin = PlsrModbusInputPins[index].pin;
HAL_GPIO_Init(PlsrModbusInputPins[index].port, &gpio);
PlsrModbusInputMappedMask |=
1UL << PlsrModbusInputPins[index].point;
}
PlsrModbusInputPoll();
return PLSR_RESULT_OK;
#endif
}

void PlsrModbusInputPoll(void)
{
#ifndef PLSR_HOST_TEST
uint32_t rawMask = 0UL;
uint32_t logicalMask = 0UL;
uint32_t index;

for (index = 0UL; index < PLSR_MODBUS_INPUT_PIN_COUNT; index++)
{
const PLSR_MODBUS_INPUT_PIN *input = &PlsrModbusInputPins[index];
uint8_t raw = ((input->port->IDR & input->pin) != 0UL) ? 1U : 0U;
uint8_t logical = (input->activeLow != 0U)
? ((raw == 0U) ? 1U : 0U)
: raw;

if (raw != 0U)
{
rawMask |= 1UL << input->point;
}
if (logical != 0U)
{
logicalMask |= 1UL << input->point;
}
(void)ModbusDataWriteBit(MODBUS_BIT_DEVICE_X,
input->point,
logical);
}
PlsrModbusInputRawMask = rawMask;
PlsrModbusInputLogicalMask = logicalMask;
#endif
}

uint32_t PlsrModbusInputGetRawMask(void)
{
#ifdef PLSR_HOST_TEST
return 0UL;
#else
return PlsrModbusInputRawMask;
#endif
}

uint32_t PlsrModbusInputGetLogicalMask(void)
{
#ifdef PLSR_HOST_TEST
return 0UL;
#else
return PlsrModbusInputLogicalMask;
#endif
}

uint32_t PlsrModbusInputGetMappedMask(void)
{
#ifdef PLSR_HOST_TEST
return 0UL;
#else
return PlsrModbusInputMappedMask;
#endif
}

+ 146
- 0
PLSR/Src/plsr_self_test.c Wyświetl plik

@@ -3,6 +3,7 @@
#include "plc_device.h"
#include "modbus_data_store.h"
#include "plsr_core.h"
#include "plsr_hal_f407.h"
#include "plsr_job.h"
#include "plsr_modbus_data.h"
#include <string.h>
@@ -25,12 +26,19 @@
#define SELF_TEST_SFD_SET_OFFSET (50U)
#define SELF_TEST_MODBUS_S0_BASE (1000UL)
#define SELF_TEST_MODBUS_S1_BASE (1100UL)
#define SELF_TEST_INPUT_CONTROL_BASE (1540UL)
#define SELF_TEST_INPUT_LEASE_TICKS (1000U)
#define SELF_TEST_INPUT_SIGNATURE (0x5019U)

static uint16_t SelfTestWords[3][SELF_TEST_WORD_CAPACITY];
volatile int32_t PlsrSelfTestLiveFrequencyHz;
volatile uint32_t PlsrSelfTestDynamicTick100us;
volatile uint8_t PlsrSelfTestDynamicPhase;
static volatile uint8_t PlsrSelfTestDynamicEnabled;
static uint16_t PlsrPhysicalInputLastLease;
static uint16_t PlsrPhysicalInputLeaseAge;
static uint16_t PlsrPhysicalInputAppliedMask;
static uint16_t PlsrPhysicalInputPublished[5];

void PlsrSelfTestControlTick100us(void)
{
@@ -1106,4 +1114,142 @@ PLSR_RESULT PlsrLongStressSelfTestPrepare(void)
return PLSR_RESULT_OK;
}

PLSR_RESULT PlsrPhysicalInputSelfTestPrepare(void)
{
const uint16_t commonBase = 900U;
const uint16_t setBase =
(uint16_t)(commonBase + SELF_TEST_SFD_SET_OFFSET);
uint32_t mappedMask;
uint8_t index;

/* Axis 0 uses Y10 as DIR so validation outputs Y1..Y4 remain independent.
* X0 is the positive hard limit and X1 is the negative hard limit. */
(void)PlcDeviceWriteSfd(commonBase, 0U);
SelfTestWriteSfdDword((uint16_t)(commonBase + 2U), 1UL);
SelfTestWriteSfdDword((uint16_t)(commonBase + 4U), 1UL);
(void)PlcDeviceWriteSfd((uint16_t)(commonBase + 6U), 10U);
(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), 0x0100U);
SelfTestWriteSfdDword((uint16_t)(commonBase + 30U), 0UL);
SelfTestWriteSfdDword((uint16_t)(commonBase + 32U), 0UL);

/* K1: 2kHz plateau and a 50ms controlled limit stop. */
SelfTestWriteSfdDword(setBase, 2000UL);
(void)PlcDeviceWriteSfd((uint16_t)(setBase + 2U), 0U);
(void)PlcDeviceWriteSfd((uint16_t)(setBase + 3U), 50U);
(void)PlcDeviceWriteSfd((uint16_t)(setBase + 4U), 0U);
(void)PlcDeviceWriteSfd((uint16_t)(setBase + 5U), 0U);
SelfTestWriteSfdDword((uint16_t)(setBase + 6U), 2000UL);
SelfTestWriteSfdDword((uint16_t)(setBase + 8U), 2000UL);
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);

PlsrHwValidationOutputsOff();
PlsrPhysicalInputLastLease = UINT16_MAX;
PlsrPhysicalInputLeaseAge = (uint16_t)(SELF_TEST_INPUT_LEASE_TICKS + 1U);
PlsrPhysicalInputAppliedMask = 0U;
for (index = 0U; index < 5U; index++)
{
PlsrPhysicalInputPublished[index] = UINT16_MAX;
}
(void)ModbusDataWriteWord(MODBUS_DATA_DEVICE_D,
SELF_TEST_INPUT_CONTROL_BASE,
0U);
(void)ModbusDataWriteWord(MODBUS_DATA_DEVICE_D,
SELF_TEST_INPUT_CONTROL_BASE + 1UL,
0U);
mappedMask = PlsrModbusInputGetMappedMask();
if ((mappedMask & 0x0FUL) != 0x0FUL)
{
return PLSR_RESULT_INVALID_RESOURCE;
}
return PLSR_RESULT_OK;
}

void PlsrPhysicalInputSelfTestPoll(void)
{
uint16_t requestedMask = 0U;
uint16_t leaseSequence = 0U;
uint16_t values[5];
uint16_t appliedMask;
uint16_t flags = 1U;
uint8_t point;
uint8_t outputError = 0U;

if ((ModbusDataReadWord(MODBUS_DATA_DEVICE_D,
SELF_TEST_INPUT_CONTROL_BASE,
&requestedMask) == 0U)
|| (ModbusDataReadWord(MODBUS_DATA_DEVICE_D,
SELF_TEST_INPUT_CONTROL_BASE + 1UL,
&leaseSequence) == 0U))
{
PlsrHwValidationOutputsOff();
return;
}
if (leaseSequence != PlsrPhysicalInputLastLease)
{
PlsrPhysicalInputLastLease = leaseSequence;
PlsrPhysicalInputLeaseAge = 0U;
}
else if (PlsrPhysicalInputLeaseAge <= SELF_TEST_INPUT_LEASE_TICKS)
{
PlsrPhysicalInputLeaseAge++;
}

if (PlsrPhysicalInputLeaseAge <= SELF_TEST_INPUT_LEASE_TICKS)
{
appliedMask = (uint16_t)(requestedMask & 0x000FU);
flags |= (1U << 1U);
}
else
{
appliedMask = 0U;
flags |= (1U << 2U);
}
for (point = 0U; point < 4U; point++)
{
if (PlsrHwSetValidationOutput(
(uint8_t)(point + 1U),
(uint8_t)((appliedMask >> point) & 1U))
!= PLSR_RESULT_OK)
{
outputError = 1U;
}
}
if (outputError != 0U)
{
appliedMask = 0U;
flags |= (1U << 3U);
PlsrHwValidationOutputsOff();
}
if ((PlsrModbusInputGetMappedMask() & 0x0FUL) == 0x0FUL)
{
flags |= (1U << 4U);
}
PlsrPhysicalInputAppliedMask = appliedMask;
values[0] = PlsrPhysicalInputAppliedMask;
values[1] = (uint16_t)PlsrModbusInputGetRawMask();
values[2] = (uint16_t)PlsrModbusInputGetLogicalMask();
values[3] = SELF_TEST_INPUT_SIGNATURE;
values[4] = flags;
for (point = 0U; point < 5U; point++)
{
if (values[point] != PlsrPhysicalInputPublished[point])
{
(void)ModbusDataWriteWord(MODBUS_DATA_DEVICE_D,
SELF_TEST_INPUT_CONTROL_BASE + 2UL
+ point,
values[point]);
PlsrPhysicalInputPublished[point] = values[point];
}
}
}

#endif

+ 67
- 0
PLSR/Test/test_plsr_hal.c Wyświetl plik

@@ -335,6 +335,71 @@ static void TestHardwareCounterLeases(void)
CHECK(PlsrHwUsesHardwareCounter(2U) == 0U);
}

static void TestPulseDirTargetUpdateBoundaryStop(void)
{
PLSR_HW_START_PARAMS params;
uint32_t activeArr;

(void)PlsrHwInit();
(void)memset(&params, 0, sizeof(params));
params.frequencyHz = 100000UL;
params.targetPulses = 3;
params.outputMode = PLSR_OUTPUT_PULSE_DIR;
params.directionPoint = 5U;
params.directionPositive = 1U;

CHECK(PlsrHwStartPulse(2U, &params) == PLSR_RESULT_OK);
CHECK(PlsrHwSetFrequency(2U, 100000UL) == PLSR_RESULT_OK);
activeArr = PlsrHwTestGetArr(2U);
PlsrHwTestTriggerUpdate(2U);
CHECK(PlsrHwGetEmittedPulses(2U) == 1);
CHECK(PlsrHwTestGetArr(2U) == activeArr);

/* target-1 pre-arms a long low tail but must not stop yet. */
PlsrHwTestTriggerUpdate(2U);
CHECK(PlsrHwGetEmittedPulses(2U) == 2);
CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_RUNNING);
CHECK(PlsrHwTestGetArr(2U) == 0xFFFFUL);
CHECK(PlsrHwTestGetCcr(2U) == 0xFFFFUL);

/* The target update is the completed physical pulse boundary. */
PlsrHwTestTriggerUpdate(2U);
CHECK(PlsrHwGetEmittedPulses(2U) == 3);
CHECK(PlsrHwGetState(2U) == PLSR_HW_STATE_DONE);
CHECK(PlsrHwTestGetPwmEnabled(2U) == 0U);
}

static void TestSoftwareCounterPeerDrain(void)
{
PLSR_HW_START_PARAMS params;
uint8_t axis;

(void)PlsrHwInit();
(void)memset(&params, 0, sizeof(params));
params.frequencyHz = 100000UL;
params.targetPulses = 10;
params.outputMode = PLSR_OUTPUT_PULSE_DIR;
params.directionPositive = 1U;

for (axis = 0U; axis < PLSR_AXIS_COUNT; axis++)
{
params.directionPoint = (uint8_t)(4U + axis);
CHECK(PlsrHwStartPulse(axis, &params) == PLSR_RESULT_OK);
CHECK(PlsrHwSetFrequency(axis, 100000UL) == PLSR_RESULT_OK);
}
CHECK(PlsrHwUsesHardwareCounter(0U) == 1U);
CHECK(PlsrHwUsesHardwareCounter(1U) == 1U);
CHECK(PlsrHwUsesHardwareCounter(2U) == 0U);
CHECK(PlsrHwUsesHardwareCounter(3U) == 0U);

PlsrHwTestSetUpdatePending(3U);
PlsrHwTestTriggerUpdate(2U);
CHECK(PlsrHwGetEmittedPulses(0U) == 0);
CHECK(PlsrHwGetEmittedPulses(1U) == 0);
CHECK(PlsrHwGetEmittedPulses(2U) == 1);
CHECK(PlsrHwGetEmittedPulses(3U) == 1);
}

static void TestCwCcwSequence(void)
{
PLSR_HW_START_PARAMS params;
@@ -2629,6 +2694,8 @@ int main(void)
TestDirDelaySequence();
TestDirectionBatch();
TestHardwareCounterLeases();
TestPulseDirTargetUpdateBoundaryStop();
TestSoftwareCounterPeerDrain();
TestCwCcwSequence();
TestFastRefreshControlTick();
TestDynamicFrequencyRetarget();


+ 61
- 0
PLSR/Test/test_plsr_path.c Wyświetl plik

@@ -1,5 +1,6 @@
#include "plc_device.h"
#include "plsr_core.h"
#include "plsr_hal_f407.h"
#include "plsr_job.h"
#include "plsr_path.h"
#include "plsr_persistence.h"
@@ -164,6 +165,21 @@ static void TestAccelToRun(void)
CHECK(TestGetStatus().state == PLSR_STATE_RUN);
}

static void TestWaitHardwareRunning(void)
{
int ticks;

for (ticks = 0; ticks < 20; ticks++)
{
if (PlsrHwIsPulseActive(0U) != 0U)
{
break;
}
PlsrProcess();
}
CHECK(PlsrHwIsPulseActive(0U) != 0U);
}

/* 启动一个 N 段任务(全部 H00 完成条件、顺序跳转)。 */
static void TestStartSequentialJob(TEST_MEMORY *memory,
uint16_t segmentCount,
@@ -355,6 +371,10 @@ static void TestActTruncatesSegment(void)
{
PlsrProcess();
}
CHECK(TestGetStatus().state == PLSR_STATE_RUN);
CHECK(PlsrHwTestGetPwmEnabled(0U) != 0U);
PlsrHwTestTriggerUpdate(0U);
PlsrProcess();
CHECK(TestGetStatus().state == PLSR_STATE_ACCEL);
CHECK(TestGetStatus().currentSegment == 2U);
}
@@ -422,12 +442,21 @@ static void TestExtEdgeTruncates(void)
}
CHECK(TestGetStatus().state == PLSR_STATE_RUN);
CHECK(TestGetStatus().currentSegment == 1U);
TestWaitHardwareRunning();

/* 先拉低再拉高:新上升沿,截断当前段。 */
memory.bits[0U][0U] = 0U;
PlsrProcess();
memory.bits[0U][0U] = 1U;
PlsrProcess();
/* EXT freezes the profile but preserves the in-flight physical high/low
* period. The job must not start segment 2 until the natural update
* boundary has stopped the old PWM and posted completion. */
CHECK(TestGetStatus().state == PLSR_STATE_RUN);
CHECK(PlsrHwTestGetPwmEnabled(0U) != 0U);
PlsrHwTestTriggerUpdate(0U);
CHECK(PlsrHwTestGetPwmEnabled(0U) == 0U);
PlsrProcess();
CHECK(TestGetStatus().state == PLSR_STATE_ACCEL);
CHECK(TestGetStatus().currentSegment == 2U);
}
@@ -458,6 +487,37 @@ static void TestExtOrComplete(void)
CHECK(TestGetStatus().currentSegment == 2U);
}

static void TestExtLastSegmentWaitsForPhysicalBoundary(void)
{
TEST_MEMORY memory;

TestResetEnvironment();
(void)memset(&memory, 0, sizeof(memory));
TestWriteDword(&memory, PLSR_DEVICE_D, TEST_S0_BASE, 1);
TestSetSegment(&memory, 1U, 2000U, 50000,
TEST_WAIT_CODE(4U, 4U), 0, 0U, 0);
{
PLSR_CALL call = TestMakeCall(&memory);

call.sequence = 81UL;
CHECK(PlsrPostCall(&call) == PLSR_RESULT_QUEUED);
}
PlsrProcess();
TestAccelToRun();
TestWaitHardwareRunning();

memory.bits[0U][0U] = 1U;
PlsrProcess();
CHECK(TestGetStatus().state == PLSR_STATE_RUN);
CHECK(PlsrHwTestGetPwmEnabled(0U) != 0U);

PlsrHwTestTriggerUpdate(0U);
CHECK(PlsrHwTestGetPwmEnabled(0U) == 0U);
PlsrProcess();
CHECK(TestGetStatus().state == PLSR_STATE_COMPLETED);
CHECK(TestGetStatus().done != 0U);
}

static void TestDynamicJump(void)
{
TEST_MEMORY memory;
@@ -664,6 +724,7 @@ int main(void)
TestActPendingAfterSegmentDone();
TestExtEdgeTruncates();
TestExtOrComplete();
TestExtLastSegmentWaitsForPhysicalBoundary();
TestDynamicJump();
TestAbsoluteZeroDisplacement();
TestZeroJumpBudgetYields();


Ładowanie…
Anuluj
Zapisz