From 8105d822c2d8a069093b867ad643fb62f78d9bbb Mon Sep 17 00:00:00 2001 From: ywh <2227158009@qq.com> Date: Tue, 11 Aug 2026 08:18:59 +0800 Subject: [PATCH] =?UTF-8?q?P17/P18=20=E6=94=B6=E5=B0=BE=EF=BC=9A=E5=8F=8CA?= =?UTF-8?q?B=E8=BE=B9=E7=95=8C=E9=97=AD=E7=8E=AF=E3=80=81=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E6=9D=BF=E6=B5=8B=E4=B8=8E=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构双AB启动、物理极性、00边界门控及双定时器紧密启动 - 修复AB调频短脉冲、PAUSE非法同跳、末周期悬高和额外边沿 - 完成100kHz→50kHz→100kHz动态调频及PAUSE/RESUME闭环 - 修复零斜率实时调频无法进入稳定频率的问题 - 扩充AB硬件计数、边界同步、极性及Profile Host回归 - 修正M/WAIT测试对START异步QUEUED应答的判定 - 修复运行中HSD检查点和Flash擦写期间Modbus读超时误报 - 固化independent、simultaneous、dynamic三组AB真机验收结果 - 补充完整PLSR测试报告并更新FOLLOW范围说明 - 删除过时交接提示和历史问题清单 - 默认板测配置切换至P18四轴长稳测试 验证: - Host 8组共4881项全部PASS - IAR Modbus构建0 errors / 0 warnings - 双AB三用例自动检查及100MS/s四通道波形PASS - HSD VBAT恢复、运行中掉电、SFD加载和运动中禁写PASS - P18四轴Modbus 60秒长稳PASS - 30分钟USB并发、真实X/HM及坏CRC回退仍待关闭 --- .../PLSR信捷对标追踪矩阵.md | 2 +- .../PLSR_document/PLSR方案设计书_V1.0.md | 6 +- .../PLSR测试报告_2026-08-10.md | 432 +++++++++++++++++ .../PLSR需求规格统计_V1.3.md | 2 +- .../交接提示词_2026-08-10.md | 80 --- .../PLSR_document/交接文档_2026-08-08.md | 89 ---- .../已知问题清单_2026-08-09.md | 42 -- ...示词_发给Codex老对话_2026-08-10.md | 69 --- .../问题总清单_2026-08-10.md | 99 ---- HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md | 7 +- HostComputer/plsr_modbus_ab_stress_test.py | 20 +- HostComputer/plsr_modbus_bit_input_test.py | 7 +- HostComputer/plsr_persistence_board_test.py | 77 ++- PLSR/Inc/plsr_build_config.h | 2 +- PLSR/Inc/plsr_hal_f407.h | 4 + PLSR/Src/plsr_hal_f407.c | 456 ++++++++++++++++-- PLSR/Src/plsr_profile.c | 18 + PLSR/Test/test_plsr_hal.c | 10 + PLSR/Test/test_plsr_profile.c | 27 ++ 19 files changed, 994 insertions(+), 455 deletions(-) create mode 100644 Document/PLSR_document/PLSR测试报告_2026-08-10.md delete mode 100644 Document/PLSR_document/交接提示词_2026-08-10.md delete mode 100644 Document/PLSR_document/交接文档_2026-08-08.md delete mode 100644 Document/PLSR_document/已知问题清单_2026-08-09.md delete mode 100644 Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md delete mode 100644 Document/PLSR_document/问题总清单_2026-08-10.md diff --git a/Document/PLSR_document/PLSR信捷对标追踪矩阵.md b/Document/PLSR_document/PLSR信捷对标追踪矩阵.md index d8f64aa..ba3673f 100644 --- a/Document/PLSR_document/PLSR信捷对标追踪矩阵.md +++ b/Document/PLSR_document/PLSR信捷对标追踪矩阵.md @@ -129,7 +129,7 @@ | 实时预算 | P16 `PlsrProcess`自身53748 cycles(319.929us)<168000,响应85655 cycles;输出和计数ISR均在预算内 | 已板测关闭 | | 软限位 | P15四轴正负方向、500/2000Hz矩阵,最终位置误差不超过±1脉冲 | 已板测关闭 | | X/M/HM位设备 | 三套独立10000位映像、FC02读取X、标准线圈访问M、PLSR `readBit`已实现 | 代码完成;X物理GPIO映射与HM保持未关闭 | -| 双AB 100kHz | 安全首沿、TIM9/TIM12双硬件计数、00边界PAUSE、延后慢收尾及D1468完整快速窗口已通过Host/IAR | 待四通道真机验证,历史7501首沿问题尚不能关闭 | +| 双AB 100kHz | `independent`与`simultaneous`四通道真机已通过:严格100000/200000及四路200000边沿、首沿2.50us±0.05us、正反相序、末周期00;D1468分别382/392 cycles | 仅`dynamic`的50kHz调频、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 9415a3a..e0bbe37 100644 --- a/Document/PLSR_document/PLSR方案设计书_V1.0.md +++ b/Document/PLSR_document/PLSR方案设计书_V1.0.md @@ -568,8 +568,10 @@ AB启动必须在GPIO仍持有安全低电平时完成以下顺序:停止两 禁止在运行中重写CNT维持相位。双AB分别以Q0→TIM9和Q3→TIM12作为完整周期计数源;到达目标前 武装落后相的`00`边界快速门控,并通过D1468~D1469记录该快速路径最大周期数。上述新序列和门控 代码已完成Host/IAR验证。PAUSE同样只在下一真实`00`边界停表,已经开始的周期计入完成数;双组 -近同时完成时,ISR只快速gate并锁存,GPIO、计数器释放和事件发布延后到任务态。D1468覆盖从IRQ -入口到二次扫描及公共ISR收尾的完整快速窗口,而非局部函数时间。在四通道真机确认严格边沿数、 +近同时完成时,ISR只快速gate并锁存,GPIO、计数器释放和事件发布延后到任务态;异常返回前再次 +扫描另一组已武装的`00`标志,消除首轮扫描后到达的等优先级竞争。D1468取实际停表门控扫描与 +返回前最终安全扫描的最大值,必须小于一个100kHz四分之一周期;整个输出ISR仍由独立ISR WCET +窗口约束。在四通道真机确认严格边沿数、 2.5us首相位间隔、变频及PAUSE/RESUME波形前仍标记为待上板验证。 ##### 3.5.1.2 16位硬件计数扩展 diff --git a/Document/PLSR_document/PLSR测试报告_2026-08-10.md b/Document/PLSR_document/PLSR测试报告_2026-08-10.md new file mode 100644 index 0000000..a42fdca --- /dev/null +++ b/Document/PLSR_document/PLSR测试报告_2026-08-10.md @@ -0,0 +1,432 @@ +# PLSR 项目测试报告 + +| 项目 | 内容 | +| --- | --- | +| 报告版本 | V1.0 | +| 状态日期 | 2026-08-10 | +| 代码基线 | `master` / HEAD `fdbbe71aeacfe2524271aa4d53792cb5831211dc` + 当前未提交工作区 | +| 目标平台 | STM32F407IG、uC/OS-II、Modbus RTU | +| 开发环境 | IAR EWARM 8.3,构建配置 `Modbus` | +| 通信参数 | COM5,9600 bit/s,8E1,从站地址 1 | +| 波形采集 | 16 通道逻辑分析仪;AB 关键用例 100 MS/s;CH0~CH3 对应 Q0~Q3 | +| 测试依据 | PLSR 需求、方案、地址映射、信捷对标追踪矩阵、当前源码及真机实测 | + +## 1. 报告口径 + +本报告汇总截至 2026-08-10 已执行的 Host 自动测试、IAR 构建、Modbus 上位机测试、 +STM32F407 真机测试及逻辑分析仪结果。状态定义如下: + +- **PASS**:自动检查和该用例要求的真机/波形验收均已通过。 +- **PASS(发布前回归)**:已有完整通过证据,但最终共享 HAL 改动后仍应按发布门禁再跑一次。 +- **PARTIAL**:只完成了内部状态或部分硬件链路,不能作为完整端子验收。 +- **PENDING**:测试代码已具备,但尚无要求范围内的最终实测证据。 +- **BLOCKED**:缺少板卡映射、硬件接口或明确存储约定,当前不能安全完成。 +- **DEFERRED**:测试条件具备但本轮明确暂缓。 + +内部 `physicalPulses`、任务计数和位置只能用于固件一致性检查,不能替代端子物理边沿。 +追踪矩阵中的“已纳入/等效实现”也不等价于代码和板测全部关闭。 +本报告中的最新结果对应当前未提交工作区,不能只用 HEAD `fdbbe71` 复现。 + +## 2. 总体结论 + +| 测试域 | 当前结论 | +| --- | --- | +| Host 自动回归 | **PASS**,8 组共 4881 项 | +| IAR 全量构建 | **PASS**,0 errors / 0 warnings | +| 四轴 PULSE/DIR、P12、P13、P15 | **PASS**;P14 作为最终发布门禁仍建议重采一次四通道波形 | +| 双 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 保持介质约定 | +| A/B 槽坏 CRC 真机回退 | **PENDING**,正常固件禁用破坏性诊断;Host 已覆盖 | + +因此,主体运动、Modbus 控制、双 AB、性能和正常持久化链路已有充分通过证据; +但在真实 X/HM、受控坏 CRC、30 分钟 USB 并发及最终发布回归关闭前,不得宣称 +“严格功能对标和全部交付验收完成”。 + +## 3. 测试环境与固件配置 + +### 3.1 固件选择 + +| 固件/选择 | 用途 | +| --- | --- | +| `PLSR_BOARD_TEST_MODBUS_DATA`(P12) | 动态频率与 Modbus 字数据源 | +| `PLSR_BOARD_TEST_MODBUS_CONTROL`(P13) | COMMIT/START/PAUSE/RESUME/STOP 控制窗口 | +| `PLSR_BOARD_TEST_HW_COUNTER`(P14) | 四轴 PULSE/DIR 计数与 P16 性能 | +| `PLSR_BOARD_TEST_DUAL_AB`(P17) | P14/P15 共用准备、双 AB、位链及持久化板测 | +| `PLSR_BOARD_TEST_LONG_STRESS`(P18) | 四轴长稳专用 K4;关闭软/硬限位,仅限安全台架 | + +报告生成时工作区默认选择为 P18。P18 不得用于带机构的常规运动验收;切换测试固件后均需 +重新编译、烧录并硬复位。DWT 最大值测试必须在硬复位后进行。 + +### 3.2 当前静态基线 + +2026-08-10 在当前工作区重新执行: + +```powershell +powershell -ExecutionPolicy Bypass -File PLSR\Test\run_host_tests.ps1 + +& 'F:\IAR Systems\Embedded Workbench 8.3\common\bin\IarBuild.exe' ` + 'EWARM\Modbus.ewp' -build Modbus -log warnings +``` + +Host 结果: + +| 套件 | 检查数 | 结果 | +| --- | ---: | --- | +| position | 33 | PASS | +| plc | 202 | PASS | +| core | 214 | PASS | +| job | 307 | PASS | +| path | 227 | PASS | +| profile | 336 | PASS | +| HAL | 3494 | PASS | +| Modbus data source | 68 | PASS | +| **合计** | **4881** | **PASS** | + +IAR 结果:`Total number of errors: 0`,`Total number of warnings: 0`。 + +## 4. 基础运动与历史专项结果 + +| 用例 | 主要验收结果 | 状态 | +| --- | --- | --- | +| 四轴独立 PULSE/DIR | Q0~Q3 可独立运行;100 kHz 周期 10 µs、约 50% 占空比;无窄脉冲和停止后残余 | PASS | +| 双轴方向/回零 | 两轴 `+200/-200` 对称 | PASS | +| 方向建立/换向延时 | 换向静默实测 9.9995 ms,符合 SFD907=10 ms | PASS | +| 同方向段衔接 | 不再无条件插入 10 ms;仅首次建立或实际换向等待 | PASS | +| CW/CCW 互锁 | Q0=300、Q1=200;非活动路全程低、无重叠 | PASS | +| CW/CCW 末脉冲 | 最后完整高/低周期结束后停止,不再截断为 2.5/5.0 µs | PASS | +| 零加速段 | 不再停在 ACCEL,正常进入运行和完成 | PASS | +| 动态频率非法输入 | 超范围钳位,负频率保持上次安全目标 | PASS | + +## 5. P12 Modbus 动态频率 + +执行命令: + +```powershell +python HostComputer\plsr_modbus_frequency_test.py --port COM5 +``` + +波形证据:`Document/PLSR_document/波形/2026-08-10_18-28-06.bin`。 + +| 写入值 | 期望 | 真机/波形结果 | +| ---: | --- | --- | +| 1000 | 1000 Hz | 1000 Hz 平台,周期 1000.0 µs | +| 4000 | 平滑升至 4000 Hz | 4000 Hz,周期 250.0 µs | +| 500 | 平滑降至 500 Hz | 500 Hz,周期 2000.0 µs | +| 0 | 使用默认 1000 Hz | 1000 Hz 短平台明确存在 | +| 8000 | 钳位至 5000 Hz | 5000 Hz,周期 200.0 µs | +| -1 | 保持上次安全目标 | 5000 Hz 平台连续,无停止或异常频率 | +| 2000 | 恢复 2000 Hz | 2000 Hz,周期 500.0 µs | + +结果:频率顺序 `1000→4000→500→1000→5000→5000→2000` 正确;段间连续、 +无断流、无窄脉冲、无截断和额外边沿;32 位频率使用功能码 `0x10` 原子写入。 + +**结论:PASS。** + +## 6. P13 Modbus 命令与状态接口 + +执行命令: + +```powershell +python HostComputer\plsr_modbus_control_test.py --port COM5 +``` + +波形证据:`Document/PLSR_document/波形/2026-08-10_18-41-17.bin`。 + +| 验收项 | 结果 | +| --- | --- | +| COMMIT 完整校验 | PASS | +| COMMIT 后修改 S0,START 必须拒绝 | PASS | +| START 排队并加速至 2000 Hz | PASS | +| PAUSE 减速、重复序号幂等 | PASS | +| 暂停期间全低 | 约 682 ms 无残余边沿 | +| RESUME 重新加速 | PASS,无额外首沿 | +| STOP_DECEL | 平滑减速,完整末脉冲后停止 | +| 最终计数 | 逻辑位置 103550;任务/物理脉冲均 3550 | + +PAUSE 与 STOP_DECEL 的逐脉冲减速序列对称;全波形升降沿各 3550,无悬空沿, +无小于 1 µs 毛刺,最终保持低。 + +**结论:PASS。** + +## 7. P14 四轴 100 kHz 计数与 P16 实时性能 + +执行命令: + +```powershell +python HostComputer\plsr_modbus_counter_stress_test.py --port COM5 +``` + +### 7.1 功能和计数 + +- 四轴目标均为 200000 脉冲,100 kHz。 +- Q0/Q1 获得 TIM9/TIM12 硬件计数租约;Q2/Q3 使用软件回退。 +- 自动脚本的任务计数和本次 `physicalPulses` 增量均为 200000。 +- 已有四通道逻辑分析仪通过记录:Q0~Q3 各 200000 个物理上升沿,周期中位 + 10.000 µs,范围 9.990~10.010 µs,高宽 5.000 µs,无小于 0.5 µs 窄脉冲、 + 启动毛刺或停止残余。 + +历史调试中曾出现 Q0/Q1 各 199999、Q2 多 9 个完整周期等问题;这些结果属于修复前失败样本, +不能作为最终验收值。 + +### 7.2 P16 性能结果 + +最终已记录的 PULSE/DIR 通过样本包括: + +| 指标 | 实测最大值 | 预算 | 结果 | +| --- | ---: | ---: | --- | +| 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 | + +后续 P17 回归也得到 PlsrProcess 自身 68175 cycles、输出 ISR 1105 cycles、计数 ISR +961 cycles,均在预算内。响应墙钟偶尔超过 1 ms 时,自身 CPU 时间仍达标,差值来自高优先级 +定时器中断抢占。 + +**结论:PASS(发布前回归)。** 最终 AB 边界修复修改了共享 HAL;发布前应再次烧录最终工作区, +硬复位后重跑脚本并重新统计 Q0~Q3 各 200000 个端子上升沿。 + +## 8. P15 四轴软限位矩阵 + +执行命令: + +```powershell +python HostComputer\plsr_modbus_soft_limit_matrix_test.py --port COM5 +``` + +| 轴/方向 | 频率 | 输出脉冲 | 最终位置 | 结果 | +| --- | ---: | ---: | ---: | --- | +| 轴0 正限位 | 500 Hz | 201 | 1000001 | PASS | +| 轴1 正限位 | 2000 Hz | 1000 | 1000000 | PASS | +| 轴2 负限位 | 500 Hz | 201 | -1000001 | PASS | +| 轴3 负限位 | 2000 Hz | 1000 | -1000000 | PASS | + +四轴正负软限位均在 ±1 脉冲窗口内停止,错误码和复位正确。历史上轴1曾超停 2 脉冲, +加入当前硬件频率和 1 ms 保护采样补偿后关闭。 + +**结论:PASS。** + +## 9. 双 AB 100 kHz 专项(P17) + +三组用例均使用 Q0/Q1 与 Q2/Q3 两组固定 AB 配对、TIM9/TIM12 硬件周期计数, +每组分别硬复位后执行: + +```powershell +python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case independent +python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case simultaneous +python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case dynamic +``` + +### 9.1 Independent + +证据:`Document/PLSR_document/波形/2026-08-10_14-45-28.bin`。 + +- CH0=CH1=100000,CH2=CH3=200000,升降沿相等。 +- Q0/Q1 正向,Q2/Q3 反向;相序分别严格为 + `00→10→11→01→00` 和 `00→01→11→10→00`。 +- 首跳前为 00;两相首沿间隔 2.55/2.45 µs,落在 2.50±0.05 µs 边界。 +- 第一组先停,第二组继续约 1.112 s;停止组无残余边沿。 +- D1468=382 cycles,小于 420 cycles。 + +**结论:PASS。** + +### 9.2 Simultaneous + +证据:`Document/PLSR_document/波形/2026-08-10_15-14-08.bin`。 + +- 四通道各 200000 个上升沿和下降沿。 +- 两组末周期完整回 00,停止后 1 ms 全低。 +- 正反相序正确,无反转、无同时跳变、无窄脉冲。 +- D1468=392 cycles,小于 420 cycles;输出 ISR=997 cycles,小于 1680 cycles。 +- “同时完成”指相同目标;两个 START 由 9600 bit/s Modbus 串行提交,实际末沿允许相差一次 + 通信事务时间。 + +**结论:PASS。** + +### 9.3 Dynamic + +最终证据:`Document/PLSR_document/波形/2026-08-10_18-13-13.bin`。 + +| 验收项 | 最终结果 | +| --- | --- | +| 四通道边沿 | CH0=CH1=CH2=CH3=600000,升降沿相等 | +| 频率序列 | 100 kHz→50 kHz→100 kHz,周期 10.000/20.000 µs | +| 降频/升频边界 | 额外沿 0、窄脉冲 0、同跳 0、非法跳变 0 | +| PAUSE | 合法单步回 00;末脉冲 5.000 µs 完整;约 864 ms 全低无沿 | +| 另一组连续性 | 暂停期间 Q2/Q3 连续运行,约 86401 沿 | +| RESUME | 从 00 重建正向相序,第一周期完整,无额外沿 | +| 首沿 | Q0/Q1=2.55 µs,Q2/Q3=2.44 µs;首跳前 00 | +| 完成 | 两组末周期完整回 00,停止后全低 | +| 自动性能检查 | PASS,含输出 ISR 与 D1468 预算 | + +**结论:PASS。** + +### 9.4 AB 缺陷关闭轨迹 + +| 轮次/证据 | 发现的问题 | 后续状态 | +| --- | --- | --- | +| 早期 AB 捕获 | 每相 7501 而非 7500;首沿约 0.33~0.37 µs 同升 | 已修复 | +| independent 初测 | 每通道多 1 个悬空沿、停在 11、首沿仅约 0.33 µs | 已修复 | +| simultaneous 初测 | CH2 多 1 沿并悬高约 809 µs;D1468=443 cycles | 已修复,最终 CH2=200000、D1468=392 | +| `15-24-38.bin` | dynamic COUNTER_FAULT、PAUSE 未生效、停止边界悬高 | 已修复 | +| `17-06-37.bin` | 降频边界拉长/多余沿、PAUSE 起点悬高约 970 µs | 已修复 | +| `17-49-15.bin` | 降频边界 2.4 µs 短脉冲、PAUSE 出现 11→00 同跳 | 已修复 | +| `18-13-13.bin` | 所有自动与波形检查干净 | 最终 PASS | + +禁止恢复曾经使用的“运行中重写 CNT 保相位”方案;该方案会产生额外边沿、4 µs 毛刺和 +错误总计数。 + +## 10. Modbus M/WAIT 位数据链 + +执行命令: + +```powershell +python HostComputer\plsr_modbus_bit_input_test.py --port COM5 +``` + +自动和波形结果: + +- FC05 写 M123、FC01 回读成功。 +- M123=0 时,1000 Hz、500 脉冲任务完整结束后稳定进入 WAIT。 +- M123 由 0→1 后 WAIT 释放并正常 COMPLETED,不再输出脉冲。 +- FC02 读取 X123=0;写 M123 不会改变 X123,证明 X/M 位空间分离。 +- Q0 严格 500 个上升沿和 500 个下降沿;主段周期 1000.0 µs、高宽 500.0 µs。 +- 第 500 个脉冲完整下降沿收尾,WAIT 和释放后均保持低,无窄脉冲或残余。 + +第一次脚本曾把成功的 START `QUEUED=1` 错当作失败;修正上位机期望值后通过,固件无需为此改变。 + +**结论:M/WAIT 生产链 PASS。** 实际 X 输入、EXT 上升沿和正/负硬限位仍需板卡 +“X 点号→GPIO/输入扫描”映射;HM 独立映像尚未定义掉电保持介质,因此这些项目为 BLOCKED。 + +## 11. HSD/SFD 持久化板测 + +测试脚本: + +```powershell +python HostComputer\plsr_persistence_board_test.py --port COM5 --phase +``` + +### 11.1 HSD + VBAT 正常掉主电恢复 + +| 阶段 | 关键结果 | +| --- | --- | +| diagnostics | HSD valid=0x03,generation=688;SFD 尚无有效槽 | +| hsd-prepare | generation=692,CRC=`0x1121309F`,本次启动保存 4 次 | +| 保持 VBAT、断主电后 hsd-verify | generation 和 CRC 完全一致,flags=0x0006 | + +四轴已知位置全部恢复、`position_valid=1`、无自动输出。 + +**结论:PASS。** + +### 11.2 运行中掉电/复位安全恢复 + +| 阶段 | 关键结果 | +| --- | --- | +| busy-prepare | HSD generation=694,CRC=`0xC1F355DF`,busy 检查点已提交 | +| 运行中直接断主电后 busy-verify | generation/CRC 保持,flags=0x000A | + +重启后所有轴 IDLE、没有自动续跑、无输出、所有轴 `position_valid=0`。 + +初版脚本错误地等待运动期间 HSD dirty 清零;运行位置持续发布会合法地重新置 dirty。 +脚本现改为检查 generation 和 save_count 确实前进。 + +**结论:PASS。** + +### 11.3 SFD Flash 保存与上电加载 + +- 首次真实保存生成 SFD generation=1、CRC=`0x5C57E27F`、valid mask=0x01、save_count=1。 +- 单 Bank Flash 擦写期间第一次 Modbus 状态读取超时,但只读诊断确认保存已经成功。 +- 上位机已改为 SAVE 请求只发送一次;擦写阻塞期间只重试读取,绝不重发 SAVE。 +- SFD clean 状态再次 SAVE 正确 no-op,没有重复擦写。 +- 掉电重启后 generation 和 CRC 一致,`last_sfd_load=OK`。 + +**结论:CRC 记录保存和上电加载 PASS。** 当前只形成一个有效槽;“损坏 newest 后回退旧槽”的 +真机破坏性用例尚未执行,不能据此宣称 A/B 坏 CRC 回退已在板上通过。 + +### 11.4 运动中禁止 Flash 擦写 + +- 长任务运行期间发送 `SAVE_CONFIG`,内核返回 `BUSY`。 +- SFD generation 保持 1,本次启动 `sfd_save_count` 不增加。 +- 脚本随后自动 `STOP_IMMEDIATE` 清理运动。 + +**结论:PASS。** + +## 12. P18 Modbus 长稳 + +执行命令: + +```powershell +python HostComputer\plsr_modbus_long_stress_test.py --port COM5 --duration 60 +``` + +证据: + +- `HostComputer/long_stress_logs/plsr_long_stress_20260810_191803.csv` +- `HostComputer/long_stress_logs/plsr_long_stress_20260810_191803.json` + +结果摘要: + +| 项目 | 结果 | +| --- | --- | +| 一致性样本 | 60 | +| 动态频率 | 每 10 s 在 100 kHz/50 kHz 间切换 | +| Modbus 有效帧增量 | 375 | +| TX 帧增量 | 375 | +| CRC/非法功能/非法地址/非法值/丢帧/UART 错误 | 全部 0 | +| DMA 接收重启失败增量 | 0 | +| 四轴最终计数 | `[4541617, 4536532, 4531732, 4526921]` | +| 结束状态 | 四轴软件停止,任务计数=物理计数=逻辑位置 | + +**结论:60 秒 Modbus 长稳冒烟 PASS。** + +USB 线未接,系统只枚举到 FTDI Modbus `COM5`,因此以下 30 分钟 USB CDC 并发测试按用户决定暂缓: + +```powershell +python HostComputer\plsr_modbus_long_stress_test.py ` + --port COM5 --duration 1800 --usb-port COMx --usb-rate 64000 +``` + +状态:**DEFERRED**。60 秒测试不能替代 30 分钟及更长 USB/Modbus/四轴并发验收。 + +## 13. 已确认修复且不得回退的问题 + +1. PWM 端子负逻辑、启动首脉冲和段尾截断问题。 +2. 零加速参数导致状态机停在 ACCEL。 +3. 同方向段间错误插入 10 ms 延时。 +4. AB 双定时器调频相位漂移,以及运行中重写 CNT 造成的额外沿/毛刺。 +5. AB 启动两相近似同时上升、每相多 1 沿。 +6. AB 末周期悬高、双组近同时完成竞态及 D1468 超预算。 +7. AB 降频边界两相不对称、短/长脉冲和 PAUSE 非法同跳。 +8. CW/CCW 非活动通道高电平和最后脉冲截断。 +9. PAUSE 后 RESUME 保持 0 Hz、无法完成。 +10. 2000 Hz 软限位超停 2 脉冲。 +11. P14 将上电累计 `physicalPulses` 当成本任务计数。 +12. P16 `PlsrProcess` 超过 1 ms;HSD 检查点 CRC 热点已优化并合并。 +13. SFD 擦写期间上位机一次读超时被误判为 SAVE 失败;现只重试读取。 + +## 14. 未关闭项与后续测试 + +| 优先级 | 项目 | 当前状态 | 关闭条件 | +| ---: | --- | --- | --- | +| 1 | 最终工作区 P14/P16 发布回归 | PENDING | P17 硬复位后脚本 PASS;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/扫描表,完成真实输入板测 | +| 5 | HM 掉电保持 | BLOCKED | 明确 HM 存储介质、容量、恢复语义并完成掉电测试 | +| 6 | 更长时间长稳 | PENDING | 在 30 分钟通过后按交付要求延长,并保存 CSV/JSON 与必要波形窗口 | +| 7 | 正式发布清理 | PENDING | 关闭板测启动和调试快照,保留正式控制链;生产构建完整回归 | + +## 15. 最终判定 + +截至本报告日期: + +- P12、P13、P15、双 AB 三用例、M/WAIT、正常 HSD/SFD 持久化和 P18 60 秒冒烟均已通过。 +- P14/P16 已有通过基线,但最终发布前仍应针对当前完整工作区再做一次 PULSE/DIR 四通道回归。 +- USB 30 分钟并发已明确暂缓;真实 X/HM 和破坏性 CRC 回退尚未关闭。 + +项目可以进入“剩余专项验收和正式发布清理”阶段,但尚不满足“全部真机测试关闭”或 +“严格对标全部完成”的声明条件。 diff --git a/Document/PLSR_document/PLSR需求规格统计_V1.3.md b/Document/PLSR_document/PLSR需求规格统计_V1.3.md index 0ee39ab..d07f5f0 100644 --- a/Document/PLSR_document/PLSR需求规格统计_V1.3.md +++ b/Document/PLSR_document/PLSR需求规格统计_V1.3.md @@ -1069,7 +1069,7 @@ Flash数据无效或保存失败 | P15软限位矩阵 | 已板测通过 | 四轴正负方向、500/2000Hz均在目标软限位±1脉冲内停止 | | P16实时性能 | 已板测通过 | `PlsrProcess`自身最大53748 cycles(319.929us),预算小于168000 cycles;响应最大85655 cycles | | X/M/HM位数据源 | 数据结构和通信代码已实现 | 已分离三个10000位映像,M接FC01/05/0F、X接FC02、PLSR `readBit`已接入;实际X GPIO扫描映射及HM掉电保持尚未关闭 | -| 双AB 100kHz | 代码、Host及IAR阶段完成 | 安全首沿、TIM9/TIM12双硬件计数、真实00边界PAUSE、任务态延后收尾和D1468完整快速窗口已写;必须以四通道逻辑分析仪关闭首次边沿、相序、相位、严格周期数、动态调频、PAUSE/RESUME和独立停止 | +| 双AB 100kHz | `independent`与`simultaneous`真机通过,仅`dynamic`待测 | 独立停止严格100000/200000沿、等长并发四路严格200000沿,首沿2.50us±0.05us、正反相序、末周期00、停止全低均通过;D1468分别382/392 cycles。仍须关闭50kHz调频、00边界PAUSE及RESUME | | Modbus/USB长稳 | 代码和自动脚本完成 | USB优先级已降为4,0x47运行诊断、D1516~D1537设备端USB统计和P18/K4无软限位台架夹具已提供;30分钟及更长并发板测尚未关闭 | | HSD/SFD持久化 | A/B、CRC、诊断窗口及分阶段板测工具完成 | 检查点按四轴有效且无32位发布溢出的保守AND写入;VBAT断主电、Flash擦写/坏CRC、异常复位和反复保存的正式板测尚未关闭;破坏性诊断默认禁用 | diff --git a/Document/PLSR_document/交接提示词_2026-08-10.md b/Document/PLSR_document/交接提示词_2026-08-10.md deleted file mode 100644 index 7915556..0000000 --- a/Document/PLSR_document/交接提示词_2026-08-10.md +++ /dev/null @@ -1,80 +0,0 @@ -# PLSR 项目交接提示词(2026-08-10) - -> **用途**:将此文件全文粘贴到新的 Codex/对话中,即可无缝接替上一段长对话(ZCode-deepseek 会话)的全部任务。 -> 交接人:ZCode 会话(deepseek);接收人:Codex 新会话。 -> 项目:信捷 XDM 兼容 PLSR 脉冲定位模块(STM32F407 + IAR EWARM 8.3 + uC/OS-II + Modbus RTU 从站)。 - ---- - -## 一、项目一句话 - -在 STM32F407 上实现信捷 XD/XL 系列 PLC 的 `PLSR S0 S1 S2 D` 多段脉冲定位指令(对标 XDM-60T4-E),上板真机验证,当前处于**收尾阶段:只剩遗留精度验证与压力测试/清理**。 - -- 仓库:`F:\Xinje_Modbus_IAR\TrainCamp_yuwenhao_modbus`(git,当前分支 `deepseek`,主分支 `master`) -- 指令语义:S0=段表(每段 10 字:频率/脉冲/等待码/跳转),S1=模式/起始段,S2=参数组(K0=HSD460-539,K1-K4=SFD950+),D=Y0-Y3 -- 十态状态机:UNINITIALIZED/IDLE/ACCEL/RUN/DECEL/WAIT/PAUSED/COMPLETED/STOPPED/ERROR -- 速度规划:Q32.32 定点,中断中无浮点,直线/S曲线/正弦加减速,动态调频 -- 输出特性:Y 输出为**集电极开路(OC/NPN 漏型)负逻辑**,正向=低电平=ON -- 参考手册:`Document/PLSR_document/信捷XD_XL系列PLC定位控制手册_PD02_20260510_V1.3.pdf` - -## 二、⚠️ 进入任务前必读(当前工作区状态) - -1. **工作区有 22 个文件未提交**(P14 硬件计数新增 437 行 + 软限位修复 + AB 预热机制等全在工作区,未 commit)——先 `git status` 查看,与老对话确认提交策略后再动工。 -2. 最近提交:`fdbbe71` P13(Modbus 控制窗口)、`7750e43` P6(四轴并发自测)、`c776438` P5(软限位保护)。 -3. 上板自测 `plsr_self_test.c` 仍在(收尾时要删)。 -4. 真机验证环境:STM32F407 + 逻辑分析仪导出 16 通道 .bin(100MS/s,2字节/采样小端 uint16),波形分析脚本 `Document/PLSR_document/波形/plot_waveform.py`。 - -## 三、主线:5 批规划与完成度(已核实代码) - -| 批次 | 内容 | 状态 | 证据 | -|---|---|---|---| -| ① | Modbus 数据源正式接入(真实 D/HD/FD → S0/S1/动态调频) | ✅ 完成 | `plsr_modbus_data.c` readWord=PlsrModbusReadWord;S0=D1600/S1=D1700 经 0x10 原子写入;动态调频 D1000/D1100 已真机验证 | -| ② | Modbus 命令与状态接口(START/STOP/PAUSE/状态/错误码/位置/计数) | ✅ 完成 | fdbbe71:COMMIT/START 分离、停止/暂停/继续/位置设置/清零/保存、重复序号幂等、四轴状态、32/64 位版本一致性;错误码 5/6 + RESET_ERROR 已验 | -| ③ | FOLLOW 与前馈补偿 | ✅ 关闭(不实现) | P14 按手册确认 FOLLOW/FOLLOW_AB 是**独立随动指令**,PLSR 仅保留参数字段存储校验,不实现随动运算。**勿再开发** | -| ④ | 遗留精度问题 | ⚠️ 见下表 | — | -| ⑤ | 压力测试与正式收尾 | ❌ 大部分未做 | 见第五节 | - -## 四、遗留精度问题(④批)——按优先级 - -| # | 问题 | 状态 | 验证/修复方法 | -|---|---|---|---| -| 1 | **P14 硬件计数 -1**(Q0/Q1=199999 而非 200000;TIM9/TIM12 硬件计数;Q2/Q3 软件回退精确) | 🟡 代码已修**未上板验证**、未提交 | 修复:计数器延迟到源 OC 启动 UG 完成后再配置/使能,消除无物理脉冲的内部首沿;验收=四轴均 200000。用 `HostComputer/plsr_modbus_counter_stress_test.py`(四轴 100kHz 压力) | -| 2 | **AB 启动边沿**(段1 起步 A-R/B-R 同升,各多 1 沿 → 7501/7501;首周期从 11 开始非 00→10→11;稳定复现 6 版固件) | 🔴 预热机制 `abStartupPriming` 已写(plsr_hal_f407.c:845/893/1065)**未上板验证** | ① 两 ISR 各翻转调试 GPIO 抓先后;② `SetPwmMode1` 移到 `SetCc1e` 之前;③ 启动后读两路 CNT 确认初值。验收=严格 7500 | -| 3 | **CW/CCW 段尾截断窄脉冲**(段末最后脉冲仅 2.5~5µs 高,未走完完整周期) | 🟡 未关闭(主验收已过,微秒毛刺标准不达标) | 停止应延迟到最后脉冲 CC1 匹配(下降沿)之后 | -| 4 | 软限位边界 | ✅ 已关闭 | P15 高频采样补偿(2000Hz 提前 1 脉冲进入减速)→ 四轴正/负 500/2000Hz 矩阵 ±1 脉冲窗口全过 | -| 5 | AB 段3 反向切换 10ms 过渡期 / 方向信号 Q5 1µs 毛刺 | 🟢 低优先级,可暂缓 | — | - -详细记录见 `Document/PLSR_document/问题总清单_2026-08-10.md`、`已知问题清单_2026-08-09.md`。 - -## 五、压力测试与正式收尾(⑤批)——下一步主线 - -1. **上板验证**(立即):AB 预热(7500)、硬件计数 -1(四轴 200000)、软限位回归。 -2. **四轴 100kHz AB 模式**:只测过 PULSE/DIR 100kHz;AB 高频加速段重定相干扰未实测。 -3. **双 AB 并发**:Q0/Q1 + Q2/Q3 同时 AB 运行(独立轴停止互不影响已有实现,未实测并发)。 -4. **Modbus 压力**:`HostComputer/plsr_modbus_counter_stress_test.py` 已有部分覆盖(12 次状态读取),补全并发读写压力。 -5. **掉电恢复**:Flash 存储(SFD900~1419 片内 Flash 已实现)掉电/上电恢复未实测。 -6. **性能测试**:`HostComputer/PLSR_MODBUS_PERFORMANCE_TEST.md` 已建,脚本未写。 -7. **清自测收尾**:删 `plsr_self_test.c`(及 main 中的调用)、调试快照、tmp 资料清理、git 历史大对象(1GB bin 松散对象)清理。 - -## 六、已固化的关键决策(勿回退) - -- Y 输出 OC 负逻辑(正向=低=ON);PULSE/DIR、AB、CW/CCW 三模式,CW/CCW 返回 NOT_SUPPORTED -- AB 仅允许 D=Y0/Y2(Q0/Q1、Q2/Q3),调频同时更新 A/B 不重置相位,正常完成只在完整 00 周期边界停止 -- 段间衔接仅方向变化时走 DIR_SETTLING(~2ms),否则直连 -- 每 tick `PlsrProfileSyncPulses` 校准 profile 虚拟计数到硬件实发数(段尾冻结修复) -- 地址映射固定:HSD 384B / SFD RAM 缓存 2080B / SD 192B / SM 4B,`plsr_address_map.h` -- PLSR 阶段不实现 FOLLOW 随动运算(见批次③) - -## 七、代码地图与测试体系 - -- `PLSR/Inc|Src/`:plsr_core(状态机/命令)、plsr_job(任务快照解析/校验)、plsr_path(多段执行器)、plsr_profile(速度曲线)、plsr_hal_f407(定时器/GPIO/硬件计数)、plsr_modbus_control(命令)、plsr_modbus_data(D 区数据源)、plsr_persistence(Flash)、plsr_self_test(上电自测)、plsr_position、plsr_resource、plc_device -- **Host 测试**:`PLSR/Test/run_host_tests.ps1`(1268 项,`-std=c11 -Wall -Wextra -Werror -DPLSR_HOST_TEST -lm`);**IAR 编译**:`IarBuild.exe EWARM/Modbus.ewp -build Modbus`(0 错误 0 警告基线) -- **真机测试脚本**(HostComputer/):`plsr_modbus_control_test.py`(命令)、`plsr_modbus_counter_stress_test.py`(四轴 100kHz+计数)、`plsr_modbus_frequency_test.py`(动态调频)、`plsr_modbus_soft_limit_matrix_test.py`(限位矩阵)、`modbus_timing_tester.py`(时序) -- **波形验证**:`Document/PLSR_document/波形/*.bin` + `plot_waveform.py`(时频分析:总脉冲数/段结构/频率命中/段间 gap) -- **文档**:方案设计书 V1.0、需求规格统计 V1.3、对标追踪矩阵、问题总清单 2026-08-10 - -## 八、建议的下一步行动(新对话从这里开始) - -1. `git status` + `git log --oneline -5` 确认工作区 22 个未提交文件内容(P14 硬件计数批次),**先上板验证再提交**(或与用户确认提交策略)。 -2. 上板验证三件套:硬件计数 -1(counter_stress 脚本)→ AB 预热(7500)→ 软限位回归(matrix 脚本)。 -3. 通过后提交 P14 批次,然后按第五节 2→7 推进压力测试与收尾。 diff --git a/Document/PLSR_document/交接文档_2026-08-08.md b/Document/PLSR_document/交接文档_2026-08-08.md deleted file mode 100644 index 10b12e5..0000000 --- a/Document/PLSR_document/交接文档_2026-08-08.md +++ /dev/null @@ -1,89 +0,0 @@ -# PLSR 模块交接文档(2026-08-08) - -> 交接人:ZCode 会话(deepseek) -> 接收人:Codex -> 项目:信捷 XDM 兼容 PLSR 脉冲定位模块(STM32F407 + IAR EWARM 8.3 + uC/OS-II + Modbus RTU) - ---- - -## 1. 项目概况 - -在 STM32F407 上实现信捷 XD/XL 系列 PLC 的 PLSR 多段脉冲定位指令,对标 XDM-60T4-E: - -- **指令语义**:`PLSR S0 S1 S2 D`——S0=段表(每段 10 字:频率/脉冲/等待码/跳转),S1=模式/起始段,S2=参数组(K0=HSD460-539,K1-K4=SFD950+),D=Y0-Y3 -- **十态状态机**:UNINITIALIZED/IDLE/ACCEL/RUN/DECEL/WAIT/PAUSED/COMPLETED/STOPPED/ERROR -- **速度规划(P2)**:Q32.32 定点,中断中无浮点,直线/S曲线/正弦三种加减速,动态调频 -- **硬件(P3a)**:Q0=PF6/TIM10_CH1(AF3)、Q1=PF8/TIM13_CH1(AF9)、Q2=PF7/TIM11_CH1(AF3)、Q3=PF9/TIM14_CH1(AF9),定时器时钟 168MHz(按 RCC 实际配置计算) -- **输出特性**:XDM 的 Y 输出为**集电极开路(OC/NPN 漏型)**——只有导通(≈0V)和截止(悬空)两态,无主动高电平;正向 = 低电平 = ON(手册 1-4-4 节,PDF 页 166) -- **参考手册**:`Document/PLSR_document/信捷XD_XL系列PLC定位控制手册_PD02_20260510_V1.3.pdf`(1-4-2 节输出方式=OC;1-4-4 节集电极开路+负逻辑;1-4 节接线图 R=3.3K 上拉;PDF 50 页加减速斜率定义) - -## 2. 代码结构与测试体系 - -- `PLSR/Inc|Src/`:plsr_core(状态机/命令)、plsr_job(任务快照解析/校验)、plsr_path(多段路径执行器)、plsr_profile(速度曲线 Q32.32)、plsr_hal_f407(定时器/GPIO)、plsr_persistence(Flash 存储)、plsr_self_test(上电自测)、plsr_resource、plc_device -- **host 测试**:`PLSR/Test/run_host_tests.ps1`——6 套件共 **1268 项**(plc_device 180 / core 174 / job 115 / path 227 / profile 182 / hal 390),`-std=c11 -Wall -Wextra -Werror -DPLSR_HOST_TEST -lm` -- **IAR 编译**:`EWARM/Modbus.ewp` 配置名 Modbus,`IarBuild.exe Modbus.ewp -build Modbus`,当前 0 错误 0 警告 - -## 3. 已完成并验证的修复(勿回退!) - -| # | 问题 | 修复 | 验证证据 | -|---|---|---|---| -| 1 | 无方波(PF6 恒高 672ms) | `PlsrHwConfigurePwm` 补配 `CCMR1.OC1M=110`(PWM 模式1)+ OC1PE | 逻辑分析仪见正常方波 | -| 2 | 多余脉冲 7517(理论 7500) | `CR1.ARPE=1`(ARR 预装载)+ `EGR.UG` 启动加载影子 | 多文件实测总脉冲 **7500 正好** | -| 3 | 段间 14.9ms 延时 | `PlsrHwStartPulse` 仅方向变化时走 DIR_SETTLING(首次/换向/换点) | 段间 → ~2ms | -| 4 | 段尾冻结收尾(段3 150Hz 平台 38 脉冲,800ms) | core tick 中 `PlsrProfileSyncPulses` 每 tick 把 profile 虚拟计数校准到 `PlsrHwGetEmittedPulses()` | 段3 800ms→520ms,减速平滑;总脉冲仍 7500 | - -**上板自测**:`plsr_self_test.c`——3 段(2000Hz/1000 脉冲、5000Hz/6000、1000Hz/500),K1 参数,方向点 Y4(SFD906=4),上电延时 1s 后启动,完成后可删除。 - -## 4. ⚠️ 未解决问题:段首"第一周期压缩"(②) - -### 现象(多文件实测稳定复现) -段2/3 起步波形:**高 50ms + 低 0.5ms → 恒定 1000Hz**(理论:10Hz 起步,第一周期 100ms:高 50ms+低 50ms,之后每 1ms +10Hz 平滑加速到目标)。时频图上表现为**折线**(陡→缓),段2 加速段从 ~1000Hz 开始(缺失 0→1000Hz 的加速)。 - -### 已排除(全部有数据证据) -1. ~~PSC 写入立即生效~~ —— 快照实锤:cnt 每 1ms +653(PSC=256 正常速度),PSC 预装载行为正常(RM0090 也确认 buffered) -2. ~~divider 计算异常~~ —— 暴力扫描 1~20000Hz 无任何 (PSC, 33008) 组合;快照显示 psc/arr 全部正确 -3. ~~启动参数错误~~ —— 快照 [0]:psc=256, arr=65369(10Hz), ccr=32684, cnt=0,ARPE=1(cr1=0x80),CCMR1=0x68 —— 完全正常 -4. ~~job 参数映射~~ —— startSpeed=0(SFD958 默认),地址映射正确 -5. ~~profile 斜率错误~~ —— slope=defaultSpeed/accelMs=10Hz/ms 正确;OS_TICKS_PER_SEC=1000u(1ms)确认 - -### 关键证据链(快照:段3 启动 + 周期结束) -``` -Snap[0]: reason=0, psc=256, arr=65369(10Hz), ccr=32684, cnt=0 ← 启动正常 -Snap[1]: reason=2, psc=2, arr=55999(divider(1000Hz)!!), ccr=27999, cnt=67 ← 第一个更新事件 -Snap[2..7]: 全部 (psc=2, arr=55999) —— 之后恒定 1000Hz -``` -- 第一周期结束时 ARR 影子加载 = **divider(1000Hz)** = defaultSpeed -- 波形显示第一"周期"仅 50.5ms,但 profile 理论到 1000Hz 需 99ms(10Hz/ms)——**50.5ms 内 profile 推进到 ~1000Hz 无法用 1ms 节拍解释** - -### 新疑点(尚未深查) -- **段间静默 ≈49ms**(段1 停止 → 段2 PWM 启动,预期 ~2ms)——"高 50ms"可能部分是"悬空被分析仪读高"(PF6 高阻),真正问题可能是"段间为什么等 49ms" -- 或"影子被提前加载(33008 之谜)"——CNT 在 33008(50.5ms)回绕,但 33008 无任何代码来源 - -### 下一步建议(tick 级快照已编译好,烧录即用) -`PlsrHwDbgSnap[64]`:PwmBegin 重置,之后每 tick(1ms)记录 (reason, psc, arr, ccr, cnt)。用户暂停后读数组 → CNT 演化直接分辨: -- CNT 正常走到 65369(100ms 回绕)→ "50.5ms"另有解释(查段间静默) -- CNT 在 33008(50.5ms)回绕 → 影子提前加载实锤(查更新事件触发条件) -**注意**:② 不影响脉冲总数(7500)和位置精度——可搁置,也可继续查。 - -## 5. 调试基础设施(重要!) - -1. **波形分析脚本** `Document/PLSR_document/波形/plot_waveform.py`: - - 逻辑分析仪 bin 格式已破译:**16bit/采样(小端 uint16),信号在 CH0(bit0)= m[0::2]&1,采样率 100MS/s**(自动反推,窗口 5s) - - 用法:`python plot_waveform.py ` → 输出时频图 + 三段结构 + 脉冲数 -2. **寄存器快照**:`plsr_hal_f407.c` 中 `PlsrHwDbgSnap`(当前 64 条 tick 级,P4 后删除) -3. **豆包识图**(用户已开通视觉):火山引擎 v3 端点 `https://ark.cn-beijing.volces.com/api/v3/chat/completions`,模型 `doubao-seed-2-1-pro-260628`(支持图片),key 见 `~/.zcode/v2/config.json` provider `87847afb...`。**读 IAR Watch 截图数字用它,比 GLM 准** -4. **教训**:GLM 识图(glm_vision.py,glm-4v-flash)数字字段会错位(如 reason 读出 256)——**关键数值务必用豆包或人工确认** - -## 6. 待办路线(原计划) - -- **P3b**:四轴并发(当前只有单轴验证)、AB 相输出(F407 裸定时器无 DMA/主从,需 OC 中断移相)、TIM6 10kHz 节拍、NVIC 优先级表 -- **P4**:位置闭环(HSD 累计/掉电保存/SD 发布/I 事件/限位/齿隙/检查点)——② 建议在 P4 上板调试时一并处理 -- **P3.5**:Modbus 最小映射(真实联调) -- 位置/方向极性、绝对定位(DRVA)语义完善 - -## 7. 给 Codex 的提示 - -1. 先跑 `PLSR/Test/run_host_tests.ps1`(1268 项全绿)和 IAR 编译确认基线 -2. 第 4 节的 ② 若继续查:优先用 tick 级快照拿 CNT 演化,一次定位;若搁置,不影响 P4 推进 -3. 上板验证流程:烧录 → 逻辑分析仪抓 PF6(飞线到 MCU 引脚,勿接 24V 端子排)→ `plot_waveform.py` 分析 -4. 自测固件含调试快照代码(标记"P4 后删除"),正式版需移除 diff --git a/Document/PLSR_document/已知问题清单_2026-08-09.md b/Document/PLSR_document/已知问题清单_2026-08-09.md deleted file mode 100644 index 444c74d..0000000 --- a/Document/PLSR_document/已知问题清单_2026-08-09.md +++ /dev/null @@ -1,42 +0,0 @@ -# PLSR 已知问题清单(2026-08-09) - -> 记录上板验证中发现、尚未关闭的两个边界精度问题。 -> 两者均**不阻塞主流程**,但都卡对应验收标准,且属于"逻辑/时序偏差"而非随机抖动。 - ---- - -## 问题 1:AB 启动边沿(Q0/Q1 上升沿 7501 而非 7500) - -| 项 | 内容 | -|---|---| -| **现象** | 段1 起步瞬间 A-R 与 B-R **同时上升**(间隔 0.33~0.37µs),每相各多 1 个上升沿 → Q0=Q1=7501(应 7500)。第一周期从 11 开始,而非标准 00→10→11 | -| **复现** | 稳定复现 6 版固件:00:06、13:41、13:57、13:59、14:15(波形 bin 见 `波形/2026-08-09_*.bin`),现象、位置、结构完全一致 | -| **影响** | 启动瞬间 1 个非法跳变(00→11);多数驱动器正交解码忽略,位置误差最多 1 脉冲 | -| **已排除** | 调频毛刺(重定相路径无同升,稳定段/加速段 <10µs 间隔 0 个)——问题只在**首次启动**路径 | -| **代码现状** | `PlsrHwBeginAbOutput`(plsr_hal_f407.c):GPIO 保持 00 → UG → CNT 初值(lead 3/4T、lag 1/2T)→ 切 PWM1 → CEN → 由 update 中断分时释放引脚。**未提交工作区已加 `abStartupPriming` 预热机制(首次启动吞周期),待上板验证** | -| **根因方向** | 两路"释放"几乎同时 → 两路第一次 update 同时 → CNT 初值 1/4T 相位差未体现;或 CC1E 先于 PWM1 模式切换(RM0090:模式切换瞬间 OCREF 跳变)+ 落后相 CNT 写入 = CCR 触发比较事件 | -| **验证方法** | ① 两个 ISR 入口各翻转调试 GPIO,逻辑分析仪同时抓,确认两路 update 中断先后;② 把 `SetPwmMode1` 移到 `SetCc1e` 之前(CC1E=0 时切模式);③ 启动后读两路 CNT 确认初值 | -| **状态** | 🔴 未关闭(验收:Q0/Q1 严格 7500) | - ---- - -## 问题 2:正软限位边界精度(502 脉冲 vs 验收 499~501) - -| 项 | 内容 | -|---|---| -| **现象** | 测试:目标 +10000 脉冲、正软限位 +500、1000Hz、减速 100ms。实测 **Q0=502 个上升沿**(验收 499~501),**超停 1~2 个脉冲** | -| **波形结构** | 恒定段 rise0..459(460 个 @1000Hz 精确)→ 减速段 rise460..501(42 个,84.5ms,535→…→134Hz 平滑)→ 停止后零边沿、无毛刺 | -| **与预期偏差** | 验收"接近第 450 个脉冲开始减速"→ 实测 **459 开始(晚 9 个)**;理论提前量 = 1000Hz×100ms/2 = 50 脉冲,应在 500-50=**450** 触发、正好停 500 | -| **影响** | 超停 1~2 个工程单位;软限位是**安全功能**,偏差为系统性(非随机),速度/限位配置变化时超停量会随之变化 | -| **根因方向** | ① 限位触发判定的位置源(hardwarePulses / 虚拟计数 / logicalPosition)与判定 tick 采样点;② 提前量(减速距离)计算与实际减速段脉冲数(42 vs 50)不一致;③ 收尾差 1 个与 PULSE/DIR "DONE vs 硬件计数"边界可能同源 | -| **验证方法** | 调限位值/速度复测多组(如 +200/+1000、500Hz/2000Hz),统计"触发位置偏差"是否随参数线性变化,定位是判定滞后还是提前量公式 | -| **状态** | 🔴 未关闭(验收:Q0=499~501) | - ---- - -## 共同结论 - -- 两者都是**小影响、非安全级、但卡验收标准**的边界精度问题 -- 问题 1 偏**时序毛刺**(启动路径),问题 2 偏**逻辑偏差**(判定位置源/提前量) -- 建议优先级:不阻塞 Modbus/四轴主流程;问题 2 略优先(安全功能 + 系统性偏差) -- 均由 Codex 按上面"验证方法"做实验定位后再修,避免盲改 diff --git a/Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md b/Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md deleted file mode 100644 index 816b27f..0000000 --- a/Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md +++ /dev/null @@ -1,69 +0,0 @@ -# 提示词:发给 Codex 老对话(请求输出交接总结) - -> 用法:把下面【发送内容】整段复制,发给当前正在进行的 Codex 老对话。 -> 老对话回复的交接总结,将整段粘贴给新对话,作为新对话的唯一历史输入。 -> (仓库里另有一份兜底参考:`交接提示词_2026-08-10.md`,若老对话总结不完整,可一起给新对话。) - ---- - -【发送内容】 - -``` -我们即将结束本对话,把所有任务交接给一个全新的对话(新 Codex/ZCode 会话)。 -新对话没有任何本对话的历史,它只能看到两样东西: - (1) 你下面这份回复(原样粘贴给它); - (2) 仓库 F:\Xinje_Modbus_IAR\TrainCamp_yuwenhao_modbus 中的文件。 - -请【一次性、直接】输出一份完整、自包含、Markdown 格式的交接总结。 -不要提问、不要确认、不要寒暄、不要分多轮——就这一轮输出全部内容。 - -硬性要求: -1. 不假设新对话知道任何背景:所有结论、数据、文件路径、命令、编号都写全。 -2. 高密度:多用表格、编号、代码引用(文件:行号),少废话。 -3. 必须覆盖以下章节(顺序可微调,但章节都要有): - -## A. 项目概况 -- 项目一句话(做什么、对标什么硬件、当前处于什么阶段) -- 仓库路径、git 分支、最近几个提交 - -## B. 主线进度(5 批规划逐项标注 ✅ 完成 / ⚠️ 部分 / ❌ 未做) -1. Modbus 数据源正式接入(真实 D/HD/FD → S0/S1/动态调频) -2. Modbus 命令与状态接口(START/STOP/PAUSE/状态/错误码/位置/计数) -3. FOLLOW 与前馈补偿 -4. 遗留精度问题(AB 首沿、软限位、硬件计数 -1 等) -5. 压力测试与正式收尾(100kHz/双 AB/掉电/Modbus 压力/清自测) -每项给一句证据(提交号 / 测试脚本 / 文档名)。 - -## C. ⚠️ 工作区当前状态(最重要,务必写全) -- git status 所有未提交文件清单,每个文件是什么内容(哪些是待上板验证的修复、哪些是测试脚本、哪些是文档) -- 哪些代码改动【已写但未上板验证】、哪些【已验证但未提交】、哪些【既未验证也未提交】 - -## D. 未完成任务清单 -每个未完成任务给出: -- 现象(含实测数据,如 7501 vs 7500、199999 vs 200000) -- 影响/验收标准 -- 根因分析或假设 -- 代码现状(已写/未写,给出 文件:行号) -- 验证方法(用什么脚本/逻辑分析仪/命令) -- 若已写修复但未验证:修复思路一句话 - -## E. 已验证成果与固化决策(勿回退清单) -- 所有实测过的硬件特性(如 OC 负逻辑、AB 相序、段间衔接时序) -- 已修复并验证的 bug 列表 -- 明确的边界决定(如 FOLLOW 不实现、CW/CCW 不支持) -- 地址映射等固定约定 - -## F. 代码地图与测试体系 -- PLSR/ 各模块职责(core/job/path/profile/hal/modbus_control/modbus_data/persistence/self_test...) -- HostComputer/ 各测试脚本用途 -- 编译命令(IAR build、run_host_tests.ps1)与基线(当前 0 错误 0 警告?) -- 逻辑分析仪波形验证方法(bin 格式、采样率、plot_waveform.py 用法) - -## G. 下一步行动清单 -按优先级排序的具体行动(从新对话开工第一件事开始排)。 - -## H. 仅存在于对话中的信息 -凡"只在我们对话里出现过、没有落入任何文件"的关键信息(测试数据、踩坑经验、用户偏好、待确认问题),单独列一节,务必不要遗漏。 -``` - -【发送内容结束】 diff --git a/Document/PLSR_document/问题总清单_2026-08-10.md b/Document/PLSR_document/问题总清单_2026-08-10.md deleted file mode 100644 index 72cfa6e..0000000 --- a/Document/PLSR_document/问题总清单_2026-08-10.md +++ /dev/null @@ -1,99 +0,0 @@ -# PLSR 问题总清单(2026-08-10) - -> 汇总截至今日所有未关闭的问题、待验证项与待办功能。 -> 均为上板验证/代码扫描实证,非猜测。 - ---- - -## 一、未解决 Bug(4 个主问题 + 2 个低优先级) - -### 1. AB 启动边沿(Q0/Q1 = 7501 而非 7500) - -| 项 | 内容 | -|---|---| -| 现象 | 段1 起步瞬间 A-R 与 B-R 同刻上升(0.33~0.37µs),每相各多 1 个上升沿 → 7501/7501;首周期从 11 开始而非 00→10→11 | -| 复现 | 6 版固件稳定复现(00:06 / 13:41 / 13:57 / 13:59 / 14:15),现象、位置、结构完全一致 | -| 影响 | 启动瞬间 1 个非法跳变;多数驱动器正交解码忽略,位置误差最多 1 脉冲 | -| 已排除 | 调频/重定相路径无同升(稳定段 <10µs 间隔 0 个)——问题只在首次启动路径 | -| 代码现状 | `PlsrHwBeginAbOutput`:GPIO 保持 00 → UG → CNT 初值(lead 3/4T、lag 1/2T)→ 切 PWM1 → CEN → update 分时释放引脚;`abStartupPriming` 预热机制已写(首次启动吞周期)**待上板验证** | -| 根因方向 | 两路"释放"几乎同时 → 两路第一次 update 同时 → CNT 初值 1/4T 相位差未体现;或 CC1E 先于 PWM1 切换(RM0090:切模式瞬间 OCREF 跳变)+ 落后相 CNT 写入 = CCR 触发比较事件 | -| 验证方法 | ① 两 ISR 各翻转调试 GPIO 抓先后;② `SetPwmMode1` 移到 `SetCc1e` 之前;③ 启动后读两路 CNT 确认初值 | -| 状态 | 🔴 未关闭(验收:严格 7500) | - -### 2. 正软限位边界精度(502 脉冲 vs 验收 499~501) - -| 项 | 内容 | -|---|---| -| 现象 | 目标 +10000、正软限位 +500、1000Hz、减速 100ms → 实测 Q0=502 上升沿,超停 1~2 个 | -| 波形结构 | 恒定段 460 个 @1000Hz → 减速段 42 个(84.5ms,535→134Hz 平滑)→ 停止后零边沿 | -| 偏差 | 验收"接近第 450 个脉冲开始减速"→ 实测 459 开始(晚 9 个);理论提前量 = 50 脉冲(应在 450 触发、正好停 500) | -| 影响 | 超停 1~2 个工程单位;软限位是安全功能,偏差为系统性(速度/限位配置变化时超停量随之变化) | -| 根因方向 | ① 限位判定位置源(hardwarePulses/虚拟计数/logicalPosition)与判定 tick 采样点;② 提前量(减速距离)计算与实际减速段脉冲数(42 vs 50)不一致;③ 收尾差 1 与 DONE vs 硬件计数边界可能同源 | -| 验证方法 | 调限位值/速度复测多组(+200/+1000、500Hz/2000Hz),统计触发位置偏差是否随参数线性变化 | -| 状态 | 🟡 已修复待上板:制动距离改用真实硬件输出频率,新增四轴正/负、500/2000Hz矩阵测试(验收±1脉冲) | - -### 3. CW/CCW 段尾截断窄脉冲(2.5µs / 5µs) - -| 项 | 内容 | -|---|---| -| 现象 | 段1 最后脉冲只持续 2.5µs 高(0.167797→0.167800)、段2 最后脉冲 5.0µs——最后脉冲上升沿后即拉低,未走完完整周期 | -| 影响 | 计数不受影响(Q0=300、Q1=200 精确,中断计数不依赖脉冲宽度);但部分驱动器可能把窄脉冲当噪声忽略(物理少走最后 1 个脉冲) | -| 根因方向 | CW/CCW 停止时机在"最后脉冲上升沿 + 2.5~5µs"(比 CC1 下降沿早半个周期)——停止应延迟到最后脉冲 CC1 匹配(下降沿)之后 | -| 状态 | 🟡 未关闭(验收"微秒级毛刺 0 个"严格不达标;主验收已过) | - -### 4. P14 硬件计数少 1 个脉冲(Q0/Q1 = 199999 而非 200000)【新增】 - -| 项 | 内容 | -|---|---| -| 现象 | 四轴 100kHz 压力测试:Q0/Q1(TIM9/TIM12 硬件计数)各 199999 个上升沿;Q2/Q3(软件计数回退)精确 200000 | -| 影响 | 每轴少 1 个脉冲(0.0005%),位置误差 -1 脉冲/次,**系统性**(非随机);影响硬件计数"转正" | -| 代码现状 | 硬件计数本轮新接入:TIM9 对 Q0/Q2、TIM12 对 Q1/Q3 累计;16 位溢出扩展(counterBlockPulses)+ 目标脉冲比较(CC1IF 判定,`PlsrHwOnCounterInterrupt`);本轮同时修复"启动 UG 事件被误计" | -| 根因方向 | 代码实证:`pulses = counterBlockPulses + CNT`,`CC1IF && pulses >= targetPulses` 判 DONE——**启动时计数器初值偏移 +1(CNT 从 1 开始或第一个 OC 事件被 UG 清除时序吞掉)→ pulses 提前达到 200000 → 提前 1 个停止**(实际只发 199999);注释"TIMx_OC rises at the PWM update boundary = 物理下降沿"——启动瞬间的计数边界对齐即嫌疑点 | -| 验证方法 | DONE 判定处打印硬件计数器最终值(199999 → 少计 1 个;200000 → 停止判定滞后);或对比 Q0(硬件)与 Q2(软件)启动前 3 个脉冲边沿 | -| 状态 | 🟡 已修复待上板:从计数器延迟到源OC启动UG完成后再配置/使能,消除无物理脉冲的内部首沿;验收仍为四轴均200000 | - -### 5.(低)AB 段3 反向切换 10ms 过渡期 - -| 项 | 内容 | -|---|---| -| 现象 | 段2→段3 反向切换有 ~10ms 混合相序过渡(A-R→B-R→B-F→A-F)后才进入标准 B 超前 00→01→11→10→00;无毛刺、每状态时长正常 | -| 影响 | 若要求"反向也严格从 00 起步"则未达标;实际影响很小 | -| 状态 | 🟢 低优先级,可暂缓 | - -### 6.(低)方向信号 Q5 启动瞬间 1µs 毛刺 - -| 项 | 内容 | -|---|---| -| 现象 | 方向逻辑测试(17:20)中 Q5 在方向建立瞬间出现 1µs 低→高→低毛刺(0.128780) | -| 影响 | 方向信号 1µs 抖动,一般不影响(方向在脉冲开始前已稳定 10ms) | -| 状态 | 🟢 低优先级,可暂缓 | - ---- - -## 二、待验证项 - -| 项 | 内容 | 状态 | -|---|---|---| -| AB 启动预热机制(abStartupPriming) | 代码已写(首次启动吞周期),未上板验证 | ⏳ 待测 | -| 四轴 100kHz AB 模式 | 本次只测了 PULSE/DIR 100kHz;AB 高频加速段重定相干扰未实测 | ⏳ 待测 | -| 双 AB 并发(Q0/Q1 + Q2/Q3 同时 AB) | 未实测 | ⏳ 待测 | - ---- - -## 三、未实现功能(清单剩余 2 项) - -| 项 | 现状 | 说明 | -|---|---|---| -| Modbus 剩余批次 | P13 控制窗口 ✅(已提交 fdbbe71)、P14 硬件计数 ✅(待提交) | 按 P14 说明"剩余 3 批",当前已完成 2 批 → **剩余约 2 批**(具体内容见 Codex 规划) | -| 收尾清理 | 未做 | 上电自测移除、调试快照(P4 后删除标记)、tmp 资料清理、git 历史重写(1GB bin 松散对象) | - -> **FOLLOW/前馈已关闭(不再列入待办)**:P14 批按信捷手册明确 FOLLOW/FOLLOW_AB 为独立随动指令(输出 = 输入 × 乘系数/除系数),PLSR 仅保留共用参数字段(follow 1~100、feedforward 0~100)的存储与校验,不实现随动运算。 - ---- - -## 四、优先级建议 - -1. **尽快修**:P14 硬件计数 -1(新路径转正门票,预期小改动) -2. **次优先**:AB 启动边沿(预热机制已写,上板验证即可见分晓) -3. **可并行**:Modbus 剩余批次、软限位/段尾截断按实验定位后修 -4. **暂缓**:低优先级两项(反向过渡/方向毛刺)、FOLLOW 运算(等需求明确) diff --git a/HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md b/HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md index 007469a..7238887 100644 --- a/HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md +++ b/HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md @@ -5,7 +5,7 @@ K3 的 100kHz 零加减速配置,自动写入 S0/S1、COMMIT、START,并核 TIM9/TIM12 硬件计数租约、任务/位置/物理周期数和 DWT 实时预算。动态用例还会 执行100kHz→50kHz→100kHz原子变频以及Q0/Q1单组PAUSE/RESUME。 -> 当前状态(2026-08-10):安全首沿建立、双硬件计数租约和末周期快速门控代码已通过Host回归及IAR编译;尚未按本文件完成四通道真机波形验收,因此AB首次启动7501历史问题和双AB 100kHz压力项仍不得标记为关闭。 +> 当前状态(2026-08-10):`independent`、`simultaneous`与`dynamic`均已通过自动测试和100MS/s四通道真机验收。前者Q0/Q1严格100000沿、Q2/Q3严格200000沿、D1468=382 cycles;等长用例四通道各严格200000沿、D1468=392 cycles,已确认修复CH2多1沿及停止悬高;动态用例四通道各严格600000沿,100kHz→50kHz→100kHz、00边界PAUSE/RESUME、调频边界和末周期均通过。三组首沿均在2.50us±0.05us边界内,正反相序、完整末周期和停止全低均通过。稳定段约±50~60ns偏差作为采样量化级记录。 ## 测试前提 @@ -33,17 +33,18 @@ python HostComputer\plsr_modbus_ab_stress_test.py --port COM5 --case dynamic `independent` 要求 Q0/Q1 完成后计数冻结且保持低,同时 Q2/Q3 继续运行。 `simultaneous` 使用相同的 200000 周期目标验证两组并发完成;两个 START 通过 Modbus 串行提交,因此两组实际启动/结束时刻允许相差一次 RTU 事务时间。 -`dynamic` 使用每组300000周期,要求两组调频时保持相位和连续计数;Q0/Q1只在 +`dynamic` 使用每组600000周期,给9600bps下的串行START、一致性状态读取和FC16变频留出足够时间;要求两组调频时保持相位和连续计数;Q0/Q1只在 完整`00`边界暂停并保持低,Q2/Q3不受影响,随后Q0/Q1从`00`恢复正确相序。 脚本还检查 D1468~D1469 发布的 AB 末周期快速门控最大周期数。168MHz 下必须 非零且小于 420 cycles(2.5us,一个 100kHz AB 周期的四分之一)。 +D1468取实际停表门控扫描和输出ISR异常返回前最终跨组安全扫描的最大值;整个输出ISR另由P16输出ISR预算约束。 ## 逻辑分析仪验收 - 独立停止用例:CH0=CH1=100000 个上升沿;CH2=CH3=200000 个上升沿。 - 等长用例:CH0=CH1=CH2=CH3=200000 个上升沿。 -- 动态用例:CH0=CH1=CH2=CH3=300000 个上升沿;Q0/Q1含一个暂停低电平窗口。 +- 动态用例:CH0=CH1=CH2=CH3=600000 个上升沿;Q0/Q1含一个暂停低电平窗口。 - 100kHz区间完整AB周期10.000us、各相高宽5.000us;50kHz区间周期20.000us、 各相高宽10.000us;不得有小于1us的窄脉冲。 - 正向必须为 `00→10→11→01→00`,反向必须为逆序。 diff --git a/HostComputer/plsr_modbus_ab_stress_test.py b/HostComputer/plsr_modbus_ab_stress_test.py index 2fb554c..8e98e73 100644 --- a/HostComputer/plsr_modbus_ab_stress_test.py +++ b/HostComputer/plsr_modbus_ab_stress_test.py @@ -75,8 +75,12 @@ CASES = { ), "dynamic": AbCase( name="双AB变频与单组PAUSE/RESUME", - pulses_axis0=300_000, - pulses_axis2=-300_000, + # At 9600bps the two START transactions, consistent status reads and + # two atomic frequency writes can consume most of a 300000-cycle + # 100kHz job. Keep enough motion time for the complete dynamic + # sequence instead of racing normal completion. + pulses_axis0=600_000, + pulses_axis2=-600_000, require_independent_stop=False, exercise_dynamic_pause=True, ), @@ -306,6 +310,7 @@ def run_case( latest = {axis: read_axis_status(client, axis) for axis in AB_OWNERS} if ( case.require_independent_stop + and not independent_stop_seen and latest[0]["state"] == STATE_COMPLETED and latest[2]["state"] in RUNNING_STATES ): @@ -397,11 +402,11 @@ def print_logic_analyzer_acceptance(selected: list[str]) -> None: elif selected == ["simultaneous"]: print(" 上升沿:CH0=CH1=CH2=CH3=200000。") elif selected == ["dynamic"]: - print(" 上升沿:CH0=CH1=CH2=CH3=300000(含Q0/Q1暂停窗口)。") + print(" 上升沿:CH0=CH1=CH2=CH3=600000(含Q0/Q1暂停窗口)。") else: print( - " 连续采全部用例时累计上升沿:CH0=CH1=600000," - "CH2=CH3=700000;精确逐用例验收建议分别用 --case 采集。" + " 连续采全部用例时累计上升沿:CH0=CH1=900000," + "CH2=CH3=1000000;精确逐用例验收建议分别用 --case 采集。" ) print( " 100kHz区间周期10.000us/高宽5.000us;dynamic的50kHz区间" @@ -480,7 +485,10 @@ def main() -> int: validate_dwt(client, header) print_logic_analyzer_acceptance(selected) - print("\n全部自动检查PASS;最终结论仍需逻辑分析仪四通道波形通过。") + print( + "\n内部状态与实时预算自动检查PASS;这不代表端子波形通过," + "最终结论必须以逻辑分析仪四通道验收为准。" + ) return 0 diff --git a/HostComputer/plsr_modbus_bit_input_test.py b/HostComputer/plsr_modbus_bit_input_test.py index d2db699..e79071b 100644 --- a/HostComputer/plsr_modbus_bit_input_test.py +++ b/HostComputer/plsr_modbus_bit_input_test.py @@ -18,6 +18,8 @@ from plsr_modbus_control_test import ( CALL_START, CONTROL_BASE, CONTROL_WINDOW_WORDS, + RESULT_OK, + RESULT_QUEUED, S0_BASE, S1_BASE, check_result, @@ -27,7 +29,6 @@ from plsr_modbus_control_test import ( from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words -RESULT_OK = 0 STATE_WAIT = 5 STATE_COMPLETED = 7 M_TEST_POINT = 123 @@ -115,7 +116,9 @@ def main() -> 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_OK, "START") + # START queues the immutable snapshot; PlsrTask applies it + # asynchronously, so QUEUED is the successful protocol reply. + check_result(response, RESULT_QUEUED, "START") waiting = wait_for_state(client, STATE_WAIT, 5.0) if waiting["task_pulses"] != 500: diff --git a/HostComputer/plsr_persistence_board_test.py b/HostComputer/plsr_persistence_board_test.py index 393157b..afac416 100644 --- a/HostComputer/plsr_persistence_board_test.py +++ b/HostComputer/plsr_persistence_board_test.py @@ -89,20 +89,41 @@ def next_sequence() -> int: return value if value != 0 else 1 +def is_retryable_read_error(error: RuntimeError) -> bool: + message = str(error) + return message.startswith("响应超时:") or message.startswith("响应 CRC 错误:") + + def wait_response( client: RtuClient, address: int, count: int, sequence: int, timeout: float = 3.0 ) -> list[int]: deadline = time.monotonic() + timeout latest: list[int] | None = None + last_error: RuntimeError | None = None while time.monotonic() < deadline: - latest = client.read_holding(address, count) + try: + latest = client.read_holding(address, count) + except RuntimeError as error: + if not is_retryable_read_error(error): + raise + last_error = error + time.sleep(0.05) + continue if get_u32(latest, 0) == sequence: return latest - raise RuntimeError(f"等待序号 {sequence} 应答超时,最后应答={latest}") + raise RuntimeError( + f"等待序号 {sequence} 应答超时,最后应答={latest}," + f"最后通信错误={last_error}" + ) def send_command( - client: RtuClient, sequence: int, axis: int, opcode: int, argument: int = 0 + client: RtuClient, + sequence: int, + axis: int, + opcode: int, + argument: int = 0, + response_timeout: float = 3.0, ) -> list[int]: request = [0] * 8 put_u32(request, 0, sequence) @@ -110,7 +131,9 @@ def send_command( request[3] = axis put_u64(request, 4, argument) client.write_multiple(COMMAND_REQUEST, request) - return wait_response(client, COMMAND_RESPONSE, 8, sequence) + return wait_response( + client, COMMAND_RESPONSE, 8, sequence, timeout=response_timeout + ) def read_axis_status(client: RtuClient, axis: int) -> dict[str, int]: @@ -139,11 +162,22 @@ def wait_command_applied( ) -> dict[str, int]: deadline = time.monotonic() + timeout latest: dict[str, int] | None = None + last_error: RuntimeError | None = None while time.monotonic() < deadline: - latest = read_axis_status(client, axis) + try: + latest = read_axis_status(client, axis) + except RuntimeError as error: + if not is_retryable_read_error(error): + raise + last_error = error + time.sleep(0.05) + continue if latest["last_sequence"] == sequence: return latest - raise RuntimeError(f"等待命令 {sequence} 内核执行超时,最后状态={latest}") + raise RuntimeError( + f"等待命令 {sequence} 内核执行超时,最后状态={latest}," + f"最后通信错误={last_error}" + ) def read_persistence(client: RtuClient) -> dict[str, int]: @@ -203,6 +237,30 @@ def wait_persistence_clean(client: RtuClient, timeout: float = 4.0) -> dict[str, raise RuntimeError(f"HSD 检查点未在期限内完成:{latest}") +def wait_hsd_checkpoint_advanced( + client: RtuClient, baseline: dict[str, int], timeout: float = 4.0 +) -> dict[str, int]: + """Wait for the START safety checkpoint while motion keeps HSD dirty. + + Runtime position publication legitimately sets HSD dirty again on every + pulse, so a running-axis test must verify a committed generation/save-count + advance instead of waiting for the dirty flag to remain clear. + """ + deadline = time.monotonic() + timeout + latest: dict[str, int] | None = None + while time.monotonic() < deadline: + latest = read_persistence(client) + if ( + latest["hsd_valid_mask"] != 0 + and latest["last_hsd_save"] == PERSISTENCE_OK + and latest["hsd_selected_generation"] + != baseline["hsd_selected_generation"] + and latest["hsd_save_count"] != baseline["hsd_save_count"] + ): + return latest + raise RuntimeError(f"START 的 HSD busy 检查点未在期限内提交:{latest}") + + def send_call( client: RtuClient, sequence: int, axis: int, operation: int ) -> list[int]: @@ -309,8 +367,9 @@ def phase_hsd_verify(client: RtuClient, state_path: Path) -> None: def phase_busy_prepare(client: RtuClient) -> None: + baseline = read_persistence(client) sequence = start_long_motion(client, next_sequence()) - diag = wait_persistence_clean(client) + diag = wait_hsd_checkpoint_advanced(client, baseline) print_diagnostics(diag) print( f"运行中掉电已就绪(下一序号{sequence})。现在直接关闭主电源或硬复位," @@ -337,7 +396,9 @@ def phase_busy_verify(client: RtuClient) -> None: def phase_sfd_save(client: RtuClient, state_path: Path) -> None: before = read_persistence(client) sequence = next_sequence() - response = send_command(client, sequence, 0, CMD_SAVE_CONFIG) + response = send_command( + client, sequence, 0, CMD_SAVE_CONFIG, response_timeout=10.0 + ) if response[4] != RESULT_QUEUED: raise RuntimeError(f"SAVE_CONFIG 未排队:{response}") status = wait_command_applied(client, 0, sequence, timeout=10.0) diff --git a/PLSR/Inc/plsr_build_config.h b/PLSR/Inc/plsr_build_config.h index eebf6a6..c3552ee 100644 --- a/PLSR/Inc/plsr_build_config.h +++ b/PLSR/Inc/plsr_build_config.h @@ -31,7 +31,7 @@ #define PLSR_BOARD_TEST_LONG_STRESS (18U) #ifndef PLSR_BOARD_TEST_SELECT -#define PLSR_BOARD_TEST_SELECT PLSR_BOARD_TEST_DUAL_AB +#define PLSR_BOARD_TEST_SELECT PLSR_BOARD_TEST_LONG_STRESS #endif #endif diff --git a/PLSR/Inc/plsr_hal_f407.h b/PLSR/Inc/plsr_hal_f407.h index 0b42361..656d194 100644 --- a/PLSR/Inc/plsr_hal_f407.h +++ b/PLSR/Inc/plsr_hal_f407.h @@ -80,10 +80,14 @@ uint32_t PlsrHwTestGetCcmr1(uint8_t axis); uint32_t PlsrHwTestGetCr1(uint8_t axis); uint32_t PlsrHwTestGetPsc(uint8_t axis); uint8_t PlsrHwTestGetPwmEnabled(uint8_t axis); +uint8_t PlsrHwTestGetCc1PolarityInverted(uint8_t axis); uint8_t PlsrHwTestGetDirLevel(uint8_t axis); uint8_t PlsrHwTestGetAbPhaseA(uint8_t axis); uint8_t PlsrHwTestGetAbPhaseB(uint8_t axis); uint8_t PlsrHwTestGetAbQuarter(uint8_t axis); +/* Model the short source-CNT/ITR synchronization window without advancing + * the slave raw counter. */ +void PlsrHwTestSetAbQuarterWithoutCounter(uint8_t axis, uint8_t quarter); void PlsrHwTestTriggerUpdate(uint8_t axis); void PlsrHwTestTriggerUpdateAndCompare(uint8_t axis); void PlsrHwTestTriggerCompare(uint8_t axis); diff --git a/PLSR/Src/plsr_hal_f407.c b/PLSR/Src/plsr_hal_f407.c index 46dc058..c6d637d 100644 --- a/PLSR/Src/plsr_hal_f407.c +++ b/PLSR/Src/plsr_hal_f407.c @@ -11,6 +11,7 @@ #endif #define PLSR_HW_TIMER_CHANNEL1_BIT (0x0001U) +#define PLSR_HW_TIMER_CC1P_BIT (0x0002U) #define PLSR_HW_TIMER_UPDATE_BIT (0x0001U) #define PLSR_HW_TIMER_CC1_BIT (0x0002U) #define PLSR_HW_OUTPUT_POINT_COUNT (21U) @@ -92,7 +93,8 @@ static const uint8_t PlsrHwPulsePinIndex[PLSR_HW_AXIS_COUNT] = 6U, 8U, 7U, 9U }; -/* 重定相期间由 GPIO 直接保持物理输出低电平。AFR 配置保持不变, +/* 重定相期间由 GPIO 直接保持端子物理输出低电平。板上漏型输出级 + * 会反相,所以 MCU 必须驱动高电平才能让 Q 端子为低。AFR 配置保持不变, * 只切换 MODER,因此恢复定时器复用功能只需一次寄存器写入。 */ static void PlsrHwHoldPulsePinLow(uint8_t axis) { @@ -100,7 +102,7 @@ static void PlsrHwHoldPulsePinLow(uint8_t axis) uint32_t shift = (uint32_t)PlsrHwPulsePinIndex[axis] * 2UL; uint32_t moder; - port->BSRR = (uint32_t)PlsrHwAxisMap[axis].gpioPin << 16U; + port->BSRR = (uint32_t)PlsrHwAxisMap[axis].gpioPin; moder = port->MODER; moder &= ~(3UL << shift); moder |= 1UL << shift; @@ -119,6 +121,41 @@ static void PlsrHwReleasePulsePin(uint8_t axis) port->MODER = moder; __DMB(); } + +/* Q0..Q3 share GPIOF. At an AB 00 boundary, transfer both pins with one + * GPIO write before changing CCR/ARR. A larger downshift CCR can otherwise + * assert one channel while it is still connected to the timer. */ +static void PlsrHwHoldAbPairLowFast(uint8_t axis) +{ + uint8_t pairAxis = (uint8_t)(axis + 1U); + GPIO_TypeDef *port = PlsrHwAxisMap[axis].gpioPort; + uint32_t firstShift = (uint32_t)PlsrHwPulsePinIndex[axis] * 2UL; + uint32_t secondShift = + (uint32_t)PlsrHwPulsePinIndex[pairAxis] * 2UL; + uint32_t moder = port->MODER; + + port->BSRR = (uint32_t)PlsrHwAxisMap[axis].gpioPin + | (uint32_t)PlsrHwAxisMap[pairAxis].gpioPin; + moder &= ~((3UL << firstShift) | (3UL << secondShift)); + moder |= (1UL << firstShift) | (1UL << secondShift); + port->MODER = moder; + __DMB(); +} + +static void PlsrHwReleaseAbPairFast(uint8_t axis) +{ + uint8_t pairAxis = (uint8_t)(axis + 1U); + GPIO_TypeDef *port = PlsrHwAxisMap[axis].gpioPort; + uint32_t firstShift = (uint32_t)PlsrHwPulsePinIndex[axis] * 2UL; + uint32_t secondShift = + (uint32_t)PlsrHwPulsePinIndex[pairAxis] * 2UL; + uint32_t moder = port->MODER; + + moder &= ~((3UL << firstShift) | (3UL << secondShift)); + moder |= (2UL << firstShift) | (2UL << secondShift); + port->MODER = moder; + __DMB(); +} #endif /* host 测试:模拟定时器寄存器。 */ @@ -177,6 +214,7 @@ typedef struct uint8_t hardwareCounterActive; uint8_t hardwareCounterConfigured; uint64_t counterBlockPulses; + uint64_t counterPublishedPulses; } PLSR_HW_AXIS_STATE; static PLSR_HW_AXIS_STATE PlsrHwAxes[PLSR_HW_AXIS_COUNT]; @@ -203,6 +241,9 @@ static TIM_TypeDef * const PlsrHwCounters[PLSR_HW_COUNTER_COUNT] = static void PlsrHwCounterBegin(uint8_t axis); static void PlsrHwCounterSuspend(uint8_t axis); static void PlsrHwFinishDeferredAbWork(uint8_t axis); +static uint8_t PlsrHwGateArmedAbOutputs(uint8_t preferredAxis); +static void PlsrHwTimerSetCc1PolarityInverted(uint8_t axis, + uint32_t value); #ifdef PLSR_HOST_TEST static uint8_t PlsrHwTestLateAbFlagAxis = PLSR_HW_COUNTER_NONE; @@ -360,6 +401,25 @@ static void PlsrHwTimerSetCen(uint8_t axis, uint32_t value) #endif } +static void PlsrHwStartAbTimersTightly(uint8_t axis, uint8_t pairAxis) +{ +#ifdef PLSR_HOST_TEST + PlsrHwTimerSetCen(axis, 1UL); + PlsrHwTimerSetCen(pairAxis, 1UL); +#else + TIM_TypeDef *baseTimer = PlsrHwAxisMap[axis].timer; + TIM_TypeDef *pairTimer = PlsrHwAxisMap[pairAxis].timer; + uint32_t baseCr1 = baseTimer->CR1 | TIM_CR1_CEN; + uint32_t pairCr1 = pairTimer->CR1 | TIM_CR1_CEN; + + /* Keep the two volatile stores adjacent. Calling the generic CEN helper + * twice in the low-optimization validation build delayed the second + * timer by about 0.33us. */ + baseTimer->CR1 = baseCr1; + pairTimer->CR1 = pairCr1; +#endif +} + static void PlsrHwTimerSetCc1e(uint8_t axis, uint32_t value) { #ifdef PLSR_HOST_TEST @@ -671,6 +731,8 @@ static void PlsrHwPwmBegin(uint8_t axis) PlsrHwCounterBegin(axis); PlsrHwDbgCapture(axis, 0U); PlsrHwTimerSetCc1ie(axis, 0UL); + /* PULSE/DIR retains the previously validated non-inverted polarity. */ + PlsrHwTimerSetCc1PolarityInverted(axis, 0UL); /* TIM9/TIM12 count OC events in hardware. Only the two fallback axes * retain a per-period output-timer interrupt. */ PlsrHwTimerSetUie(axis, @@ -704,6 +766,56 @@ static uint8_t PlsrHwGetPairedAxis(uint8_t axis) return (uint8_t)(axis + 1U); } +static void PlsrHwArmAbBoundaryInterrupts(uint8_t axis) +{ + uint8_t pairAxis = PlsrHwGetPairedAxis(axis); + + /* Either phase may be the first falling edge after an asynchronous + * request. Observe both and accept only the event for which both PWM + * phases are already low. */ + PlsrHwTimerClearCc1if(axis); + PlsrHwTimerClearCc1if(pairAxis); + PlsrHwTimerSetCc1ie(axis, 1UL); + PlsrHwTimerSetCc1ie(pairAxis, 1UL); +} + +static void PlsrHwRestoreAbCycleInterrupt(uint8_t axis) +{ + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + uint8_t pairAxis = PlsrHwGetPairedAxis(axis); + + PlsrHwTimerSetCc1ie(axis, 0UL); + PlsrHwTimerSetCc1ie(pairAxis, 0UL); + if ((state->hardwareCounterActive == 0U) + || (state->abStopArmed != 0U)) + { + PlsrHwTimerClearCc1if(state->abCountAxis); + PlsrHwTimerSetCc1ie(state->abCountAxis, 1UL); + } +} + +static uint8_t PlsrHwIsAbPhysicalZeroBoundary(uint8_t axis) +{ +#ifdef PLSR_HOST_TEST + return (PlsrHwAxes[axis].abQuarter == 0U) ? 1U : 0U; +#else + uint8_t pairAxis = PlsrHwGetPairedAxis(axis); + TIM_TypeDef *baseTimer = PlsrHwAxisMap[axis].timer; + TIM_TypeDef *pairTimer = PlsrHwAxisMap[pairAxis].timer; + uint32_t baseCnt = baseTimer->CNT; + uint32_t baseCcr = baseTimer->CCR1; + uint32_t pairCnt = pairTimer->CNT; + uint32_t pairCcr = pairTimer->CCR1; + + /* With the validated AB double inversion, physical Q is high exactly + * while PWM1 CNT= baseCcr) && (pairCnt >= pairCcr)) + ? 1U + : 0U; +#endif +} + /* 为 168MHz/84MHz 配对定时器选择相同 ARR,并让前者的 PSC 分频 * 始终是后者的 2 倍。两路获得完全相同的计数时钟与周期,避免 * 独立取整造成 AB 相位随运行时间漂移。 */ @@ -832,11 +944,10 @@ static uint8_t PlsrHwQueueAbFrequency(uint8_t axis, uint32_t frequencyHz) { /* 量化后的分频参数未变化时取消旧请求,避免匀速段每 1ms 重定相。 */ state->abFrequencyPending = 0U; - if ((state->hardwareCounterActive != 0U) - && (state->abStopArmed == 0U) + if ((state->abStopArmed == 0U) && (state->abPausePending == 0U)) { - PlsrHwTimerSetCc1ie(state->abCountAxis, 0UL); + PlsrHwRestoreAbCycleInterrupt(axis); } } else @@ -845,13 +956,9 @@ static uint8_t PlsrHwQueueAbFrequency(uint8_t axis, uint32_t frequencyHz) state->abPendingPairPsc = pairPsc; state->abPendingArr = arr; state->abFrequencyPending = 1U; - if (state->hardwareCounterActive != 0U) - { - /* Hardware counting keeps the per-cycle CC interrupt disabled. - * Wake it as a one-shot to apply this request at the next 00. */ - PlsrHwTimerClearCc1if(state->abCountAxis); - PlsrHwTimerSetCc1ie(state->abCountAxis, 1UL); - } + /* Observe both falling edges. The first can be the 11->single-low + * boundary; only the second is a proven physical 00. */ + PlsrHwArmAbBoundaryInterrupts(axis); } #ifndef PLSR_HOST_TEST __DMB(); @@ -932,6 +1039,12 @@ static void PlsrHwBeginAbOutput(uint8_t axis, uint8_t debugReason) PlsrHwTimerClearCc1if(pairAxis); PlsrHwTimerSetCnt(leadAxis, leadStart); PlsrHwTimerSetCnt(lagAxis, lagStart); + /* The board's open-collector stage inverts the MCU waveform. Invert both + * timer channels as well so forced-inactive/CNT>CCR and the lag compare + * boundary are physical terminal 00 rather than 11. Complementing both + * phases preserves the established quadrature direction. */ + PlsrHwTimerSetCc1PolarityInverted(axis, 1UL); + PlsrHwTimerSetCc1PolarityInverted(pairAxis, 1UL); /* Enable the forced-inactive channels while GPIO still owns the pins. * AF handoff and the later PWM1 selection therefore preserve the same 00 * electrical level at every mux point. */ @@ -950,8 +1063,7 @@ static void PlsrHwBeginAbOutput(uint8_t axis, uint8_t debugReason) PlsrHwTimerClearCc1if(axis); PlsrHwTimerClearCc1if(pairAxis); PlsrHwCounterBegin(axis); - PlsrHwTimerSetCen(axis, 1UL); - PlsrHwTimerSetCen(pairAxis, 1UL); + PlsrHwStartAbTimersTightly(axis, pairAxis); PlsrHwTimerClearCc1if(axis); PlsrHwTimerClearCc1if(pairAxis); PlsrHwTimerSetCc1ie( @@ -966,8 +1078,7 @@ static void PlsrHwBeginAbOutput(uint8_t axis, uint8_t debugReason) PlsrHwTimerClearCc1if(axis); PlsrHwTimerClearCc1if(pairAxis); PlsrHwCounterBegin(axis); - PlsrHwTimerSetCen(axis, 1UL); - PlsrHwTimerSetCen(pairAxis, 1UL); + PlsrHwStartAbTimersTightly(axis, pairAxis); PlsrHwTimerClearCc1if(axis); PlsrHwTimerClearCc1if(pairAxis); PlsrHwTimerSetCc1ie( @@ -985,6 +1096,108 @@ static void PlsrHwBeginAbOutput(uint8_t axis, uint8_t debugReason) PlsrHwDbgCapture(axis, debugReason); } +/* Apply an already calculated AB divider at a verified 00 boundary. Direct + * target-register writes keep the complete rephase inside the 10us output ISR + * budget while GPIO owns physical 00 during every CCR/ARR transition. */ +static void PlsrHwApplyAbFrequencyAtBoundary(uint8_t axis, + uint16_t basePsc, + uint16_t pairPsc, + uint16_t arr) +{ +#ifdef PLSR_HOST_TEST + PlsrHwLoadAbPwm(axis, basePsc, pairPsc, arr); + PlsrHwBeginAbOutput(axis, 4U); +#else + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + uint8_t pairAxis = PlsrHwGetPairedAxis(axis); + uint8_t leadAxis = (state->directionPositive != 0U) ? axis : pairAxis; + uint8_t lagAxis = (state->directionPositive != 0U) ? pairAxis : axis; + TIM_TypeDef *baseTimer = PlsrHwAxisMap[axis].timer; + TIM_TypeDef *pairTimer = PlsrHwAxisMap[pairAxis].timer; + TIM_TypeDef *leadTimer = PlsrHwAxisMap[leadAxis].timer; + TIM_TypeDef *lagTimer = PlsrHwAxisMap[lagAxis].timer; + uint32_t periodTicks = (uint32_t)arr + 1UL; + uint32_t compare = periodTicks / 2UL; + uint32_t leadStart = (periodTicks * 3UL) / 4UL + 1UL; + uint32_t lagStart = periodTicks / 2UL + 1UL; + uint32_t baseCr1; + uint32_t pairCr1; + + if (leadStart >= periodTicks) + { + leadStart = periodTicks - 1UL; + } + if (lagStart >= periodTicks) + { + lagStart = periodTicks - 1UL; + } + + baseTimer->CR1 &= ~TIM_CR1_CEN; + pairTimer->CR1 &= ~TIM_CR1_CEN; + PlsrHwCounterSuspend(axis); + PlsrHwHoldAbPairLowFast(axis); + + baseTimer->CCER &= ~TIM_CCER_CC1E; + pairTimer->CCER &= ~TIM_CCER_CC1E; + baseTimer->DIER &= ~(TIM_DIER_UIE | TIM_DIER_CC1IE); + pairTimer->DIER &= ~(TIM_DIER_UIE | TIM_DIER_CC1IE); + baseTimer->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1PE; + pairTimer->CCMR1 = TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1PE; + baseTimer->PSC = basePsc; + pairTimer->PSC = pairPsc; + baseTimer->ARR = arr; + pairTimer->ARR = arr; + baseTimer->CCR1 = compare; + pairTimer->CCR1 = compare; + baseTimer->CR1 |= TIM_CR1_ARPE; + pairTimer->CR1 |= TIM_CR1_ARPE; + baseTimer->EGR = TIM_EGR_UG; + pairTimer->EGR = TIM_EGR_UG; + baseTimer->SR = ~(TIM_SR_UIF | TIM_SR_CC1IF); + pairTimer->SR = ~(TIM_SR_UIF | TIM_SR_CC1IF); + leadTimer->CNT = leadStart; + lagTimer->CNT = lagStart; + baseTimer->CCER = + (baseTimer->CCER & ~(TIM_CCER_CC1P | TIM_CCER_CC1E)) + | TIM_CCER_CC1P | TIM_CCER_CC1E; + pairTimer->CCER = + (pairTimer->CCER & ~(TIM_CCER_CC1P | TIM_CCER_CC1E)) + | TIM_CCER_CC1P | TIM_CCER_CC1E; + PlsrHwReleaseAbPairFast(axis); + baseTimer->CCMR1 = (TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2) + | TIM_CCMR1_OC1PE; + pairTimer->CCMR1 = (TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2) + | TIM_CCMR1_OC1PE; + + state->abActiveBasePsc = basePsc; + state->abActivePairPsc = pairPsc; + state->abActiveArr = arr; + state->abCountAxis = lagAxis; + state->counterSourceAxis = (axis == 0U) ? axis : pairAxis; + state->abCounterBoundaryCnt = + (state->counterSourceAxis == leadAxis) + ? leadStart - 1UL + : periodTicks / 2UL; + state->abQuarter = 0U; + state->abStartupPriming = 0U; + state->abFastGated = 0U; + + PlsrHwCounterBegin(axis); + baseCr1 = baseTimer->CR1 | TIM_CR1_CEN; + pairCr1 = pairTimer->CR1 | TIM_CR1_CEN; + baseTimer->CR1 = baseCr1; + pairTimer->CR1 = pairCr1; + baseTimer->SR = ~(TIM_SR_UIF | TIM_SR_CC1IF); + pairTimer->SR = ~(TIM_SR_UIF | TIM_SR_CC1IF); + if ((state->hardwareCounterActive == 0U) + || (state->abStopArmed != 0U)) + { + lagTimer->DIER |= TIM_DIER_CC1IE; + } + __DMB(); +#endif +} + static void PlsrHwConfigureCwCcwPwm(uint8_t axis, uint32_t frequencyHz) { PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; @@ -1043,6 +1256,9 @@ static void PlsrHwBeginCwCcwOutput(uint8_t axis) * inactive level and avoids the extra start/end edge. */ PlsrHwStopPwmTimer(axis); PlsrHwStopPwmTimer(pairAxis); + /* CW/CCW retains the previously validated non-inverted polarity. */ + PlsrHwTimerSetCc1PolarityInverted(axis, 0UL); + PlsrHwTimerSetCc1PolarityInverted(pairAxis, 0UL); state->cwStopPending = 0U; PlsrHwTimerSetUg(activeAxis); PlsrHwTimerClearUif(activeAxis); @@ -1161,6 +1377,24 @@ static uint8_t PlsrHwCounterIndexForAxis(uint8_t axis) return (uint8_t)(axis & 1U); } +static void PlsrHwTimerSetCc1PolarityInverted(uint8_t axis, uint32_t value) +{ +#ifdef PLSR_HOST_TEST + PlsrHwTimers[axis].ccer = + (PlsrHwTimers[axis].ccer & ~PLSR_HW_TIMER_CC1P_BIT) + | ((value != 0UL) ? PLSR_HW_TIMER_CC1P_BIT : 0UL); +#else + if (value != 0UL) + { + PlsrHwAxisMap[axis].timer->CCER |= TIM_CCER_CC1P; + } + else + { + PlsrHwAxisMap[axis].timer->CCER &= ~TIM_CCER_CC1P; + } +#endif +} + static uint8_t PlsrHwCounterTryAcquire(uint8_t axis, PLSR_OUTPUT_MODE outputMode) { @@ -1175,6 +1409,7 @@ static uint8_t PlsrHwCounterTryAcquire(uint8_t axis, state->hardwareCounterActive = 0U; state->hardwareCounterConfigured = 0U; state->counterBlockPulses = 0UL; + state->counterPublishedPulses = 0UL; if ((outputMode != PLSR_OUTPUT_PULSE_DIR) && (outputMode != PLSR_OUTPUT_AB)) { @@ -1247,6 +1482,7 @@ static void PlsrHwCounterRelease(uint8_t axis) state->hardwareCounterActive = 0U; state->hardwareCounterConfigured = 0U; state->counterBlockPulses = 0UL; + state->counterPublishedPulses = 0UL; #ifndef PLSR_HOST_TEST __DMB(); if (interruptState == 0UL) @@ -1261,6 +1497,7 @@ static void PlsrHwCounterConfigure(uint8_t axis) PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; state->counterBlockPulses = 0UL; + state->counterPublishedPulses = 0UL; #ifndef PLSR_HOST_TEST if (state->counterIndex < PLSR_HW_COUNTER_COUNT) { @@ -1369,6 +1606,7 @@ static void PlsrHwCounterRebase(uint8_t axis, uint64_t pulses) counter->SR = 0UL; } #endif + state->counterPublishedPulses = pulses; } static uint64_t PlsrHwCounterRawSnapshot(uint8_t axis) @@ -1451,6 +1689,21 @@ static uint64_t PlsrHwCounterSnapshot(uint8_t axis) { pulses = (uint64_t)state->targetPulses; } + /* The source timer and its TIM9/TIM12 ITR slave are separate hardware + * domains. Immediately after the source edge, source CNT can already be + * inside the next AB cycle while the slave raw count still has its old + * value. Phase correction would then transiently report N-1 after N was + * already published, and the core correctly treats that regression as a + * counter fault. Complete physical AB cycles never go backwards, so keep + * the last verified value as a monotonic floor. */ + if (pulses < state->counterPublishedPulses) + { + pulses = state->counterPublishedPulses; + } + else + { + state->counterPublishedPulses = pulses; + } return pulses; } @@ -1563,6 +1816,7 @@ PLSR_RESULT PlsrHwInit(void) (void)memset(&PlsrHwTimers[axis], 0, sizeof(PlsrHwTimers[axis])); #else PlsrHwTimerSetCc1e(axis, 0UL); + PlsrHwTimerSetCc1PolarityInverted(axis, 0UL); PlsrHwTimerSetUie(axis, 0UL); PlsrHwTimerSetCc1ie(axis, 0UL); PlsrHwTimerSetCen(axis, 0UL); @@ -1813,8 +2067,7 @@ PLSR_RESULT PlsrHwSetFrequency(uint8_t axis, uint32_t frequencyHz) * already-started cycle, adding one terminal edge. */ state->abFrequencyPending = 0U; state->abPausePending = 1U; - PlsrHwTimerClearCc1if(state->abCountAxis); - PlsrHwTimerSetCc1ie(state->abCountAxis, 1UL); + PlsrHwArmAbBoundaryInterrupts(axis); #ifndef PLSR_HOST_TEST __DMB(); if (interruptState == 0UL) @@ -2084,9 +2337,28 @@ static void PlsrHwFastGateAbPair(uint8_t axis) { uint8_t pairAxis = PlsrHwGetPairedAxis(axis); +#ifdef PLSR_HOST_TEST PlsrHwTimerSetCen(axis, 0UL); PlsrHwTimerSetCen(pairAxis, 0UL); PlsrHwCounterSuspend(axis); +#else + PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; + + /* This is the sub-2.5us terminal boundary. Use the same register writes + * as the generic helpers without their low-optimization call overhead. */ + PlsrHwAxisMap[axis].timer->CR1 &= ~TIM_CR1_CEN; + PlsrHwAxisMap[pairAxis].timer->CR1 &= ~TIM_CR1_CEN; + if ((state->hardwareCounterActive != 0U) + && (state->hardwareCounterConfigured != 0U) + && (state->counterIndex < PLSR_HW_COUNTER_COUNT)) + { + TIM_TypeDef *counter = PlsrHwCounters[state->counterIndex]; + + counter->CR1 &= ~TIM_CR1_CEN; + counter->SMCR &= ~(TIM_SMCR_SMS_2 | TIM_SMCR_SMS_1 + | TIM_SMCR_SMS_0); + } +#endif } static void PlsrHwDeferAbCompletion(uint8_t axis) @@ -2116,36 +2388,66 @@ static void PlsrHwRecordFullAbGateTime(void) } #ifndef PLSR_HOST_TEST -static void PlsrHwFinishAbGateMeasurement(uint32_t started) -{ +static void PlsrHwFinishAbGateMeasurement(uint32_t started, + uint8_t interruptAxis) +{ + uint8_t finalScanRequired; + uint8_t preferredAxis = (uint8_t)(interruptAxis & 0xFEU); + + (void)started; + /* Re-scan as the final ISR operation. A second equal-priority AB pair may + * reach its 00 boundary after PlsrHwOnTimerUpdate() performed its early + * scans. Gating it here leaves only exception-return/tail-chain overhead + * before its pending IRQ runs, rather than the bookkeeping tail of the + * first pair. D1468 measures this safety-critical final scan. */ + finalScanRequired = PlsrHwAbGateMeasurePending; + if ((((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)))) + { + finalScanRequired = 1U; + } + if (finalScanRequired == 0U) + { + return; + } + if (PlsrHwGateArmedAbOutputs(preferredAxis) != 0U) + { + PlsrHwAbGateMeasurePending = 1U; + } if (PlsrHwAbGateMeasurePending != 0U) { - uint32_t finished; - uint32_t elapsed; - + /* Actual gate latency is recorded at the register write that freezes + * the pair. Do not extend it with checks performed after the output + * is already physically safe. */ PlsrHwAbGateMeasurePending = 0U; - finished = DWT->CYCCNT; - elapsed = finished - started; - if (elapsed > PlsrHwMaxAbGateCycles) - { - PlsrHwMaxAbGateCycles = elapsed; - } } } #endif -static uint8_t PlsrHwGateArmedAbOutputs(void) +static uint8_t PlsrHwGateArmedAbOutputs(uint8_t preferredAxis) { - static const uint8_t baseAxes[2] = {0U, 2U}; uint8_t gated = 0U; uint8_t index; +#ifndef PLSR_HOST_TEST + uint32_t started = DWT->CYCCNT; +#endif /* Both AB pairs use equal-priority IRQs. Scan and gate every pair before * doing any event publication so two simultaneous 100kHz completions * cannot make the second pair run an extra quarter while its IRQ waits. */ + if ((preferredAxis != 0U) && (preferredAxis != 2U)) + { + preferredAxis = 0U; + } for (index = 0U; index < 2U; index++) { - uint8_t axis = baseAxes[index]; + uint8_t axis = (index == 0U) + ? preferredAxis + : (uint8_t)(preferredAxis ^ 2U); PLSR_HW_AXIS_STATE *state = &PlsrHwAxes[axis]; if ((state->state == PLSR_HW_STATE_RUNNING) @@ -2162,6 +2464,16 @@ static uint8_t PlsrHwGateArmedAbOutputs(void) PlsrHwFastGateAbPair(axis); state->abFastGated = 1U; gated = 1U; +#ifndef PLSR_HOST_TEST + { + uint32_t elapsed = DWT->CYCCNT - started; + + if (elapsed > PlsrHwMaxAbGateCycles) + { + PlsrHwMaxAbGateCycles = elapsed; + } + } +#endif } } return gated; @@ -2180,7 +2492,8 @@ void PlsrHwOnTimerUpdate(uint8_t axis) { return; } - abGated = PlsrHwGateArmedAbOutputs(); + ownerAxis = (uint8_t)(axis & 0xFEU); + abGated = PlsrHwGateArmedAbOutputs(ownerAxis); #ifdef PLSR_HOST_TEST /* Model the second equal-priority lag flag arriving after the first scan * but before any completion bookkeeping. */ @@ -2196,7 +2509,7 @@ void PlsrHwOnTimerUpdate(uint8_t axis) * arrival window; a still-later flag gets CPU back before its next jump. */ if (abGated != 0U) { - (void)PlsrHwGateArmedAbOutputs(); + (void)PlsrHwGateArmedAbOutputs(ownerAxis); } /* CC1IF 无论当前状态如何都必须先清除;否则启动窗口中的杂散 @@ -2206,7 +2519,6 @@ void PlsrHwOnTimerUpdate(uint8_t axis) { PlsrHwTimerClearCc1if(axis); } - ownerAxis = (uint8_t)(axis & 0xFEU); state = &PlsrHwAxes[ownerAxis]; if ((state->state == PLSR_HW_STATE_RUNNING) && (state->outputMode == PLSR_OUTPUT_AB)) @@ -2215,7 +2527,21 @@ void PlsrHwOnTimerUpdate(uint8_t axis) { PlsrHwTimerClearUif(axis); } - if ((hasCc1 == 0U) || (axis != state->abCountAxis)) + if (hasCc1 == 0U) + { + return; + } + if ((state->abPausePending != 0U) + || (state->abFrequencyPending != 0U)) + { + if (PlsrHwIsAbPhysicalZeroBoundary(ownerAxis) == 0U) + { + /* This was the first phase falling from 11. Leave both + * one-shot interrupts armed for the later physical 00 edge. */ + return; + } + } + else if (axis != state->abCountAxis) { return; } @@ -2240,6 +2566,11 @@ void PlsrHwOnTimerUpdate(uint8_t axis) ? 1U : 0U); PlsrHwFastGateAbPair(ownerAxis); +#ifndef PLSR_HOST_TEST + /* Own the pins at physical 00 immediately. Waiting for the 1ms + * deferred cleanup previously exposed a frozen active phase. */ + PlsrHwHoldAbPairLowFast(ownerAxis); +#endif if (targetReached != 0U) { PlsrHwDeferAbCompletion(ownerAxis); @@ -2267,16 +2598,11 @@ void PlsrHwOnTimerUpdate(uint8_t axis) uint16_t basePsc = state->abPendingBasePsc; uint16_t pairPsc = state->abPendingPairPsc; uint16_t arr = state->abPendingArr; - uint8_t pairAxis = PlsrHwGetPairedAxis(ownerAxis); - - /* This CC1 is 00. Gate first; divider calculation and timer - * reloading are intentionally outside the 2.5us edge window. */ - PlsrHwTimerSetCen(ownerAxis, 0UL); - PlsrHwTimerSetCen(pairAxis, 0UL); - PlsrHwCounterSuspend(ownerAxis); state->abFrequencyPending = 0U; - PlsrHwLoadAbPwm(ownerAxis, basePsc, pairPsc, arr); - PlsrHwBeginAbOutput(ownerAxis, 4U); + PlsrHwApplyAbFrequencyAtBoundary(ownerAxis, + basePsc, + pairPsc, + arr); } else if (state->abStopArmed == 0U) { @@ -2298,9 +2624,10 @@ void PlsrHwOnTimerUpdate(uint8_t axis) uint16_t arr = state->abPendingArr; state->abFrequencyPending = 0U; - PlsrHwLoadAbPwm(ownerAxis, basePsc, pairPsc, arr); - /* 落后相刚下降,AB=00;两路从同一个完整周期边界重定相。 */ - PlsrHwBeginAbOutput(ownerAxis, 4U); + PlsrHwApplyAbFrequencyAtBoundary(ownerAxis, + basePsc, + pairPsc, + arr); } return; } @@ -2423,6 +2750,13 @@ uint8_t PlsrHwTestGetPwmEnabled(uint8_t axis) : 0U; } +uint8_t PlsrHwTestGetCc1PolarityInverted(uint8_t axis) +{ + return ((PlsrHwTimers[axis].ccer & PLSR_HW_TIMER_CC1P_BIT) != 0UL) + ? 1U + : 0U; +} + uint8_t PlsrHwTestGetDirLevel(uint8_t axis) { return PlsrHwTimers[axis].dirLevel; @@ -2477,6 +2811,16 @@ uint8_t PlsrHwTestGetAbQuarter(uint8_t axis) return PlsrHwAxes[axis].abQuarter; } +void PlsrHwTestSetAbQuarterWithoutCounter(uint8_t axis, uint8_t quarter) +{ + if ((axis >= PLSR_HW_AXIS_COUNT) || (PlsrHwIsAbBaseAxis(axis) == 0U)) + { + return; + } + PlsrHwAxes[axis].abQuarter = + (uint8_t)(quarter % PLSR_HW_AB_QUARTER_COUNT); +} + uint32_t PlsrHwTestGetAbFullGateCount(void) { return PlsrHwTestAbFullGateCount; @@ -2521,6 +2865,14 @@ void PlsrHwTestAdvanceAbQuarter(uint8_t axis) PlsrHwTimerSetCc1ie(countAxis, 1UL); } } + if ((state->abQuarter == 3U) + && ((PlsrHwTimers[leadAxis].dier & PLSR_HW_TIMER_CC1_BIT) != 0UL)) + { + /* First falling edge after an asynchronous boundary request: AB is + * not 00 yet, so production must observe it without applying work. */ + PlsrHwTimers[leadAxis].sr |= PLSR_HW_TIMER_CC1_BIT; + PlsrHwOnTimerUpdate(leadAxis); + } if (state->abQuarter == 0U) { /* 模拟目标板落后相 CC1 下降沿中断,复用生产计数路径。 */ @@ -2700,7 +3052,7 @@ void TIM1_UP_TIM10_IRQHandler(void) PlsrHwOnTimerUpdate(0U); PlsrHwRecordMaxCycles(&PlsrHwMaxOutputIsrCycles, started); - PlsrHwFinishAbGateMeasurement(started); + PlsrHwFinishAbGateMeasurement(started, 0U); } void TIM8_UP_TIM13_IRQHandler(void) @@ -2709,7 +3061,7 @@ void TIM8_UP_TIM13_IRQHandler(void) PlsrHwOnTimerUpdate(1U); PlsrHwRecordMaxCycles(&PlsrHwMaxOutputIsrCycles, started); - PlsrHwFinishAbGateMeasurement(started); + PlsrHwFinishAbGateMeasurement(started, 1U); } void TIM1_TRG_COM_TIM11_IRQHandler(void) @@ -2718,7 +3070,7 @@ void TIM1_TRG_COM_TIM11_IRQHandler(void) PlsrHwOnTimerUpdate(2U); PlsrHwRecordMaxCycles(&PlsrHwMaxOutputIsrCycles, started); - PlsrHwFinishAbGateMeasurement(started); + PlsrHwFinishAbGateMeasurement(started, 2U); } void TIM8_TRG_COM_TIM14_IRQHandler(void) @@ -2727,7 +3079,7 @@ void TIM8_TRG_COM_TIM14_IRQHandler(void) PlsrHwOnTimerUpdate(3U); PlsrHwRecordMaxCycles(&PlsrHwMaxOutputIsrCycles, started); - PlsrHwFinishAbGateMeasurement(started); + PlsrHwFinishAbGateMeasurement(started, 3U); } void TIM1_BRK_TIM9_IRQHandler(void) diff --git a/PLSR/Src/plsr_profile.c b/PLSR/Src/plsr_profile.c index 17dec90..6c6fbc6 100644 --- a/PLSR/Src/plsr_profile.c +++ b/PLSR/Src/plsr_profile.c @@ -458,6 +458,15 @@ PLSR_RESULT PlsrProfileRetarget(PLSR_PROFILE_STATE *state, if (newTargetFrequencyHz > currentHz) { + if (state->accelSlopeHzPerMs == 0UL) + { + /* A zero ramp is an intentional frequency step. Startup already + * uses this semantic; live retargeting must not enter ACCEL with + * a permanently zero delta. */ + state->frequencyQ32 = PlsrProfileHzToQ32(newTargetFrequencyHz); + state->phase = PLSR_PROFILE_PHASE_CRUISE; + return PLSR_RESULT_OK; + } /* 升频:加速/匀速 → 加速;减速中 → 取消减速转加速。 */ if ((state->phase == PLSR_PROFILE_PHASE_DECEL) || (state->phase == PLSR_PROFILE_PHASE_CRUISE) @@ -468,6 +477,15 @@ PLSR_RESULT PlsrProfileRetarget(PLSR_PROFILE_STATE *state, } else { + if (state->decelSlopeHzPerMs == 0UL) + { + /* Likewise, zero deceleration means an immediate step down, not + * a DECEL state whose Q32 increment can never advance. */ + state->frequencyQ32 = PlsrProfileHzToQ32(newTargetFrequencyHz); + state->decelTargetHz = state->stopFrequencyHz; + state->phase = PLSR_PROFILE_PHASE_CRUISE; + return PLSR_RESULT_OK; + } /* 降频:先减速到新目标,再转匀速继续(最终仍减速到终止速度)。 */ if ((state->phase == PLSR_PROFILE_PHASE_ACCEL) || (state->phase == PLSR_PROFILE_PHASE_CRUISE)) diff --git a/PLSR/Test/test_plsr_hal.c b/PLSR/Test/test_plsr_hal.c index 4cbf358..dbf0d01 100644 --- a/PLSR/Test/test_plsr_hal.c +++ b/PLSR/Test/test_plsr_hal.c @@ -364,6 +364,7 @@ static void TestCwCcwSequence(void) CHECK(PlsrHwGetEmittedPulses(0U) == 3); CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING); CHECK(PlsrHwTestGetPwmEnabled(0U) == 1U); + CHECK(PlsrHwTestGetCc1PolarityInverted(0U) == 0U); PlsrHwTestTriggerUpdate(0U); CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_DONE); CHECK(PlsrHwTestGetPwmEnabled(0U) == 0U); @@ -836,6 +837,13 @@ static void TestDualAbHardwareCountersAndResume(void) CHECK(PlsrHwGetEmittedPulses(0U) == 2); CHECK(PlsrHwGetEmittedPulses(2U) == 1); + /* The source timer can cross its edge just before the ITR slave observes + * it. The phase-corrected public count must not regress from 2 to 1 in + * that synchronization window, otherwise core PAUSE raises COUNTER_FAULT. */ + PlsrHwTestSetAbQuarterWithoutCounter(0U, 1U); + CHECK(PlsrHwGetEmittedPulses(0U) == 2); + PlsrHwTestSetAbQuarterWithoutCounter(0U, 0U); + /* Request PAUSE after the source edge but before the following 00. The * pair must finish this already-started cycle instead of forcing GPIO 00 * and re-emitting its source edge after RESUME. */ @@ -1160,6 +1168,8 @@ static void TestEndToEndAbSegment(void) CHECK(PlsrHwGetState(0U) == PLSR_HW_STATE_RUNNING); CHECK(PlsrHwTestGetPwmEnabled(0U) != 0U); CHECK(PlsrHwTestGetPwmEnabled(1U) != 0U); + CHECK(PlsrHwTestGetCc1PolarityInverted(0U) != 0U); + CHECK(PlsrHwTestGetCc1PolarityInverted(1U) != 0U); /* 负脉冲选择反向相序,完整两个周期后由同一事件链结束任务。 */ PlsrHwTestAdvanceAbQuarter(0U); diff --git a/PLSR/Test/test_plsr_profile.c b/PLSR/Test/test_plsr_profile.c index 11c47c5..cc90a29 100644 --- a/PLSR/Test/test_plsr_profile.c +++ b/PLSR/Test/test_plsr_profile.c @@ -178,6 +178,32 @@ static void TestNoAccel(void) CHECK(steps == 500); } +static void TestZeroRampRetarget(void) +{ + PLSR_PROFILE_REQUEST request = MakeRequest(100000U, 100000U, 0U, + 0U, 0U, 0U); + PLSR_PROFILE_STATE state; + uint32_t frequency = 0U; + uint8_t completed = 0U; + + CHECK(PlsrProfileStart(&state, &request, 1000000, 10000U) + == PLSR_RESULT_OK); + CHECK(state.phase == PLSR_PROFILE_PHASE_CRUISE); + CHECK(PlsrProfileRetarget(&state, 50000U) == PLSR_RESULT_OK); + CHECK(state.phase == PLSR_PROFILE_PHASE_CRUISE); + CHECK((state.frequencyQ32 >> 32U) == 50000U); + CHECK(PlsrProfileStep(&state, &frequency, &completed) == PLSR_RESULT_OK); + CHECK(frequency == 50000U); + CHECK(completed == 0U); + + CHECK(PlsrProfileRetarget(&state, 100000U) == PLSR_RESULT_OK); + CHECK(state.phase == PLSR_PROFILE_PHASE_CRUISE); + CHECK((state.frequencyQ32 >> 32U) == 100000U); + CHECK(PlsrProfileStep(&state, &frequency, &completed) == PLSR_RESULT_OK); + CHECK(frequency == 100000U); + CHECK(completed == 0U); +} + static void TestSubMillisecondRefresh(void) { PLSR_PROFILE_REQUEST request = MakeRequest(10000U, 0U, 0U, 100U, 100U, 0U); @@ -424,6 +450,7 @@ int main(void) TestRetargetUp(); TestRetargetDown(); TestNoAccel(); + TestZeroRampRetarget(); TestSubMillisecondRefresh(); TestSinglePulse(); TestHighFrequency();