Sfoglia il codice sorgente

修了一些bug,多段单段基本没问题了

codex/plsr-2026-minimal
ywh 1 mese fa
parent
commit
8d678f99d9
25 ha cambiato i file con 4849 aggiunte e 1895 eliminazioni
  1. +2
    -0
      .gitignore
  2. +114
    -0
      Document/PLSR_document/PLSR_5段理论频率包络.svg
  3. +12
    -12
      Document/PLSR_document/PLSR_PULSE_DIR测试用例与验收标准_2026-08-13.md
  4. +1093
    -0
      Document/PLSR_document/PLSR多段测试用例与验收标准_2026-08-14.md
  5. +250
    -0
      Document/PLSR_document/PLSR波形验收报告_2026-08-14_完成模式10段阶梯频率.md
  6. +294
    -0
      Document/PLSR_document/PLSR测试报告_2026-08-12.md
  7. +388
    -0
      Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md
  8. BIN
      Document/PLSR_document/波形/111.bin
  9. +6
    -0
      EWARM/Modbus.ewp
  10. BIN
      HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc
  11. BIN
      HostComputer/__pycache__/plsr_modbus_product_test.cpython-314.pyc
  12. BIN
      HostComputer/__pycache__/wave_viewer.cpython-314.pyc
  13. +271
    -0
      HostComputer/bin_to_time_freq.py
  14. +7
    -3
      HostComputer/plsr_control_panel.py
  15. +37
    -7
      HostComputer/plsr_modbus_product_test.py
  16. +289
    -0
      HostComputer/wave_viewer.py
  17. +0
    -5
      PLSR/Inc/plsr.h
  18. +425
    -1493
      PLSR/Src/plsr.c
  19. +10
    -0
      PLSR/Src/plsr_internal.h
  20. +735
    -0
      PLSR/Src/plsr_planner.c
  21. +89
    -0
      PLSR/Src/plsr_planner.h
  22. +8
    -6
      PLSR/Src/plsr_platform.h
  23. +232
    -154
      PLSR/Src/plsr_platform_f407.c
  24. +1
    -0
      tests/plsr_host/run_tests.ps1
  25. +586
    -215
      tests/plsr_host/test_plsr_host.c

+ 2
- 0
.gitignore Vedi File

@@ -28,3 +28,5 @@ Thumbs.db
Desktop.ini
tmp/
.codex-tmp/
Document/PLSR_document/波形/
Document/PLSR_document/上位机图片/

+ 114
- 0
Document/PLSR_document/PLSR_5段理论频率包络.svg Vedi File

@@ -0,0 +1,114 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="760" viewBox="0 0 1200 760">
<style>
text { font-family: "Microsoft YaHei", "Noto Sans CJK SC", sans-serif; fill: #172033; }
.title { font-size: 22px; font-weight: 700; }
.sub { font-size: 15px; fill: #526078; }
.axis { stroke: #39465e; stroke-width: 1.5; }
.grid { stroke: #d9dfeb; stroke-width: 1; stroke-dasharray: 4 5; }
.boundary { stroke: #97a2b7; stroke-width: 1; stroke-dasharray: 5 5; }
.good { fill: none; stroke: #087f5b; stroke-width: 4; stroke-linejoin: round; stroke-linecap: round; }
.bad { fill: none; stroke: #d94841; stroke-width: 4; stroke-linejoin: round; stroke-linecap: round; }
.label { font-size: 14px; }
.small { font-size: 13px; fill: #526078; }
.pill-good { fill: #dff5ec; stroke: #087f5b; }
.pill-bad { fill: #ffebe9; stroke: #d94841; }
</style>

<rect width="1200" height="760" fill="#fbfcff"/>
<text x="600" y="32" text-anchor="middle" class="title">5 段 PLSR 理论频率包络(横轴:累计脉冲数)</text>
<text x="600" y="55" text-anchor="middle" class="sub">等待条件均为 4;同向连续执行;理想连续值,实际定时器频率会有少量量化</text>

<!-- Recommended global-lookahead chart -->
<text x="80" y="82" class="title" font-size="18">A. 满足脉冲数、加减速度、最终停止速度的全轨迹规划</text>
<rect x="80" y="92" width="66.25" height="238" fill="#eef4ff"/>
<rect x="146.25" y="92" width="198.75" height="238" fill="#f7f9fd"/>
<rect x="345" y="92" width="331.25" height="238" fill="#eef4ff"/>
<rect x="676.25" y="92" width="397.5" height="238" fill="#f7f9fd"/>
<rect x="1073.75" y="92" width="66.25" height="238" fill="#eef4ff"/>

<line x1="80" y1="330" x2="1140" y2="330" class="axis"/>
<line x1="80" y1="70" x2="80" y2="330" class="axis"/>
<line x1="80" y1="278" x2="1140" y2="278" class="grid"/>
<line x1="80" y1="226" x2="1140" y2="226" class="grid"/>
<line x1="80" y1="174" x2="1140" y2="174" class="grid"/>
<line x1="80" y1="122" x2="1140" y2="122" class="grid"/>
<line x1="80" y1="70" x2="1140" y2="70" class="grid"/>

<g>
<line x1="146.25" y1="92" x2="146.25" y2="330" class="boundary"/>
<line x1="345" y1="92" x2="345" y2="330" class="boundary"/>
<line x1="676.25" y1="92" x2="676.25" y2="330" class="boundary"/>
<line x1="1073.75" y1="92" x2="1073.75" y2="330" class="boundary"/>
</g>

<!-- Frequency envelope: S1, S2, S3, S4 accel/plateau/brake, S5 brake -->
<path class="good" d="M80 327.4 Q81.2 310 83.31 304 L146.25 304
Q150 285 156.19 278 L345 278
Q360 238 384.75 226 L676.25 226
Q735 145 835.25 122 L928 122
Q990 147 1073.75 213.7
Q1112 247 1140 327.4"/>

<rect x="760" y="86" width="292" height="30" rx="15" class="pill-good"/>
<text x="906" y="106" text-anchor="middle" class="label">段 4 提前制动,段 5 完成最终减速</text>

<text x="113" y="350" text-anchor="middle" class="label">段1</text>
<text x="246" y="350" text-anchor="middle" class="label">段2</text>
<text x="511" y="350" text-anchor="middle" class="label">段3</text>
<text x="875" y="350" text-anchor="middle" class="label">段4</text>
<text x="1107" y="350" text-anchor="middle" class="label">段5</text>
<text x="1075" y="207" text-anchor="end" class="label">≈4473 Hz</text>
<text x="1140" y="322" text-anchor="end" class="label">100 Hz</text>

<g class="small" text-anchor="end">
<text x="72" y="334">0</text>
<text x="72" y="282">2000</text>
<text x="72" y="230">4000</text>
<text x="72" y="178">6000</text>
<text x="72" y="126">8000</text>
<text x="72" y="75">10000 Hz</text>
</g>
<g class="small" text-anchor="middle">
<text x="80" y="369">0</text>
<text x="146.25" y="369">1000</text>
<text x="345" y="369">4000</text>
<text x="676.25" y="369">9000</text>
<text x="1073.75" y="369">15000</text>
<text x="1140" y="369">16000</text>
</g>

<!-- Current local-per-segment behavior chart -->
<text x="80" y="420" class="title" font-size="18">B. 若强制段 4 以 8000 Hz 结束:段 5 距离不足,只能高速切断</text>
<line x1="80" y1="690" x2="1140" y2="690" class="axis"/>
<line x1="80" y1="430" x2="80" y2="690" class="axis"/>
<line x1="80" y1="638" x2="1140" y2="638" class="grid"/>
<line x1="80" y1="586" x2="1140" y2="586" class="grid"/>
<line x1="80" y1="534" x2="1140" y2="534" class="grid"/>
<line x1="80" y1="482" x2="1140" y2="482" class="grid"/>
<line x1="80" y1="430" x2="1140" y2="430" class="grid"/>
<g>
<line x1="146.25" y1="430" x2="146.25" y2="690" class="boundary"/>
<line x1="345" y1="430" x2="345" y2="690" class="boundary"/>
<line x1="676.25" y1="430" x2="676.25" y2="690" class="boundary"/>
<line x1="1073.75" y1="430" x2="1073.75" y2="690" class="boundary"/>
</g>
<path class="bad" d="M80 687.4 Q81.2 670 83.31 664 L146.25 664
Q150 645 156.19 638 L345 638
Q360 598 384.75 586 L676.25 586
Q735 505 835.25 482 L1073.75 482
Q1105 495 1140 517.5 L1140 687.4"/>
<rect x="810" y="445" width="286" height="30" rx="15" class="pill-bad"/>
<text x="953" y="465" text-anchor="middle" class="label">段 5:8000 → 约6633 Hz → Gate</text>
<text x="1136" y="512" text-anchor="end" class="label">≈6633 Hz</text>
<text x="1140" y="708" text-anchor="middle" class="small">第16000脉冲后高速切断</text>
<g class="small" text-anchor="end">
<text x="72" y="694">0</text>
<text x="72" y="642">2000</text>
<text x="72" y="590">4000</text>
<text x="72" y="538">6000</text>
<text x="72" y="486">8000</text>
<text x="72" y="435">10000 Hz</text>
</g>

<text x="600" y="744" text-anchor="middle" class="sub">注意:图示是频率包络,不是 Y0 的高低电平方波;正常连续段边界不应出现额外低电平间隔。</text>
</svg>

+ 12
- 12
Document/PLSR_document/PLSR_PULSE_DIR测试用例与验收标准_2026-08-13.md Vedi File

@@ -311,12 +311,12 @@ py -B HostComputer\plsr_control_panel.py

| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 |
|---|---|:---:|:---:|---|
| 段顺序 | 依次经过 1、2、3、4、5、6、7、8、9、10 | | | |
| 段完整性 | 无漏段、重复段或提前完成 | | | |
| 最终状态/错误码 | 完成 7 / 错误 0 | | | |
| 最终位置 | `1045` | | | |
| 外部总上升沿 | `1045` | | | |
| 末段内部诊断 | 期望 109、实测 109、误差 0 | | | |
| 段顺序 | 依次经过 1、2、3、4、5、6、7、8、9、10 | 1 | | |
| 段完整性 | 无漏段、重复段或提前完成 | 1 | | |
| 最终状态/错误码 | 完成 7 / 错误 0 | 1 | | |
| 最终位置 | `1045` | 1 | | |
| 外部总上升沿 | `1045` | 1 | | |
| 末段内部诊断 | 期望 109、实测 109、误差 0 | 1 | | |

### TC-PD-012 起始段为第 10 段

@@ -328,11 +328,11 @@ py -B HostComputer\plsr_control_panel.py

| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 |
|---|---|:---:|:---:|---|
| 首个活动段 | 直接为第 10 段 | | | |
| 第 1~9 段 | 均未执行 | | | |
| 最终状态/错误码 | 完成 7 / 错误 0 | | | |
| 位置/外部计数 | 均为 200 | | | |
| 内部诊断 | 期望 200、实测 200、误差 0 | | | |
| 首个活动段 | 直接为第 10 段 | 1 | | |
| 第 1~9 段 | 均未执行 | 1 | | |
| 最终状态/错误码 | 完成 7 / 错误 0 | 1 | | |
| 位置/外部计数 | 均为 200 | 1 | | |
| 内部诊断 | 期望 200、实测 200、误差 0 | 1 | | |

### TC-PD-013 完成模式与后续模式

@@ -703,4 +703,4 @@ py -B HostComputer\plsr_control_panel.py --self-test
- 备注至少填写关键实测值;逻辑分析仪用例还应填写原始采集文件名。
- 每次执行前在本文件顶部或测试报告中记录固件 SHA256、日期和测试人员。
- 每次重烧固件后不得沿用上一版波形或诊断结果。
- 任一必测指标选择“否”,该用例结论即为 FAIL,并在备注填写问题编号和复现条件。
- 任一必测指标选择“否”,该用例结论即为 FAIL,并在备注填写问题编号和复现条件。

+ 1093
- 0
Document/PLSR_document/PLSR多段测试用例与验收标准_2026-08-14.md
File diff soppresso perché troppo grande
Vedi File


+ 250
- 0
Document/PLSR_document/PLSR波形验收报告_2026-08-14_完成模式10段阶梯频率.md Vedi File

@@ -0,0 +1,250 @@
# PLSR 波形验收报告:完成模式 10 段阶梯频率

> 测试日期:2026-08-14
> 测试方式:上位机 `plsr_control_panel.py` + 逻辑分析仪(6.25 MS/s,Y0 单通道数字采样)
> 波形文件:`Document\PLSR_document\波形\10段.bin`(28,565,856 采样 ≈ 4.571 s)
> 截图:`上位机图片\Snipaste_2026-08-14_13-19-30.png`(公共参数)、`Snipaste_2026-08-14_13-19-44.png`(10 段参数)
> 结论:**验收不通过**(脉冲总数正确,但段 2~8 恒速频率严重错误,且存在首脉冲偏长、段尾慢爬等已知问题复现)

---

## 1. 测试配置(与截图一致)

### 1.1 公共参数

| 字段 | 值 |
|---|---|
| 脉冲输出 | Y0 (0) |
| 方向输出 | Y12 (0) |
| 方向延时 ms | 0 |
| WAIT 输入 | X4 (0) |
| EXT 输入 | X4 (0) |
| 发送模式 | 脉冲发送完毕 (0)(完成模式) |
| 方向逻辑 | 正逻辑 (0) |
| 曲线模式 | 直线 (0) |
| 位置模式 | 相对位置 (0) |
| 段数 | 10 |
| 起始段 | 1 |
| 默认速度 Hz | 10000 |
| 启动速度 Hz | 100 |
| 停止速度 Hz | 100 |
| 加速时间 ms | 10 |
| 减速时间 ms | 10 |
| 输出模式 0x1200 | PULSE/DIR (0) |

### 1.2 段参数(每段 100 脉冲,等待类型 外部或完成 4,WAIT/ACT/跳转均为 0)

| 段 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| 配置频率 Hz | 1000 | 2000 | 3000 | 4000 | 5000 | 6000 | 7000 | 8000 | 9000 | 10000 |

---

## 2. 波形总览

| 项目 | 实测 | 判定 |
|---|---|---|
| 总采样 | 28,565,856(@6.25MS/s = 4.571 s) | — |
| 上升沿(脉冲总数) | **1000** = 10 段 × 100 脉冲 | ✅ 正确 |
| 波形起始 | 采集开始后 1725.9 ms 低电平,随后才有脉冲 | ℹ️ 采集窗口早于启动,不计故障 |
| 段间间隙 | 每段之间低电平约 **5.4 ~ 6.0 ms** | ⚠️ 异常(Bug 4 报告为 ~2.5 ms,此处更大) |
| 每段内部结构 | 首脉冲(5 ms 长高)+ ~96 个恒速脉冲 + 3 个 100 Hz 慢爬 | ⚠️ 异常 |

---

## 3. 逐段频率实测(核心结论)

每段恒速段频率取排除首脉冲与尾慢爬后的中位数:

| 段 | 配置频率 Hz | 实测恒速频率 Hz | 偏差 | 判定 |
|---|---:|---:|---:|:---:|
| 1 | 1000 | **1000** | 0 % | ✅ |
| 2 | 2000 | **1049** | −48 % | ❌ |
| 3 | 3000 | **1066** | −64 % | ❌ |
| 4 | 4000 | **1074** | −73 % | ❌ |
| 5 | 5000 | **1080** | −78 % | ❌ |
| 6 | 6000 | **1083** | −82 % | ❌ |
| 7 | 7000 | **1085** | −84 % | ❌ |
| 8 | 8000 | **1087** | −86 % | ❌ |
| 9 | 9000 | **9006**(含完整 ramp) | +0.07 % | ✅ |
| 10 | 10000 | **9905**(峰值,含完整 ramp) | −0.95 % | ✅ |

### 3.1 逐段频率形态

- **段 1**:首脉冲 182 Hz → 恒速 1000 Hz ×94 → 尾部 3 个 100 Hz 慢爬。恒速正确。
- **段 2~8**:首脉冲 ~183 Hz → 恒速 **~1049~1087 Hz**(每段仅比上一段高 15~25 Hz)→ 尾部 3 个 100 Hz 慢爬。**恒速频率几乎都落在 ~1 kHz 附近,与配置 2~8 kHz 严重不符**。
- **段 9**:首脉冲 100 Hz → ramp 196→6463→9006 Hz → 恒速 9006 Hz → 平滑减速(9006→8480→8013→…→183 Hz)→ 尾部慢爬。**加减速 ramp 完整且频率正确**。
- **段 10**:首脉冲 1050 Hz → ramp 1050→1928→…→9905 Hz(对称三角)→ 尾部 1050 Hz。**ramp 完整,峰值接近 10000 Hz**。

结论:加减速曲线路径(`PlsrRampAdvance`/短轮廓/Profile 队列)工作正常;**问题集中在完成模式下段 2~8 的恒速频率设定**,表现为"段启动后频率被限制在 ~1 kHz 附近缓慢爬升"。

---

## 4. 发现的问题清单

| # | 严重度 | 现象 | 疑似原因 | 关联已知问题 |
|---|---|---|---|---|
| 1 | **严重** | 段 2~8 恒速段输出 ~1049~1087 Hz,与配置 2000~8000 Hz 严重不符;段 9/10 正常 | 完成模式下多段恒速频率设定/段间预装交接(Handoff 双缓冲预装、频率队列写定时器 PSC/ARR)路径 | 未见对应历史记录,疑似新问题 |
| 2 | 中 | 每段第 1 个脉冲为约 5 ms 长高电平(~182 Hz),全 10 段复现 | 段启动首脉冲相位/定时器预载 | TC-PD-008 遗留"首脉冲偏长" |
| 3 | 中 | 每段尾部 3 个脉冲以 100 Hz(高 5 ms 低 5 ms,恰为停止速度)慢爬后停 | 减速预算不足时提前跳停止速度发完剩余 | 8-13 Bug 报告 Bug 1 复现 |
| 4 | 低 | 段间低电平间隙 5.4~6.0 ms | 有限路径段完成与下一段启动跨主循环周期 | 8-13 Bug 报告 Bug 4(原 ~2.5 ms,此处更大) |
| 5 | 信息 | 采集起始 1725.9 ms 无输出 | 逻辑分析仪先于启动命令开始采集 | 不计故障 |

---

## 5. 验收判定

| 验收项 | 标准 | 实测 | 结果 |
|---|---|---|---|
| 脉冲总数 | 1000 | 1000 | ✅ |
| 段顺序 | 1→2→…→10 | 顺序正确 | ✅ |
| 每段恒速频率 | 与配置一致(±1 Hz 量化差) | 段 1/9/10 正确;**段 2~8 错误** | ❌ |
| 段 9/10 加减速 ramp | 平滑、峰值正确 | 196→9006 Hz、1050→9905 Hz | ✅ |
| 首脉冲宽度 | 该段频率对应半周期 | 实测恒为 5 ms | ❌ |
| 段尾无慢爬 | 无 100 Hz 尾段 | 每段 3 个 100 Hz 慢爬 | ❌ |
| 段间间隙 | ≤ 2 ms | 5.4~6.0 ms | ❌ |

**总体结论:不通过。** 阻塞项为问题 #1(段 2~8 恒速频率错误)与 #2/#3(首脉冲偏长、段尾慢爬)。

---

## 6. 建议

1. **优先排查 #1**:在 `tests\plsr_host` 宿主测试中构造同配置(10 段、1000~10000 Hz 阶梯、每段 100 脉冲、完成模式),用 `PlsrTestOutputFrequency` 断言每段恒速频率,复现段 2~8 被限制在 ~1 kHz 的问题;重点审查完成模式段间交接(`PlsrBuildHandoffPlan`、`PlsrPreparedHandoffPlans` 双缓冲、`PlsrResolvedSegmentFrequency`、频率队列写 PSC/ARR 路径)。
2. **修复后回归**:`tests\plsr_host\run_tests.ps1` 全量通过(基线 94 tests / 515,670 assertions / 0 failures),并新增"完成模式多段阶梯频率逐段正确输出"用例。
3. **实板复测**:同配置重采波形,段 2~8 恒速频率与配置一致(±1 Hz),首脉冲宽度正常,段尾无 100 Hz 慢爬,段间间隙 ≤ 2 ms。

## 7. 分析脚本(复现用)

`tmp\analyze_wave_bin.py`(总览/频率统计)、`tmp\wave_final.py`(按段边界切段统计)、`tmp\wave_pulse_seq.py`(逐脉冲频率序列)、`tmp\wave_runs.py`(原始电平段明细)。

---

## 8. 13:58 复测与慢尾修复状态

> 当前 `Document\PLSR_document\波形\10段.bin` 已在 13:58 被新波形覆盖。前文第 2~5 节保留首次测试记录,本节以当前文件为准。

### 8.1 当前波形复测结果

| 项目 | 结果 |
|---|---|
| 文件时间 | 2026-08-14 13:58:17 |
| 采样 | 17,729,200 点,6.25 MS/s,2.836672 s |
| 上升沿 | 1000,计数正确 |
| 段 1~9 峰值/恒速 | 1000、2000、3000.5、4001.3、5000、6003.8、7006.7、8002.6、9005.8 Hz |
| 段 10 峰值 | 9936.4 Hz,短轮廓达到目标的 99.36% |

原阻塞问题“段 2~8 卡在约 1.05 kHz”已修复。当前文件仍能看到段尾提前到达 100 Hz,段 1~9 约残留 3~10 个低速脉冲;其低半周期将段间低电平拉长到约 5~6 ms。因此,当前 `10段.bin` 是**频率交接修复通过、慢尾修复前**的证据。

### 8.2 慢尾修复

根因是边界减速预算重复叠加了按频率和频差计算的余量,短斜坡被过早启动;同时首个减速频点要等下一次 1 ms 轮询才提交,长斜坡又依赖过大的提前量才能到达停止速度。

修复内容:

1. 删除重复的频差余量,按曲线积分估算所需脉冲。
2. 边界条件命中后立即提交首个减速频点,消除额外 1 ms 启动延迟。
3. 仅当一个轮询周期的脉冲数不超过斜坡面积 5% 时保留轮询余量,避免短斜坡出现停止速度慢尾。
4. 新增 10 段逐脉冲慢尾检查和 10 ms 边界斜坡专项回归。

### 8.3 自动验证结果

| 验证 | 结果 |
|---|---|
| Host 全量 | 100 tests / 525,879 assertions / 0 failures |
| 10 段 Host 慢尾 | 段 1~9 最后连续 `<=200 Hz` 脉冲均 `<=2` |
| 10 ms 边界专项 | 峰值 `>=99%`,最后连续 `<=200 Hz` 脉冲 `<=2` |
| IAR EWARM 8.3.4 | 0 errors / 1 warning |
| HEX | 194,945 B,SHA256 `12930062D965B1473B8AA3DEFFC950AF71E2056785BBCCF2DD0BE3F4A8360169` |
| 持久化区保护 | 烧录前后 SHA256 均为 `64946F391E62A1D9250E2E7F5D5E56DC55EC29F9CE6D269BE6808FD6205462C8` |
| COM5 Pulse/Direction 矩阵 | 12/12 通过 |
| COM5 延长 10 段 | 逐段捕获 1000~10000 Hz,总位置 10000,count error 0,mismatch 0 |
| 四路有限计数最终 IRQ | Y0~Y3 最大 344 / 400 / 340 / 404 cycles,均 `<1680` |
| 10 脉冲短轮廓性能 | IRQ 最大 1657 cycles,生产者最大 1380 cycles,均 `<1680` |
| 65,535 脉冲长 ramp 性能 | IRQ 最大 159 cycles;另测 ramp 更新最大 1320 cycles,均 `<1680` |

仓库 timing 脚本已改为在旧 `PlsrIrqCount` 无样本时读取有限硬件计数的 block/final IRQ 统计,消除新架构下的误报。完整 timing 阶段的 Pulse/Direction 部分全部通过;阶段最后的 AB Y2/Y3 负向诊断仍报 `flags=0x00FA`,属于已暂缓的 AB 路径,不计入本次 Pulse/Direction 结论。

### 8.4 最终波形验收状态

新固件已烧录,板上已恢复为 10 段、每段 100 脉冲、1~10 kHz、位置 0、IDLE。还需用逻辑分析仪覆盖采集一次 `10段.bin`,确认:

- 总上升沿为 1000;
- 段 1~9 无连续 100 Hz 慢尾(允许最多 2 个边界量化脉冲);
- 段间低电平不再被 100 Hz 半周期拉长到 5~6 ms;
- 各段峰值/恒速频率仍满足目标。

在新波形完成上述检查前,最终物理波形结论为:**待复测**。

### 8.5 14:34 第二次复测

`10段.bin` 于 14:34:09 再次覆盖,SHA256 为 `9C3D00869726986E0391C4D1D11E0E6A0A3601ACFE9D2BE01FAC24C0F94127B1`。

| 项目 | 结果 |
|---|---|
| 采样 | 11,009,376 点,6.25 MS/s,1.761500 s |
| 上升沿 | 1000,计数正确 |
| 段 1~10 峰值 | 1000、2000、3000.5、4001.3、5000、6003.8、7006.7、8002.6、9005.8、9952.2 Hz |
| 段 1~10 连续 `<=200 Hz` 尾脉冲 | 2、2、3、1、0、2、4、3、4、0 |
| 段 1~8 边界低电平 | 5.057920~5.989120 ms |
| 段 9 边界低电平 | 1.129280 ms |

结论:频率与计数通过,但段 3、7、8、9 的慢尾仍超过 2 个,最终波形验收不通过。实测表明 Host 模型比硬件少覆盖约 2 个定时器预装边界脉冲;边界低电平还包含下一段以 100 Hz 启动时的低半周期,需与慢尾分别判断。

随后已删除估算函数中固定追加的 2 个预装脉冲余量。新版本 Host 仍为 `100 tests / 525,879 assertions / 0 failures`,IAR 为 `0 errors / 1 warning`,新 HEX SHA256 为 `4E4ED664CE05F364B7443711DC85E719960B52BCBCD5FFC347D76F0CCFE5D863`,已烧录并通过 COM5 延长 10 段计数/频率测试。当前板上已恢复 10 段 x 100 脉冲、位置 0、IDLE,等待第三次逻辑分析仪采样。

## 9. 完成模式连续轨迹语义修复

14:34 波形显示每个中间段都重新从 100 Hz 启动。根据最终确认的语义,10 个“脉冲发送完成”段组成一条连续轨迹:启动速度只作用于第 1 段,停止速度只作用于第 10 段,同向中间边界按相邻段目标频率衔接。

该语义已写入代码并完成以下验证:

| 项目 | 结果 |
|---|---|
| Host 全量 | 102 tests / 526,542 assertions / 0 failures |
| 三段顺序/跳转专项 | 中间段不回到启动速度,跳转使用目标段频率 |
| 方向反转 | 不带速穿越,保留停机、方向建立和安全重启 |
| IAR EWARM 8.3.4 | 0 errors / 1 warning |
| HEX SHA256 | `7C6534FA05FB61BBA8CAF7838F36BAF908612CBDE525A17F23354E8C702FB5A2` |
| ST-LINK | `Verification...OK`;持久化 Sector 10~13 前后哈希一致 |
| COM5 三段板测 | 1000/2000/3000 Hz,位置 3000,错误 0,计数误差 0,曲线失配 0 |

板上已恢复本报告第 1 节的 10 段配置并处于 IDLE。现有 `10段.bin` 来自旧语义固件;必须覆盖采集新波形后,才能验收启动/停止速度作用范围和段间低电平。当前最终物理波形状态仍为:**待复测**。

## 10. 15:24 第三次复测与末段慢尾修复

`10段.bin` 于 15:24:39 覆盖,SHA256 为 `49BB40FE59A70DF41A82122AC6EAE253C48661F64E2B74C27F3EE01B3E03860B`。

| 项目 | 实测 | 判定 |
|---|---:|:---:|
| 采样 | 10,011,664 点,6.25 MS/s,1.601866 s | — |
| 上升沿/下降沿 | 1000 / 1000 | 通过 |
| 段 1~10 恒速/峰值 | 1~10 kHz 均达到目标 | 通过 |
| 中间段启动速度 | 第 2~10 段均从对应目标频率开始,未回到 100 Hz | 通过 |
| 段间低电平 | 0.250080~0.949760 ms | 通过 |
| 第 10 段减速 | 10 kHz 平滑下降,停止速度只作用于第 10 段 | 通过 |
| 第 10 段连续 `<=200 Hz` 周期 | 4 个,末脉冲高电平 5.000160 ms | 不通过 |

本次波形确认连续轨迹语义和段间低电平已修复,但发现最终段仍提前约 5 脉冲到达停止速度。根因是连续梯形面积与实际 1 ms 右端点离散更新面积不一致。

离散面积修复后的验证结果:Host `102 tests / 526,543 assertions / 0 failures`,IAR `0 errors / 1 warning`,HEX SHA256 `5851B7714454770DD8EF8A0ED8B01379791F4EF71BDF1B795B36639B18F258AB`,ST-LINK 校验及持久化区保护通过。COM5 同配置 10×100 脉冲计数通过,末段最终板内诊断频率为 3071 Hz,不再提前进入 100 Hz 慢尾。

板子已恢复同一配置并处于 IDLE,等待第四次覆盖采样。当前最终物理波形状态:**待复测**。

## 11. 非零加减速段间停波根因修复

用户确认当前 `10段.bin` 未被覆盖,因此不再重复分析该文件。另通过对比测试确认:加减速时间为 0 ms 时段间连续,非 0 ms 时出现约 250~900 us 间隔。

根因是 0 ms 配置使用硬件多段有限序列,而非 0 ms 配置此前退回每段独立有限计数,在主循环处理边界期间发生 stop/start。现已改为非零加减速同样保持一条硬件多段序列,首段加速、中间变速、末段减速都在序列持续运行期间完成。

修复后自动验证:Host `102 tests / 526,561 assertions / 0 failures`,9 个非末段边界均断言硬件序列和脉冲输出保持 active;IAR `0 errors / 1 warning`;HEX SHA256 为 `8778C24D3FDA364DC4B1D6FAD92E69D9090C5B8EC57D1C65DFCAC89EA0B5AC45`;ST-LINK 和持久化区保护通过;COM5 10×100 脉冲累计位置、逐段边界事件和末段诊断均通过。

当前板上为同一 10 段配置、位置 0、IDLE。物理零间隔结论等待新波形确认。

## 12. `bbb.bin` 单段短斜坡复核

`bbb.bin` 对应单段 1000 Hz x 100 脉冲,默认速度 10000 Hz,启动/停止 100 Hz,加减速 10 ms。直接解析原始边沿得到 100 个上升沿、100 个下降沿;首个上升沿至最后一个下降沿为 104.001120 ms,而不是理论/实测图片标注的约 526 ms。

图片中的长下降线是查看器缺陷:第 100 个脉冲后的采集空闲低电平被错误计入最后一个周期。查看器现已改为相邻上升沿计算周期,同一文件正确报告 100 个脉冲和 99 个可测完整周期。

原固件仍存在真实差异:首周期为 5.500160 ms(181.8129 Hz),末脉冲高电平仍为 0.500000 ms。根因是 1 ms 主循环无法及时提交持续时间仅约 1 ms 的启停斜坡。新固件已让不超过 1000 脉冲的单段 1 ms 短斜坡使用逐脉冲预计算,理论输出为首末约 550 Hz、中间 98 个 1000 Hz。Host `103 tests / 526,778 assertions / 0 failures`、IAR `0 errors / 1 warning`、烧录校验和持久化保护均已通过。COM5 板测位置 100、错误 0、expected/actual 100/100、count error 0、curve mismatch 0,末个诊断频率 550 Hz;等待覆盖采集新 `bbb.bin` 做最终物理波形验收。

+ 294
- 0
Document/PLSR_document/PLSR测试报告_2026-08-12.md Vedi File

@@ -488,3 +488,297 @@ CPU 时钟 168 MHz,使用 Cortex-M4 DWT CYCCNT 实测。
### 14.5 TC-PD-008 结论边界

验收表中 TC-PD-008 已填写的“多 1 个脉冲、首脉冲偏长、尾部窄脉冲”是修复前固件数据。本批固件已包含 PULSE/DIR 启动相位和最终边界终止修复,Host、板内计数及诊断均通过,但仍需用逻辑分析仪对当前 SHA256 固件复测,才能关闭端子外部 N+1、首周期和尾窄脉冲问题。原验收数据保留,不以板内结果覆盖。

## 15. 2026-08-14 默认频率、10 段交接与段尾慢脉冲修复

### 15.1 功能修复

- 段频率为 0 时运行使用默认频率,Modbus 寄存器仍读回原始值 0;启动前和运行中写 0 均覆盖。
- 修复有限脉冲动态 ramp 的 queued→active 提交卡住后,段 2~8 长时间停留在约 1.05 kHz 的问题。
- ramp 提交返回 STALE 时保持 ramp 活跃,终点频率必须成功提交后才结束。
- 边界条件命中后立即提交首个减速频点,消除额外 1 ms 轮询延迟。
- 删除重复的频差余量;仅当一个轮询周期的脉冲数不超过斜坡面积 5% 时保留轮询余量,避免短斜坡提前到达停止速度并发送慢尾脉冲。
- timing 上位机在旧逐脉冲 IRQ 统计为空时读取有限硬件计数 block/final IRQ 统计,适配当前硬件计数架构。

### 15.2 Host 全量回归

新增和强化用例:

- `finite_ramp_retries_terminal_frequency_after_stale`
- `complete_ten_segment_frequency_staircase`
- `ten_ms_boundary_ramp_no_slow_tail`
- `zero_segment_frequency_uses_default_speed`
- `dynamic_zero_frequency_uses_default_speed`

10 段用例逐脉冲检查 1~10 kHz 峰值,并要求段 1~9 最后连续 `<=200 Hz` 的脉冲不超过 2 个;10 ms 边界专项同时要求峰值达到 99% 且无慢尾。

```text
100 tests / 525,879 assertions / 0 failures
```

### 15.3 IAR、固件与持久化区

| 项目 | 结果 |
|---|---|
| IAR EWARM 8.3.4 | 0 errors / 1 warning |
| 唯一警告 | `PlsrCounterTriggerSourceAxis` set but never used |
| 只读代码 | 67,770 bytes |
| 只读数据 | 1,517 bytes |
| 读写数据 | 39,623 bytes |
| HEX 大小 | 194,945 bytes |
| HEX SHA256 | `4E4ED664CE05F364B7443711DC85E719960B52BCBCD5FFC347D76F0CCFE5D863` |
| ST-LINK | 扇区 0~3 擦写,`Verification...OK`,复位运行 |
| 持久化区 | 最新烧录前后 SHA256 均为 `44094568B88CC30AFB82D70D343AC7E67DC374A568854E6426EE21D431349783` |

### 15.4 COM5 Pulse/Direction 板测

产品矩阵 12 项全部通过:固定寄存器与异常码、正负/绝对/零位移、10 段顺序、从第 10 段启动、方向与极性矩阵、四路 100 kHz、短最终轮廓、WAIT、ACT、动态频率与重复 STOP、未来段改频、后续模式交接。

延长 10 段测试将每段脉冲数改为 1000,逐段捕获:

```text
1000, 2000, 3000, 4000, 5000,
6000, 7000, 8000, 9000, 10000 Hz
```

最终位置 10,000、错误 0;末段诊断 expected 1000、actual 1000、count error 0、mismatch 0。测试后已恢复 10 段 x 100 脉冲、位置 0、IDLE。

### 15.5 DWT 性能

门限为 `<1680 cycles`(100 kHz 周期)。

| 路径 | 实测最大值 | 结果 |
|---|---:|---|
| 四路有限计数最终 IRQ(Y0~Y3) | 344 / 400 / 340 / 404 cycles | PASS |
| 10 脉冲短轮廓 IRQ | 1657 cycles | PASS |
| 10 脉冲短轮廓生产者 | 1380 cycles | PASS |
| 65,535 脉冲长 ramp IRQ | 159 cycles | PASS |
| 65,535 脉冲 ramp 更新 | 1320 cycles(197 次) | PASS |

完整 timing 阶段的 Pulse/Direction 部分通过;最后进入 AB Y2/Y3 负向用例时,诊断标志为 `0x00FA`,该 AB 路径按当前调试范围暂缓,不计入本批 Pulse/Direction 结论。

### 15.6 逻辑分析仪状态

14:34 的第二次 `10段.bin` 已证明段 1~10 分别达到目标频率,但段 3、7、8、9 仍有 3~4 个连续低速尾脉冲。根据实测又移除了固定 2 脉冲预装余量并重新烧录;最终还需覆盖采集一次同配置波形,确认段尾连续低速脉冲不超过 2 个。段间约 5 ms 还包含下一段 100 Hz 启动的低半周期,需与慢尾分开判断。详细数据见 `PLSR波形验收报告_2026-08-14_完成模式10段阶梯频率.md`。

## 16. 2026-08-14 完成模式多段启动/停止速度语义修复

### 16.1 修复语义

当各段等待条件均为“外部或脉冲发送完成”,且由脉冲发送完成自然进入下一段时,多段按一条连续轨迹处理:

- 启动速度只用于第一执行段;
- 同方向中间段在前一段内向下一段频率过渡,完成时把当前速度带入下一段;
- 停止速度只作为最后执行段的段尾目标;
- 跳转段使用实际跳转目标段频率,不误用顺序下一段;
- 外部截断、WAIT/ACT 结束、STOP 和方向反转不带速穿越,仍执行安全停机/换向流程;
- 硬件多段常频序列只在加速和减速时间都为 0 ms 时启用,避免优化路径跳过首段加速或末段减速。

短段脉冲预算不足时,最后一段可能在到达停止速度前耗尽设定脉冲;此时要求按减速斜率平滑收尾,不额外补发脉冲以强行达到停止速度。

### 16.2 Host 全量回归

新增 `complete_natural_boundaries_carry_speed` 和 `complete_jump_carries_speed`,并更新零时间有限序列、同向/反向方向写入的旧语义断言。覆盖三段同向顺序、跳转段、第一段启动速度、中间段速度携带、最后一段停止减速及反向安全路径。

```text
102 tests / 526,542 assertions / 0 failures
```

### 16.3 IAR、固件与烧录

| 项目 | 结果 |
|---|---|
| IAR EWARM 8.3.4 全量构建 | 0 errors / 1 warning |
| 唯一警告 | `PlsrCounterTriggerSourceAxis` set but never used |
| 只读代码 | 67,806 bytes |
| 只读数据 | 1,517 bytes |
| 读写数据 | 39,623 bytes |
| HEX 大小 | 195,043 bytes |
| HEX SHA256 | `7C6534FA05FB61BBA8CAF7838F36BAF908612CBDE525A17F23354E8C702FB5A2` |
| ST-LINK | Sector 0~3 擦写,`Verification...OK`,复位运行 |
| 持久化区 | Sector 10~13 分块读取,四块烧录前后 SHA256 分别一致 |

### 16.4 COM5 实板内部验证

三段配置为 1000/2000/3000 Hz、每段 1000 脉冲,启动 100 Hz、停止 200 Hz、加减速 10 ms。结果:

- 终态 `COMPLETED`,错误 0,位置 3000;
- 三段首次可读运行频率为 1000/2000/3000 Hz,中间段未回到 100 Hz;
- 末段诊断 expected 1000、actual 1000、count error 0、mismatch 0;
- 末段最后诊断请求频率为 1041 Hz,证明停止减速只发生在最后一段,且短段在达到 200 Hz 前按设定脉冲数正常结束;
- 固定映射与异常码 smoke 复测通过。

测试后恢复 10 段、每段 100 脉冲、1~10 kHz、默认 10 kHz、启动/停止 100 Hz、加减速 10 ms、位置 0、IDLE。

### 16.5 待逻辑分析仪确认

当前代码、Host、构建、烧录和板内计数均通过。新语义固件仍需覆盖采集 `10段.bin`,确认启动速度只出现在第一段、停止减速只出现在第 10 段,并重新测量段间低电平;旧波形不作为本固件的物理波形结论。

## 17. 2026-08-14 15:24 连续轨迹波形与末段慢尾修复

### 17.1 15:24 波形结果

波形 SHA256:`49BB40FE59A70DF41A82122AC6EAE253C48661F64E2B74C27F3EE01B3E03860B`,共 10,011,664 点,6.25 MS/s,1000 个上升沿和 1000 个下降沿。

- 第 2~10 段起始运行频率分别为 2000、3000、4000、5000、约 6000、7000、8000、9000、10000 Hz;中间段不再回到 100 Hz,连续轨迹语义通过。
- 段 1~9恒速频率和向下一段的段尾过渡正确。
- 9 个段间低电平为 0.250080~0.949760 ms,均小于 1 ms;旧固件约 5~6 ms 的低电平消失。
- 第 10 段按 10000→100 Hz 减速,但脉冲 96~100 出现 183/100/100/100 Hz 周期和最终 5 ms 高电平;连续 `<=200 Hz` 周期为 4 个,超过最多 2 个的门限。

因此 15:24 波形证明多段启动/停止速度作用范围修复成功,但末段慢尾仍不通过。

### 17.2 末段慢尾根因与修复

边界估算原来使用连续梯形面积;实际 `PlsrRampAdvance` 每 1 ms 使用 `1/duration` 到 `duration/duration` 的右端点频率。10 kHz→100 Hz、10 ms 的连续面积约为 51 脉冲,右端点离散面积约为 46 脉冲,导致减速提前约 5 脉冲到达停止速度。

修复后:

- 短斜坡按实际右端点离散面积估算,避免提前进入停止速度慢尾;
- 允许完整轮询余量的长斜坡补偿半个端点差,保持 65,535 脉冲长轮廓能够到达停止速度;
- 10 段 Host 用例现在也检查第 10 段连续 `<=200 Hz` 尾脉冲不超过 2 个。

### 17.3 修复后验证

| 项目 | 结果 |
|---|---|
| Host 全量 | 102 tests / 526,543 assertions / 0 failures |
| 65,535 脉冲三曲线长轮廓 | 末频回归通过 |
| IAR EWARM 8.3.4 | 0 errors / 1 warning |
| 只读代码/数据、读写数据 | 67,886 / 1,517 / 39,623 bytes |
| HEX 大小 | 195,268 bytes |
| HEX SHA256 | `5851B7714454770DD8EF8A0ED8B01379791F4EF71BDF1B795B36639B18F258AB` |
| ST-LINK | `Verification...OK`;Sector 10~13 烧录前后分块哈希一致 |
| COM5 10×100 | 位置 1000、错误 0、末段 expected/actual 100/100、count error 0、mismatch 0 |
| 末段最终板内诊断频率 | 3071 Hz,未提前进入 100 Hz 慢尾 |

板上已恢复 10 段 x 100 脉冲、位置 0、IDLE。需再覆盖采集一次 `10段.bin`,确认物理端子的末段连续低速尾脉冲不超过 2 个。

## 18. 2026-08-14 非零加减速段间连续输出修复

### 18.1 根因

实测发现加减速时间为 0 ms 时段间没有间隔,而加减速时间非 0 ms 时仍有约 250~900 us 低电平。根因是两种配置走了不同硬件路径:

- 0 ms:整条同向轨迹使用硬件多段有限序列,计数器在段边界直接装载下一段,定时器不停机;
- 非 0 ms:此前限制了硬件多段序列,每段分别启动有限计数,段完成后等待主循环处理边界再启动下一段,产生一个主循环周期以内的停波。

该间隔不是启动/停止速度数值本身造成,而是非零斜坡配置触发了逐段 stop/start 路径。

### 18.2 修复

- 非零加减速也使用同一条硬件多段有限序列;
- 序列第一步从启动速度开始,并在硬件序列持续运行期间执行 1 ms 斜坡更新;
- 中间硬件边界只切换软件段状态和诊断,不停止脉冲定时器;
- 最后一个段仍在同一序列内执行停止减速并由硬件总计数终止;
- 同向可连续段以外的方向反转和不满足序列条件的路径仍保留安全停机。

### 18.3 验证

| 项目 | 结果 |
|---|---|
| Host 全量 | 102 tests / 526,561 assertions / 0 failures |
| 非末段边界 active 断言 | 9 个边界均保持有限序列和脉冲输出 active |
| IAR EWARM 8.3.4 | 0 errors / 1 warning |
| 只读代码/数据、读写数据 | 67,954 / 1,517 / 39,623 bytes |
| HEX 大小 | 195,456 bytes |
| HEX SHA256 | `8778C24D3FDA364DC4B1D6FAD92E69D9090C5B8EC57D1C65DFCAC89EA0B5AC45` |
| ST-LINK | `Verification...OK`;Sector 10~13 烧录前后分块哈希一致 |
| COM5 10×100 | 位置 1000、错误 0、末段 expected/actual 100/100、count error 0、mismatch 0 |

板上已恢复 10 段 x 100 脉冲、位置 0、IDLE。尚需新逻辑分析仪波形确认 9 个物理段边界不再出现额外低电平;未覆盖的旧 `10段.bin` 不重复分析。

## 19. 2026-08-14 单段 1 kHz 短斜坡与波形解析修复

### 19.1 `bbb.bin` 原始边沿复核

配置为 1 段、默认速度 10000 Hz、启动/停止速度 100 Hz、加减速时间 10 ms、段频率 1000 Hz、脉冲数 100。原始文件 `bbb.bin` 为 9,708,896 点、6.25 MS/s,共检测到 100 个上升沿和 100 个下降沿。

| 项目 | 原固件实测 |
|---|---:|
| 首个上升沿至最后一个下降沿 | 104.001120 ms |
| 首脉冲高/低电平 | 5.000160 / 0.500000 ms |
| 首个上升沿周期 | 5.500160 ms,即 181.8129 Hz |
| 中间可测周期 | 基本为 1.000000 ms,即 1000 Hz |
| 末脉冲高电平 | 0.500000 ms |

原理论/实测图片中的约 526 ms 红色减速尾线不是硬件输出。旧查看器把第 100 个脉冲后的采集空闲低电平当成最后一个周期,从而生成一个接近 0 Hz 的伪频率点并与前一点连线。查看器已改为只使用相邻两个上升沿计算周期;同一文件现在报告 100 个脉冲、99 个可测完整周期,不再绘制伪尾线。

### 19.2 固件问题与修复

原固件仍有真实的首末脉冲轮廓问题。100→1000 Hz 和 1000→100 Hz 按默认速度 10000 Hz、加减速 10 ms 计算,斜坡持续时间均约 0.9 ms,离散为 1 ms。有限计数路径依赖 1 ms 主循环提交频率,无法在已经开始的 100 Hz 首个高半周期内及时改频,也无法保证末脉冲前提交停止斜坡。

修复后,单段脉冲数不超过 1000 且存在 1 ms 短斜坡时,优先使用整段逐脉冲预计算队列;正常的较长有限斜坡、多段连续硬件序列和零斜坡恒速路径保持不变。该配置的预计算结果为:首脉冲约 550 Hz、中间 98 个脉冲为 1000 Hz、末脉冲约 550 Hz,完整周期累计约 101.64 ms。

### 19.3 自动验证与烧录

| 项目 | 结果 |
|---|---|
| 新增精确回归 | 首/末 549~551 Hz;中间 98 个 1000 Hz;总周期 101.6~101.7 ms |
| Host 全量 | 103 tests / 526,778 assertions / 0 failures |
| IAR EWARM 8.3.4 | 0 errors / 1 warning |
| 只读代码/数据、读写数据 | 68,006 / 1,517 / 39,623 bytes |
| HEX 大小 | 195,612 bytes |
| HEX SHA256 | `068BDC9D417E3C1DEE377BA3053CBB850B32ED3B7EBBB1E9F7653623F29F713A` |
| 烧录 | STM32CubeProgrammer 下载、校验、复位成功 |
| 持久化区 `0x080C0000~0x080FFFFF` | 烧录前后 SHA256 均为 `FC494806B0465476E51678F961A3EC63873C32915A214B31DFB4307048D3949A` |

COM5 板测已通过:位置 100、状态 COMPLETED、错误 0;诊断 expected/actual 为 100/100、count error 0、curve mismatch 0,末个诊断请求/定时器频率均为 550 Hz。测试后已清位置并恢复为 IDLE,保留同一单段配置。新逻辑分析仪波形仍待覆盖采集。

## 20. 2026-08-14 短斜坡单段/多段通用根治

### 20.1 架构修复

第 19 节的单段短轮廓分流仅能覆盖单段样本,不作为最终修复。根因是有限序列曾把一个逻辑段直接等同于一个硬件 step,首段加速和末段减速只能依赖 1 ms 主循环更新;只要斜坡在一个或少数脉冲内完成,单段和多段都会遇到同一问题。

最终实现将“硬件频率 step”和“逻辑段完成边界”拆开:

- 首段入口短斜坡和最终段出口短斜坡在启动前按脉冲域生成硬件 step;
- 计数器 IRQ 只在脉冲边界预装下一组定时器参数,不在 IRQ 内计算曲线;
- 每个硬件 step 保存逻辑段内脉冲偏移,只有逻辑段最后一个 step 发布段完成事件;
- 段内位置、剩余脉冲、诊断计数仍按逻辑段累计,不把斜坡子 step 当成新段;
- step 索引扩展为 16 bit,统一上限为 2010;step 缓冲由 Core 静态持有,平台只保存运行期指针,避免四路重复占用 RAM;
- 已删除“单段、脉冲数不超过 1000、斜坡不超过 1 ms”的临时特判。

### 20.2 Host 回归

| 用例 | 覆盖内容 | 结果 |
|---|---|---|
| 单段 100 脉冲 | 100→1000→100 Hz,首末约 550 Hz,中间 98 个 1000 Hz | PASS |
| 3 段无 Poll | 入口斜坡、1000→2000→3000 Hz 硬件边界、最终出口斜坡;300 个脉冲期间不调用 `PlsrPoll1ms()` | PASS |
| 10 段无 Poll | 启停 100 Hz、段频率 1000 Hz、默认速度 10000 Hz、加减速 10 ms;9 个边界持续输出 | PASS |
| 既有完整回归 | 有限计数、动态频率、方向反转、短轮廓、AB 相、诊断、持久化和异常路径 | PASS |

最终结果:

```text
104 tests / 527,097 assertions / 0 failures
```

### 20.3 IAR、资源与固件

| 项目 | 结果 |
|---|---|
| IAR EWARM 8.3.4 | 0 errors / 1 个既有未使用变量 warning |
| 只读代码 / 只读数据 / 读写数据 | 69,258 / 1,517 / 87,096 bytes |
| 通用有限 step 缓冲 | 48,240 bytes |
| HEX 大小 | 199,130 bytes |
| HEX SHA256 | `9B296CF02101F78A70F4CCF2BB89DC75AA27E8261F8AED451764854EE2106C4C` |
| ST-LINK | 下载、校验、复位和运行成功 |

### 20.4 最坏速率门限

板上使用 90 kHz→100 kHz、默认速度 10 kHz、加速时间 10 ms、2000 脉冲测试。该轮廓在定时器量化合并后触发 188 次硬件 step IRQ:

| 指标 | 实测 | 100 kHz 门限 |
|---|---:|---:|
| block IRQ 最大周期 | 396 cycles,约 2.36 us | 1680 cycles,10 us |
| final IRQ 周期 | 410 cycles,约 2.44 us | 1680 cycles,10 us |
| 最小余量 | 约 7.56 us | 必须大于 0 |

性能门限 PASS。IRQ 内无除法、开方或轮廓计算。

### 20.5 COM5 与持久化

同一最坏速率配置板测结果:状态 COMPLETED、位置 2000、错误 0、expected/actual 2000/2000、count error 0、curve mismatch 0、reason 0。

性能测试会按设计写入配置和位置。测试前已备份 `0x080C0000~0x080FFFFF`,测试后仅恢复 sector 10/11;恢复镜像 SHA256 与测试前均为 `7BE5A392BCCB26A510516B80F44FE8FEA287CE58E653325AA7964DDAEC38E4F1`。板上最终运行关闭 timing 的 release 固件。

+ 388
- 0
Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md Vedi File

@@ -0,0 +1,388 @@
# PLSR 重构设计书:加减速与多段统一(V1.1)

> 范围:仅 PULSE/DIR 模式。AB 正交模式维持现状,作为第二阶段另行设计。
> V1.1 变更:采纳审查意见,补齐四根承重梁——①多段边界语义解析层;②唯一硬件执行器
> (所有权状态机);③STOP/EXT/动态改频统一的 generation 原子重规划;④硬件量化进入
> 规划闭环。修正脉冲守恒公式表述,修正"预算不足=错误"的文档矛盾。
> 配套依据:`PLSR_Bug报告_2026-08-13_短轮廓与段间延迟.md`、`PLSR方案设计书_V1.0.md`。

---

## 1. 问题定义

当前固件中"加减速"存在两套并行实现(时间域 ramp 与脉冲域短轮廓),四条输出路径,
接力点由启发式估算决定且不守恒。逻辑分析仪实测(Bug 报告)已证实四类缺陷:

| Bug | 现象 | 结构性根因 |
|---|---|---|
| 1 | 减速预算不足时跳停止速度,尾部慢爬 | 长 ramp 路径没有"按剩余脉冲重算斜坡" |
| 2 | 加速 ramp 整体消失,首脉冲即目标频率 | ramp 按挂钟计时,与首脉冲等待时间脱节 |
| 3 | 整段以启动/停止速度输出 | 同上极端版 + 0 脉冲消耗判定缺陷 |
| 4 | 有限路径段间固定 ~2.5ms 低电平间隙 | 段完成事件跨两个 1ms 周期消费 |

### 1.1 无唯一执行所有权(代码事实)

平台层 PSC/ARR 写入散在 5 个点,任务域与中断域两条通道并存,互斥靠临界区 +
写保护 guard + generation 计数拼凑:

| 写入点 | 域 | 触发场景 |
|---|---|---|
| `PlsrPlatformUpdateFinitePrepared` | 任务域 | 有限轮廓运行中调频/ramp 更新 |
| `PlsrPlatformUpdateFiniteStep` | 任务域 | 有限序列运行中改写旧 step 表 |
| `PlsrPlatformQueueFrequency` | 任务域 | 时间域 ramp 逐 tick 调频 |
| `PlsrPlatformLoadPreparedFromIrq` | 中断域 | profile 队列/handoff 预装载 |
| `PlsrFinitePrepareNextStepIrq` | 中断域 | step 表切换预装载 |

`PlsrCutRequested` 的消费点只在 `PlsrPulseTimerIrq`,而有限路径由计数器中断驱动、
不走该函数——**有限轮廓运行中触发 EXT 时 cut 语义丢失**,要等段自然结束才被当作
普通边界处理。

### 1.2 重构原则

1. **不重写硬件层**:定时器预装载时序、TIM9/TIM12 计数、掉电保持均为已验证资产。
2. **单一所有权**:任何时刻只有一个执行状态在推进;只有硬件预装载引擎写 PSC/ARR。
3. **规划与执行分离**:规划器是纯函数(流式),执行器只负责在安全边界装载下一项。
4. **每个阶段可编译、可宿主机测试、可板级波形回归**。
5. 第一轮不动 AB 模式(执行状态机预留 `AB_STREAM` 占位)。

---

## 2. 目标架构总览

```
原始段参数(PLSR_CONFIG + 当前段 + 实际位置)
┌───────────────────────────────┐
│ 多段边界语义解析器(新) │ SEND_COMPLETE / SEND_SUBSEQUENT /
│ 段序列 → MotionBlock 序列 │ WAIT 类型 / 跳转 / 同向连续 / 换向
└───────────────────────────────┘
│ MotionBlock(entryHz, cruiseHz, exitHz, pulseBudget, boundaryAction)
┌───────────────────────────────┐
│ 脉冲域规划器(新,纯函数,流式) │ 相位法:∫f dt = k → t_k → f[k]
│ 量化闭环:以实际定时器周期累计 │ 输出 PLSR_STREAM_ITEM(含 repeat 压缩)
└───────────────────────────────┘
│ 轨迹流
┌───────────────────────────────┐
│ 唯一硬件执行器(PLSR_EXEC_MODE) │ STEP_TABLE / STREAM 两种供给方式,
│ 只有它写 PSC/ARR │ 一个执行状态机
└───────────────────────────────┘
状态机 / 命令 / Modbus(PlsrPoll1ms 保持 1ms 节拍,不再逐 tick 调频)
```

---

## 3. 多段边界语义解析层(承重梁①)

规划器只管"一个 MotionBlock 内的脉冲域轨迹"。段序列语义在上层解析为块序列,
每次边界推进时用**实际位置与方向**解析下一块(绝对位置、换向因此天然支持)。

```c
typedef enum
{
PLSR_BOUNDARY_STOP, /* 运动结束 */
PLSR_BOUNDARY_NEXT_FRESH, /* 完成发送:减速到停止速度,下一块从启动速度起 */
PLSR_BOUNDARY_NEXT_CARRY, /* 后续发送同向连续:exitHz = 下一块 cruiseHz */
PLSR_BOUNDARY_WAIT_TIME, /* WAIT 时间 → 状态机等待,不属执行器 */
PLSR_BOUNDARY_WAIT_SIGNAL, /* WAIT 信号 → 同上 */
PLSR_BOUNDARY_WAIT_EXT, /* EXT 信号 → 同上 */
PLSR_BOUNDARY_JUMP, /* 跳转段 */
PLSR_BOUNDARY_EXT_CUT /* EXT 提前截断(立即切断语义) */
} PLSR_BOUNDARY_ACTION;

typedef struct
{
uint32_t entryHz; /* 块起点:启动速度 或 上一块 carry */
uint32_t cruiseHz; /* 段目标频率 */
uint32_t exitHz; /* 块终点:停止速度 或 下一块 cruiseHz */
uint32_t pulseBudget; /* 本块脉冲预算(uint32_t) */
PLSR_BOUNDARY_ACTION boundary;
} PLSR_MOTION_BLOCK;
```

解析规则(落实 SEND 模式与边界语义,避免"完成发送/后续发送被当成同一结果"):

1. **SEND_COMPLETE**:`exitHz = stopSpeedHz`(decelerationTimeMs>0 时),
`boundary = NEXT_FRESH`;下一块 `entryHz = startSpeedHz`。
2. **SEND_SUBSEQUENT + 同向 + waitType==EXT_OR_COMPLETE + 无跳转**:
`exitHz = 下一段 cruiseHz`,`boundary = NEXT_CARRY`;下一块 `entryHz = carry`。
3. **换向**:强制 NEXT_FRESH 语义(exitHz = stopSpeedHz),方向延时在块间由状态机
处理,不属于执行器。
4. **绝对位置模式**:段位移 = pulses − 实际位置,方向在解析时确定;解析器在每个
边界用 `PlsrPosition` 的最新值解析下一块,不允许跨段预缓存方向。
5. **WAIT_TIME / WAIT_SIGNAL / WAIT_EXT**:块在脉冲完成处结束,边界转入状态机
等待,重入运动时从解析器重新取块。
6. **EXT_OR_COMPLETE**:正常运行语义 = NEXT_FRESH 或 NEXT_CARRY(同 1/2);
EXT 边沿事件 = `BOUNDARY_EXT_CUT`,走 §6 原子重规划(立即切断)。

---

## 4. 脉冲域规划器(承重梁④)

### 4.1 守恒定义(修正 V1.0 公式错误)

- **脉冲数守恒**:`N = ∫ f(t) dt`。
- **第 k 个脉冲边界** `t_k` 满足 `∫[0,t_k] f(t) dt = k`(k = 1..N)。
- **第 k 个脉冲周期**:`Δt_k = t_k − t_{k−1}`,目标频率 `f[k] = 1/Δt_k`。
- **量化闭环**:`f_timer[k] = BuildTimerSetting(f[k])` 的 actualHz(PSC/ARR 量化
结果),并以**量化后的真实周期**反推相位推进,后续边界在此基础上继续求解——
量化误差不累积。`BuildTimerSetting` 必须无副作用、可在宿主机纯运行。

现有实现对照:`PlsrRampAreaQ32` 即 ∫f dt 的 Q32 积分、`PlsrExactRampBoundaryQ32`
即相位边界求解,二者原样迁入;**新增**量化反馈环。

### 4.2 流式接口(不再有 1000 脉冲算法边界)

```c
typedef struct
{
uint16_t psc; /* 量化后定时器设定 */
uint16_t pairPsc; /* PULSE/DIR 恒 0(AB 复用) */
uint32_t arr;
uint32_t compare;
uint32_t actualHz;
} PLSR_TIMER_STEP;

typedef struct
{
PLSR_TIMER_STEP step;
uint32_t repeatCount; /* 匀速区压缩:连续同频脉冲合并 */
} PLSR_STREAM_ITEM;

typedef struct
{
PLSR_MOTION_BLOCK block;
uint32_t appliedHz; /* 起点:实际生效频率(见 §6.3) */
uint64_t phasePulses; /* 起点相位:已实际输出脉冲数 */
/* 内部:曲线积分表索引、Q32 相位累计、量化误差累计 */
uint8_t curveMode;
/* ... */
} PLSR_PLANNER_CONTEXT;

typedef enum
{
PLSR_PLANNER_OK = 0, /* 正常生成(含完整梯形稳态段) */
PLSR_PLANNER_CLIPPED, /* 预算不足,结果为可执行的截断形态(三角/纯加减速) */
PLSR_PLANNER_DONE, /* 全部脉冲已生成 */
PLSR_PLANNER_INVALID /* 参数非法 */
} PLSR_PLANNER_STATUS;

PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *ctx,
const PLSR_MOTION_BLOCK *block,
uint32_t appliedHz,
uint64_t phasePulses);
uint16_t PlsrPlannerGenerate(PLSR_PLANNER_CONTEXT *ctx,
PLSR_STREAM_ITEM *out,
uint16_t capacity);
```

要点:

- `pulseBudget` 为 `uint32_t`;`phasePulses` 为 `uint64_t`(长运动相位累计)。
- 匀速区以 `{step, repeatCount}` 压缩;ramp 区逐脉冲(或按可合并步长)输出。
- 环形执行队列只保存未来几十到几百项;1000 仅是执行缓存上限,不再是算法边界。
- **CLIPPED 不是错误**:三角、纯加速/减速、可达终速轨迹都是短距离运动的正常结果。

### 4.3 宿主机单测(并入 `tests/plsr_host`)

- 相位守恒:随机 (from, cruise, exit, N, curveMode),对每个生成脉冲验证
`|Σ 1/f_timer[k] − ∫dt|` 误差有界且不随 N 增长(量化闭环验收)。
- 脉冲数精确:`Σ repeatCount = N`(含 CLIPPED 形态)。
- 单调性:加速序列非降、减速序列非升。
- 边界:N=1、N=2、预算恰等于完整梯形所需(稳态段恰为 0~1)。
- Bug 1/2/3 配置重演:无 100Hz 尾巴;首脉冲后规划;10Hz 启动重规划。
- 峰值对照:`f_peak² ≈ 2·N·defaultSpeedHz·1000/(t1+t2) + 端点加权`(补推导注释)。

---

## 5. 唯一硬件执行器(承重梁②)

### 5.1 执行状态机

```c
typedef enum
{
PLSR_EXEC_IDLE = 0, /* 无运动 */
PLSR_EXEC_STEP_TABLE, /* 供给方式一:整体预展开 step 表(有限序列) */
PLSR_EXEC_STREAM, /* 供给方式二:队列续块流式(长段/方向切换/CLIPPED) */
PLSR_EXEC_AB_STREAM, /* AB 模式占位(第二阶段迁入) */
PLSR_EXEC_REPLANNING, /* 原子重规划过渡态(STOP/EXT/改频统一入口) */
PLSR_EXEC_STOPPING /* 受控停止排空 */
} PLSR_EXEC_MODE;
```

**所有权规则(写死)**:

1. STEP_TABLE 与 STREAM 只是两种**数据供给方式**,不是两个执行器;二者共享同一个
执行状态机、同一套计数器/预装载推进逻辑。
2. 只有执行器内部的 `PlsrExecLoadNext()` 写 PSC/ARR(写保护 guard、UIF 提交、
generation 校验全部集中在这一个函数里)。
3. `PlsrPoll1ms`、Modbus 写寄存器、STOP/EXT/动态改频、规划器:**均不得直接写
定时器寄存器**,只能向执行器提交请求(进入 REPLANNING)。
4. AB 模式第一轮仍走旧路径,但必须经 `PlsrExecLoadNext()` 兼容出口(或加
`PLSR_RAMP_LEGACY` 隔离段),保证"写 PSC/ARR 的通道唯一"这一约束自 Phase 1
起全局成立。

### 5.2 供给方式选择(决策点收敛)

```c
status = PlsrPlannerBegin(&ctx, &block, appliedHz, phase);
if (status == PLSR_PLANNER_OK || status == PLSR_PLANNER_CLIPPED) {
/* 可整体预展开(总步数≤上限、无运行时变数)→ STEP_TABLE
否则 → STREAM
两种方式都由同一执行器消费,禁止第四条路径 */
} else {
fault();
}
```

- STEP_TABLE 限制(同向、完成模式、相对位置)**放松**为"规划器可整体预计算即可";
其余一律 STREAM。
- STREAM 的队列生产者从"逐项二分求解"改为"从规划器输出缓冲复制",ISR 侧不再跑
数学。

### 5.3 保留的硬件语义

预装载时序(ARPE/OC1PE、UI 提交、写保护窗口)、计数器块式执行(
`PLSR_COUNTER_BLOCK_PULSES`、CC1 提前一脉冲准备、UI 块完成)、下降沿安全停止
(`PlsrFiniteStopAtFallingEdge`)、停止重定向(`PlsrPlatformRetargetFiniteStop`)
全部保留;只是这些函数变成执行器内部实现,外部不再直接调用。

---

## 6. 原子重规划协议(承重梁③)

STOP、EXT、动态改频统一走同一套过程,不再分别散落在
`PlsrExecuteStop` / `PlsrRequestCut` / `PlsrFrequencyUpdatePending`。

### 6.1 统一协议

```
事件源:STOP 命令 | EXT 边沿(CUT 或受控) | 动态改频
① 冻结:临界区内读三件事:
countedPulses = 硬件已计数脉冲(含未发布块)
appliedHz = 正在实际输出的频率(PlsrTimerActiveSetting)
committed = 已预装载、不可撤销的下一脉冲数(1~2,现
PlsrStopDrainPulseCount 概念的推广)
② 失效:planGeneration++(旧 step 表 / profile 队列 / handoff 计划全部原子失效)
③ 重规划:pulseBudget = targetPulses − countedPulses − committed
PlsrPlannerBegin(appliedHz, phase = countedPulses)
(STOP 必须从 appliedHz 起,不能从 commanded/preloaded 起)
④ 切换:在下一个安全脉冲边界(UI 提交点 / 写保护窗口)由 PlsrExecLoadNext 装载
新计划;committed 项按原计划排空后生效
状态迁移:任意态 → REPLANNING → (STEP_TABLE | STREAM | STOPPING | IDLE)
```

### 6.2 EXT 两种语义(明确闭环)

| 语义 | 触发 | 行为 |
|---|---|---|
| 立即切断 | `EXT_OR_COMPLETE` 段中 EXT 边沿 | 重规划为空计划,最近安全下降沿停止(与信捷 PLSR"提前结束"对齐) |
| 受控停止 | (若需求扩展配置位) | 从 appliedHz 按剩余预算重规划减速,走 STOP 同一条协议 |

两者都必须经 REPLANNING,**不允许只设一个逻辑标志等普通脉冲 IRQ 消费**——这正是
当前 `PlsrCutRequested` 在有限路径无消费点的缺陷。

### 6.3 三种频率的明确区分

```c
commandedHz /* 逻辑层已命令(规划器请求值) */
preloadedHz /* 已写 PSC/ARR 预装载、尚未经 UI 提交 */
appliedHz /* 正在实际输出(PlsrTimerActiveSetting) */
```

重规划的起点只能是 `appliedHz`,预算必须扣掉 `committed`(已预装载不可撤销项),
否则产生一脉冲错位或 STOP 初始频率跳变。

### 6.4 动态改频

`PlsrFrequencyUpdatePending` 路径替换为:REPLANNING(新目标 Hz + 剩余预算)→
生成新轨迹 → 安全边界切换。**不再局部修改旧 step 表**
(`PlsrPlatformUpdateFiniteStep` 退役),保证已预计算的后续减速段同步更新。
有限序列运行中的段频率改写亦走同一协议。

---

## 7. 失败测试先行(最高优先级动作)

在动架构之前,先把三组失败测试落在宿主机框架里(当前代码预期失败,记录失败模式):

| # | 用例 | 预期行为 | 当前代码失败模式 |
|---|---|---|---|
| T1 | 有限轮廓运行中触发 EXT | 最近安全边界切断,位置=已输出脉冲 | `CutRequested` 无消费点,段跑完,cut 丢失 |
| T2 | 有限加速过程中触发 STOP | 从 appliedHz 受控减速,总脉冲精确,无频率跳变 | 时间域 ramp 与脉冲域 retarget 混用,预算不含 committed |
| T3 | 有限加减速过程中动态修改目标频率 | 从 appliedHz 重规划到新目标,后续减速同步更新 | 局部改旧 step 表 + 时间域 ramp,旧减速段残留 |

补充用例(Phase 2 后追加):T4 stream 模式运行中 EXT/STOP;T5 段边界 NEXT_CARRY
与 NEXT_FRESH 的 exitHz 正确性(宿主机断言规划器输入块)。

三组失败测试在 Phase 1/2 的通过情况即为验收闸门。

---

## 8. 迁移路线(优先级:所有权 → 重规划 → 统一规划器 → 曲线校准)

| Phase | 内容 | 验收闸门 |
|---|---|---|
| 0 | 基线锁定(bug 报告 4 配置波形存档)+ 三组失败测试落地(预期红) | 基线入库;失败测试红且失败模式与 §7 表一致 |
| 1 | 执行状态机 `PLSR_EXEC_MODE` + 单一所有权:PSC/ARR 写入收敛到 `PlsrExecLoadNext`,AB 加 LEGACY 隔离 | 编译干净;旧功能全量回归;T1 失败模式变为"协议未实现"(不再是静默丢 cut) |
| 2 | 原子重规划:STOP/EXT/动态改频统一入口(REPLANNING),appliedHz 起点 + committed 扣减 | T1/T2/T3 转绿;板级 EXT/STOP 波形验收 |
| 3 | 流式规划器 + 边界语义解析器落地;时间域 `PlsrRamp`/`PlsrMaybePlanBoundaryRamp`/`PlsrRampPulseEstimate` 退役;量化闭环生效 | 宿主机全绿;Bug 1/2/3/4 配置回归通过 |
| 4 | 曲线校准:正弦/S 曲线与信捷对标数据(`PLSR信捷对标追踪矩阵.md`)逐点校准 | 对标矩阵通过;波形时频曲线容差达标 |
| 5 | AB 第二阶段:AB 迁入 STREAM(规划器支持 pairPsc) | AB 验收用例全量回归 |

每 Phase 的板级对比容差:总脉冲数精确、时频曲线形状一致、无速度跳变;逐 tick
频率值不作为对比基准(脉冲域规划无 1ms 台阶,理论上优于时间域)。

---

## 9. 验收标准汇总

1. 宿主机测试:现有 `test_plsr_host.c` 全绿 + 新增规划器/执行器/重规划单测全绿。
2. 失败测试:T1/T2/T3 转绿(Phase 2 后),T4/T5 转绿(Phase 3 后)。
3. Bug 报告回归表:

| 配置 | 预期 |
|---|---|
| bug1:100000/100/100/100/100/1000 | 三角平滑到 100Hz,总时长 ~64ms,无 100Hz 尾巴 |
| bug2:加速 10ms/启动 100Hz/1000 脉冲 | 首脉冲后完整加速可见,总时长 ~20ms |
| bug3:启动 10Hz/1000 脉冲 | 首脉冲等待 100ms 后按剩余脉冲重新规划 |
| TC-PD-011:10 段恒速 1000Hz | 段间间隙 <1ms |

4. 波形验收:`wave_viewer.py` / `bin_to_time_freq.py` 时频曲线 vs 理论曲线。
5. 诊断寄存器:`COUNT_PASS` / `FREQUENCY_PASS` / `CURVE_PASS` 全置位。

---

## 10. 风险与回退

1. **量化闭环的定点取舍**:相位累计用 Q32 整数(沿用 `PlsrRampAreaQ32`),量化
反馈用 `actualHz` 反推真实周期,宿主机单测必须覆盖"长期累计误差有界"。
2. **REPLANNING 与中断竞态**:冻结读三件事与 generation 失效必须在同一临界区;
宿主机测试覆盖"重规划瞬间恰好产生脉冲"用例(现有 `PlsrTestEmitPulseOnCriticalEntry`
等桩已具备此能力)。
3. **STREAM 队列生产者耗时**:改纯复制后 `PLSR_DEBUG_TIMING` 对比改前改后
(`PlsrProfileProducerMaxItemCycles` 应显著下降)。
4. **AB 隔离**:Phase 1 起 `PlsrExecLoadNext` 是唯一写通道;AB 旧路径经 LEGACY
出口兼容,防止出现第二写通道。
5. **回退**:Phase 2 之前不改平台层;Phase 3 是唯一大删除点,要求 Phase 2 的
T1~T3 全绿 + 板级波形对比通过后才合并。

---

## 版本历史

- V1.0(2026-08-15):审查初版。规划器接口、Phase 路线;存在四缺:无多段语义层、
无唯一执行器所有权、重规划不统一、量化不进闭环;脉冲守恒公式表述错误。
- V1.1(2026-08-15):采纳审查意见修订。新增 §3 边界语义解析层、§5 执行状态机与
所有权规则、§6 原子重规划协议、§7 失败测试先行;修正 §4 守恒公式与 CLIPPED
语义;迁移路线按"所有权 → 重规划 → 统一规划器 → 曲线校准"重排。

BIN
Document/PLSR_document/波形/111.bin Vedi File


+ 6
- 0
EWARM/Modbus.ewp Vedi File

@@ -1273,6 +1273,12 @@
<file>
<name>$PROJ_DIR$\..\PLSR\Src\plsr_platform.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\PLSR\Src\plsr_planner.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\PLSR\Src\plsr_planner.c</name>
</file>
<file>
<name>$PROJ_DIR$\..\PLSR\Src\plsr.c</name>
<configuration>


BIN
HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc Vedi File


BIN
HostComputer/__pycache__/plsr_modbus_product_test.cpython-314.pyc Vedi File


BIN
HostComputer/__pycache__/wave_viewer.cpython-314.pyc Vedi File


+ 271
- 0
HostComputer/bin_to_time_freq.py Vedi File

@@ -0,0 +1,271 @@
# -*- coding: utf-8 -*-
"""
.bin 波形文件 -> 时频曲线查看/出图工具(PLSR 项目用)

数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点;
电平 >= 阈值(默认128) 判为高(脉冲),否则为低。
频率定义: 每脉冲频率 = 采样率 / 脉冲周期;
周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点;
最后一个脉冲无下一脉冲,按高电平宽度 x2 近似(50% 占空比)。
时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)。

用法:
python bin_to_time_freq.py <bin文件> [采样率Hz] [选项]
示例:
python bin_to_time_freq.py "Document/PLSR_document/波形/10段.bin" 6250000
python bin_to_time_freq.py xxx.bin 6250000 --ymax=12000
python bin_to_time_freq.py xxx.bin 6250000 --save=out.png
python bin_to_time_freq.py xxx.bin 6250000 --selftest

选项:
--thresh=128 电平阈值(默认128)
--ymax=12000 频率轴上限(默认自适应)
--save=路径 保存 PNG 后退出(不弹窗)
--selftest 仅打印统计,不画图

交互(弹窗模式):
滚轮 缩放 X 轴(向上放大/向下缩小,以鼠标位置为中心)
Ctrl+滚轮 缩放 Y 轴(以鼠标 Y 位置为中心)
左键拖拽 双向平移(上下左右跟随鼠标)
双击 复位到全图
鼠标悬停 高亮最近的点并显示其时间与频率

依赖: pip install numpy matplotlib
"""

import sys
import os

import numpy as np

try:
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei']
matplotlib.rcParams['axes.unicode_minus'] = False
except Exception:
pass


def load_time_freq(path, fs, threshold=128):
"""读取 bin,返回 (t_sec, freq_hz, hi_width_sec, meta)。
t_sec 以第一个脉冲为 0 时刻;hi_width 为每脉冲高电平宽度。"""
data = np.fromfile(path, dtype=np.uint8)
if data.size == 0:
raise ValueError("文件为空: %s" % path)
samples = (data >= threshold).astype(np.int8)

# run 级压缩
changes = np.flatnonzero(np.diff(samples) != 0) + 1
starts = np.concatenate(([0], changes))
ends = np.concatenate((changes, [len(samples)]))
runs = np.column_stack((starts, ends, samples[starts]))

# 脉冲 = 高电平 run,且其后紧跟低电平 run
hi = np.flatnonzero(runs[:, 2] == 1)
hi = hi[hi + 1 < len(runs)]
lo_ok = runs[hi + 1, 2] == 0
hi = hi[lo_ok]

t_start = runs[hi, 0].astype(np.int64)
hi_width = (runs[hi, 1] - runs[hi, 0]).astype(np.int64)

# 周期:高到高;末脉冲按 2x 高电平宽度近似
next_start = np.append(t_start[1:], [t_start[-1] + 2 * hi_width[-1]])
period = next_start - t_start
freq = fs / np.maximum(period, 1)

# 时间:高电平中点,去起始偏移
t_sec = (t_start + hi_width / 2.0) / fs
t_sec = t_sec - t_sec[0]

meta = {
'total_samples': int(len(samples)),
'duration': len(samples) / fs,
'pulses': int(len(freq)),
'offset_ms': t_start[0] / fs * 1000,
}
return t_sec, freq, hi_width / fs, meta


def selftest(path, fs, thresh):
import statistics
t, freq, hi_w, meta = load_time_freq(path, fs, thresh)
print("文件: %s" % path)
print("总采样: %d (%.3f s @ %.2f MS/s)" % (meta['total_samples'], meta['duration'], fs / 1e6))
print("起始采集偏移: %.1f ms" % meta['offset_ms'])
print("脉冲总数: %d" % meta['pulses'])
if meta['pulses']:
print("频率 min=%.0f max=%.0f 中位=%.0f Hz" % (
freq.min(), freq.max(), statistics.median(freq)))
print("末脉冲: t=%.3f ms, 高电平 %.3f ms, %.0f Hz" % (
t[-1] * 1000, hi_w[-1] * 1000, freq[-1]))
print("波形活动时长: %.1f ms" % ((t[-1] + hi_w[-1] / 2) * 1000))


def plot_show(path, fs, thresh, ymax, save_path=None):
t, freq, hi_w, meta = load_time_freq(path, fs, thresh)

import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator

fig, ax = plt.subplots(figsize=(15, 7))
fig.subplots_adjust(bottom=0.10, top=0.92)

# 全脉冲散点
ax.plot(t * 1000, freq, '.', ms=1.5, color='C0', alpha=0.5, zorder=1)
ax.set_xlim(0.0, (t[-1] + hi_w[-1]) * 1000 * 1.02)
ax.set_ylim(0, ymax if ymax > 0 else freq.max() * 1.08)
ax.set_title("%s 时频曲线(%d 脉冲,活动时长 %.0f ms)" % (
os.path.basename(path), meta['pulses'],
(t[-1] + hi_w[-1] / 2) * 1000))
ax.set_xlabel("时间 (ms)")
ax.set_ylabel("频率 (Hz)")
# 更密的刻度
ax.xaxis.set_major_locator(MaxNLocator(nbins=20))
ax.yaxis.set_major_locator(MaxNLocator(nbins=15))
ax.grid(True, which='major', alpha=0.35)
ax.grid(True, which='minor', alpha=0.15)
ax.minorticks_on()

if save_path:
fig.savefig(save_path, dpi=130)
print("已保存: %s" % save_path)
plt.close(fig)
return

# 交互:滚轮缩放 X(Ctrl+滚轮缩放 Y)/ 左键双向拖拽平移 / 双击复位 / 悬停高亮
state = {'press_x': None, 'press_y': None,
'press_xlim': None, 'press_ylim': None}

# 悬停高亮:一个红点标记 + 一个带框文本
(hl_marker,) = ax.plot([], [], 'o', ms=9, mfc='red', mec='white',
mew=1.0, zorder=5, visible=False)
hl_text = ax.text(0, 0, '', fontsize=10, color='black',
bbox=dict(boxstyle='round,pad=0.3', fc='yellow', ec='red', alpha=0.9),
zorder=6, visible=False)
hover_last = {'idx': -1, 'visible': False}

def on_scroll(event):
if event.inaxes is not ax or event.xdata is None:
return
zoom_in = event.button == 'up'
factor = 1.0 / 1.5 if zoom_in else 1.5
if event.key in ('control', 'ctrl'):
# Ctrl+滚轮:缩放 Y 轴,以鼠标 Y 位置为锚点(锚点数据点不动)
y0, y1 = ax.get_ylim()
cy = event.ydata
n0 = cy - (cy - y0) * factor
n1 = cy + (y1 - cy) * factor
if n1 - n0 < 1.0:
return
ax.set_ylim(n0, n1)
else:
# 普通滚轮:缩放 X 轴,以鼠标 X 位置为锚点(锚点数据点不动)
x0, x1 = ax.get_xlim()
n0 = event.xdata - (event.xdata - x0) * factor
n1 = event.xdata + (x1 - event.xdata) * factor
if n1 - n0 < 1e-6:
return
ax.set_xlim(n0, n1)
fig.canvas.draw_idle()

def on_press(event):
if event.inaxes is ax and event.button == 1:
state['press_x'] = event.xdata
state['press_y'] = event.ydata
state['press_xlim'] = ax.get_xlim()
state['press_ylim'] = ax.get_ylim()

def on_motion(event):
if event.inaxes is not ax or event.xdata is None:
return
if state['press_x'] is not None:
# 拖拽平移:X/Y 双向跟随鼠标
x0, x1 = state['press_xlim']
y0, y1 = state['press_ylim']
dx = event.xdata - state['press_x']
dy = event.ydata - state['press_y']
ax.set_xlim(x0 - dx, x1 - dx)
ax.set_ylim(y0 - dy, y1 - dy)
fig.canvas.draw_idle()
return
# 悬停:找可见范围内距鼠标最近的脉冲点(屏幕像素距离 < 20px 才高亮)
x0, x1 = ax.get_xlim()
tx_ms = t * 1000
mask = (tx_ms >= x0) & (tx_ms <= x1)
if not np.any(mask):
hl_marker.set_visible(False)
hl_text.set_visible(False)
hover_last['visible'] = False
fig.canvas.draw_idle()
return
px, py = ax.transData.transform(np.column_stack([tx_ms[mask], freq[mask]])).T
dist = np.hypot(px - event.x, py - event.y)
k = int(np.argmin(dist))
if dist[k] <= 20.0:
idx = int(np.flatnonzero(mask)[k])
hl_marker.set_data([tx_ms[idx]], [freq[idx]])
hl_text.set_text("t = %.3f ms\nf = %.0f Hz" % (tx_ms[idx], freq[idx]))
hl_text.set_position((tx_ms[idx] + (x1 - x0) * 0.01,
freq[idx] + (ax.get_ylim()[1] - ax.get_ylim()[0]) * 0.02))
hl_marker.set_visible(True)
hl_text.set_visible(True)
if hover_last['idx'] != idx or not hover_last['visible']:
fig.canvas.draw_idle()
hover_last['idx'] = idx
hover_last['visible'] = True
else:
if hover_last['visible']:
hl_marker.set_visible(False)
hl_text.set_visible(False)
fig.canvas.draw_idle()
hover_last['visible'] = False

def on_release(event):
state['press_x'] = None

def on_double(event):
if event.dblclick:
ax.set_xlim(0.0, (t[-1] + hi_w[-1]) * 1000 * 1.02)
ax.set_ylim(0, ymax if ymax > 0 else freq.max() * 1.08)
fig.canvas.draw_idle()

fig.canvas.mpl_connect('scroll_event', on_scroll)
fig.canvas.mpl_connect('button_press_event', on_press)
fig.canvas.mpl_connect('button_release_event', on_release)
fig.canvas.mpl_connect('motion_notify_event', on_motion)
fig.canvas.mpl_connect('button_press_event', on_double)

print("打开窗口:滚轮缩放X / Ctrl+滚轮缩放Y / 左键双向拖拽平移 / 双击复位")
print("脉冲 %d 个,活动时长 %.1f ms" % (
meta['pulses'], (t[-1] + hi_w[-1] / 2) * 1000))
plt.show()


def main():
args = [a for a in sys.argv[1:] if not a.startswith('--')]
opts = {a.split('=', 1)[0]: (a.split('=', 1)[1] if '=' in a else True)
for a in sys.argv[1:] if a.startswith('--')}

if len(args) < 1 or '--help' in opts or '-h' in opts:
print(__doc__)
sys.exit(0)

path = args[0]
fs = float(args[1]) if len(args) > 1 else 6.25e6
thresh = int(opts.get('--thresh', 128))
ymax = float(opts.get('--ymax', 0))
save_path = opts.get('--save', '')

if not os.path.isfile(path):
print("错误:文件不存在 - %s" % path)
sys.exit(1)

if '--selftest' in opts:
selftest(path, fs, thresh)
else:
plot_show(path, fs, thresh, ymax, save_path or None)


if __name__ == '__main__':
main()

+ 7
- 3
HostComputer/plsr_control_panel.py Vedi File

@@ -536,7 +536,7 @@ class PlsrControlPanel:

self._add_entry(parent, 3, 0, "segment_count", "段数", default="1")
self._add_entry(parent, 3, 1, "start_segment", "起始段", default="1")
self._add_entry(parent, 3, 2, "default_speed", "基准速度 Hz", default="1000")
self._add_entry(parent, 3, 2, "default_speed", "默认速度 Hz", default="1000")

self._add_entry(parent, 4, 0, "start_speed", "启动速度 Hz", default="100")
self._add_entry(parent, 4, 1, "stop_speed", "停止速度 Hz", default="100")
@@ -750,7 +750,7 @@ class PlsrControlPanel:
words[10] = start_segment
words[11:13] = split_u32(
parse_integer(
values["default_speed"].get(), "基准速度", 1, MAX_FREQUENCY_HZ
values["default_speed"].get(), "默认速度", 1, MAX_FREQUENCY_HZ
)
)
words[13:15] = split_u32(
@@ -776,7 +776,7 @@ class PlsrControlPanel:
for index, variables in enumerate(self.segment_vars):
name = "第 %d 段" % (index + 1)
frequency = parse_integer(
variables["frequency"].get(), name + "频率", 1, MAX_FREQUENCY_HZ
variables["frequency"].get(), name + "频率", 0, MAX_FREQUENCY_HZ
)
pulses = parse_integer(
variables["pulses"].get(), name + "脉冲数", -(1 << 31), (1 << 31) - 1
@@ -1103,6 +1103,10 @@ def self_test():
assert all(len(words) == SEGMENT_WORDS for words in segments)
assert segments[0] == [1000, 0, 0xFFFF, 0xFFFF, 4, 0, 1, 0]

panel.segment_vars[0]["frequency"].set(0)
segments = panel._segment_words()
assert segments[0][0:2] == [0, 0]

panel.common_vars["segment_count"].set(1)
panel.segment_vars[0]["jump"].set(2)
try:


+ 37
- 7
HostComputer/plsr_modbus_product_test.py Vedi File

@@ -394,6 +394,12 @@ class TimingFixture:
"PlsrIrqLastCycles",
"PlsrIrqMaxCycles",
)
FINITE_IRQ_SYMBOLS = (
"PlsrFiniteBlockIrqCount",
"PlsrFiniteBlockIrqMaxCycles",
"PlsrFiniteFinalIrqLastCycles",
"PlsrFiniteFinalIrqMaxCycles",
)
PRODUCER_SYMBOLS = (
"PlsrProfileProducerItemCount",
"PlsrProfileProducerTotalCycles",
@@ -417,26 +423,50 @@ class TimingFixture:
% (map_path, error)) from error
self.addresses = {
symbol: parse_iar_map_symbol_address(map_text, symbol)
for symbol in (self.IRQ_SYMBOLS + self.PRODUCER_SYMBOLS
for symbol in (self.IRQ_SYMBOLS + self.FINITE_IRQ_SYMBOLS
+ self.PRODUCER_SYMBOLS
+ self.FINAL_ARM_SYMBOLS)
}

def reset(self):
for symbol in self.IRQ_SYMBOLS:
self.probe.write_words(self.addresses[symbol], [0] * 4)
for symbol in self.FINITE_IRQ_SYMBOLS:
self.probe.write_words(self.addresses[symbol], [0] * 4)
for symbol in self.PRODUCER_SYMBOLS:
self.probe.write_words(self.addresses[symbol], [0])
for symbol in self.FINAL_ARM_SYMBOLS:
self.probe.write_words(self.addresses[symbol], [0] * 4)

def read(self):
irq_count = self.probe.read_words(
self.addresses["PlsrIrqCount"], 4)
irq_last = self.probe.read_words(
self.addresses["PlsrIrqLastCycles"], 4)
irq_maximum = self.probe.read_words(
self.addresses["PlsrIrqMaxCycles"], 4)
finite_block_count = self.probe.read_words(
self.addresses["PlsrFiniteBlockIrqCount"], 4)
finite_block_maximum = self.probe.read_words(
self.addresses["PlsrFiniteBlockIrqMaxCycles"], 4)
finite_final_last = self.probe.read_words(
self.addresses["PlsrFiniteFinalIrqLastCycles"], 4)
finite_final_maximum = self.probe.read_words(
self.addresses["PlsrFiniteFinalIrqMaxCycles"], 4)
for output in range(4):
if irq_count[output] == 0:
final_count = 1 if finite_final_maximum[output] != 0 else 0

irq_count[output] = finite_block_count[output] + final_count
irq_last[output] = finite_final_last[output]
irq_maximum[output] = max(
finite_block_maximum[output],
finite_final_maximum[output],
)
irq = IrqCycleStats(
count=tuple(self.probe.read_words(
self.addresses["PlsrIrqCount"], 4)),
last_cycles=tuple(self.probe.read_words(
self.addresses["PlsrIrqLastCycles"], 4)),
maximum_cycles=tuple(self.probe.read_words(
self.addresses["PlsrIrqMaxCycles"], 4)),
count=tuple(irq_count),
last_cycles=tuple(irq_last),
maximum_cycles=tuple(irq_maximum),
)
producer = ProducerCycleStats(
item_count=self.probe.read_words(


+ 289
- 0
HostComputer/wave_viewer.py Vedi File

@@ -0,0 +1,289 @@
# -*- coding: utf-8 -*-
"""
逻辑分析仪时频曲线查看器(PLSR 项目用)

主视图:频率-时间曲线(每个脉冲的瞬时频率,阶梯线绘制)
辅助视图:原始波形(run 级绘制,大文件也流畅)

用法:
python wave_viewer.py <bin文件> [采样率Hz] [--thresh=阈值] [--nowave] [--nofreq] [--ymax=Hz]
示例:
python wave_viewer.py "Document/PLSR_document/波形/10段.bin" 6250000
python wave_viewer.py xxx.bin 6250000 --nowave # 只看时频曲线
python wave_viewer.py xxx.bin 6250000 --ymax=12000 # 固定频率轴上限

数据格式: 每字节 1 个采样点;电平 >= 阈值(默认128) 判为高,否则为低。
频率定义: 每脉冲频率 = 采样率 / (该脉冲高电平+下一低电平的周期)。

交互:
滚轮 放大/缩小(以鼠标位置为中心)
左键拖拽 平移
双击 复位到全图
上子图 时频曲线(阶梯线 + 散点)
下子图 原始波形
红色虚线 长低电平(>=2ms,段间间隙/慢爬所在处)

自检模式(不弹窗,仅打印统计):
python wave_viewer.py <bin文件> 6250000 --selftest

依赖: pip install numpy matplotlib
"""

import sys
import os

import numpy as np

# 中文字体(GUI 与无头渲染统一使用,避免文件名/标签中的中文缺字形)
try:
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei']
matplotlib.rcParams['axes.unicode_minus'] = False
except Exception:
pass


class WaveViewer:
def __init__(self, path, fs, threshold=128):
self.path = path
self.fs = float(fs)
self.threshold = threshold

# 读取并二值化(0/1)
data = np.fromfile(path, dtype=np.uint8)
if data.size == 0:
raise ValueError("文件为空")
self.samples = (data >= threshold).astype(np.int8)
self.total = int(self.samples.size)
self.duration = self.total / self.fs

# run 级压缩:连续相同电平段 (start_idx, end_idx, level)
changes = np.flatnonzero(np.diff(self.samples) != 0) + 1
starts = np.concatenate(([0], changes))
ends = np.concatenate((changes, [self.total]))
levels = self.samples[starts]
self.runs = np.column_stack((starts, ends, levels))

# 脉冲周期与频率:每个"高电平 run"与其后"低电平 run"配对
self._build_pulses()

def _build_pulses(self):
hi = np.flatnonzero(self.runs[:, 2] == 1)
observed_rise = self.runs[hi, 0]
observed_rise = observed_rise[observed_rise > 0]
self.pulse_count = len(observed_rise)

# A period is measurable only between two observed rising edges. The
# trailing capture idle after the final pulse is not part of a period.
t_start = observed_rise[:-1]
t_end = observed_rise[1:]
period = t_end - t_start
self.pulse_start = t_start / self.fs
self.pulse_end = t_end / self.fs
self.pulse_time = (t_start + t_end) / 2.0 / self.fs
self.pulse_freq = self.fs / np.maximum(period, 1)

# ---------- 交互 ----------
def on_scroll(self, event):
if event.inaxes not in (self.ax_wave, self.ax_freq):
return
if event.xdata is None:
return
x0, x1 = self.ax_wave.get_xlim()
factor = 1.0 / 1.5 if event.button == 'up' else 1.5
n0 = event.xdata - (event.xdata - x0) * factor
n1 = event.xdata + (x1 - event.xdata) * factor
if n1 - n0 < 5.0 / self.fs: # 最小窗口:5 个采样
return
self.ax_wave.set_xlim(max(n0, 0.0), min(n1, self.duration))
self.redraw()

def on_press(self, event):
if event.inaxes in (self.ax_wave, self.ax_freq) and event.button == 1:
self._press_x = event.xdata
self._press_lim = self.ax_wave.get_xlim()

def on_motion(self, event):
if getattr(self, '_press_x', None) is None:
return
if event.inaxes not in (self.ax_wave, self.ax_freq) or event.xdata is None:
return
dx = event.xdata - self._press_x
x0, x1 = self._press_lim
n0 = x0 - dx
n1 = x1 - dx
if n1 - n0 < 1e-9:
return
self.ax_wave.set_xlim(n0, n1)
self.redraw()

def on_release(self, event):
self._press_x = None

def on_double_click(self, event):
if event.dblclick:
self.ax_wave.set_xlim(0.0, self.duration)
self.redraw()

def on_mouse_move_status(self, event):
if event.xdata is None:
self.status.set_text("")
return
# 鼠标处对应的频率(找最近的脉冲)
near = np.searchsorted(self.pulse_time, event.xdata)
txt = "t = %.6f s (%.1f ms)" % (event.xdata, event.xdata * 1000)
for i in (near, near - 1):
if 0 <= i < len(self.pulse_time) and abs(self.pulse_time[i] - event.xdata) < 0.05:
txt += " f = %.0f Hz" % self.pulse_freq[i]
break
self.status.set_text(txt)

# ---------- 绘制 ----------
def redraw(self):
x0, x1 = self.ax_wave.get_xlim()
t0, t1 = min(x0, x1), max(x0, x1)
i0 = max(int(t0 * self.fs), 0)
i1 = min(int(t1 * self.fs), self.total)

# ===== 主图:时频曲线 =====
self.ax_freq.clear()
pm = (self.pulse_time >= t0) & (self.pulse_time <= t1)
if np.any(pm):
t = self.pulse_time[pm]
f = self.pulse_freq[pm]
# 阶梯线:每个脉冲频率在 [pulse_start, pulse_end] 保持
st = self.pulse_start[pm]
en = self.pulse_end[pm]
xs = np.empty(2 * len(t))
ys = np.empty(2 * len(t))
xs[0::2] = st
xs[1::2] = en
ys[0::2] = f
ys[1::2] = f
self.ax_freq.plot(xs, ys, color='C1', lw=1.0, alpha=0.9, zorder=2)
self.ax_freq.plot(t, f, '.', ms=2.5, color='C3', zorder=3)
# y 轴:自适应或固定
if self.ymax > 0:
self.ax_freq.set_ylim(0, self.ymax)
else:
fmax = np.max(f)
self.ax_freq.set_ylim(0, fmax * 1.08)
self.ax_freq.set_ylabel("frequency (Hz)")
self.ax_freq.grid(True, alpha=0.3)
self.ax_freq.set_title(
"%s [%.4f, %.4f] s / total %.3f s, samples %d, pulses %d"
% (os.path.basename(self.path), t0, t1, self.duration,
self.total, self.pulse_count))

# ===== 辅助图:原始波形 =====
if self.show_wave:
self.ax_wave.clear()
rs, re = self.runs[:, 0], self.runs[:, 1]
vis = (re >= i0) & (rs <= i1)
if np.any(vis):
s = np.maximum(rs[vis], i0)
e = np.minimum(re[vis], i1)
lv = self.runs[vis, 2]
self.ax_wave.hlines(lv, s / self.fs, e / self.fs,
color='C0', linewidth=1.0, zorder=2)
self.ax_wave.hlines(0, t0, t1, color='C0', linewidth=1.0, zorder=1)
self.ax_wave.set_ylim(-0.15, 1.15)
self.ax_wave.set_ylabel("Y0")
self.ax_wave.grid(True, alpha=0.3)

# 长低电平(>= 2ms)红色虚线:段间间隙 / 慢爬
low_w = (self.runs[:, 2] == 0) & ((self.runs[:, 1] - self.runs[:, 0]) / self.fs >= 0.002)
li = np.flatnonzero(low_w & (self.runs[:, 1] >= i0) & (self.runs[:, 0] <= i1))
for j in li:
self.ax_wave.axvline(self.runs[j, 0] / self.fs,
color='r', ls='--', lw=0.8, alpha=0.6, zorder=3)
self.ax_wave.set_xlabel("time (s)")
self.fig.canvas.draw_idle()

def run(self):
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt

if self.show_wave:
self.fig, (self.ax_freq, self.ax_wave) = plt.subplots(
2, 1, figsize=(14, 8), sharex=True, gridspec_kw={'height_ratios': [2, 1]})
else:
self.fig, self.ax_freq = plt.subplots(figsize=(14, 6))
self.ax_wave = self.ax_freq # 占位,实际不绘制
self.fig.subplots_adjust(bottom=0.08, top=0.93)
self.status = self.fig.text(0.01, 0.01, "", fontsize=9)

self._press_x = None
self._press_lim = None
self.ax_wave.set_xlim(0.0, self.duration)
self.redraw()

self.fig.canvas.mpl_connect('scroll_event', self.on_scroll)
self.fig.canvas.mpl_connect('button_press_event', self.on_press)
self.fig.canvas.mpl_connect('button_release_event', self.on_release)
self.fig.canvas.mpl_connect('motion_notify_event', self.on_motion)
self.fig.canvas.mpl_connect('motion_notify_event', self.on_mouse_move_status)
self.fig.canvas.mpl_connect('button_press_event', self.on_double_click)

print("打开窗口:滚轮缩放 / 左键拖拽平移 / 双击复位")
print("波形总时长 %.3f s, 采样 %d, 检测到脉冲 %d 个"
% (self.duration, self.total, self.pulse_count))
plt.show()

def selftest(self):
import statistics
print("文件: %s" % self.path)
print("总采样: %d (%.3f s @ %.2f MS/s)" % (self.total, self.duration, self.fs / 1e6))
print("高电平段: %d 低电平段: %d" % (
int(np.sum(self.runs[:, 2] == 1)), int(np.sum(self.runs[:, 2] == 0))))
print("检测到脉冲(上升沿): %d" % self.pulse_count)
print("可测完整周期(相邻上升沿): %d" % len(self.pulse_freq))
if len(self.pulse_freq):
print("频率 min=%.0f max=%.0f 中位=%.0f Hz"
% (np.min(self.pulse_freq), np.max(self.pulse_freq),
statistics.median(self.pulse_freq)))
low_w = (self.runs[:, 2] == 0) & ((self.runs[:, 1] - self.runs[:, 0]) / self.fs >= 0.002)
print("长低电平(>=2ms)处数: %d" % int(np.sum(low_w)))


def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
path = sys.argv[1]
fs = 6.25e6
thresh = 128
show_wave = True
ymax = 0.0
selftest = False
for a in sys.argv[1:]:
if a == '--selftest':
selftest = True
elif a == '--nowave':
show_wave = False
elif a == '--nofreq':
show_wave = True # 无此开关时保留波形视图(兼容旧参数名,忽略)
elif a.startswith('--thresh='):
thresh = int(a.split('=', 1)[1])
elif a.startswith('--ymax='):
ymax = float(a.split('=', 1)[1])
elif a != path:
try:
fs = float(a)
except ValueError:
pass
if not os.path.isfile(path):
print("错误:文件不存在 - %s" % path)
sys.exit(1)
viewer = WaveViewer(path, fs, thresh)
viewer.show_wave = show_wave
viewer.ymax = ymax
if selftest:
viewer.selftest()
else:
viewer.run()


if __name__ == '__main__':
main()

+ 0
- 5
PLSR/Inc/plsr.h Vedi File

@@ -72,9 +72,6 @@ void PlsrPulseTimerIrq(uint8_t pulseOutput);
void PlsrFinalArmJobIrq(uint8_t pulseOutput);

#ifdef PLSR_HOST_TEST
uint64_t PlsrTestDivideU64ByU32(uint64_t dividend,
uint32_t divisor,
uint32_t *remainder);
void PlsrTestSetInput(uint8_t inputSelection, uint8_t level);
void PlsrTestEmitPulses(uint32_t pulseCount);
void PlsrTestEnableFinitePulseTrain(uint8_t enable);
@@ -128,8 +125,6 @@ uint32_t PlsrTestSaveCount(void);
extern volatile uint32_t PlsrProfileProducerItemCount;
extern volatile uint32_t PlsrProfileProducerTotalCycles;
extern volatile uint32_t PlsrProfileProducerMaxItemCycles;
extern volatile uint32_t PlsrFiniteRampUpdateCount;
extern volatile uint32_t PlsrFiniteRampUpdateMaxCycles;
extern volatile uint32_t PlsrFiniteBlockIrqCount[4];
extern volatile uint32_t PlsrFiniteBlockIrqMaxCycles[4];
extern volatile uint32_t PlsrFiniteFinalIrqLastCycles[4];


+ 425
- 1493
PLSR/Src/plsr.c
File diff soppresso perché troppo grande
Vedi File


+ 10
- 0
PLSR/Src/plsr_internal.h Vedi File

@@ -4,11 +4,21 @@
#include <stdint.h>

#define PLSR_SEGMENT_COUNT_MAX (10U)
#define PLSR_FINITE_PROFILE_PULSE_LIMIT (1000U)
#define PLSR_PLATFORM_FINITE_STEP_MAX \
(PLSR_SEGMENT_COUNT_MAX + 2U * PLSR_FINITE_PROFILE_PULSE_LIMIT)
#define PLSR_FREQUENCY_MAX_HZ (100000UL)

#define PLSR_OUTPUT_PULSE_DIR (0U)
#define PLSR_OUTPUT_AB (1U)

typedef enum
{
PLSR_EXEC_IDLE = 0,
PLSR_EXEC_STREAM,
PLSR_EXEC_AB_LEGACY
} PLSR_EXEC_MODE;

typedef struct
{
uint32_t frequencyHz;


+ 735
- 0
PLSR/Src/plsr_planner.c Vedi File

@@ -0,0 +1,735 @@
#include "plsr_planner.h"

#include <stddef.h>
#include <string.h>

#define PLSR_PLANNER_Q32_ONE (4294967296ULL)

static const uint32_t PlsrPlannerSmoothIntegralQ24[65] =
{
0UL, 64UL, 504UL, 1688UL, 3968UL, 7688UL, 13176UL, 20752UL,
30720UL, 43376UL, 59000UL, 77864UL, 100224UL, 126328UL, 156408UL,
190688UL, 229376UL, 272672UL, 320760UL, 373816UL, 432000UL,
495464UL, 564344UL, 638768UL, 718848UL, 804688UL, 896376UL,
993992UL, 1097600UL, 1207256UL, 1323000UL, 1444864UL, 1572864UL,
1707008UL, 1847288UL, 1993688UL, 2146176UL, 2304712UL, 2469240UL,
2639696UL, 2816000UL, 2998064UL, 3185784UL, 3379048UL, 3577728UL,
3781688UL, 3990776UL, 4204832UL, 4423680UL, 4647136UL, 4875000UL,
5107064UL, 5343104UL, 5582888UL, 5826168UL, 6072688UL, 6322176UL,
6574352UL, 6828920UL, 7085576UL, 7344000UL, 7603864UL, 7864824UL,
8126528UL, 8388608UL
};

static const uint32_t PlsrPlannerSineIntegralQ24[65] =
{
0UL, 53UL, 421UL, 1420UL, 3362UL, 6560UL, 11321UL, 17949UL,
26744UL, 38000UL, 52007UL, 69047UL, 89393UL, 113314UL, 141066UL,
172899UL, 209052UL, 249753UL, 295221UL, 345662UL, 401269UL,
462225UL, 528698UL, 600845UL, 678806UL, 762711UL, 852672UL,
948789UL, 1051146UL, 1159812UL, 1274841UL, 1396271UL, 1524127UL,
1658415UL, 1799129UL, 1946244UL, 2099722UL, 2259509UL, 2425536UL,
2597719UL, 2775958UL, 2960141UL, 3150138UL, 3345809UL, 3546997UL,
3753534UL, 3965237UL, 4181913UL, 4403356UL, 4629347UL, 4859658UL,
5094050UL, 5332273UL, 5574071UL, 5819175UL, 6067312UL, 6318200UL,
6571549UL, 6827065UL, 7084448UL, 7343394UL, 7603596UL, 7864741UL,
8126517UL, 8388608UL
};

static uint32_t PlsrPlannerAbsDifference(uint32_t first, uint32_t second)
{
return (first > second) ? (first - second) : (second - first);
}

static uint32_t PlsrPlannerRampTime(const PLSR_MOTION_BLOCK *block,
uint32_t fromHz,
uint32_t toHz)
{
uint32_t baseTimeMs;
uint64_t durationMs;

if (fromHz == toHz)
{
return 0UL;
}
baseTimeMs = (toHz > fromHz) ? block->accelerationTimeMs
: block->decelerationTimeMs;
if (baseTimeMs == 0UL)
{
return 0UL;
}
durationMs = ((uint64_t)PlsrPlannerAbsDifference(fromHz, toHz)
* baseTimeMs + block->referenceSpeedHz - 1UL)
/ block->referenceSpeedHz;
return (durationMs > 0xFFFFFFFFULL) ? 0xFFFFFFFFUL
: (uint32_t)durationMs;
}

static uint16_t PlsrPlannerBaseRampTime(const PLSR_MOTION_BLOCK *block,
uint32_t fromHz,
uint32_t toHz)
{
if (toHz > fromHz)
{
return block->accelerationTimeMs;
}
if (toHz < fromHz)
{
return block->decelerationTimeMs;
}
return 0U;
}

static uint64_t PlsrPlannerRampWeight(uint32_t fromHz,
uint32_t toHz,
uint16_t baseTimeMs)
{
uint64_t fromSquared = (uint64_t)fromHz * fromHz;
uint64_t toSquared = (uint64_t)toHz * toHz;
uint64_t difference = (fromSquared > toSquared)
? (fromSquared - toSquared)
: (toSquared - fromSquared);

return difference * baseTimeMs;
}

static uint64_t PlsrPlannerRequiredPulses(const PLSR_MOTION_BLOCK *block,
uint64_t rampWeight)
{
uint64_t denominator = (uint64_t)2U * block->referenceSpeedHz * 1000UL;

return (rampWeight == 0ULL) ? 0ULL
: (rampWeight + denominator - 1ULL)
/ denominator;
}

static uint32_t PlsrPlannerIntegerSquareRoot(uint64_t value)
{
uint64_t bit = (uint64_t)1U << 62U;
uint64_t root = 0ULL;

while (bit > value)
{
bit >>= 2U;
}
while (bit != 0ULL)
{
if (value >= root + bit)
{
value -= root + bit;
root = (root >> 1U) + bit;
}
else
{
root >>= 1U;
}
bit >>= 2U;
}
return (uint32_t)root;
}

static uint32_t PlsrPlannerReachableFrequency(
const PLSR_MOTION_BLOCK *block,
uint32_t fromHz,
uint32_t towardHz,
uint32_t pulseCount)
{
uint16_t baseTimeMs = PlsrPlannerBaseRampTime(block, fromHz, towardHz);
uint64_t frequencySquared = (uint64_t)fromHz * fromHz;
uint64_t changeSquared;
uint32_t reachableHz;

if ((baseTimeMs == 0U) || (fromHz == towardHz))
{
return towardHz;
}
changeSquared = (uint64_t)2U * pulseCount * block->referenceSpeedHz
* 1000UL / baseTimeMs;
if (towardHz > fromHz)
{
reachableHz = PlsrPlannerIntegerSquareRoot(
frequencySquared + changeSquared);
return (reachableHz > towardHz) ? towardHz : reachableHz;
}
frequencySquared = (changeSquared >= frequencySquared)
? 0ULL : (frequencySquared - changeSquared);
reachableHz = PlsrPlannerIntegerSquareRoot(frequencySquared);
if ((uint64_t)reachableHz * reachableHz < frequencySquared)
{
reachableHz++;
}
return (reachableHz < towardHz) ? towardHz : reachableHz;
}

static uint32_t PlsrPlannerPeak(const PLSR_MOTION_BLOCK *block,
uint32_t startHz,
uint32_t endHz)
{
uint32_t targetHz = block->cruiseHz;
uint32_t upperEndpoint = (startHz > endHz) ? startHz : endHz;
uint32_t lowerEndpoint = (startHz < endHz) ? startHz : endHz;
uint16_t entryTimeMs;
uint16_t exitTimeMs;
uint32_t timeSumMs;
uint64_t weightedEndpoints;
uint64_t availableArea;
uint64_t peakSquared;
uint32_t peakHz;

if ((targetHz <= upperEndpoint) && (targetHz >= lowerEndpoint))
{
return targetHz;
}
entryTimeMs = PlsrPlannerBaseRampTime(block, startHz, targetHz);
exitTimeMs = PlsrPlannerBaseRampTime(block, targetHz, endHz);
timeSumMs = (uint32_t)entryTimeMs + exitTimeMs;
if (timeSumMs == 0UL)
{
return targetHz;
}
weightedEndpoints = (uint64_t)startHz * startHz * entryTimeMs
+ (uint64_t)endHz * endHz * exitTimeMs;
availableArea = (uint64_t)2U * block->pulseBudget
* block->referenceSpeedHz * 1000UL;

if (targetHz > upperEndpoint)
{
peakSquared = (availableArea + weightedEndpoints) / timeSumMs;
peakHz = PlsrPlannerIntegerSquareRoot(peakSquared);
if (peakHz < upperEndpoint)
{
peakHz = upperEndpoint;
}
return (peakHz > targetHz) ? targetHz : peakHz;
}

if (availableArea >= weightedEndpoints)
{
return targetHz;
}
peakSquared = (weightedEndpoints - availableArea) / timeSumMs;
peakHz = PlsrPlannerIntegerSquareRoot(peakSquared);
if (peakHz < targetHz)
{
peakHz = targetHz;
}
return (peakHz > lowerEndpoint) ? lowerEndpoint : peakHz;
}

static uint64_t PlsrPlannerCurveIntegralQ32(uint64_t progressQ32,
uint16_t curveMode)
{
const uint32_t *table;
uint64_t scaled;
uint32_t index;
uint32_t fraction;
uint64_t first;
uint64_t second;

if (progressQ32 >= PLSR_PLANNER_Q32_ONE)
{
return PLSR_PLANNER_Q32_ONE / 2ULL;
}
if (curveMode == 0U)
{
return (progressQ32 * progressQ32) >> 33U;
}
table = (curveMode == 1U) ? PlsrPlannerSmoothIntegralQ24
: PlsrPlannerSineIntegralQ24;
scaled = progressQ32 * 64ULL;
index = (uint32_t)(scaled >> 32U);
fraction = (uint32_t)scaled;
first = (uint64_t)table[index] << 8U;
second = (uint64_t)table[index + 1UL] << 8U;
return first + (((second - first) * fraction) >> 32U);
}

static uint64_t PlsrPlannerRampAreaQ32(const PLSR_PLANNER_CONTEXT *context,
uint32_t fromHz,
uint32_t toHz,
uint64_t progressQ32)
{
int64_t delta = (int64_t)toHz - (int64_t)fromHz;
int64_t area = (int64_t)((uint64_t)fromHz * progressQ32)
+ delta * (int64_t)PlsrPlannerCurveIntegralQ32(
progressQ32, context->block.curveMode);

return (uint64_t)area;
}

static uint64_t PlsrPlannerExactBoundaryQ32(
const PLSR_PLANNER_CONTEXT *context,
uint64_t previousBoundaryQ32,
uint64_t targetAreaQ32)
{
uint64_t lowerQ32 = previousBoundaryQ32;
uint64_t upperQ32 = PLSR_PLANNER_Q32_ONE;
uint64_t middleQ32;
uint32_t iteration;

for (iteration = 0UL; iteration < 32UL; iteration++)
{
middleQ32 = lowerQ32 + ((upperQ32 - lowerQ32) >> 1U);
if (PlsrPlannerRampAreaQ32(context, context->rampFromHz,
context->rampToHz, middleQ32)
< targetAreaQ32)
{
lowerQ32 = middleQ32;
}
else
{
upperQ32 = middleQ32;
}
}
return upperQ32;
}

static uint32_t PlsrPlannerInstantFrequency(
const PLSR_PLANNER_CONTEXT *context,
uint64_t progressQ32)
{
const uint32_t *table;
uint64_t scaled;
uint64_t curveProgressQ32;
uint32_t index;
uint32_t gap;

if (progressQ32 >= PLSR_PLANNER_Q32_ONE)
{
return context->rampToHz;
}
if (context->block.curveMode == 0U)
{
curveProgressQ32 = progressQ32;
}
else
{
table = (context->block.curveMode == 1U)
? PlsrPlannerSmoothIntegralQ24
: PlsrPlannerSineIntegralQ24;
scaled = progressQ32 * 64ULL;
index = (uint32_t)(scaled >> 32U);
curveProgressQ32 =
(uint64_t)(table[index + 1UL] - table[index]) << 14U;
}
if (context->rampToHz >= context->rampFromHz)
{
gap = context->rampToHz - context->rampFromHz;
return context->rampFromHz
+ (uint32_t)(((uint64_t)gap * curveProgressQ32) >> 32U);
}
gap = context->rampFromHz - context->rampToHz;
return context->rampFromHz
- (uint32_t)(((uint64_t)gap * curveProgressQ32) >> 32U);
}

static uint64_t PlsrPlannerPredictedBoundaryQ32(
const PLSR_PLANNER_CONTEXT *context,
uint64_t targetAreaQ32)
{
uint64_t previousQ32 = context->rampBoundaryQ32;
uint64_t currentAreaQ32 = (previousQ32 == 0ULL)
? 0ULL
: PlsrPlannerRampAreaQ32(
context, context->rampFromHz,
context->rampToHz, previousQ32);
uint64_t candidateQ32;
uint64_t candidateAreaQ32;
uint64_t differenceQ32;
uint64_t correctionQ32;
uint32_t derivativeHz;

if (context->rampLastPhaseStepQ32
>= PLSR_PLANNER_Q32_ONE - previousQ32)
{
candidateQ32 = PLSR_PLANNER_Q32_ONE;
}
else if (context->rampLastPhaseStepQ32 != 0ULL)
{
candidateQ32 = previousQ32 + context->rampLastPhaseStepQ32;
}
else
{
derivativeHz = PlsrPlannerInstantFrequency(context, previousQ32);
if (derivativeHz == 0UL)
{
derivativeHz = 1UL;
}
differenceQ32 = targetAreaQ32 - currentAreaQ32;
correctionQ32 = (differenceQ32 + derivativeHz - 1UL)
/ derivativeHz;
candidateQ32 = (correctionQ32
>= PLSR_PLANNER_Q32_ONE - previousQ32)
? PLSR_PLANNER_Q32_ONE
: previousQ32 + correctionQ32;
}

candidateAreaQ32 = PlsrPlannerRampAreaQ32(
context, context->rampFromHz, context->rampToHz, candidateQ32);
derivativeHz = PlsrPlannerInstantFrequency(context, candidateQ32);
if (derivativeHz == 0UL)
{
derivativeHz = 1UL;
}
if (candidateAreaQ32 < targetAreaQ32)
{
differenceQ32 = targetAreaQ32 - candidateAreaQ32;
correctionQ32 = (differenceQ32 + derivativeHz - 1UL)
/ derivativeHz;
candidateQ32 = (correctionQ32
>= PLSR_PLANNER_Q32_ONE - candidateQ32)
? PLSR_PLANNER_Q32_ONE
: candidateQ32 + correctionQ32;
}
else if (candidateAreaQ32 > targetAreaQ32)
{
differenceQ32 = candidateAreaQ32 - targetAreaQ32;
correctionQ32 = differenceQ32 / derivativeHz;
if (correctionQ32 == 0ULL)
{
correctionQ32 = 1ULL;
}
candidateQ32 = (correctionQ32 >= candidateQ32 - previousQ32)
? previousQ32 + 1ULL
: candidateQ32 - correctionQ32;
}
return candidateQ32;
}

static void PlsrPlannerStartRamp(PLSR_PLANNER_CONTEXT *context,
uint8_t rampKind,
uint32_t fromHz,
uint32_t toHz,
uint32_t pulseCount)
{
context->rampKind = rampKind;
context->rampRelativePulse = 0UL;
context->rampPulseCount = pulseCount;
context->rampFromHz = fromHz;
context->rampToHz = toHz;
context->rampDurationMs = PlsrPlannerRampTime(&context->block,
fromHz, toHz);
context->rampTotalAreaQ32 = PlsrPlannerRampAreaQ32(
context, fromHz, toHz, PLSR_PLANNER_Q32_ONE);
context->rampAreaStepQ32 = context->rampTotalAreaQ32 / pulseCount;
context->rampAreaRemainder =
(uint32_t)(context->rampTotalAreaQ32 % pulseCount);
context->rampRemainderAccumulator = 0UL;
context->rampTargetAreaQ32 = 0ULL;
context->rampBoundaryQ32 = 0ULL;
context->rampActualTimeQ32 = 0ULL;
context->rampLastPhaseStepQ32 = 0ULL;
context->lastRampHz = 0UL;
context->rampFirstBoundaryQ32 = PlsrPlannerExactBoundaryQ32(
context, 0ULL, context->rampAreaStepQ32);
if (pulseCount > 1UL)
{
uint64_t secondTargetAreaQ32 = context->rampAreaStepQ32 * 2ULL
+ ((uint64_t)context->rampAreaRemainder * 2ULL) / pulseCount;
context->rampSecondBoundaryQ32 = PlsrPlannerExactBoundaryQ32(
context, context->rampFirstBoundaryQ32,
secondTargetAreaQ32);
}
else
{
context->rampSecondBoundaryQ32 = PLSR_PLANNER_Q32_ONE;
}
}

static uint8_t PlsrPlannerSameSetting(
const PLSR_PLATFORM_TIMER_SETTING *first,
const PLSR_PLATFORM_TIMER_SETTING *second)
{
return ((first->actualFrequencyHz == second->actualFrequencyHz)
&& (first->prescaler == second->prescaler)
&& (first->pairPrescaler == second->pairPrescaler)
&& (first->period == second->period)
&& (first->compare == second->compare)) ? 1U : 0U;
}

static uint8_t PlsrPlannerBuildStep(PLSR_PLANNER_CONTEXT *context,
uint32_t requestedHz,
PLSR_PLATFORM_TIMER_SETTING *setting)
{
if (requestedHz == 0UL)
{
requestedHz = 1UL;
}
if (requestedHz > PLSR_FREQUENCY_MAX_HZ)
{
requestedHz = PLSR_FREQUENCY_MAX_HZ;
}
return PlsrPlatformBuildTimerSetting(context->block.pulseOutput,
PLSR_OUTPUT_PULSE_DIR,
requestedHz, setting);
}

static uint8_t PlsrPlannerTakeRampStep(
PLSR_PLANNER_CONTEXT *context,
PLSR_PLATFORM_TIMER_SETTING *setting)
{
uint64_t nextBoundaryQ32;
uint64_t desiredDeltaQ32;
uint64_t denominator;
uint64_t requestedHz;
uint64_t actualDeltaQ32;

context->rampTargetAreaQ32 += context->rampAreaStepQ32;
context->rampRemainderAccumulator += context->rampAreaRemainder;
if (context->rampRemainderAccumulator >= context->rampPulseCount)
{
context->rampTargetAreaQ32++;
context->rampRemainderAccumulator -= context->rampPulseCount;
}
if (context->rampRelativePulse + 1UL >= context->rampPulseCount)
{
context->rampTargetAreaQ32 = context->rampTotalAreaQ32;
nextBoundaryQ32 = PLSR_PLANNER_Q32_ONE;
}
else if (context->rampRelativePulse == 0UL)
{
nextBoundaryQ32 = context->rampFirstBoundaryQ32;
}
else if (context->rampRelativePulse == 1UL)
{
nextBoundaryQ32 = context->rampSecondBoundaryQ32;
}
else
{
nextBoundaryQ32 = PlsrPlannerPredictedBoundaryQ32(
context, context->rampTargetAreaQ32);
}

desiredDeltaQ32 = (nextBoundaryQ32 > context->rampActualTimeQ32)
? (nextBoundaryQ32 - context->rampActualTimeQ32)
: 1ULL;
/* The allocated pulse count closes the ramp area exactly. Derive the
physical duration from N/averageHz instead of rounding it to whole
milliseconds; short clipped ramps can be well below 1 ms. */
denominator = (uint64_t)context->rampPulseCount * desiredDeltaQ32;
requestedHz = (denominator == 0ULL)
? context->rampToHz
: (context->rampTotalAreaQ32
+ denominator / 2ULL) / denominator;
if (requestedHz == 0ULL)
{
requestedHz = 1ULL;
}
if (requestedHz > PLSR_FREQUENCY_MAX_HZ)
{
requestedHz = PLSR_FREQUENCY_MAX_HZ;
}
if ((context->lastRampHz != 0UL)
&& (((context->rampToHz > context->rampFromHz)
&& (requestedHz < context->lastRampHz))
|| ((context->rampToHz < context->rampFromHz)
&& (requestedHz > context->lastRampHz))))
{
requestedHz = context->lastRampHz;
}
if (PlsrPlannerBuildStep(context, (uint32_t)requestedHz, setting) == 0U)
{
return 0U;
}

denominator = (uint64_t)context->rampPulseCount
* setting->actualFrequencyHz;
actualDeltaQ32 = (denominator == 0ULL)
? desiredDeltaQ32
: (context->rampTotalAreaQ32
+ denominator / 2ULL) / denominator;
context->rampActualTimeQ32 += actualDeltaQ32;
context->rampLastPhaseStepQ32 =
nextBoundaryQ32 - context->rampBoundaryQ32;
context->rampBoundaryQ32 = nextBoundaryQ32;
context->lastRampHz = setting->actualFrequencyHz;
context->rampRelativePulse++;
return 1U;
}

static uint8_t PlsrPlannerTakeStep(PLSR_PLANNER_CONTEXT *context,
PLSR_PLATFORM_TIMER_SETTING *setting,
uint32_t *repeatCount)
{
uint32_t entryEnd = context->entryPulses;
uint32_t steadyEnd = entryEnd + context->steadyPulses;

*repeatCount = 1UL;
if (context->generatedPulses >= context->block.pulseBudget)
{
return 0U;
}
if (context->generatedPulses < entryEnd)
{
if (context->rampKind != 1U)
{
PlsrPlannerStartRamp(context, 1U, context->startHz,
context->peakHz, context->entryPulses);
}
return PlsrPlannerTakeRampStep(context, setting);
}
if (context->generatedPulses < steadyEnd)
{
*repeatCount = steadyEnd - context->generatedPulses;
context->rampKind = 0U;
return PlsrPlannerBuildStep(context, context->peakHz, setting);
}
if (context->rampKind != 2U)
{
PlsrPlannerStartRamp(context, 2U, context->peakHz,
context->endHz, context->exitPulses);
}
return PlsrPlannerTakeRampStep(context, setting);
}

PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *context,
const PLSR_MOTION_BLOCK *block,
uint32_t appliedHz,
uint64_t phasePulses)
{
uint64_t directRequired;
uint64_t entryRequired;
uint64_t exitRequired;
uint64_t entryWeight;
uint64_t exitWeight;
uint64_t totalWeight;
uint64_t scaledEntry;

if ((context == NULL) || (block == NULL)
|| (block->pulseBudget == 0UL)
|| (block->referenceSpeedHz == 0UL)
|| (block->referenceSpeedHz > PLSR_FREQUENCY_MAX_HZ)
|| (block->entryHz > PLSR_FREQUENCY_MAX_HZ)
|| (block->cruiseHz == 0UL)
|| (block->cruiseHz > PLSR_FREQUENCY_MAX_HZ)
|| (block->exitHz > PLSR_FREQUENCY_MAX_HZ)
|| (appliedHz > PLSR_FREQUENCY_MAX_HZ)
|| (block->curveMode > 2U) || (block->pulseOutput > 3U))
{
return PLSR_PLANNER_INVALID;
}

(void)memset(context, 0, sizeof(*context));
context->block = *block;
context->phasePulses = phasePulses;
context->startHz = (appliedHz != 0UL) ? appliedHz : block->entryHz;
if (context->startHz == 0UL)
{
context->startHz = 1UL;
}
context->endHz = (block->exitHz == 0UL) ? 1UL : block->exitHz;

directRequired = PlsrPlannerRequiredPulses(
block, PlsrPlannerRampWeight(
context->startHz, context->endHz,
PlsrPlannerBaseRampTime(block, context->startHz,
context->endHz)));
if (directRequired > block->pulseBudget)
{
context->peakHz = PlsrPlannerReachableFrequency(
block, context->startHz, context->endHz, block->pulseBudget);
context->endHz = context->peakHz;
context->entryPulses = block->pulseBudget;
context->clipped = 1U;
context->active = 1U;
return PLSR_PLANNER_CLIPPED;
}

context->peakHz = PlsrPlannerPeak(block, context->startHz,
context->endHz);
entryWeight = PlsrPlannerRampWeight(
context->startHz, context->peakHz,
PlsrPlannerBaseRampTime(block, context->startHz, context->peakHz));
exitWeight = PlsrPlannerRampWeight(
context->peakHz, context->endHz,
PlsrPlannerBaseRampTime(block, context->peakHz, context->endHz));
entryRequired = PlsrPlannerRequiredPulses(block, entryWeight);
exitRequired = PlsrPlannerRequiredPulses(block, exitWeight);

if ((entryRequired + exitRequired) <= block->pulseBudget)
{
context->entryPulses = (uint32_t)entryRequired;
context->exitPulses = (uint32_t)exitRequired;
context->steadyPulses = block->pulseBudget
- context->entryPulses
- context->exitPulses;
}
else if (entryRequired == 0ULL)
{
context->exitPulses = block->pulseBudget;
context->clipped = 1U;
}
else if (exitRequired == 0ULL)
{
context->entryPulses = block->pulseBudget;
context->clipped = 1U;
}
else
{
totalWeight = entryWeight + exitWeight;
scaledEntry = ((uint64_t)block->pulseBudget * entryWeight
+ totalWeight / 2ULL) / totalWeight;
if (scaledEntry == 0ULL)
{
scaledEntry = 1ULL;
}
if (scaledEntry >= block->pulseBudget)
{
scaledEntry = block->pulseBudget - 1UL;
}
context->entryPulses = (uint32_t)scaledEntry;
context->exitPulses = block->pulseBudget
- context->entryPulses;
context->clipped = 1U;
}
if (context->peakHz != block->cruiseHz)
{
context->clipped = 1U;
}
context->active = 1U;
return (context->clipped != 0U) ? PLSR_PLANNER_CLIPPED
: PLSR_PLANNER_OK;
}

uint16_t PlsrPlannerGenerate(PLSR_PLANNER_CONTEXT *context,
PLSR_STREAM_ITEM *output,
uint16_t capacity)
{
PLSR_PLATFORM_TIMER_SETTING setting;
uint32_t repeatCount;
uint16_t produced = 0U;

if ((context == NULL) || (output == NULL) || (capacity == 0U)
|| (context->active == 0U))
{
return 0U;
}
while ((produced < capacity)
&& (context->generatedPulses < context->block.pulseBudget))
{
if (PlsrPlannerTakeStep(context, &setting, &repeatCount) == 0U)
{
context->active = 0U;
break;
}
if ((produced != 0U)
&& (PlsrPlannerSameSetting(&output[produced - 1U].setting,
&setting) != 0U)
&& (output[produced - 1U].repeatCount
<= 0xFFFFFFFFUL - repeatCount))
{
output[produced - 1U].repeatCount += repeatCount;
}
else
{
output[produced].setting = setting;
output[produced].repeatCount = repeatCount;
produced++;
}
context->generatedPulses += repeatCount;
}
if (context->generatedPulses >= context->block.pulseBudget)
{
context->active = 0U;
}
return produced;
}

+ 89
- 0
PLSR/Src/plsr_planner.h Vedi File

@@ -0,0 +1,89 @@
#ifndef PLSR_PLANNER_H
#define PLSR_PLANNER_H

#include "plsr_platform.h"

#include <stdint.h>

typedef enum
{
PLSR_BOUNDARY_STOP = 0,
PLSR_BOUNDARY_NEXT_FRESH,
PLSR_BOUNDARY_NEXT_CARRY,
PLSR_BOUNDARY_WAIT_TIME,
PLSR_BOUNDARY_WAIT_SIGNAL,
PLSR_BOUNDARY_WAIT_EXT,
PLSR_BOUNDARY_JUMP,
PLSR_BOUNDARY_EXT_CUT
} PLSR_BOUNDARY_ACTION;

/* A block is self-contained: the planner must not read live Modbus state. */
typedef struct
{
uint32_t entryHz;
uint32_t cruiseHz;
uint32_t exitHz;
uint32_t pulseBudget;
uint32_t referenceSpeedHz;
uint16_t accelerationTimeMs;
uint16_t decelerationTimeMs;
uint16_t curveMode;
uint8_t pulseOutput;
PLSR_BOUNDARY_ACTION boundary;
} PLSR_MOTION_BLOCK;

typedef struct
{
PLSR_PLATFORM_TIMER_SETTING setting;
uint32_t repeatCount;
} PLSR_STREAM_ITEM;

typedef enum
{
PLSR_PLANNER_OK = 0,
PLSR_PLANNER_CLIPPED,
PLSR_PLANNER_DONE,
PLSR_PLANNER_INVALID
} PLSR_PLANNER_STATUS;

typedef struct
{
PLSR_MOTION_BLOCK block;
uint64_t phasePulses;
uint32_t startHz;
uint32_t peakHz;
uint32_t endHz;
uint32_t entryPulses;
uint32_t steadyPulses;
uint32_t exitPulses;
uint32_t generatedPulses;
uint32_t rampRelativePulse;
uint32_t rampPulseCount;
uint32_t rampFromHz;
uint32_t rampToHz;
uint32_t rampDurationMs;
uint32_t lastRampHz;
uint64_t rampTotalAreaQ32;
uint64_t rampAreaStepQ32;
uint64_t rampTargetAreaQ32;
uint64_t rampBoundaryQ32;
uint64_t rampActualTimeQ32;
uint64_t rampLastPhaseStepQ32;
uint64_t rampFirstBoundaryQ32;
uint64_t rampSecondBoundaryQ32;
uint32_t rampAreaRemainder;
uint32_t rampRemainderAccumulator;
uint8_t rampKind;
uint8_t clipped;
uint8_t active;
} PLSR_PLANNER_CONTEXT;

PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *context,
const PLSR_MOTION_BLOCK *block,
uint32_t appliedHz,
uint64_t phasePulses);
uint16_t PlsrPlannerGenerate(PLSR_PLANNER_CONTEXT *context,
PLSR_STREAM_ITEM *output,
uint16_t capacity);

#endif /* PLSR_PLANNER_H */

+ 8
- 6
PLSR/Src/plsr_platform.h Vedi File

@@ -38,9 +38,13 @@ typedef struct
{
PLSR_PLATFORM_TIMER_SETTING setting;
uint32_t pulseCount;
uint32_t segmentPulseOffset;
uint8_t segmentNumber;
uint8_t completesSegment;
uint8_t updatesWithSegment;
} PLSR_PLATFORM_FINITE_STEP;


uint8_t PlsrPlatformInit(void);
uint8_t PlsrPlatformPrepare(uint8_t pulseOutput,
uint8_t directionOutput,
@@ -71,8 +75,8 @@ uint8_t PlsrPlatformStartFinitePrepared(
uint32_t *actualFrequencyHz);
uint8_t PlsrPlatformStartFiniteSequencePrepared(
uint8_t pulseOutput,
const PLSR_PLATFORM_FINITE_STEP *steps,
uint8_t stepCount,
PLSR_PLATFORM_FINITE_STEP *steps,
uint16_t stepCount,
uint32_t *actualFrequencyHz);
PLSR_PLATFORM_QUEUE_RESULT PlsrPlatformUpdateFinitePrepared(
uint8_t pulseOutput,
@@ -80,10 +84,8 @@ PLSR_PLATFORM_QUEUE_RESULT PlsrPlatformUpdateFinitePrepared(
uint32_t *actualFrequencyHz);
uint8_t PlsrPlatformRetargetFiniteStop(uint8_t pulseOutput,
uint32_t drainPulses);
uint8_t PlsrPlatformUpdateFiniteStep(
uint8_t pulseOutput,
uint8_t segmentNumber,
const PLSR_PLATFORM_TIMER_SETTING *setting);
uint8_t PlsrPlatformFiniteRetargetReady(uint8_t pulseOutput,
uint32_t *activeFrequencyHz);
uint8_t PlsrPlatformFiniteProgress(uint8_t pulseOutput,
uint32_t *completedPulses);
uint8_t PlsrPlatformTakeFiniteCompletion(uint8_t pulseOutput,


+ 232
- 154
PLSR/Src/plsr_platform_f407.c Vedi File

@@ -65,13 +65,14 @@ static uint32_t PlsrHostSaveCount;
static uint8_t PlsrHostFiniteEnabled;
static uint8_t PlsrHostFiniteActive[4];
static uint8_t PlsrHostFiniteComplete[4];
static uint8_t PlsrHostFiniteFrequencyPending[4];
static uint32_t PlsrHostFiniteTarget[4];
static uint32_t PlsrHostFiniteEmitted[4];
static PLSR_PLATFORM_FINITE_STEP PlsrHostFiniteSteps[4][PLSR_SEGMENT_COUNT_MAX];
static uint8_t PlsrHostFiniteStepCount[4];
static uint8_t PlsrHostFiniteStepIndex[4];
static uint8_t PlsrHostFiniteBoundaryReadIndex[4];
static uint8_t PlsrHostFiniteCompletedStepCount[4];
static PLSR_PLATFORM_FINITE_STEP *PlsrHostFiniteSteps[4];
static uint16_t PlsrHostFiniteStepCount[4];
static uint16_t PlsrHostFiniteStepIndex[4];
static uint16_t PlsrHostFiniteBoundaryReadIndex[4];
static uint16_t PlsrHostFiniteCompletedStepCount[4];

static void PlsrHostServiceFinalArmJob(void);

@@ -235,6 +236,8 @@ uint8_t PlsrPlatformInit(void)
(void)memset(PlsrHostFiniteActive, 0, sizeof(PlsrHostFiniteActive));
(void)memset(PlsrHostFiniteComplete, 0,
sizeof(PlsrHostFiniteComplete));
(void)memset(PlsrHostFiniteFrequencyPending, 0,
sizeof(PlsrHostFiniteFrequencyPending));
(void)memset(PlsrHostFiniteTarget, 0, sizeof(PlsrHostFiniteTarget));
(void)memset(PlsrHostFiniteEmitted, 0, sizeof(PlsrHostFiniteEmitted));
(void)memset(PlsrHostFiniteSteps, 0, sizeof(PlsrHostFiniteSteps));
@@ -399,6 +402,7 @@ uint8_t PlsrPlatformStartFinitePrepared(
PlsrHostPulseActive[pulseOutput] = 1U;
PlsrHostFiniteActive[pulseOutput] = 1U;
PlsrHostFiniteComplete[pulseOutput] = 0U;
PlsrHostFiniteFrequencyPending[pulseOutput] = 0U;
PlsrHostFiniteTarget[pulseOutput] = pulseCount;
PlsrHostFiniteEmitted[pulseOutput] = 0UL;
PlsrHostFiniteStepCount[pulseOutput] = 0U;
@@ -416,20 +420,19 @@ uint8_t PlsrPlatformStartFinitePrepared(

uint8_t PlsrPlatformStartFiniteSequencePrepared(
uint8_t pulseOutput,
const PLSR_PLATFORM_FINITE_STEP *steps,
uint8_t stepCount,
PLSR_PLATFORM_FINITE_STEP *steps,
uint16_t stepCount,
uint32_t *actualFrequencyHz)
{
if ((steps == NULL) || (stepCount == 0U)
|| (stepCount > PLSR_SEGMENT_COUNT_MAX)
|| (stepCount > PLSR_PLATFORM_FINITE_STEP_MAX)
|| (PlsrPlatformStartFinitePrepared(
pulseOutput, &steps[0].setting, steps[0].pulseCount,
actualFrequencyHz) == 0U))
{
return 0U;
}
(void)memcpy(PlsrHostFiniteSteps[pulseOutput], steps,
(size_t)stepCount * sizeof(steps[0]));
PlsrHostFiniteSteps[pulseOutput] = steps;
PlsrHostFiniteStepCount[pulseOutput] = stepCount;
PlsrHostFiniteStepIndex[pulseOutput] = 0U;
PlsrHostFiniteBoundaryReadIndex[pulseOutput] = 0U;
@@ -437,21 +440,35 @@ uint8_t PlsrPlatformStartFiniteSequencePrepared(
return 1U;
}


PLSR_PLATFORM_QUEUE_RESULT PlsrPlatformUpdateFinitePrepared(
uint8_t pulseOutput,
const PLSR_PLATFORM_TIMER_SETTING *setting,
uint32_t *actualFrequencyHz)
{
if (PlsrHostFailNextFrequencyAtUpdate != 0U)
{
PlsrHostFailNextFrequencyAtUpdate = 0U;
return PLSR_PLATFORM_QUEUE_FAILED;
}
if ((pulseOutput > 3U) || (setting == NULL)
|| (actualFrequencyHz == NULL)
|| (PlsrHostFiniteActive[pulseOutput] == 0U))
{
return PLSR_PLATFORM_QUEUE_STALE;
}
PlsrHostActiveSetting[pulseOutput] = *setting;
if (PlsrHostStaleNextFrequencyAtUpdate != 0U)
{
PlsrHostStaleNextFrequencyAtUpdate = 0U;
return PLSR_PLATFORM_QUEUE_STALE;
}
if (PlsrHostFiniteFrequencyPending[pulseOutput] != 0U)
{
return PLSR_PLATFORM_QUEUE_STALE;
}
PlsrHostQueuedSetting[pulseOutput] = *setting;
PlsrHostFrequency[pulseOutput] = setting->actualFrequencyHz;
PlsrHostQueuedFrequency[pulseOutput] = setting->actualFrequencyHz;
PlsrHostFiniteFrequencyPending[pulseOutput] = 1U;
*actualFrequencyHz = setting->actualFrequencyHz;
return PLSR_PLATFORM_QUEUE_APPLIED;
}
@@ -459,13 +476,21 @@ PLSR_PLATFORM_QUEUE_RESULT PlsrPlatformUpdateFinitePrepared(
uint8_t PlsrPlatformRetargetFiniteStop(uint8_t pulseOutput,
uint32_t drainPulses)
{
uint32_t completed;

if ((pulseOutput > 3U) || (drainPulses == 0UL)
|| (PlsrHostFiniteActive[pulseOutput] == 0U))
{
return 0U;
}
PlsrHostFiniteTarget[pulseOutput] =
PlsrHostFiniteEmitted[pulseOutput] + drainPulses;
completed = PlsrHostFiniteEmitted[pulseOutput];
if (PlsrHostFiniteStepCount[pulseOutput] != 0U)
{
completed += PlsrHostFiniteSteps[pulseOutput][
PlsrHostFiniteStepIndex[pulseOutput]].segmentPulseOffset;
}
PlsrHostFiniteTarget[pulseOutput] = completed + drainPulses;
PlsrHostFiniteEmitted[pulseOutput] = completed;
PlsrHostFiniteStepCount[pulseOutput] = 0U;
PlsrHostFiniteStepIndex[pulseOutput] = 0U;
PlsrHostFiniteBoundaryReadIndex[pulseOutput] = 0U;
@@ -473,29 +498,16 @@ uint8_t PlsrPlatformRetargetFiniteStop(uint8_t pulseOutput,
return 1U;
}

uint8_t PlsrPlatformUpdateFiniteStep(
uint8_t pulseOutput,
uint8_t segmentNumber,
const PLSR_PLATFORM_TIMER_SETTING *setting)
uint8_t PlsrPlatformFiniteRetargetReady(uint8_t pulseOutput,
uint32_t *activeFrequencyHz)
{
uint8_t index;

if ((pulseOutput > 3U) || (segmentNumber == 0U) || (setting == NULL)
if ((pulseOutput > 3U) || (activeFrequencyHz == NULL)
|| (PlsrHostFiniteActive[pulseOutput] == 0U))
{
return 0U;
}
for (index = PlsrHostFiniteStepIndex[pulseOutput];
index < PlsrHostFiniteStepCount[pulseOutput]; index++)
{
if (PlsrHostFiniteSteps[pulseOutput][index].segmentNumber
== segmentNumber)
{
PlsrHostFiniteSteps[pulseOutput][index].setting = *setting;
return 1U;
}
}
return 0U;
*activeFrequencyHz = PlsrHostFrequency[pulseOutput];
return (PlsrHostFiniteStepCount[pulseOutput] == 0U) ? 1U : 0U;
}

uint8_t PlsrPlatformFiniteProgress(uint8_t pulseOutput,
@@ -507,7 +519,19 @@ uint8_t PlsrPlatformFiniteProgress(uint8_t pulseOutput,
{
return 0U;
}
*completedPulses = PlsrHostFiniteEmitted[pulseOutput];
if (PlsrHostFiniteStepCount[pulseOutput] != 0U)
{
const PLSR_PLATFORM_FINITE_STEP *step =
&PlsrHostFiniteSteps[pulseOutput][
PlsrHostFiniteStepIndex[pulseOutput]];

*completedPulses = step->segmentPulseOffset
+ PlsrHostFiniteEmitted[pulseOutput];
}
else
{
*completedPulses = PlsrHostFiniteEmitted[pulseOutput];
}
return 1U;
}

@@ -520,7 +544,18 @@ uint8_t PlsrPlatformTakeFiniteCompletion(uint8_t pulseOutput,
return 0U;
}
PlsrHostFiniteComplete[pulseOutput] = 0U;
*completedPulses = PlsrHostFiniteTarget[pulseOutput];
if (PlsrHostFiniteStepCount[pulseOutput] != 0U)
{
const PLSR_PLATFORM_FINITE_STEP *step =
&PlsrHostFiniteSteps[pulseOutput][
PlsrHostFiniteStepCount[pulseOutput] - 1U];

*completedPulses = step->segmentPulseOffset + step->pulseCount;
}
else
{
*completedPulses = PlsrHostFiniteTarget[pulseOutput];
}
return 1U;
}

@@ -532,22 +567,31 @@ uint8_t PlsrPlatformTakeFiniteBoundary(uint8_t pulseOutput,
{
if ((pulseOutput > 3U) || (segmentNumber == NULL)
|| (completedPulses == NULL) || (sequenceContinues == NULL)
|| (activeFrequencyHz == NULL)
|| (PlsrHostFiniteBoundaryReadIndex[pulseOutput]
>= PlsrHostFiniteCompletedStepCount[pulseOutput]))
|| (activeFrequencyHz == NULL))
{
return 0U;
}
while (PlsrHostFiniteBoundaryReadIndex[pulseOutput]
< PlsrHostFiniteCompletedStepCount[pulseOutput])
{
uint8_t index = PlsrHostFiniteBoundaryReadIndex[pulseOutput]++;
uint16_t index = PlsrHostFiniteBoundaryReadIndex[pulseOutput]++;
const PLSR_PLATFORM_FINITE_STEP *step =
&PlsrHostFiniteSteps[pulseOutput][index];

*segmentNumber = PlsrHostFiniteSteps[pulseOutput][index].segmentNumber;
*completedPulses = PlsrHostFiniteSteps[pulseOutput][index].pulseCount;
*sequenceContinues =
(index + 1U < PlsrHostFiniteStepCount[pulseOutput]) ? 1U : 0U;
if (step->completesSegment != 0U)
{
*segmentNumber = step->segmentNumber;
*completedPulses = step->segmentPulseOffset + step->pulseCount;
*sequenceContinues =
(index + 1U < PlsrHostFiniteStepCount[pulseOutput]) ? 1U : 0U;
*activeFrequencyHz = (*sequenceContinues != 0U)
? PlsrHostFiniteSteps[pulseOutput][index + 1U]
.setting.actualFrequencyHz
: step->setting.actualFrequencyHz;
return 1U;
}
}
*activeFrequencyHz = PlsrHostFrequency[pulseOutput];
return 1U;
return 0U;
}

PLSR_PLATFORM_QUEUE_RESULT PlsrPlatformLoadPreparedFromIrq(
@@ -905,44 +949,64 @@ void PlsrTestEmitPulses(uint32_t pulseCount)
if (PlsrHostFiniteActive[PlsrHostSelectedPulse] != 0U)
{
uint8_t pulseOutput = PlsrHostSelectedPulse;
uint32_t available = PlsrHostFiniteTarget[pulseOutput]
- PlsrHostFiniteEmitted[pulseOutput];
uint32_t emitted = (pulseCount < available) ? pulseCount : available;

PlsrHostFiniteEmitted[pulseOutput] += emitted;
PlsrHostObservedPulses[pulseOutput] += emitted;
if (PlsrHostFiniteEmitted[pulseOutput]
== PlsrHostFiniteTarget[pulseOutput])
while ((pulseCount != 0UL)
&& (PlsrHostFiniteActive[pulseOutput] != 0U))
{
uint8_t index = PlsrHostFiniteStepIndex[pulseOutput];
uint8_t hasNext = (index + 1U
< PlsrHostFiniteStepCount[pulseOutput])
? 1U : 0U;
uint32_t available = PlsrHostFiniteTarget[pulseOutput]
- PlsrHostFiniteEmitted[pulseOutput];
uint32_t emitted = (pulseCount < available)
? pulseCount : available;

if (PlsrHostFiniteStepCount[pulseOutput] != 0U)
if ((emitted != 0UL)
&& (PlsrHostFiniteFrequencyPending[pulseOutput] != 0U))
{
PlsrHostFiniteCompletedStepCount[pulseOutput] =
(uint8_t)(index + 1U);
PlsrHostActiveSetting[pulseOutput] =
PlsrHostQueuedSetting[pulseOutput];
PlsrHostFrequency[pulseOutput] =
PlsrHostQueuedFrequency[pulseOutput];
PlsrHostFiniteFrequencyPending[pulseOutput] = 0U;
}
if (hasNext != 0U)
PlsrHostFiniteEmitted[pulseOutput] += emitted;
PlsrHostObservedPulses[pulseOutput] += emitted;
pulseCount -= emitted;
if (PlsrHostFiniteEmitted[pulseOutput]
== PlsrHostFiniteTarget[pulseOutput])
{
const PLSR_PLATFORM_FINITE_STEP *next =
&PlsrHostFiniteSteps[pulseOutput][index + 1U];

PlsrHostFiniteStepIndex[pulseOutput]++;
PlsrHostFiniteTarget[pulseOutput] = next->pulseCount;
PlsrHostFiniteEmitted[pulseOutput] = 0UL;
PlsrHostFrequency[pulseOutput] = next->setting.actualFrequencyHz;
PlsrHostQueuedFrequency[pulseOutput] =
next->setting.actualFrequencyHz;
}
else
{
PlsrHostFiniteActive[pulseOutput] = 0U;
PlsrHostFiniteComplete[pulseOutput] = 1U;
PlsrHostPulseActive[pulseOutput] = 0U;
PlsrHostFrequency[pulseOutput] = 0UL;
PlsrHostQueuedFrequency[pulseOutput] = 0UL;
uint16_t index = PlsrHostFiniteStepIndex[pulseOutput];
uint8_t hasNext = (index + 1U
< PlsrHostFiniteStepCount[pulseOutput])
? 1U : 0U;

if (PlsrHostFiniteStepCount[pulseOutput] != 0U)
{
PlsrHostFiniteCompletedStepCount[pulseOutput] =
(uint16_t)(index + 1U);
}
if (hasNext != 0U)
{
const PLSR_PLATFORM_FINITE_STEP *next =
&PlsrHostFiniteSteps[pulseOutput][index + 1U];

PlsrHostFiniteStepIndex[pulseOutput]++;
PlsrHostFiniteTarget[pulseOutput] = next->pulseCount;
PlsrHostFiniteEmitted[pulseOutput] = 0UL;
PlsrHostFrequency[pulseOutput] =
next->setting.actualFrequencyHz;
PlsrHostQueuedFrequency[pulseOutput] =
next->setting.actualFrequencyHz;
PlsrHostActiveSetting[pulseOutput] = next->setting;
PlsrHostQueuedSetting[pulseOutput] = next->setting;
PlsrHostFiniteFrequencyPending[pulseOutput] = 0U;
}
else
{
PlsrHostFiniteActive[pulseOutput] = 0U;
PlsrHostFiniteComplete[pulseOutput] = 1U;
PlsrHostPulseActive[pulseOutput] = 0U;
PlsrHostFiniteFrequencyPending[pulseOutput] = 0U;
PlsrHostFrequency[pulseOutput] = 0UL;
PlsrHostQueuedFrequency[pulseOutput] = 0UL;
}
}
}
return;
@@ -1411,7 +1475,6 @@ static TIM_TypeDef * const PlsrCounters[PLSR_COUNTER_COUNT] =
TIM9, TIM12
};
static uint8_t PlsrCounterOwner[PLSR_COUNTER_COUNT];
static uint8_t PlsrCounterTriggerSourceAxis[PLSR_COUNTER_COUNT];
static volatile uint64_t PlsrCounterOverflowPulses[PLSR_COUNTER_COUNT];
static uint8_t PlsrCounterIndexByOutput[4];
static uint64_t PlsrObservedPulseBase[4];
@@ -1425,12 +1488,11 @@ static uint32_t PlsrFiniteRetargetDrainPulses[4];
static uint32_t PlsrFiniteTargetPulses[4];
static uint32_t PlsrFiniteRemainingPulses[4];
static uint8_t PlsrFiniteCounterPreload[4];
static PLSR_PLATFORM_FINITE_STEP
PlsrFiniteSteps[4][PLSR_SEGMENT_COUNT_MAX];
static volatile uint8_t PlsrFiniteStepCount[4];
static volatile uint8_t PlsrFiniteStepIndex[4];
static volatile uint8_t PlsrFiniteBoundaryReadIndex[4];
static volatile uint8_t PlsrFiniteCompletedStepCount[4];
static PLSR_PLATFORM_FINITE_STEP *PlsrFiniteSteps[4];
static volatile uint16_t PlsrFiniteStepCount[4];
static volatile uint16_t PlsrFiniteStepIndex[4];
static volatile uint16_t PlsrFiniteBoundaryReadIndex[4];
static volatile uint16_t PlsrFiniteCompletedStepCount[4];
static volatile uint16_t PlsrPlatformFaultPending;

static void PlsrHandleTimerIrq(uint8_t pulseOutput);
@@ -2345,7 +2407,6 @@ static void PlsrCounterStop(uint8_t pulseOutput)
counter->SR = 0UL;
if (PlsrCounterOwner[index] == pulseOutput)
{
PlsrCounterTriggerSourceAxis[index] = PLSR_COUNTER_NONE;
PlsrCounterOwner[index] = PLSR_COUNTER_NONE;
}
}
@@ -2355,9 +2416,6 @@ static void PlsrCounterStop(uint8_t pulseOutput)
static uint8_t PlsrCounterConfigure(uint8_t pulseOutput, uint8_t outputMode)
{
uint8_t index = PlsrCounterIndex(pulseOutput, outputMode);
uint8_t sourceAxis = (outputMode == PLSR_OUTPUT_AB)
? ((pulseOutput == 0U) ? 0U : 3U)
: pulseOutput;
TIM_TypeDef *counter = PlsrCounters[index];
uint32_t triggerSelection = ((pulseOutput & 2U) == 0U)
? TIM_SMCR_TS_1
@@ -2370,7 +2428,6 @@ static uint8_t PlsrCounterConfigure(uint8_t pulseOutput, uint8_t outputMode)
return 0U;
}
PlsrCounterOwner[index] = pulseOutput;
PlsrCounterTriggerSourceAxis[index] = sourceAxis;
PlsrCounterIndexByOutput[pulseOutput] = index;
PlsrCounterOverflowPulses[index] = 0UL;
counter->CR1 = 0UL;
@@ -2758,7 +2815,6 @@ uint8_t PlsrPlatformInit(void)
for (index = 0U; index < PLSR_COUNTER_COUNT; index++)
{
PlsrCounterOwner[index] = PLSR_COUNTER_NONE;
PlsrCounterTriggerSourceAxis[index] = PLSR_COUNTER_NONE;
PlsrCounterOverflowPulses[index] = 0UL;
PlsrCounters[index]->CR1 = 0UL;
PlsrCounters[index]->DIER = 0UL;
@@ -3144,15 +3200,15 @@ uint8_t PlsrPlatformStartFinitePrepared(

uint8_t PlsrPlatformStartFiniteSequencePrepared(
uint8_t pulseOutput,
const PLSR_PLATFORM_FINITE_STEP *steps,
uint8_t stepCount,
PLSR_PLATFORM_FINITE_STEP *steps,
uint16_t stepCount,
uint32_t *actualFrequencyHz)
{
TIM_TypeDef *counter;
uint8_t index;
uint16_t index;

if ((steps == NULL) || (stepCount == 0U)
|| (stepCount > PLSR_SEGMENT_COUNT_MAX))
|| (stepCount > PLSR_PLATFORM_FINITE_STEP_MAX))
{
return 0U;
}
@@ -3173,8 +3229,7 @@ uint8_t PlsrPlatformStartFiniteSequencePrepared(
{
return 0U;
}
(void)memcpy(PlsrFiniteSteps[pulseOutput], steps,
(size_t)stepCount * sizeof(steps[0]));
PlsrFiniteSteps[pulseOutput] = steps;
PlsrFiniteStepCount[pulseOutput] = stepCount;
PlsrFiniteStepIndex[pulseOutput] = 0U;
PlsrFiniteBoundaryReadIndex[pulseOutput] = 0U;
@@ -3213,12 +3268,29 @@ PLSR_PLATFORM_QUEUE_RESULT PlsrPlatformUpdateFinitePrepared(
timer = PlsrTimerMap[pulseOutput].timer;
criticalState = PlsrPlatformEnterCritical();
if ((PlsrFiniteActive[pulseOutput] == 0U)
|| ((timer->CR1 & TIM_CR1_CEN) == 0UL)
|| (PlsrFiniteFrequencyPending[pulseOutput] != 0U))
|| ((timer->CR1 & TIM_CR1_CEN) == 0UL))
{
PlsrPlatformExitCritical(criticalState);
return PLSR_PLATFORM_QUEUE_STALE;
}
if (PlsrFiniteFrequencyPending[pulseOutput] != 0U)
{
if ((timer->SR & TIM_SR_UIF) == 0UL)
{
PlsrPlatformExitCritical(criticalState);
return PLSR_PLATFORM_QUEUE_STALE;
}

/* The update IRQ normally commits this preload. Also consume a
latched update here so a delayed/shared IRQ cannot stall a ramp. */
timer->SR = ~TIM_SR_UIF;
timer->DIER &= ~TIM_DIER_UIE;
PlsrTimerActiveFrequencyHz[pulseOutput] =
PlsrTimerQueuedFrequencyHz[pulseOutput];
PlsrTimerActiveSetting[pulseOutput] =
PlsrTimerQueuedSetting[pulseOutput];
PlsrFiniteFrequencyPending[pulseOutput] = 0U;
}
activePeriod = PlsrTimerActiveSetting[pulseOutput].period;
counter = timer->CNT;
if ((counter > activePeriod)
@@ -3265,45 +3337,23 @@ uint8_t PlsrPlatformRetargetFiniteStop(uint8_t pulseOutput,
return 1U;
}

uint8_t PlsrPlatformUpdateFiniteStep(
uint8_t pulseOutput,
uint8_t segmentNumber,
const PLSR_PLATFORM_TIMER_SETTING *setting)
uint8_t PlsrPlatformFiniteRetargetReady(uint8_t pulseOutput,
uint32_t *activeFrequencyHz)
{
uint8_t index;
uint8_t currentIndex;
uint32_t criticalState;
uint8_t ready;

if ((pulseOutput > 3U) || (segmentNumber == 0U) || (setting == NULL)
|| (PlsrFiniteActive[pulseOutput] == 0U))
if ((pulseOutput > 3U) || (activeFrequencyHz == NULL))
{
return 0U;
}
currentIndex = PlsrFiniteStepIndex[pulseOutput];
for (index = currentIndex;
index < PlsrFiniteStepCount[pulseOutput]; index++)
{
if (PlsrFiniteSteps[pulseOutput][index].segmentNumber
== segmentNumber)
{
PlsrFiniteSteps[pulseOutput][index].setting = *setting;
if ((index == (uint8_t)(currentIndex + 1U))
&& ((PlsrCounters[
PlsrCounterIndexByOutput[pulseOutput]]->DIER
& TIM_DIER_CC1IE) == 0UL))
{
TIM_TypeDef *timer = PlsrTimerMap[pulseOutput].timer;

timer->PSC = setting->prescaler;
timer->ARR = setting->period;
timer->CCR1 = setting->compare;
PlsrTimerQueuedSetting[pulseOutput] = *setting;
PlsrTimerQueuedFrequencyHz[pulseOutput] =
setting->actualFrequencyHz;
}
return 1U;
}
}
return 0U;
criticalState = PlsrPlatformEnterCritical();
ready = ((PlsrFiniteActive[pulseOutput] != 0U)
&& (PlsrFiniteRetargetPending[pulseOutput] == 0U)
&& (PlsrFiniteStepCount[pulseOutput] == 0U)) ? 1U : 0U;
*activeFrequencyHz = PlsrTimerActiveFrequencyHz[pulseOutput];
PlsrPlatformExitCritical(criticalState);
return ready;
}

uint8_t PlsrPlatformFiniteProgress(uint8_t pulseOutput,
@@ -3349,6 +3399,11 @@ uint8_t PlsrPlatformFiniteProgress(uint8_t pulseOutput,
PlsrPlatformExitCritical(criticalState);
return 0U;
}
if (PlsrFiniteStepCount[pulseOutput] != 0U)
{
completed += PlsrFiniteSteps[pulseOutput][
PlsrFiniteStepIndex[pulseOutput]].segmentPulseOffset;
}
*completedPulses = completed;
PlsrPlatformExitCritical(criticalState);
return 1U;
@@ -3367,7 +3422,18 @@ uint8_t PlsrPlatformTakeFiniteCompletion(uint8_t pulseOutput,
}

counterIndex = PlsrCounterIndexByOutput[pulseOutput];
*completedPulses = PlsrFiniteTargetPulses[pulseOutput];
if (PlsrFiniteStepCount[pulseOutput] != 0U)
{
const PLSR_PLATFORM_FINITE_STEP *step =
&PlsrFiniteSteps[pulseOutput][
PlsrFiniteStepCount[pulseOutput] - 1U];

*completedPulses = step->segmentPulseOffset + step->pulseCount;
}
else
{
*completedPulses = PlsrFiniteTargetPulses[pulseOutput];
}
if (PlsrFiniteStepCount[pulseOutput] == 0U)
{
PlsrObservedPulseBase[pulseOutput] += *completedPulses;
@@ -3394,7 +3460,6 @@ uint8_t PlsrPlatformTakeFiniteCompletion(uint8_t pulseOutput,
counter->SR = 0UL;
PlsrCounterOverflowPulses[counterIndex] = 0UL;
PlsrCounterOwner[counterIndex] = PLSR_COUNTER_NONE;
PlsrCounterTriggerSourceAxis[counterIndex] = PLSR_COUNTER_NONE;
}
PlsrCounterIndexByOutput[pulseOutput] = PLSR_COUNTER_NONE;
return 1U;
@@ -3407,8 +3472,8 @@ uint8_t PlsrPlatformTakeFiniteBoundary(uint8_t pulseOutput,
uint32_t *activeFrequencyHz)
{
uint32_t criticalState;
uint8_t readIndex;
uint8_t completedCount;
uint16_t readIndex;
uint16_t completedCount;

if ((pulseOutput > 3U) || (segmentNumber == NULL)
|| (completedPulses == NULL) || (sequenceContinues == NULL)
@@ -3419,23 +3484,29 @@ uint8_t PlsrPlatformTakeFiniteBoundary(uint8_t pulseOutput,
criticalState = PlsrPlatformEnterCritical();
readIndex = PlsrFiniteBoundaryReadIndex[pulseOutput];
completedCount = PlsrFiniteCompletedStepCount[pulseOutput];
if (readIndex >= completedCount)
while (readIndex < completedCount)
{
PlsrPlatformExitCritical(criticalState);
return 0U;
}
{
uint8_t index = readIndex;
uint16_t index = readIndex++;
const PLSR_PLATFORM_FINITE_STEP *step =
&PlsrFiniteSteps[pulseOutput][index];

PlsrFiniteBoundaryReadIndex[pulseOutput] = (uint8_t)(index + 1U);
*segmentNumber = PlsrFiniteSteps[pulseOutput][index].segmentNumber;
*completedPulses = PlsrFiniteSteps[pulseOutput][index].pulseCount;
*sequenceContinues =
(index + 1U < PlsrFiniteStepCount[pulseOutput]) ? 1U : 0U;
PlsrFiniteBoundaryReadIndex[pulseOutput] = readIndex;
if (step->completesSegment != 0U)
{
*segmentNumber = step->segmentNumber;
*completedPulses = step->segmentPulseOffset + step->pulseCount;
*sequenceContinues =
(index + 1U < PlsrFiniteStepCount[pulseOutput]) ? 1U : 0U;
*activeFrequencyHz = (*sequenceContinues != 0U)
? PlsrFiniteSteps[pulseOutput][index + 1U]
.setting.actualFrequencyHz
: step->setting.actualFrequencyHz;
PlsrPlatformExitCritical(criticalState);
return 1U;
}
}
*activeFrequencyHz = PlsrTimerActiveFrequencyHz[pulseOutput];
PlsrPlatformExitCritical(criticalState);
return 1U;
return 0U;
}

PLSR_PLATFORM_QUEUE_RESULT PlsrPlatformLoadPreparedFromIrq(
@@ -4671,7 +4742,8 @@ static void PlsrFiniteArmNextStepPrepare(uint8_t pulseOutput,
TIM_TypeDef *counter,
uint32_t blockPulses)
{
uint8_t nextIndex = (uint8_t)(PlsrFiniteStepIndex[pulseOutput] + 1U);
uint16_t nextIndex =
(uint16_t)(PlsrFiniteStepIndex[pulseOutput] + 1U);

counter->DIER &= ~TIM_DIER_CC1IE;
counter->SR = ~TIM_SR_CC1IF;
@@ -4694,7 +4766,8 @@ static void PlsrFiniteArmNextStepPrepare(uint8_t pulseOutput,
static void PlsrFinitePrepareNextStepIrq(uint8_t pulseOutput,
TIM_TypeDef *counter)
{
uint8_t nextIndex = (uint8_t)(PlsrFiniteStepIndex[pulseOutput] + 1U);
uint16_t nextIndex =
(uint16_t)(PlsrFiniteStepIndex[pulseOutput] + 1U);
TIM_TypeDef *timer;
const PLSR_PLATFORM_TIMER_SETTING *next;

@@ -4742,8 +4815,8 @@ static void PlsrFiniteCounterIrq(uint8_t pulseOutput,

if (PlsrFiniteStepCount[pulseOutput] != 0U)
{
uint8_t completedIndex = PlsrFiniteStepIndex[pulseOutput];
uint8_t nextIndex = (uint8_t)(completedIndex + 1U);
uint16_t completedIndex = PlsrFiniteStepIndex[pulseOutput];
uint16_t nextIndex = (uint16_t)(completedIndex + 1U);
uint8_t hasNext = (nextIndex
< PlsrFiniteStepCount[pulseOutput]) ? 1U : 0U;

@@ -4752,7 +4825,7 @@ static void PlsrFiniteCounterIrq(uint8_t pulseOutput,
PlsrObservedPulsePublished[pulseOutput] =
PlsrObservedPulseBase[pulseOutput];
PlsrFiniteCompletedStepCount[pulseOutput] =
(uint8_t)(completedIndex + 1U);
(uint16_t)(completedIndex + 1U);

if (hasNext != 0U)
{
@@ -4815,6 +4888,11 @@ static void PlsrFiniteRetargetAtFallingEdge(uint8_t pulseOutput)
}
completed = PlsrFiniteTargetPulses[pulseOutput]
- PlsrFiniteRemainingPulses[pulseOutput] + blockCount;
if (PlsrFiniteStepCount[pulseOutput] != 0U)
{
completed += PlsrFiniteSteps[pulseOutput][
PlsrFiniteStepIndex[pulseOutput]].segmentPulseOffset;
}
if (completed > PlsrFiniteTargetPulses[pulseOutput])
{
completed = PlsrFiniteTargetPulses[pulseOutput];


+ 1
- 0
tests/plsr_host/run_tests.ps1 Vedi File

@@ -19,6 +19,7 @@ $compileArguments = @(
"-I$repoRoot\PLSR\Inc"
"-I$repoRoot\PLSR\Src"
"$repoRoot\PLSR\Src\plsr.c"
"$repoRoot\PLSR\Src\plsr_planner.c"
"$repoRoot\PLSR\Src\plsr_platform_f407.c"
"$PSScriptRoot\test_plsr_host.c"
"-o"


+ 586
- 215
tests/plsr_host/test_plsr_host.c Vedi File

@@ -123,59 +123,6 @@ static uint16_t HighWord(uint32_t value)
return (uint16_t)(value >> 16U);
}

static void TestU64ByU32Division(void)
{
static const uint64_t dividends[] =
{
0ULL, 1ULL, 0xFFFFFFFFULL, 0x100000000ULL,
0x100000001ULL, 0xFFFFFFFFFFFFFFFFULL,
0x8000000000000000ULL, 0x7FFFFFFFFFFFFFFFULL
};
static const uint32_t divisors[] =
{
1UL, 2UL, 3UL, 0xFFFFUL, 0x10000UL, 100000UL,
0x7FFFFFFFUL, 0x80000000UL, 0xFFFFFFFFUL
};
uint32_t state = 0xA5C39E17UL;
uint64_t dividend;
uint64_t quotient;
uint32_t divisor;
uint32_t remainder;
uint32_t dividendIndex;
uint32_t divisorIndex;
uint32_t iteration;

for (dividendIndex = 0U;
dividendIndex < (uint32_t)(sizeof(dividends) / sizeof(dividends[0]));
dividendIndex++)
{
for (divisorIndex = 0U;
divisorIndex < (uint32_t)(sizeof(divisors) / sizeof(divisors[0]));
divisorIndex++)
{
dividend = dividends[dividendIndex];
divisor = divisors[divisorIndex];
quotient = PlsrTestDivideU64ByU32(dividend, divisor, &remainder);
EXPECT_TRUE(quotient == dividend / divisor);
EXPECT_U((uint32_t)(dividend % divisor), remainder);
}
}

for (iteration = 0U; iteration < 250000UL; iteration++)
{
state = state * 1664525UL + 1013904223UL;
dividend = (uint64_t)state << 32U;
state = state * 1664525UL + 1013904223UL;
dividend |= state;
state = state * 1664525UL + 1013904223UL;
divisor = state | 1UL;

quotient = PlsrTestDivideU64ByU32(dividend, divisor, &remainder);
EXPECT_TRUE(quotient == dividend / divisor);
EXPECT_U((uint32_t)(dividend % divisor), remainder);
}
}

static uint64_t PulsePeriodNs(uint32_t frequencyHz)
{
return (1000000000ULL + frequencyHz / 2UL) / frequencyHz;
@@ -584,16 +531,38 @@ static void TestFiniteZeroDurationRampAppliesTargetImmediately(void)
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_U(400UL, PlsrTestOutputFrequency());

PlsrTestCompleteFinitePulseTrain();
PlsrPoll1ms();
PlsrTestEmitPulses(200UL);
PlsrPoll1ms();
EXPECT_U(2U, ReadCurrentSegment());
EXPECT_U(1U, PlsrTestFinitePulseTrainActive());
EXPECT_U(900UL, PlsrTestOutputFrequency());
PlsrTestCompleteFinitePulseTrain();
PlsrTestEmitPulses(200UL);
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
}

static void TestFiniteRampUsesPulseBoundaryProfile(void)
{
ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 1U,
PLSR_SEND_COMPLETE, 2000UL, 100UL,
2000UL, 2U, 0U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 2000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_TRUE(PlsrTestOutputFrequency() > 700UL);
EXPECT_TRUE(PlsrTestOutputFrequency() < 800UL);
PlsrTestEmitPulses(1UL);
EXPECT_TRUE(PlsrTestOutputFrequency() > 1600UL);
EXPECT_TRUE(PlsrTestOutputFrequency() < 1800UL);
PlsrTestEmitPulses(1UL);
EXPECT_U(2000UL, PlsrTestOutputFrequency());
PlsrTestCompleteFinitePulseTrain();
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
}

static void TestFiniteSubsequentThreeSegmentsWithoutPoll(void)
@@ -1931,6 +1900,365 @@ static void TestAllTenSegmentsRunInSequence(void)
EXPECT_I(55L, ReadPosition());
}

static void TestCompleteTenSegmentFrequencyStaircase(void)
{
uint64_t elapsedNs = 0ULL;
uint64_t nextPollNs = 1000000ULL;
uint16_t segment;

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 10U,
PLSR_SEND_COMPLETE, 10000UL, 100UL, 100UL,
10U, 10U);
for (segment = 1U; segment <= 10U; segment++)
{
EXPECT_U(PLSR_MB_OK,
SetSegment((uint8_t)segment, (uint32_t)segment * 1000UL,
100L, PLSR_EXT_OR_COMPLETE, 0U, 0U, 0U));
}
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));

for (segment = 1U; segment <= 10U; segment++)
{
uint32_t maximumFrequencyHz = 0UL;
uint16_t trailingLowSpeedPulses = 0U;
uint16_t pulse;

while ((ReadCurrentSegment() != segment)
&& (ReadStatus() != PLSR_STATUS_ERROR))
{
PlsrPoll1ms();
elapsedNs += 1000000ULL;
nextPollNs = elapsedNs + 1000000ULL;
}
EXPECT_U(segment, ReadCurrentSegment());
for (pulse = 0U; pulse < 100U; pulse++)
{
uint32_t frequencyHz = PlsrTestOutputFrequency();

EXPECT_U(1U, PlsrTestPulseIsActive());
if (frequencyHz > maximumFrequencyHz)
{
maximumFrequencyHz = frequencyHz;
}
if (frequencyHz <= 200UL)
{
trailingLowSpeedPulses++;
}
else
{
trailingLowSpeedPulses = 0U;
}
elapsedNs += PulsePeriodNs(frequencyHz);
PlsrTestEmitPulses(1UL);
while ((pulse + 1U < 100U) && (elapsedNs >= nextPollNs))
{
PlsrPoll1ms();
nextPollNs += 1000000ULL;
}
}
if (segment < 10U)
{
EXPECT_U(1U, PlsrTestFinitePulseTrainActive());
EXPECT_U(1U, PlsrTestPulseIsActive());
}
EXPECT_TRUE(maximumFrequencyHz
>= ((uint32_t)segment * 1000UL * 99UL) / 100UL);
EXPECT_TRUE(trailingLowSpeedPulses <= 2U);
}

while ((ReadStatus() != PLSR_STATUS_COMPLETED)
&& (ReadStatus() != PLSR_STATUS_ERROR))
{
PlsrPoll1ms();
}
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_I(1000L, ReadPosition());
}

static void TestCompleteCarriesSpeedAcrossNaturalBoundaries(void)
{
uint64_t elapsedNs = 0ULL;
uint64_t nextPollNs = 1000000ULL;
uint32_t firstFrequencyHz[3];
uint32_t lastFrequencyHz[3] = {0UL, 0UL, 0UL};
uint32_t minimumFrequencyHz[3] = {
0xFFFFFFFFUL, 0xFFFFFFFFUL, 0xFFFFFFFFUL
};
uint8_t segment;

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 3U,
PLSR_SEND_COMPLETE, 3000UL, 100UL, 200UL,
10U, 10U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 1000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK,
SetSegment(2U, 2000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK,
SetSegment(3U, 3000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));

for (segment = 0U; segment < 3U; segment++)
{
uint16_t pulse;

while (((ReadCurrentSegment() != (uint32_t)segment + 1UL)
|| (PlsrTestPulseIsActive() == 0U))
&& (ReadStatus() != PLSR_STATUS_ERROR)
&& (ReadStatus() != PLSR_STATUS_COMPLETED))
{
PlsrPoll1ms();
nextPollNs = elapsedNs + 1000000ULL;
}
EXPECT_U((uint32_t)segment + 1UL, ReadCurrentSegment());
EXPECT_U(1U, PlsrTestPulseIsActive());
firstFrequencyHz[segment] = PlsrTestOutputFrequency();
for (pulse = 0U; pulse < 100U; pulse++)
{
uint32_t frequencyHz = PlsrTestOutputFrequency();

EXPECT_U(1U, PlsrTestPulseIsActive());
EXPECT_TRUE(frequencyHz != 0UL);
if (frequencyHz == 0UL)
{
break;
}
if (frequencyHz < minimumFrequencyHz[segment])
{
minimumFrequencyHz[segment] = frequencyHz;
}
lastFrequencyHz[segment] = frequencyHz;
elapsedNs += PulsePeriodNs(frequencyHz);
PlsrTestEmitPulses(1UL);
while ((pulse + 1U < 100U) && (elapsedNs >= nextPollNs))
{
PlsrPoll1ms();
nextPollNs += 1000000ULL;
}
}
}

EXPECT_TRUE(firstFrequencyHz[0] >= 400UL);
EXPECT_TRUE(firstFrequencyHz[0] <= 410UL);
EXPECT_U(2000UL, firstFrequencyHz[1]);
EXPECT_U(3000UL, firstFrequencyHz[2]);
EXPECT_U(1000UL, lastFrequencyHz[0]);
EXPECT_U(2000UL, lastFrequencyHz[1]);
EXPECT_TRUE(minimumFrequencyHz[1] >= 1900UL);
EXPECT_TRUE(lastFrequencyHz[2] <= 1500UL);
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_I(300L, ReadPosition());
}

static void TestCompleteJumpCarriesSpeedToTargetSegment(void)
{
uint64_t elapsedNs = 0ULL;
uint64_t nextPollNs = 1000000ULL;
uint16_t pulse;

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 3U,
PLSR_SEND_COMPLETE, 3000UL, 100UL, 200UL,
10U, 10U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 1000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 3U));
EXPECT_U(PLSR_MB_OK,
SetSegment(2U, 2000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK,
SetSegment(3U, 3000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_TRUE(PlsrTestOutputFrequency() >= 400UL);
EXPECT_TRUE(PlsrTestOutputFrequency() <= 410UL);

for (pulse = 0U; pulse < 100U; pulse++)
{
uint32_t frequencyHz = PlsrTestOutputFrequency();

elapsedNs += PulsePeriodNs(frequencyHz);
PlsrTestEmitPulses(1UL);
while ((pulse + 1U < 100U) && (elapsedNs >= nextPollNs))
{
PlsrPoll1ms();
nextPollNs += 1000000ULL;
}
}
while ((ReadCurrentSegment() != 3U)
&& (ReadStatus() != PLSR_STATUS_ERROR))
{
PlsrPoll1ms();
nextPollNs = elapsedNs + 1000000ULL;
}
EXPECT_U(3U, ReadCurrentSegment());
EXPECT_U(1U, PlsrTestPulseIsActive());
EXPECT_TRUE(PlsrTestOutputFrequency() >= 2900UL);

for (pulse = 0U; pulse < 100U; pulse++)
{
uint32_t frequencyHz = PlsrTestOutputFrequency();

elapsedNs += PulsePeriodNs(frequencyHz);
PlsrTestEmitPulses(1UL);
while ((pulse + 1U < 100U) && (elapsedNs >= nextPollNs))
{
PlsrPoll1ms();
nextPollNs += 1000000ULL;
}
}
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_I(200L, ReadPosition());
}

static void TestTenMillisecondBoundaryRampHasNoSlowTail(void)
{
uint64_t elapsedNs = 0ULL;
uint64_t nextPollNs = 1000000ULL;
uint32_t maximumFrequencyHz = 0UL;
uint16_t trailingLowSpeedPulses = 0U;
uint16_t pulse;

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 1U,
PLSR_SEND_COMPLETE, 10000UL, 100UL, 100UL,
10U, 10U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 10000UL, 1000L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));

for (pulse = 0U; pulse < 1000U; pulse++)
{
uint32_t frequencyHz = PlsrTestOutputFrequency();

EXPECT_U(1U, PlsrTestPulseIsActive());
if (frequencyHz > maximumFrequencyHz)
{
maximumFrequencyHz = frequencyHz;
}
if (frequencyHz <= 200UL)
{
trailingLowSpeedPulses++;
}
else
{
trailingLowSpeedPulses = 0U;
}
elapsedNs += PulsePeriodNs(frequencyHz);
PlsrTestEmitPulses(1UL);
while ((pulse + 1U < 1000U) && (elapsedNs >= nextPollNs))
{
PlsrPoll1ms();
nextPollNs += 1000000ULL;
}
}

EXPECT_TRUE(maximumFrequencyHz >= 9900UL);
EXPECT_TRUE(trailingLowSpeedPulses <= 2U);
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_I(1000L, ReadPosition());
}

static void TestFiniteShortSingleSegmentUsesPulseProfile(void)
{
uint32_t samples[100];
uint64_t durationNs = 0ULL;
uint16_t pulse;

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 1U,
PLSR_SEND_COMPLETE, 10000UL, 100UL, 100UL,
10U, 10U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 1000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_U(0U, PlsrTestProfileQueueCount());
EXPECT_U(1U, PlsrTestFinitePulseTrainActive());

for (pulse = 0U; pulse < 100U; pulse++)
{
samples[pulse] = PlsrTestOutputFrequency();
durationNs += PulsePeriodNs(samples[pulse]);
EXPECT_U(1U, PlsrTestPulseIsActive());
PlsrTestEmitPulses(1UL);
}

EXPECT_TRUE((samples[0] >= 549UL) && (samples[0] <= 551UL));
EXPECT_TRUE((samples[99] >= 549UL) && (samples[99] <= 551UL));
for (pulse = 1U; pulse < 99U; pulse++)
{
EXPECT_U(1000UL, samples[pulse]);
}
EXPECT_TRUE(durationNs >= 101600000ULL);
EXPECT_TRUE(durationNs <= 101700000ULL);
EXPECT_U(0U, PlsrTestPulseIsActive());
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_I(100L, ReadPosition());
}

static void TestFiniteProfileThreeSegmentsRunsWithoutPollGap(void)
{
uint32_t samples[300];
uint16_t pulse;

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 3U,
PLSR_SEND_COMPLETE, 10000UL, 100UL, 100UL,
10U, 10U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 1000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK,
SetSegment(2U, 2000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK,
SetSegment(3U, 3000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));

for (pulse = 0U; pulse < 300U; pulse++)
{
samples[pulse] = PlsrTestOutputFrequency();
EXPECT_U(1U, PlsrTestFinitePulseTrainActive());
PlsrTestEmitPulses(1UL);
}

EXPECT_TRUE((samples[0] >= 549UL) && (samples[0] <= 551UL));
EXPECT_U(1000UL, samples[1]);
EXPECT_U(1000UL, samples[99]);
EXPECT_U(2000UL, samples[100]);
EXPECT_U(2000UL, samples[199]);
EXPECT_U(3000UL, samples[200]);
EXPECT_TRUE(samples[299] < samples[298]);
EXPECT_TRUE(samples[298] < samples[297]);
EXPECT_U(0U, PlsrTestFinitePulseTrainActive());
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_I(300L, ReadPosition());
}

static void TestAllTenSegmentsRunThroughJumpChain(void)
{
static const uint8_t jumps[10] =
@@ -2326,7 +2654,8 @@ static void ExpectExtBoundaryCarry(void)
PlsrPoll1ms();
EXPECT_U(1U, PlsrTestPulseIsActive());
EXPECT_U(2U, ReadCurrentSegment());
EXPECT_U(2000UL, PlsrTestOutputFrequency());
EXPECT_TRUE(PlsrTestOutputFrequency() >= 1999UL);
EXPECT_TRUE(PlsrTestOutputFrequency() <= 2001UL);
EXPECT_I(1L, ReadPosition());
}

@@ -2690,6 +3019,39 @@ static void TestZeroRampTimesIgnoreStartAndStopSpeeds(void)
}
}

static void TestZeroSegmentFrequencyUsesDefaultSpeed(void)
{
uint32_t explicitSamples[1000];
uint32_t defaultSamples[1000];
uint16_t pulse;

CaptureShortProfileFrequencies(0U, 1000U,
100UL, 100UL, 100U, 100U,
explicitSamples);

ResetCore();
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 1U,
PLSR_SEND_COMPLETE, 100000UL,
100UL, 100UL, 100U, 100U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 0UL, 1000L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(0UL, ReadU32(0x1100U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
for (pulse = 0U; pulse < 1000U; pulse++)
{
defaultSamples[pulse] = PlsrTestOutputFrequency();
PlsrTestEmitPulses(1UL);
}
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_I(1000L, ReadPosition());
for (pulse = 0U; pulse < 1000U; pulse++)
{
EXPECT_U(explicitSamples[pulse], defaultSamples[pulse]);
}
}

static void TestShortProfile258IsFullyPrecomputed(void)
{
uint16_t curveMode;
@@ -2772,131 +3134,6 @@ static void TestShortFinalDeceleratesWithoutPoll(void)
}
}

static void TestZeroToMaximumRampKeepsConfiguredDuration(void)
{
uint16_t curveMode;
uint32_t firstFrequencyHz[3];
uint32_t frequencyHz;
uint32_t pulseAccumulator;
uint32_t pulsesThisMillisecond;
uint32_t emittedPulses;
uint32_t tick;
const uint32_t pulseCount = 3000UL;

for (curveMode = 0U; curveMode <= 2U; curveMode++)
{
ResetCore();
ConfigureCommon(curveMode, PLSR_POSITION_RELATIVE, 1U,
PLSR_SEND_COMPLETE, 100000UL, 0UL, 100000UL,
20U, 0U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 100000UL, (int32_t)pulseCount,
PLSR_EXT_OR_COMPLETE, 0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_U(0U, PlsrTestPulseIsActive());

pulseAccumulator = 0UL;
emittedPulses = 0UL;
for (tick = 1UL; tick <= 20UL; tick++)
{
PlsrPoll1ms();
EXPECT_U(1U, PlsrTestPulseIsActive());
frequencyHz = PlsrTestOutputFrequency();
EXPECT_TRUE(frequencyHz != 0UL);
EXPECT_TRUE(frequencyHz <= 100000UL);
if (tick == 1UL)
{
firstFrequencyHz[curveMode] = frequencyHz;
}
if (tick < 20UL)
{
EXPECT_U(PLSR_STATUS_ACCELERATING, ReadStatus());
}
else
{
EXPECT_U(PLSR_STATUS_RUNNING, ReadStatus());
}

pulseAccumulator += frequencyHz;
pulsesThisMillisecond = pulseAccumulator / 1000UL;
pulseAccumulator %= 1000UL;
PlsrTestEmitPulses(pulsesThisMillisecond);
emittedPulses += pulsesThisMillisecond;
}

EXPECT_TRUE(emittedPulses < pulseCount);
PlsrTestEmitPulses(pulseCount - emittedPulses);
EXPECT_U(0U, PlsrTestPulseIsActive());
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_I((long)pulseCount, ReadPosition());
ExpectCompletedDiagnostic(pulseCount);
}
EXPECT_TRUE(firstFrequencyHz[0] > firstFrequencyHz[1]);
EXPECT_TRUE(firstFrequencyHz[1] > firstFrequencyHz[2]);
}

typedef struct
{
uint32_t emittedPulses;
uint32_t elapsedMs;
uint32_t maximumFrequencyHz;
uint32_t finalFrequencyHz;
} REAL_TIME_PROFILE_STATS;

static void RunProfileAtOneMillisecondCadence(
uint32_t pulseCount,
REAL_TIME_PROFILE_STATS *stats)
{
uint32_t pulseAccumulator = 0UL;
uint32_t frequencyHz;
uint32_t pulsesThisMillisecond;
uint32_t remainingPulses;

stats->emittedPulses = 0UL;
stats->elapsedMs = 0UL;
stats->maximumFrequencyHz = 0UL;
stats->finalFrequencyHz = 0UL;

while ((stats->emittedPulses < pulseCount)
&& (PlsrTestPulseIsActive() != 0U)
&& (stats->elapsedMs < 2000UL))
{
PlsrPoll1ms();
stats->elapsedMs++;
if (PlsrTestPulseIsActive() == 0U)
{
break;
}

frequencyHz = PlsrTestOutputFrequency();
EXPECT_TRUE(frequencyHz != 0UL);
if (frequencyHz > stats->maximumFrequencyHz)
{
stats->maximumFrequencyHz = frequencyHz;
}
stats->finalFrequencyHz = frequencyHz;
pulseAccumulator += frequencyHz;
pulsesThisMillisecond = pulseAccumulator / 1000UL;
pulseAccumulator %= 1000UL;
remainingPulses = pulseCount - stats->emittedPulses;
if (pulsesThisMillisecond > remainingPulses)
{
pulsesThisMillisecond = remainingPulses;
}
PlsrTestEmitPulses(pulsesThisMillisecond);
stats->emittedPulses += pulsesThisMillisecond;
}

PlsrPoll1ms();
EXPECT_U(pulseCount, stats->emittedPulses);
EXPECT_U(0U, PlsrTestPulseIsActive());
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_I((long)pulseCount, ReadPosition());
}

static void ExpectCompletedDiagnostic(uint32_t pulseCount)
{
uint16_t flags = ReadWord(PLSR_DIAG_FLAGS_ADDRESS);
@@ -3013,8 +3250,8 @@ static void TestFiniteCompleteTenSegmentsWithoutPoll(void)
ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 10U,
PLSR_SEND_COMPLETE, 1000UL, 1000UL, 1000UL,
0U, 0U);
PLSR_SEND_COMPLETE, 10000UL, 100UL, 100UL,
10U, 10U);
for (segment = 1U; segment <= 10U; segment++)
{
EXPECT_U(PLSR_MB_OK,
@@ -3037,32 +3274,6 @@ static void TestFiniteCompleteTenSegmentsWithoutPoll(void)
EXPECT_I(1045L, ReadPosition());
}

static void TestMaximumProfileOneMillisecondCadence(void)
{
REAL_TIME_PROFILE_STATS stats;
uint16_t curveMode;

for (curveMode = 0U; curveMode <= 2U; curveMode++)
{
ResetCore();
ConfigureCommon(curveMode, PLSR_POSITION_RELATIVE, 1U,
PLSR_SEND_COMPLETE, 100000UL, 1000UL, 1000UL,
100U, 100U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 100000UL, 65535L,
PLSR_EXT_OR_COMPLETE, 0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_U(0U, PlsrTestProfileQueueCount());
EXPECT_U(0UL, PlsrTestProfileQueueReadIndex());
EXPECT_U(0UL, PlsrTestProfileQueueWriteIndex());

RunProfileAtOneMillisecondCadence(65535UL, &stats);
EXPECT_TRUE(stats.maximumFrequencyHz >= 99990UL);
EXPECT_U(1000UL, stats.finalFrequencyHz);
ExpectCompletedDiagnostic(65535UL);
}
}

static void TestShortProfileTimerFailure(void)
{
ResetCore();
@@ -3237,6 +3448,128 @@ static void TestShortProfileExtCut(void)
EXPECT_TRUE(frequencyBeforeCut > 100UL);
}

static void TestFiniteProfileExtCut(void)
{
ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 1U, PLSR_SEND_COMPLETE,
5000UL, 500UL, 100UL, 100U, 100U);
PlsrTestSetInput(0U, 0U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 5000UL, 1000L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_U(1U, PlsrTestFinitePulseTrainActive());

PlsrTestEmitPulses(10UL);
PlsrPoll1ms();
EXPECT_I(10L, ReadPosition());
PlsrTestSetInput(0U, 1U);
PlsrPoll1ms();

/* The already committed terminal pulse is allowed to drain. */
PlsrTestEmitPulses(1UL);
EXPECT_U(0U, PlsrTestFinitePulseTrainActive());
EXPECT_U(0U, PlsrTestPulseIsActive());
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_COMPLETED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_I(11L, ReadPosition());
}

static void TestFiniteProfileStopOwnsTimer(void)
{
unsigned int pulse;
uint32_t previousFrequency;

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 1U, PLSR_SEND_COMPLETE,
5000UL, 500UL, 100UL, 100U, 100U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 5000UL, 1000L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_U(1U, PlsrTestFinitePulseTrainActive());
PlsrTestEmitPulses(20UL);
PlsrPoll1ms();

EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_STOP));
EXPECT_U(PLSR_STATUS_DECELERATING, ReadStatus());

/* Allow the active and shadow settings to drain at the cutover. */
PlsrTestEmitPulses(2UL);
PlsrPoll1ms();
previousFrequency = PlsrTestOutputFrequency();
for (pulse = 0U;
(pulse < 200U) && (PlsrTestPulseIsActive() != 0U);
pulse++)
{
uint32_t currentFrequency;

PlsrPoll1ms();
PlsrTestEmitPulses(1UL);
currentFrequency = PlsrTestOutputFrequency();
if (currentFrequency != 0UL)
{
EXPECT_TRUE(currentFrequency <= previousFrequency);
previousFrequency = currentFrequency;
}
}
PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_STOPPED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
EXPECT_TRUE(ReadPosition() < 1000L);
}

static void TestFiniteProfileDynamicRetargetOwnsTimer(void)
{
unsigned int pulse;
uint32_t previousFrequency;

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 1U, PLSR_SEND_COMPLETE,
5000UL, 500UL, 100UL, 100U, 100U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 5000UL, 1000L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_U(1U, PlsrTestFinitePulseTrainActive());
PlsrTestEmitPulses(20UL);
PlsrPoll1ms();

EXPECT_U(PLSR_MB_OK, WriteU32(0x1100U, 1000UL));
PlsrPoll1ms();
PlsrTestEmitPulses(2UL);
PlsrPoll1ms();
previousFrequency = PlsrTestOutputFrequency();
for (pulse = 0U; pulse < 40U; pulse++)
{
uint32_t currentFrequency;

PlsrPoll1ms();
PlsrTestEmitPulses(1UL);
currentFrequency = PlsrTestOutputFrequency();
EXPECT_TRUE(currentFrequency <= previousFrequency);
previousFrequency = currentFrequency;
}
EXPECT_TRUE(previousFrequency >= 1000UL);
EXPECT_TRUE(previousFrequency < 5000UL);
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_STOP));
for (pulse = 0U;
(pulse < 300U) && (PlsrTestPulseIsActive() != 0U);
pulse++)
{
PlsrPoll1ms();
PlsrTestEmitPulses(1UL);
}
PlsrPoll1ms();
EXPECT_U(0U, PlsrTestPulseIsActive());
EXPECT_U(PLSR_STATUS_STOPPED, ReadStatus());
EXPECT_U(PLSR_ERROR_NONE, ReadError());
}

static void TestCurrentFrequencyIsDynamic(void)
{
ResetCore();
@@ -3263,6 +3596,26 @@ static void TestCurrentFrequencyIsDynamic(void)
StopAndSettle();
}

static void TestDynamicZeroFrequencyUsesDefaultSpeed(void)
{
ResetCore();
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 1U, PLSR_SEND_COMPLETE,
4321UL, 1000UL, 100UL, 0U, 0U);
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 1000UL, 100L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_U(1000UL, PlsrTestOutputFrequency());

EXPECT_U(PLSR_MB_OK, WriteU32(0x1100U, 0UL));
EXPECT_U(0UL, ReadU32(0x1100U));
PlsrPoll1ms();
PlsrTestEmitPulses(2UL);
EXPECT_U(4321UL, PlsrTestOutputFrequency());
EXPECT_U(4321UL, ReadFrequency());
StopAndSettle();
}

static void StartMaximumShortProfile(void)
{
ResetCore();
@@ -4012,6 +4365,8 @@ static const TEST_CASE TestCases[] =
TestFiniteCompleteStartsAtConfiguredSegment},
{"finite_zero_duration_ramp_target",
TestFiniteZeroDurationRampAppliesTargetImmediately},
{"finite_ramp_uses_pulse_boundary_profile",
TestFiniteRampUsesPulseBoundaryProfile},
{"finite_subsequent_three_segments_no_poll",
TestFiniteSubsequentThreeSegmentsWithoutPoll},
{"finite_subsequent_ten_segment_jump_chain",
@@ -4022,7 +4377,6 @@ static const TEST_CASE TestCases[] =
TestFiniteSubsequentDirectionReversalRestarts},
{"finite_constant_pulse_train_boundaries",
TestFiniteConstantPulseTrainBoundaries},
{"u64_by_u32_division", TestU64ByU32Division},
{"protocol_boundaries", TestProtocolBoundaries},
{"ab_output_pairs_phase_sequence", TestAbOutputPairsAndPhaseSequence},
{"ab_single_pulse_100khz_matrix",
@@ -4071,6 +4425,18 @@ static const TEST_CASE TestCases[] =
{"act_zero_no_pulse", TestActZeroSkipsWithoutPulse},
{"relative_absolute_zero", TestRelativeAndAbsoluteZeroDisplacement},
{"ten_segments_sequence", TestAllTenSegmentsRunInSequence},
{"complete_ten_segment_frequency_staircase",
TestCompleteTenSegmentFrequencyStaircase},
{"complete_natural_boundaries_carry_speed",
TestCompleteCarriesSpeedAcrossNaturalBoundaries},
{"complete_jump_carries_speed",
TestCompleteJumpCarriesSpeedToTargetSegment},
{"ten_ms_boundary_ramp_no_slow_tail",
TestTenMillisecondBoundaryRampHasNoSlowTail},
{"finite_short_single_segment_pulse_profile",
TestFiniteShortSingleSegmentUsesPulseProfile},
{"finite_profile_three_segments_no_poll_gap",
TestFiniteProfileThreeSegmentsRunsWithoutPollGap},
{"ten_segments_jump_chain", TestAllTenSegmentsRunThroughJumpChain},
{"start_segment_ten", TestStartSegmentTenRunsNormally},
{"self_jump_stop", TestSelfJumpCanStop},
@@ -4090,16 +4456,14 @@ static const TEST_CASE TestCases[] =
{"short_profile_boundary_matrix", TestShortProfileBoundaryMatrix},
{"zero_ramp_times_ignore_edge_speeds",
TestZeroRampTimesIgnoreStartAndStopSpeeds},
{"zero_segment_frequency_uses_default_speed",
TestZeroSegmentFrequencyUsesDefaultSpeed},
{"short_profile_258_fully_precomputed",
TestShortProfile258IsFullyPrecomputed},
{"short_final_no_poll", TestShortFinalDeceleratesWithoutPoll},
{"zero_to_maximum_ramp_duration",
TestZeroToMaximumRampKeepsConfiguredDuration},
{"bug_report_short_profiles", TestBugReportShortProfiles},
{"finite_complete_ten_segments_no_poll",
TestFiniteCompleteTenSegmentsWithoutPoll},
{"maximum_profile_1ms_cadence",
TestMaximumProfileOneMillisecondCadence},
{"profile_queue_stale_no_advance", TestProfileQueueStaleDoesNotAdvance},
{"profile_queue_retarget_generation",
TestProfileQueueRetargetInvalidatesGeneration},
@@ -4111,7 +4475,13 @@ static const TEST_CASE TestCases[] =
{"short_zero_speed_edges", TestShortProfileZeroSpeedEdges},
{"short_dynamic_retarget", TestShortProfileDynamicRetarget},
{"short_ext_cut", TestShortProfileExtCut},
{"finite_ext_cut", TestFiniteProfileExtCut},
{"finite_stop_single_timer_owner", TestFiniteProfileStopOwnsTimer},
{"finite_dynamic_single_timer_owner",
TestFiniteProfileDynamicRetargetOwnsTimer},
{"current_frequency_dynamic", TestCurrentFrequencyIsDynamic},
{"dynamic_zero_frequency_uses_default_speed",
TestDynamicZeroFrequencyUsesDefaultSpeed},
{"deferred_frequency_retries_after_stale",
TestDeferredFrequencyRetriesAfterStale},
{"short_profile_retries_same_sample_after_stale",
@@ -4174,3 +4544,4 @@ int main(void)
AssertionCount, FailureCount);
return (FailureCount == 0U) ? 0 : 1;
}


Caricamento…
Annulla
Salva