- 重构双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回退仍待关闭master
| @@ -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、异常复位和反复擦写板测 | | |||
| @@ -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位硬件计数扩展 | |||
| @@ -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 <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 回退尚未关闭。 | |||
| 项目可以进入“剩余专项验收和正式发布清理”阶段,但尚不满足“全部真机测试关闭”或 | |||
| “严格对标全部完成”的声明条件。 | |||
| @@ -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、异常复位和反复保存的正式板测尚未关闭;破坏性诊断默认禁用 | | |||
| @@ -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 推进压力测试与收尾。 | |||
| @@ -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 <xxx.bin>` → 输出时频图 + 三段结构 + 脉冲数 | |||
| 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 后删除"),正式版需移除 | |||
| @@ -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 按上面"验证方法"做实验定位后再修,避免盲改 | |||
| @@ -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. 仅存在于对话中的信息 | |||
| 凡"只在我们对话里出现过、没有落入任何文件"的关键信息(测试数据、踩坑经验、用户偏好、待确认问题),单独列一节,务必不要遗漏。 | |||
| ``` | |||
| 【发送内容结束】 | |||
| @@ -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 运算(等需求明确) | |||
| @@ -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`,反向必须为逆序。 | |||
| @@ -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 | |||
| @@ -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: | |||
| @@ -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) | |||
| @@ -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 | |||
| @@ -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); | |||
| @@ -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<CCR. At the later of the two falling-edge compares both | |||
| * counters are in their low half-cycle. */ | |||
| return ((baseCnt >= 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) | |||
| @@ -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)) | |||
| @@ -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); | |||
| @@ -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(); | |||