diff --git a/.gitignore b/.gitignore index 45c820d..2bccd70 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ Document/PLSR_document/截图/ Document/PLSR_document/波形/ Document/PLSR_document/~$需求规格书.docx tmp/ +.plsr_audit_pages/ diff --git a/Core/Src/main.c b/Core/Src/main.c index e83e175..c6f64a7 100644 --- a/Core/Src/main.c +++ b/Core/Src/main.c @@ -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; diff --git a/Document/PLSR_document/PLSR信捷对标追踪矩阵.md b/Document/PLSR_document/PLSR信捷对标追踪矩阵.md index ba3673f..0e33246 100644 --- a/Document/PLSR_document/PLSR信捷对标追踪矩阵.md +++ b/Document/PLSR_document/PLSR信捷对标追踪矩阵.md @@ -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、异常复位和反复擦写板测 | diff --git a/Document/PLSR_document/PLSR方案设计书_V1.0.md b/Document/PLSR_document/PLSR方案设计书_V1.0.md index e0bbe37..7ad64b9 100644 --- a/Document/PLSR_document/PLSR方案设计书_V1.0.md +++ b/Document/PLSR_document/PLSR方案设计书_V1.0.md @@ -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 错误输出信息 diff --git a/Document/PLSR_document/PLSR测试报告_2026-08-10.md b/Document/PLSR_document/PLSR测试报告_2026-08-10.md index a42fdca..f385343 100644 --- a/Document/PLSR_document/PLSR测试报告_2026-08-10.md +++ b/Document/PLSR_document/PLSR测试报告_2026-08-10.md @@ -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 ## 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 掉电保持。三项均不得写成已通过。 diff --git a/Document/PLSR_document/XDM-60T4-E资料索引与IO映射.md b/Document/PLSR_document/XDM-60T4-E资料索引与IO映射.md new file mode 100644 index 0000000..3ade583 --- /dev/null +++ b/Document/PLSR_document/XDM-60T4-E资料索引与IO映射.md @@ -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 映射未由项目附件提供,当前实现不得虚构这些映射。 diff --git a/Document/PLSR_document/信捷XD_XL系列PLC硬件手册_PD01_20260410_V1.6.pdf b/Document/PLSR_document/信捷XD_XL系列PLC硬件手册_PD01_20260410_V1.6.pdf new file mode 100644 index 0000000..eebc233 Binary files /dev/null and b/Document/PLSR_document/信捷XD_XL系列PLC硬件手册_PD01_20260410_V1.6.pdf differ diff --git a/Document/PLSR_document/信捷XD系列PLC随机手册_S121041_20250508_V2.2.pdf b/Document/PLSR_document/信捷XD系列PLC随机手册_S121041_20250508_V2.2.pdf new file mode 100644 index 0000000..9e5a6b8 Binary files /dev/null and b/Document/PLSR_document/信捷XD系列PLC随机手册_S121041_20250508_V2.2.pdf differ diff --git a/HostComputer/plsr_modbus_long_stress_test.py b/HostComputer/plsr_modbus_long_stress_test.py index ce09ec9..f54ad0c 100644 --- a/HostComputer/plsr_modbus_long_stress_test.py +++ b/HostComputer/plsr_modbus_long_stress_test.py @@ -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: diff --git a/HostComputer/plsr_modbus_physical_input_test.py b/HostComputer/plsr_modbus_physical_input_test.py new file mode 100644 index 0000000..1bf92a5 --- /dev/null +++ b/HostComputer/plsr_modbus_physical_input_test.py @@ -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) diff --git a/PLSR/Inc/plsr_build_config.h b/PLSR/Inc/plsr_build_config.h index c3552ee..c5c62c4 100644 --- a/PLSR/Inc/plsr_build_config.h +++ b/PLSR/Inc/plsr_build_config.h @@ -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 diff --git a/PLSR/Inc/plsr_hal_f407.h b/PLSR/Inc/plsr_hal_f407.h index 656d194..bed8797 100644 --- a/PLSR/Inc/plsr_hal_f407.h +++ b/PLSR/Inc/plsr_hal_f407.h @@ -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); diff --git a/PLSR/Inc/plsr_modbus_data.h b/PLSR/Inc/plsr_modbus_data.h index 892a6b1..1bd4979 100644 --- a/PLSR/Inc/plsr_modbus_data.h +++ b/PLSR/Inc/plsr_modbus_data.h @@ -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 diff --git a/PLSR/Inc/plsr_self_test.h b/PLSR/Inc/plsr_self_test.h index 53fa143..b3b2258 100644 --- a/PLSR/Inc/plsr_self_test.h +++ b/PLSR/Inc/plsr_self_test.h @@ -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 diff --git a/PLSR/Src/plsr_core.c b/PLSR/Src/plsr_core.c index 359eba2..51a0fa0 100644 --- a/PLSR/Src/plsr_core.c +++ b/PLSR/Src/plsr_core.c @@ -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) diff --git a/PLSR/Src/plsr_hal_f407.c b/PLSR/Src/plsr_hal_f407.c index c6d637d..ea8cb4b 100644 --- a/PLSR/Src/plsr_hal_f407.c +++ b/PLSR/Src/plsr_hal_f407.c @@ -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 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); } } diff --git a/PLSR/Src/plsr_modbus_data.c b/PLSR/Src/plsr_modbus_data.c index cfdf558..773ec52 100644 --- a/PLSR/Src/plsr_modbus_data.c +++ b/PLSR/Src/plsr_modbus_data.c @@ -2,6 +2,57 @@ #include "modbus_data_store.h" #include +#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 +} diff --git a/PLSR/Src/plsr_self_test.c b/PLSR/Src/plsr_self_test.c index 1712689..5a767b6 100644 --- a/PLSR/Src/plsr_self_test.c +++ b/PLSR/Src/plsr_self_test.c @@ -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 @@ -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 diff --git a/PLSR/Test/test_plsr_hal.c b/PLSR/Test/test_plsr_hal.c index dbf0d01..d1d037a 100644 --- a/PLSR/Test/test_plsr_hal.c +++ b/PLSR/Test/test_plsr_hal.c @@ -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(¶ms, 0, sizeof(params)); + params.frequencyHz = 100000UL; + params.targetPulses = 3; + params.outputMode = PLSR_OUTPUT_PULSE_DIR; + params.directionPoint = 5U; + params.directionPositive = 1U; + + CHECK(PlsrHwStartPulse(2U, ¶ms) == 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(¶ms, 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, ¶ms) == 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(); diff --git a/PLSR/Test/test_plsr_path.c b/PLSR/Test/test_plsr_path.c index 7b6d1b9..f258c28 100644 --- a/PLSR/Test/test_plsr_path.c +++ b/PLSR/Test/test_plsr_path.c @@ -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();