浏览代码

P17/P18 收尾:双AB边界闭环、持久化板测与测试报告

- 重构双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
ywh 1 个月前
父节点
当前提交
8105d822c2
共有 19 个文件被更改,包括 994 次插入455 次删除
  1. +1
    -1
      Document/PLSR_document/PLSR信捷对标追踪矩阵.md
  2. +4
    -2
      Document/PLSR_document/PLSR方案设计书_V1.0.md
  3. +432
    -0
      Document/PLSR_document/PLSR测试报告_2026-08-10.md
  4. +1
    -1
      Document/PLSR_document/PLSR需求规格统计_V1.3.md
  5. +0
    -80
      Document/PLSR_document/交接提示词_2026-08-10.md
  6. +0
    -89
      Document/PLSR_document/交接文档_2026-08-08.md
  7. +0
    -42
      Document/PLSR_document/已知问题清单_2026-08-09.md
  8. +0
    -69
      Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md
  9. +0
    -99
      Document/PLSR_document/问题总清单_2026-08-10.md
  10. +4
    -3
      HostComputer/PLSR_MODBUS_AB_STRESS_TEST.md
  11. +14
    -6
      HostComputer/plsr_modbus_ab_stress_test.py
  12. +5
    -2
      HostComputer/plsr_modbus_bit_input_test.py
  13. +69
    -8
      HostComputer/plsr_persistence_board_test.py
  14. +1
    -1
      PLSR/Inc/plsr_build_config.h
  15. +4
    -0
      PLSR/Inc/plsr_hal_f407.h
  16. +404
    -52
      PLSR/Src/plsr_hal_f407.c
  17. +18
    -0
      PLSR/Src/plsr_profile.c
  18. +10
    -0
      PLSR/Test/test_plsr_hal.c
  19. +27
    -0
      PLSR/Test/test_plsr_profile.c

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



+ 4
- 2
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位硬件计数扩展


+ 432
- 0
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 <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 回退尚未关闭。

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

+ 1
- 1
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、异常复位和反复保存的正式板测尚未关闭;破坏性诊断默认禁用 |



+ 0
- 80
Document/PLSR_document/交接提示词_2026-08-10.md 查看文件

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

+ 0
- 89
Document/PLSR_document/交接文档_2026-08-08.md 查看文件

@@ -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 后删除"),正式版需移除

+ 0
- 42
Document/PLSR_document/已知问题清单_2026-08-09.md 查看文件

@@ -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 按上面"验证方法"做实验定位后再修,避免盲改

+ 0
- 69
Document/PLSR_document/提示词_发给Codex老对话_2026-08-10.md 查看文件

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

【发送内容结束】

+ 0
- 99
Document/PLSR_document/问题总清单_2026-08-10.md 查看文件

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

+ 4
- 3
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`,反向必须为逆序。


+ 14
- 6
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




+ 5
- 2
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:


+ 69
- 8
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)


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

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


+ 404
- 52
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<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)


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


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


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


正在加载...
取消
保存