From 8d678f99d9e1225b32eaee3ad001568ef7880b5b Mon Sep 17 00:00:00 2001 From: ywh <2227158009@qq.com> Date: Fri, 14 Aug 2026 22:16:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E4=BA=86=E4=B8=80=E4=BA=9Bbug?= =?UTF-8?q?=EF=BC=8C=E5=A4=9A=E6=AE=B5=E5=8D=95=E6=AE=B5=E5=9F=BA=E6=9C=AC?= =?UTF-8?q?=E6=B2=A1=E9=97=AE=E9=A2=98=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../PLSR_5段理论频率包络.svg | 114 + ...R测试用例与验收标准_2026-08-13.md | 24 +- ...测试用例与验收标准_2026-08-14.md | 1093 ++++++++++ ...026-08-14_完成模式10段阶梯频率.md | 250 +++ .../PLSR测试报告_2026-08-12.md | 294 +++ ...构设计书_加减速与多段统一.md | 388 ++++ Document/PLSR_document/波形/111.bin | Bin 17511776 -> 11486896 bytes EWARM/Modbus.ewp | 6 + .../bin_to_time_freq.cpython-314.pyc | Bin 0 -> 16966 bytes .../plsr_modbus_product_test.cpython-314.pyc | Bin 135478 -> 136832 bytes .../__pycache__/wave_viewer.cpython-314.pyc | Bin 0 -> 19137 bytes HostComputer/bin_to_time_freq.py | 271 +++ HostComputer/plsr_control_panel.py | 10 +- HostComputer/plsr_modbus_product_test.py | 44 +- HostComputer/wave_viewer.py | 289 +++ PLSR/Inc/plsr.h | 5 - PLSR/Src/plsr.c | 1918 ++++------------- PLSR/Src/plsr_internal.h | 10 + PLSR/Src/plsr_planner.c | 735 +++++++ PLSR/Src/plsr_planner.h | 89 + PLSR/Src/plsr_platform.h | 14 +- PLSR/Src/plsr_platform_f407.c | 386 ++-- tests/plsr_host/run_tests.ps1 | 1 + tests/plsr_host/test_plsr_host.c | 801 +++++-- 25 files changed, 4849 insertions(+), 1895 deletions(-) create mode 100644 Document/PLSR_document/PLSR_5段理论频率包络.svg create mode 100644 Document/PLSR_document/PLSR多段测试用例与验收标准_2026-08-14.md create mode 100644 Document/PLSR_document/PLSR波形验收报告_2026-08-14_完成模式10段阶梯频率.md create mode 100644 Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md create mode 100644 HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc create mode 100644 HostComputer/__pycache__/wave_viewer.cpython-314.pyc create mode 100644 HostComputer/bin_to_time_freq.py create mode 100644 HostComputer/wave_viewer.py create mode 100644 PLSR/Src/plsr_planner.c create mode 100644 PLSR/Src/plsr_planner.h diff --git a/.gitignore b/.gitignore index adf6b05..3e1eceb 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ Thumbs.db Desktop.ini tmp/ .codex-tmp/ +Document/PLSR_document/波形/ +Document/PLSR_document/上位机图片/ diff --git a/Document/PLSR_document/PLSR_5段理论频率包络.svg b/Document/PLSR_document/PLSR_5段理论频率包络.svg new file mode 100644 index 0000000..99049ee --- /dev/null +++ b/Document/PLSR_document/PLSR_5段理论频率包络.svg @@ -0,0 +1,114 @@ + + + + + 5 段 PLSR 理论频率包络(横轴:累计脉冲数) + 等待条件均为 4;同向连续执行;理想连续值,实际定时器频率会有少量量化 + + + A. 满足脉冲数、加减速度、最终停止速度的全轨迹规划 + + + + + + + + + + + + + + + + + + + + + + + + + + 段 4 提前制动,段 5 完成最终减速 + + 段1 + 段2 + 段3 + 段4 + 段5 + ≈4473 Hz + 100 Hz + + + 0 + 2000 + 4000 + 6000 + 8000 + 10000 Hz + + + 0 + 1000 + 4000 + 9000 + 15000 + 16000 + + + + B. 若强制段 4 以 8000 Hz 结束:段 5 距离不足,只能高速切断 + + + + + + + + + + + + + + + + 段 5:8000 → 约6633 Hz → Gate + ≈6633 Hz + 第16000脉冲后高速切断 + + 0 + 2000 + 4000 + 6000 + 8000 + 10000 Hz + + + 注意:图示是频率包络,不是 Y0 的高低电平方波;正常连续段边界不应出现额外低电平间隔。 + diff --git a/Document/PLSR_document/PLSR_PULSE_DIR测试用例与验收标准_2026-08-13.md b/Document/PLSR_document/PLSR_PULSE_DIR测试用例与验收标准_2026-08-13.md index b05a01d..d78ea4f 100644 --- a/Document/PLSR_document/PLSR_PULSE_DIR测试用例与验收标准_2026-08-13.md +++ b/Document/PLSR_document/PLSR_PULSE_DIR测试用例与验收标准_2026-08-13.md @@ -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,并在备注填写问题编号和复现条件。 diff --git a/Document/PLSR_document/PLSR多段测试用例与验收标准_2026-08-14.md b/Document/PLSR_document/PLSR多段测试用例与验收标准_2026-08-14.md new file mode 100644 index 0000000..439af4a --- /dev/null +++ b/Document/PLSR_document/PLSR多段测试用例与验收标准_2026-08-14.md @@ -0,0 +1,1093 @@ +# PLSR 多段测试用例与验收标准 + +> 文档日期:2026-08-14 +> 适用分支:`codex/plsr-2026-minimal` +> 测试方法:上位机 `plsr_control_panel.py`(串口 COM5 / 9600 8E1 / 从站 1),波形类指标需逻辑分析仪 +> 范围:多段路径(段数 ≥ 2)的功能与验收;单段用例见《PLSR_PULSE_DIR测试用例与验收标准_2026-08-13.md》 +> 判定口径:上位机“实时状态”区(位置/频率/状态/当前段/错误码)为通过依据;“输出诊断”区(0x2100)为内部一致性佐证;标注“逻辑分析仪”的指标在未接线时记“未测”,不视为失败 + +## 1. 通用操作步骤(每个用例均按此执行) + +1. 连接上位机:串口选择 `COM5`,从站地址 `1`,点击连接,确认右上角显示 `COM5 / 从站 1`。 +2. 在“公共参数 0x1000-0x1013”页按用例的**公共参数表**填写,点击 `写入公共参数`。 +3. 在“10 段参数 0x1100-0x1197”页按用例的**段参数表**填写各段,点击 `写入 10 段参数`。 +4. 点击 `读取全部参数`,确认回读值与写入值一致。 +5. 点击 `清零`,确认累计位置为 0、状态为空闲 (1)。 +6. 点击 `启动`,观察“实时状态”区:状态、当前段、当前频率、累计位置的变化过程。 +7. 等待状态进入 `完成 (7)` / `停止 (8)` / `错误 (9)`,记录最终位置、当前段、错误码。 +8. 到“输出诊断 0x2100-0x2119”页记录期望脉冲数、实测脉冲数、计数误差和诊断标志。 + +## 2. 通用验收指标(未单独列出的用例必须满足) + +| 项目 | 验收指标 | +|---|---| +| 最终状态 | `完成 (7)`(STOP 用例为 `停止 (8)`) | +| 错误码 | `0` | +| 累计位置 | 等于各段有符号位移之和;绝对模式按目标位置判定 | +| 内部诊断 | 末段期望脉冲 = 该段脉冲绝对值,实测 = 期望,计数误差 `0`,诊断计数“通过” | +| 通信质量 | 无超时、CRC 错误或非预期 Modbus 异常 | + +## 3. 公共参数填写说明(上位机界面字段 → 用例取值) + +下表为公共参数页的全部字段与取值含义,各用例的公共参数表按此顺序给出具体值: + +| # | 界面字段 | 取值含义 | +|---|---|---| +| 1 | 脉冲输出 (单逻辑 PLSR) | `0`=Y0、`1`=Y1、`2`=Y2、`3`=Y3 | +| 2 | 方向输出 | `0`=Y12、`1`=Y13、`2`=Y14、`3`=Y15 | +| 3 | 方向延时 ms | 方向建立延时,`0..`;AB 模式忽略 | +| 4 | WAIT 输入 | `0`=X4、`1`=X5 | +| 5 | EXT 输入 | `0`=X4、`1`=X5 | +| 6 | 发送模式 | `0`=脉冲发送完毕(完成模式)、`1`=后续发送(后续模式) | +| 7 | 方向逻辑 | `0`=正逻辑、`1`=负逻辑 | +| 8 | 曲线模式 | `0`=直线、`1`=平滑、`2`=正弦 | +| 9 | 位置模式 | `0`=相对位置、`1`=绝对位置 | +| 10 | 段数 | `1..10` | +| 11 | 起始段 | `1..10`,启动时不得大于段数 | +| 12 | 默认速度 Hz | `1..100000` | +| 13 | 启动速度 Hz | `0..100000` | +| 14 | 停止速度 Hz | `0..100000` | +| 15 | 加速时间 ms | `0..` | +| 16 | 减速时间 ms | `0..` | +| 17 | 输出模式 0x1200 | `0`=PULSE/DIR、`1`=AB 正交 | + +--- + +## 4. P0 必测用例 + +### TC-MS-001 完成模式 10 段阶梯频率连续执行 + +- 优先级:`P0` +- 方法:上位机(可选:逻辑分析仪测段间波形) + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | 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 | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数: + +| 段 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 频率 Hz | 1000 | 2000 | 3000 | 4000 | 5000 | 6000 | 7000 | 8000 | 9000 | 10000 | +| 脉冲数 | 100 | 100 | 100 | 100 | 100 | 100 | 100 | 100 | 100 | 100 | +| 等待类型 | 外部或完成 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 段顺序 | 依次经过 1、2、3、…、10 | 1 | | | +| 各段频率 | 执行中当前频率依次约为 1000→2000→…→10000 Hz(允许 ±1 Hz 量化差) | 1 | | | +| 无漏段/重复段 | 当前段单调递增,无跳变、回退 | 1 | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | 1 | | | +| 最终位置 | `1000` | 1 | | | +| 末段内部诊断 | 期望 100、实测 100、误差 0 | 1 | | | +| 段间间隙(逻辑分析仪) | 完成模式下段间无脉冲时间 ≤ 2 ms | 1 | | | +| 段间无额外脉冲 | 段边界处无窄脉冲、无额外边沿 | 1 | | | + +### TC-MS-002 后续模式 3 段无缝衔接 + +- 优先级:`P0` +- 方法:上位机 + 逻辑分析仪(重点验证段间不停轴) + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | **后续发送 (1)** | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 3 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数: + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | 1000 | 4000 | 2000 | +| 脉冲数 | 1000 | 2000 | 3000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 段顺序 | 1 → 2 → 3 | | | | +| 衔接不停轴 | 段边界处输出无中断(后续模式特性) | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `6000` | | | | +| 外部总上升沿(逻辑分析仪) | `6000`,无漏脉冲、无额外脉冲 | | | | +| 段边界波形(逻辑分析仪) | 频率从 1000 Hz 平滑过渡到 4000 Hz,无毛刺、无窄脉冲 | | | | +| 末段内部诊断 | 期望 3000、实测 3000、误差 0 | | | | + +### TC-MS-003 混合方向多段(正向/反向交替) + +- 优先级:`P0` +- 方法:上位机(可选:逻辑分析仪测方向电平) + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | **10** | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 3 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数: + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | 2000 | 2000 | 2000 | +| 脉冲数 | **+1000** | **−500** | **+200** | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 段顺序 | 1 → 2 → 3 | | | | +| 方向切换 | 段 2 期间方向输出与段 1 相反;段 3 恢复与段 1 相同 | | | | +| 方向延时 | 段 1→2、2→3 边界均有约 `10 ms ± 2 ms` 方向建立延时 | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `1000 − 500 + 200 = 700` | | | | +| 内部诊断(末段) | 期望 200、实测 200、误差 0 | | | | +| 方向电平稳定(逻辑分析仪) | 发波期间方向电平无抖动、无毛刺 | | | | + +### TC-MS-004 非顺序跳转链(1→3→5→7→9) + +- 优先级:`P0` +- 方法:上位机 + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 9 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数(偶数段 2/4/6/8 保留脉冲数但**不应执行**): + +| 段 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 频率 Hz | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 | +| 脉冲数 | 100 | 999 | 300 | 999 | 500 | 999 | 700 | 999 | 900 | +| 等待类型 | 外部或完成 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| 跳转段 | **3** | 0 | **5** | 0 | **7** | 0 | **9** | 0 | **0** | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 执行顺序 | 1 → 3 → 5 → 7 → 9 | | | | +| 偶数段 | 第 2/4/6/8 段未执行(当前段从不出现这些值) | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `100+300+500+700+900 = 2500` | | | | +| 末段内部诊断 | 期望 900、实测 900、误差 0 | | | | + +### TC-MS-005 段频率为 0 时回退默认速度(多段混合) + +- 优先级:`P0`(对应 2026-08-13 需求明确版“频率 0 用默认速度”) +- 方法:上位机 + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 3 | +| 起始段 | 1 | +| 默认速度 Hz | **5000** | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数(段 1、段 3 频率填 `0`,应回退到默认速度): + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | **0** | 2000 | **0** | +| 脉冲数 | 1000 | 1000 | 1000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 写入接受 | 段 1/3 频率 0 写入成功、回读为 0(不返回非法值) | | | | +| 段 1 频率 | 执行段 1 时当前频率约为 `5000 Hz`(默认速度,±1 Hz) | | | | +| 段 2 频率 | 执行段 2 时当前频率约为 `2000 Hz` | | | | +| 段 3 频率 | 执行段 3 时当前频率恢复约为 `5000 Hz` | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `3000` | | | | +| 末段内部诊断 | 期望 1000、实测 1000、误差 0 | | | | + +--- + +## 5. P1 完整功能用例 + +### TC-MS-101 段间 WAIT 时间等待 + +- 优先级:`P1` +- 方法:上位机 + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 2 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数: + +| 段 | 1 | 2 | +|---|---:|---:| +| 频率 Hz | 1000 | 2000 | +| 脉冲数 | 100 | 200 | +| 等待类型 | **定时等待 0** | 外部或完成 4 | +| WAIT ms | **300** | 0 | +| ACT ms | 0 | 0 | +| 跳转段 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 进入等待 | 段 1 完成后状态变为 `等待 (5)`,当前段仍为 1 | | | | +| 等待时长 | `300 ms ± 30 ms` 后自动进入段 2 | | | | +| 等待期间位置 | 保持 `100` 不变 | | | | +| 等待期间输出 | 无新增脉冲(当前频率为 0) | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `300` | | | | + +### TC-MS-102 段间 WAIT 信号(X4) + +- 优先级:`P1` +- 方法:上位机 + 可控输入(Y4→X4 回环或手动给 X4) + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 2 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数: + +| 段 | 1 | 2 | +|---|---:|---:| +| 频率 Hz | 1000 | 2000 | +| 脉冲数 | 100 | 200 | +| 等待类型 | **信号等待 1** | 外部或完成 4 | +| WAIT ms | 0 | 0 | +| ACT ms | 0 | 0 | +| 跳转段 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 进入等待 | 段 1 完成后状态为 `等待 (5)`,位置 100 | | | | +| X4=OFF 保持 | X4 低电平期间持续等待,不误启动段 2 | | | | +| X4=ON 后继续 | X4 置 ON 后 1 ms 内(下个轮询周期)进入段 2 | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `300` | | | | + +### TC-MS-103 段间 ACT 时间切断并继续 + +- 优先级:`P1` +- 方法:上位机 + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 2 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数: + +| 段 | 1 | 2 | +|---|---:|---:| +| 频率 Hz | 10000 | 2000 | +| 脉冲数 | 100000 | 2000 | +| 等待类型 | **定时动作 2** | 外部或完成 4 | +| WAIT ms | 0 | 0 | +| ACT ms | **100** | 0 | +| 跳转段 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 段 1 切断 | 启动约 `100 ms ± 30 ms` 后段 1 被切断 | | | | +| 切断位置 | `0 < 段 1 实际脉冲 < 100000` | | | | +| 进入段 2 | 切断后进入段 2,段 2 完整执行 2000 脉冲 | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `段1实际 + 2000`(记录实测值) | | | | +| 末段内部诊断 | 期望 2000、实测 2000、误差 0 | | | | + +### TC-MS-104 段间 EXT 信号切断并继续(X4) + +- 优先级:`P1` +- 方法:上位机 + 可控输入(Y4→X4 回环或手动给 X4) + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 2 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数: + +| 段 | 1 | 2 | +|---|---:|---:| +| 频率 Hz | 10000 | 2000 | +| 脉冲数 | 100000 | 2000 | +| 等待类型 | **外部信号 3** | 外部或完成 4 | +| WAIT ms | 0 | 0 | +| ACT ms | 0 | 0 | +| 跳转段 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 段 1 切断 | 发波过程中 X4 置 ON 后,段 1 在下一个脉冲边界被切断 | | | | +| 切断位置 | `0 < 段 1 实际脉冲 < 100000` | | | | +| 进入段 2 | 切断后进入段 2,段 2 完整执行 2000 脉冲 | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `段1实际 + 2000`(记录实测值) | | | | + +### TC-MS-105 起始段为第 5 段 + +- 优先级:`P1` +- 方法:上位机 + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 10 | +| 起始段 | **5** | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数(第 1~4 段设 1000 脉冲但**不应执行**,第 5~10 段各 100 脉冲): + +| 段 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | +|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 频率 Hz | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 | 1000 | +| 脉冲数 | 1000 | 1000 | 1000 | 1000 | 100 | 100 | 100 | 100 | 100 | 100 | +| 等待类型 | 外部或完成 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 首个活动段 | 启动后直接为第 5 段 | | | | +| 第 1~4 段 | 未执行(位置不包含其脉冲数) | | | | +| 段顺序 | 5 → 6 → 7 → 8 → 9 → 10 | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `600` | | | | +| 末段内部诊断 | 期望 100、实测 100、误差 0 | | | | + +### TC-MS-106 运行中修改未来段频率(完成模式) + +- 优先级:`P1` +- 方法:上位机 + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 3 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数(段 2 频率**初始**为 2000 Hz,运行中改为 5000 Hz): + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | 1000 | 2000 → **5000** | 3000 | +| 脉冲数 | 1000 | 1000 | 1000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +步骤:段 1 执行期间(状态为运行/加速/减速),在“10 段参数”页把段 2 频率改为 `5000`,点击 `写入 10 段参数`。 + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 段 1 不受影响 | 写未来段期间段 1 仍按 1000 Hz 完成 1000 脉冲 | | | | +| 段 2 采用新频率 | 到达段 2 后当前频率约为 `5000 Hz`(±1 Hz) | | | | +| 段 3 正常 | 段 3 按 3000 Hz 完成 | | | | +| 最终状态/错误码 | 完成 7 / 错误 0 | | | | +| 最终位置 | `3000` | | | | +| 末段内部诊断 | 期望 1000、实测 1000、误差 0 | | | | + +### TC-MS-107 运行中修改未来段频率(后续模式) + +- 优先级:`P1` +- 方法:上位机 + 逻辑分析仪 +- 配置:与 TC-MS-106 相同,仅发送模式改为 **后续发送 (1)**。 + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | **后续发送 (1)** | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 3 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数与步骤同 TC-MS-106。 + +验收记录(TC-MS-106 全部指标 + 以下): + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 衔接不停轴 | 段 1→2 边界输出无中断,频率平滑过渡到 5000 Hz | | | | +| 段边界波形(逻辑分析仪) | 无窄脉冲、无额外边沿 | | | | + +### TC-MS-108 自跳转循环运行 + STOP + +- 优先级:`P1` +- 方法:上位机 + +公共参数: + +| 字段 | 值 | +|---|---| +| 脉冲输出 | Y0 (0) | +| 方向输出 | Y12 (0) | +| 方向延时 ms | 0 | +| WAIT 输入 | X4 (0) | +| EXT 输入 | X4 (0) | +| 发送模式 | 脉冲发送完毕 (0) | +| 方向逻辑 | 正逻辑 (0) | +| 曲线模式 | 直线 (0) | +| 位置模式 | 相对位置 (0) | +| 段数 | 1 | +| 起始段 | 1 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | +| 输出模式 0x1200 | PULSE/DIR (0) | + +段参数(段 1 自跳转循环): + +| 段 | 1 | +|---|---:| +| 频率 Hz | 1000 | +| 脉冲数 | 1000 | +| 等待类型 | 外部或完成 4 | +| WAIT ms | 0 | +| ACT ms | 0 | +| 跳转段 | **1(自跳转循环)** | + +步骤:启动后等待至少 3 轮循环(位置 ≥ 3000),点击 `停止`。 + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 循环执行 | 启动后位置持续增加,越过 3000 后仍在运行 | | | | +| 循环无错误 | 循环期间错误码恒为 0,状态在 运行/加速/减速/等待 间正常流转 | | | | +| STOP 生效 | 点击停止后进入 `停止 (8)`,输出不再发脉冲 | | | | +| STOP 位置 | `1000 ≤ 位置`(记录实测值) | | | | +| 重复 STOP | 再次点击停止无异常(幂等) | | | | +| 重新启动 | 再次点击启动可重新循环,位置继续累计 | | | | + +--- + +## 6. 极限参数专项用例(速度/加减速边界与组合) + +> 本节覆盖启动速度、停止速度、加速时间、减速时间、默认速度的边界值与危险组合。 +> 执行时重点观察:① 是否出现错误码或状态异常;② 每段是否按时完成(无异常长时间慢爬); +> ③ ramp 形态是否符合预期(平滑过渡,无跳变);④ 边界语义(如启动速度 > 段频率)下固件行为是否合理。 +> 公共参数表中"脉冲输出 Y0 / 方向输出 Y12 / WAIT X4 / EXT X4 / 方向逻辑正 / 曲线直线 / 位置相对 / 起始段 1 / 方向延时 0 / 输出模式 PULSE/DIR"为固定项,不再逐表重复,各表只列差异项。 + +### TC-MS-201 启动/停止速度为 0 + 非零加减速(零速 ramp 边界) + +- 优先级:`P0` +- 方法:上位机 + 逻辑分析仪 +- 关注点:速度上下限为 0,ramp 从 0 Hz 起步/收束到 0 Hz,验证定时器在零速边界的行为。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 发送模式 | 脉冲发送完毕 (0) | +| 段数 | 3 | +| 默认速度 Hz | 20000 | +| 启动速度 Hz | **0** | +| 停止速度 Hz | **0** | +| 加速时间 ms | **100** | +| 减速时间 ms | **100** | + +段参数(每段均 2000 脉冲): + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | 20000 | 20000 | 20000 | +| 脉冲数 | 2000 | 2000 | 2000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 写入接受 | 启动/停止速度 0 写入成功 | | | | +| 正常执行 | 3 段全部完成,无 ERROR/TIMER | | | | +| 首脉冲 | 首脉冲以非零可实现频率发出(0 Hz 按硬件最小频率处理),无死等 | | | | +| 最终状态/位置 | 完成 7 / 错误 0,位置 `6000` | | | | +| 末段内部诊断 | 期望 2000、实测 2000、误差 0 | | | | +| 波形(逻辑分析仪) | 段尾频率平滑收束到 0 附近停止,无窄脉冲 | | | | + +### TC-MS-202 启动速度高于段频率(向下 ramp) + +- 优先级:`P1` +- 方法:上位机 +- 关注点:启动速度 50000 Hz > 段频率 1000 Hz,ramp 方向与常规相反,验证固件行为合理(平滑向下过渡或按段频率执行),无错误。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 段数 | 3 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | **50000** | +| 停止速度 Hz | 100 | +| 加速时间 ms | **100** | +| 减速时间 ms | **100** | + +段参数(每段均 1000 脉冲): + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | **1000** | **1000** | **1000** | +| 脉冲数 | 1000 | 1000 | 1000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 无错误 | 3 段完成,错误码 0,无 ERROR/TIMER | | | | +| 频率行为 | 记录实际频率变化(预期从 50000 Hz 平滑向下过渡到 1000 Hz,或按段频率 1000 Hz 执行) | | | | +| 最终位置 | `3000` | | | | +| 末段内部诊断 | 期望 1000、实测 1000、误差 0 | | | | + +### TC-MS-203 停止速度高于段频率("向上"减速边界) + +- 优先级:`P1` +- 方法:上位机 +- 关注点:停止速度 50000 Hz > 段频率 1000 Hz,段末减速目标高于当前频率,验证不产生异常或长时间输出。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 段数 | 3 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | **50000** | +| 加速时间 ms | **100** | +| 减速时间 ms | **100** | + +段参数(每段均 1000 脉冲): + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | **1000** | **1000** | **1000** | +| 脉冲数 | 1000 | 1000 | 1000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 无错误 | 3 段完成,错误码 0 | | | | +| 无异常输出 | 段尾不出现长时间高速输出或无法停止 | | | | +| 频率行为 | 记录实际频率变化形态 | | | | +| 最终位置 | `3000` | | | | +| 末段内部诊断 | 期望 1000、实测 1000、误差 0 | | | | + +### TC-MS-204 默认速度极低 + 高频段(ramp 折算超长) + +- 优先级:`P1` +- 方法:上位机 +- 关注点:加减速时长按 `duration = 频率差 × baseTime / 默认速度` 折算,默认速度 100 Hz、段频率 100000 Hz 时 ramp 折算到近 100 秒级,短段必然截断,验证短轮廓路径处理。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 段数 | 3 | +| 默认速度 Hz | **100** | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | **100** | +| 减速时间 ms | **100** | + +段参数(每段均 1000 脉冲): + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | **100000** | **100000** | **100000** | +| 脉冲数 | 1000 | 1000 | 1000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 无错误 | 3 段完成,错误码 0 | | | | +| 按时完成 | 每段总时长记录实测值;应远小于 ramp 折算时长(截断三角),无秒级异常 | | | | +| 频率形态 | 记录峰值频率(预期远低于 100000 Hz)与波形形态 | | | | +| 最终位置 | `3000` | | | | +| 末段内部诊断 | 期望 1000、实测 1000、误差 0 | | | | + +### TC-MS-205 加减速时间极端不对称(加速 1 ms / 减速 10000 ms) + +- 优先级:`P1` +- 方法:上位机 + 逻辑分析仪 +- 关注点:加速 1 ms 接近瞬间,减速 10000 ms 折算约 10 秒级(远大于段执行时长),验证边界减速规划不误判。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 段数 | 3 | +| 默认速度 Hz | 20000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | **1** | +| 减速时间 ms | **10000** | + +段参数(每段均 5000 脉冲): + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | 20000 | 20000 | 20000 | +| 脉冲数 | 5000 | 5000 | 5000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 加速形态 | 加速几乎瞬间完成(1 ms 级 ramp) | | | | +| 减速规划 | 段尾减速按剩余脉冲边界触发,不提前在段中段开始长达秒级的减速 | | | | +| 无错误 | 3 段完成,错误码 0 | | | | +| 最终位置 | `15000` | | | | +| 末段内部诊断 | 期望 5000、实测 5000、误差 0 | | | | + +### TC-MS-206 零加减速 + 起停速度与段频率不同(跳频边界) + +- 优先级:`P1` +- 方法:上位机 +- 关注点:加减速时间 0 但启动/停止速度与段频率不同,验证零时长 ramp 直接跳频的正确性。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 段数 | 3 | +| 默认速度 Hz | 10000 | +| 启动速度 Hz | **500** | +| 停止速度 Hz | **500** | +| 加速时间 ms | 0 | +| 减速时间 ms | 0 | + +段参数(每段均 1000 脉冲): + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | **8000** | **8000** | **8000** | +| 脉冲数 | 1000 | 1000 | 1000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 跳频正确 | 每段直接以 8000 Hz 输出(无 500 Hz ramp 过程) | | | | +| 无错误 | 3 段完成,错误码 0 | | | | +| 最终位置 | `3000` | | | | +| 末段内部诊断 | 期望 1000、实测 1000、误差 0 | | | | +| 波形(逻辑分析仪) | 段起止频率跳变无窄脉冲、无额外边沿 | | | | + +### TC-MS-207 全部速度一致恒速(无 ramp 机会) + +- 优先级:`P1` +- 方法:上位机 +- 关注点:默认速度 = 启动速度 = 停止速度 = 段频率,频率差为 0,验证 ramp 差值为 0 时稳定恒速。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 段数 | 5 | +| 默认速度 Hz | **20000** | +| 启动速度 Hz | **20000** | +| 停止速度 Hz | **20000** | +| 加速时间 ms | **100** | +| 减速时间 ms | **100** | + +段参数(每段均 2000 脉冲): + +| 段 | 1 | 2 | 3 | 4 | 5 | +|---|---:|---:|---:|---:|---:| +| 频率 Hz | 20000 | 20000 | 20000 | 20000 | 20000 | +| 脉冲数 | 2000 | 2000 | 2000 | 2000 | 2000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 恒定频率 | 每段当前频率约 `20000 Hz`,无 ramp 波动 | | | | +| 无错误 | 5 段完成,错误码 0 | | | | +| 最终位置 | `10000` | | | | +| 末段内部诊断 | 期望 2000、实测 2000、误差 0 | | | | + +### TC-MS-208 高频段与低频段混排 + 大加减速 + +- 优先级:`P1` +- 方法:上位机 + 逻辑分析仪 +- 关注点:100000 Hz 与 100 Hz 段交替,大加减速下验证高频→低频的减速与低频→高频的加速在短段内正确截断。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 段数 | 3 | +| 默认速度 Hz | 100000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | **100** | +| 减速时间 ms | **100** | + +段参数: + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | **100000** | **100** | **100000** | +| 脉冲数 | 5000 | 50 | 5000 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 段 1/3 | 100000 Hz 段正常执行,无错误 | | | | +| 段 2 | 100 Hz 短段正常执行,无慢爬、无跳变 | | | | +| 段间过渡 | 高频→低频减速与低频→高频加速形态合理(记录实测) | | | | +| 最终状态/位置 | 完成 7 / 错误 0,位置 `10050` | | | | +| 末段内部诊断 | 期望 5000、实测 5000、误差 0 | | | | +| 波形(逻辑分析仪) | 无窄脉冲、无额外边沿 | | | | + +### TC-MS-209 超大加减速(10000 ms)+ 极短段(100 脉冲) + +- 优先级:`P1` +- 方法:上位机 + 逻辑分析仪 +- 关注点:加减速折算约 10 秒级而每段仅 100 脉冲,验证极短轮廓截断路径的峰值规划与时长。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 段数 | 3 | +| 默认速度 Hz | 100000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | **10000** | +| 减速时间 ms | **10000** | + +段参数(每段均 100 脉冲): + +| 段 | 1 | 2 | 3 | +|---|---:|---:|---:| +| 频率 Hz | **100000** | **100000** | **100000** | +| 脉冲数 | 100 | 100 | 100 | +| 等待类型 | 外部或完成 4 | 4 | 4 | +| WAIT ms | 0 | 0 | 0 | +| ACT ms | 0 | 0 | 0 | +| 跳转段 | 0 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 短轮廓执行 | 每段按截断三角完成,峰值远低于 100000 Hz(记录实测) | | | | +| 按时完成 | 每段总时长毫秒级,无秒级慢爬 | | | | +| 无错误 | 3 段完成,错误码 0 | | | | +| 最终位置 | `300` | | | | +| 末段内部诊断 | 期望 100、实测 100、误差 0 | | | | + +### TC-MS-210 后续模式 + 段间频率差 1000→100000 Hz + +- 优先级:`P1` +- 方法:上位机 + 逻辑分析仪 +- 关注点:后续模式下段 1 运行中提前变频至 100000 Hz,段间频率差 100 倍,验证无缝衔接与大幅变频。 + +公共参数(差异项): + +| 字段 | 值 | +|---|---| +| 发送模式 | **后续发送 (1)** | +| 段数 | 2 | +| 默认速度 Hz | 100000 | +| 启动速度 Hz | 100 | +| 停止速度 Hz | 100 | +| 加速时间 ms | **100** | +| 减速时间 ms | **100** | + +段参数: + +| 段 | 1 | 2 | +|---|---:|---:| +| 频率 Hz | **1000** | **100000** | +| 脉冲数 | 1000 | 5000 | +| 等待类型 | 外部或完成 4 | 4 | +| WAIT ms | 0 | 0 | +| ACT ms | 0 | 0 | +| 跳转段 | 0 | 0 | + +验收记录: + +| 测试指标 | 验收标准 | 是 | 否 | 备注/实测值 | +|---|---|---|---|---| +| 段 1 频率 | 段 1 以 1000 Hz 输出(后续模式下提前向 100000 Hz 变频) | | | | +| 衔接不停轴 | 段 1→2 边界输出无中断 | | | | +| 段 2 频率 | 段 2 达到约 `100000 Hz` | | | | +| 最终状态/位置 | 完成 7 / 错误 0,位置 `6000` | | | | +| 末段内部诊断 | 期望 5000、实测 5000、误差 0 | | | | +| 波形(逻辑分析仪) | 变频过程无窄脉冲、无额外边沿 | | | | + +--- + +## 7. 备注与已知限制 + +1. **TC-MS-001/002/003** 的段间波形指标需要逻辑分析仪;未接线时记录“未测”。已知 8-13 Bug 报告指出有限路径段间存在 ~2.5 ms 间隙(Bug 4),本次执行时重点观察该项,若复现请记录。 +2. **TC-MS-005**(频率 0 用默认速度)依赖固件 `PlsrResolvedSegmentFrequency` 语义(工作区未提交改动),若当前烧录固件不含该修复,段 1/3 写入 0 可能被拒绝(返回非法值)或按 0 Hz 处理,请记录实际行为。 +3. 所有用例执行前先 `清零`,执行后记录“输出诊断”页数据;异常退出时先 `停止` 再处理。 +4. 上位机轮询显示存在 1 ms 级延迟,频率/状态的瞬态值以逻辑分析仪为准,上位机显示值允许 ±1 Hz 量化误差。 +5. 多段测试中的方向延时(TC-MS-003)默认 10 ms,若修改公共参数中的方向延时,需同步调整验收范围(`延时 ± 2 ms`)。 diff --git a/Document/PLSR_document/PLSR波形验收报告_2026-08-14_完成模式10段阶梯频率.md b/Document/PLSR_document/PLSR波形验收报告_2026-08-14_完成模式10段阶梯频率.md new file mode 100644 index 0000000..ba207cc --- /dev/null +++ b/Document/PLSR_document/PLSR波形验收报告_2026-08-14_完成模式10段阶梯频率.md @@ -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` 做最终物理波形验收。 diff --git a/Document/PLSR_document/PLSR测试报告_2026-08-12.md b/Document/PLSR_document/PLSR测试报告_2026-08-12.md index 22933e8..1580c1f 100644 --- a/Document/PLSR_document/PLSR测试报告_2026-08-12.md +++ b/Document/PLSR_document/PLSR测试报告_2026-08-12.md @@ -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 固件。 diff --git a/Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md b/Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md new file mode 100644 index 0000000..4ed3b43 --- /dev/null +++ b/Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md @@ -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 + 语义;迁移路线按"所有权 → 重规划 → 统一规划器 → 曲线校准"重排。 diff --git a/Document/PLSR_document/波形/111.bin b/Document/PLSR_document/波形/111.bin index 680830ec0e15c1ec8c5f6cc8a1a2cfffc3f0efbe..1089305e62be27aa8831fce0494bf1bbc4a1ffb7 100644 GIT binary patch delta 11066 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 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjn7unlZyjeA1grjfP{Ue+ zm*S<_OY>5^gc{bMzf#}2nMjMYC;~(P1j+LTkCp_H)IlH+fOPXGK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U 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 z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5*Be|G3V000000LcHf5fU6YaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2ed=S0ssI20E7Ik zC$X~-GGM@f0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd X0RsjM7%*VKfB^#r3>YwAz`!^V{6q=q diff --git a/EWARM/Modbus.ewp b/EWARM/Modbus.ewp index 63a8cd1..cdde10f 100644 --- a/EWARM/Modbus.ewp +++ b/EWARM/Modbus.ewp @@ -1273,6 +1273,12 @@ $PROJ_DIR$\..\PLSR\Src\plsr_platform.h + + $PROJ_DIR$\..\PLSR\Src\plsr_planner.h + + + $PROJ_DIR$\..\PLSR\Src\plsr_planner.c + $PROJ_DIR$\..\PLSR\Src\plsr.c diff --git a/HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc b/HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..caf358e7f4f08de9e3520a46c8ad5266522d05e5 GIT binary patch literal 16966 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 Q8iK#%>l{Dy(B^9Y9{~yykpKVy delta 13458 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 ILzdzH0R-2(9RL6T diff --git a/HostComputer/__pycache__/wave_viewer.cpython-314.pyc b/HostComputer/__pycache__/wave_viewer.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00818bbceaf5aa08aa7bd5d3efdf1929147032ac GIT binary patch literal 19137 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

Dv&z#!X;`1Nj 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 时频曲线查看/出图工具(PLSR 项目用) + +数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点; + 电平 >= 阈值(默认128) 判为高(脉冲),否则为低。 +频率定义: 每脉冲频率 = 采样率 / 脉冲周期; + 周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点; + 最后一个脉冲无下一脉冲,按高电平宽度 x2 近似(50% 占空比)。 +时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)。 + +用法: + python bin_to_time_freq.py [采样率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() diff --git a/HostComputer/plsr_control_panel.py b/HostComputer/plsr_control_panel.py index 8712b8f..4f5e3a8 100644 --- a/HostComputer/plsr_control_panel.py +++ b/HostComputer/plsr_control_panel.py @@ -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: diff --git a/HostComputer/plsr_modbus_product_test.py b/HostComputer/plsr_modbus_product_test.py index baa44cc..2712939 100644 --- a/HostComputer/plsr_modbus_product_test.py +++ b/HostComputer/plsr_modbus_product_test.py @@ -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( diff --git a/HostComputer/wave_viewer.py b/HostComputer/wave_viewer.py new file mode 100644 index 0000000..bf85d09 --- /dev/null +++ b/HostComputer/wave_viewer.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +""" +逻辑分析仪时频曲线查看器(PLSR 项目用) + +主视图:频率-时间曲线(每个脉冲的瞬时频率,阶梯线绘制) +辅助视图:原始波形(run 级绘制,大文件也流畅) + +用法: + python wave_viewer.py [采样率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 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() diff --git a/PLSR/Inc/plsr.h b/PLSR/Inc/plsr.h index 7d2a054..21d753d 100644 --- a/PLSR/Inc/plsr.h +++ b/PLSR/Inc/plsr.h @@ -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]; diff --git a/PLSR/Src/plsr.c b/PLSR/Src/plsr.c index 0137006..662ba4b 100644 --- a/PLSR/Src/plsr.c +++ b/PLSR/Src/plsr.c @@ -1,5 +1,6 @@ #include "plsr.h" #include "plsr_internal.h" +#include "plsr_planner.h" #include "plsr_platform.h" #include #include @@ -48,8 +49,9 @@ #define PLSR_POSITION_CHECKPOINT_MS (10U) #define PLSR_PROFILE_QUEUE_CAPACITY (1024U) #define PLSR_PROFILE_QUEUE_MASK (PLSR_PROFILE_QUEUE_CAPACITY - 1U) -#define PLSR_SHORT_PROFILE_MAX_PULSES (1000UL) -#define PLSR_Q32_ONE (4294967296ULL) +#define PLSR_HANDOFF_WARMUP_ITEMS (8U) +#define PLSR_REPLAN_WARMUP_ITEMS (8U) +#define PLSR_RAMP_POLL_RESERVE_AREA_DIVISOR (20ULL) typedef struct { @@ -62,25 +64,17 @@ typedef struct typedef struct { + PLSR_PLANNER_CONTEXT planner; + PLSR_STREAM_ITEM pendingItem; uint32_t startHz; uint32_t peakHz; uint32_t endHz; - uint16_t pulseCount; - uint16_t entryPulses; - uint16_t steadyPulses; - uint16_t exitPulses; - volatile uint16_t nextPeriod; - uint64_t rampBoundaryQ32; - uint64_t rampTotalAreaQ32; - uint64_t rampTargetAreaQ32; - uint64_t rampAreaStepQ32; - uint32_t rampAreaRemainder; - uint32_t rampRemainderAccumulator; - uint64_t lastRampPhaseStepQ32; - uint64_t entryFirstBoundaryQ32; - uint64_t entrySecondBoundaryQ32; - uint64_t exitFirstBoundaryQ32; - uint64_t exitSecondBoundaryQ32; + uint32_t pulseCount; + uint32_t entryPulses; + uint32_t steadyPulses; + uint32_t exitPulses; + volatile uint32_t nextPeriod; + uint32_t pendingRepeats; uint32_t lastRampFrequencyHz; volatile uint8_t active; } PLSR_SHORT_PROFILE; @@ -114,7 +108,9 @@ typedef struct uint32_t secondRequestedFrequencyHz; PLSR_PLATFORM_TIMER_SETTING firstSetting; PLSR_PLATFORM_TIMER_SETTING secondSetting; + PLSR_PROFILE_ENTRY warmup[PLSR_HANDOFF_WARMUP_ITEMS]; PLSR_SHORT_PROFILE profile; + uint8_t warmupCount; uint8_t nextSegment; uint8_t positive; volatile uint8_t valid; @@ -141,6 +137,12 @@ typedef struct volatile uint8_t state; } PLSR_COMMAND_MAILBOX; +typedef struct +{ + volatile PLSR_EXEC_MODE mode; + volatile uint32_t generation; +} PLSR_EXECUTOR; + typedef struct { uint16_t flags; @@ -173,46 +175,12 @@ static const uint16_t PlsrSineProgressQ16[65] = 64124U, 64553U, 64905U, 65180U, 65377U, 65496U, 65535U }; -static const uint32_t PlsrSmoothIntegralQ24[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 PlsrSineIntegralQ24[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 PLSR_CONFIG PlsrShadowConfig; static PLSR_CONFIG PlsrActiveConfig; static PLSR_CONFIG PlsrCandidateConfig; static volatile int32_t PlsrPosition; static volatile uint64_t PlsrRemainingPulses; static volatile uint8_t PlsrPulseActive; -static volatile uint8_t PlsrFinitePulseActive; -static volatile uint8_t PlsrFiniteSequenceActive; -static uint32_t PlsrFinitePublishedPulses; -static uint32_t PlsrFiniteTargetPulses; static volatile uint8_t PlsrCutRequested; static volatile uint8_t PlsrBoundaryPending; static volatile uint8_t PlsrBoundaryWasCut; @@ -266,6 +234,7 @@ static uint8_t PlsrPersistenceDirty; static uint16_t PlsrPersistenceDelayMs; static uint8_t PlsrPositionCheckpointElapsedMs; static PLSR_COMMAND_MAILBOX PlsrCommandMailbox; +static PLSR_EXECUTOR PlsrExecutor; static volatile PLSR_DIAGNOSTIC PlsrDiagnostic; static volatile uint64_t PlsrDiagnosticSegmentCounterBase; static volatile uint32_t PlsrDiagnosticQueuedExpectedHz; @@ -275,14 +244,14 @@ static volatile uint16_t PlsrDiagnosticFailedChecks; volatile uint32_t PlsrProfileProducerItemCount; volatile uint32_t PlsrProfileProducerTotalCycles; volatile uint32_t PlsrProfileProducerMaxItemCycles; -volatile uint32_t PlsrFiniteRampUpdateCount; -volatile uint32_t PlsrFiniteRampUpdateMaxCycles; #endif static uint8_t PlsrIsBusy(void); static void PlsrSetDefaults(PLSR_CONFIG *config); static uint8_t PlsrConfigIsValid(const PLSR_CONFIG *config, uint8_t validateActivePath); +static uint32_t PlsrResolvedSegmentFrequency(const PLSR_CONFIG *config, + uint8_t segmentIndex); static uint16_t PlsrReadConfigWord(const PLSR_CONFIG *config, uint16_t address); static PLSR_WORD_RESULT PlsrWriteConfigWord(PLSR_CONFIG *config, @@ -304,38 +273,39 @@ static uint32_t PlsrEffectiveStartFrequency(uint32_t segmentFrequencyHz, uint32_t carryFrequencyHz); static uint32_t PlsrEffectiveStopFrequency(uint32_t segmentFrequencyHz); static uint8_t PlsrBeginSegmentOutput(uint32_t startFrequencyHz); -static uint8_t PlsrBeginFiniteConstantOutput(uint32_t frequencyHz); -static uint8_t PlsrBeginFiniteProfileOutput(uint32_t startFrequencyHz, - uint32_t targetFrequencyHz); -static uint8_t PlsrBeginFiniteSequenceOutput(void); -static uint8_t PlsrSyncFiniteProgress(void); -static uint8_t PlsrServiceFiniteBoundaries(void); -static uint8_t PlsrServiceFiniteCompletion(void); static void PlsrHandleBoundary(uint8_t extEdge); static void PlsrTransitionToNext(uint8_t allowCarry); static void PlsrFinishCompleted(void); static void PlsrFinishStopped(void); static void PlsrEnterError(PLSR_ERROR error); +static void PlsrEnterErrorIfEpoch(PLSR_ERROR error, + uint32_t expectedEpoch); static void PlsrMarkPersistenceDirty(uint16_t delayMs); static void PlsrCheckpointPosition(uint8_t wasBusy); static void PlsrPollPositionCheckpoint(void); -static PLSR_PLATFORM_QUEUE_RESULT PlsrTrySubsequentHandoff(void); +static PLSR_PLATFORM_QUEUE_RESULT PlsrTryContinuousHandoff(void); static uint8_t PlsrPrepareShortProfile(PLSR_SHORT_PROFILE *profile, uint8_t segmentNumber, uint32_t startFrequencyHz, uint32_t targetFrequencyHz, uint64_t pulseCount); static uint32_t PlsrShortProfileTakeFrequency(PLSR_SHORT_PROFILE *profile); +static uint8_t PlsrReplanPulseDir(uint32_t targetHz, + uint32_t totalPulses); static void PlsrProfileQueueReset(void); +static void PlsrProfileQueueInvalidateGeneration(void); static uint8_t PlsrProfileQueueBegin( const PLSR_SHORT_PROFILE *producerProfile, uint32_t producerEpoch, uint8_t producerSegment, uint8_t preparedHandoffBank); -static uint8_t PlsrProfileQueueFill(void); +static uint8_t PlsrProfileQueueFill(uint16_t targetCount); static PLSR_PLATFORM_QUEUE_RESULT PlsrProfileQueueCommitNext( uint8_t pulseOutput); static uint8_t PlsrPrepareFutureHandoffQueue(void); +static void PlsrActivateHandoffQueueFromIrq( + const PLSR_HANDOFF_PLAN *plan, + uint32_t producerEpoch); static void PlsrCopyShortProfile(PLSR_SHORT_PROFILE *destination, const PLSR_SHORT_PROFILE *source); static void PlsrInvalidateHandoffPlans(void); @@ -345,13 +315,14 @@ static uint8_t PlsrSelectPreparedHandoffPlan(PLSR_HANDOFF_PLAN *plan); static PLSR_PLATFORM_QUEUE_RESULT PlsrPrimeHandoff(void); static uint8_t PlsrBuildHandoffPlan(uint8_t sourceSegment, const PLSR_CONFIG *frequencyConfig, + uint32_t carryFrequencyHz, PLSR_HANDOFF_PLAN *plan); +static void PlsrRefreshCurrentHandoffPlan(uint32_t terminalFrequencyHz); static void PlsrDiagnosticReset(void); static void PlsrDiagnosticBeginSegment(uint8_t segmentNumber, uint64_t expectedCycles, uint8_t directionPositive); static void PlsrDiagnosticFinishSegment(uint8_t completedNormally); -static void PlsrDiagnosticFinishFiniteSegment(uint32_t observedCycles); static uint8_t PlsrDiagnosticStartPulse(uint8_t pulseOutput, uint32_t requestedFirstHz, uint32_t requestedQueuedHz, @@ -874,8 +845,7 @@ static uint8_t PlsrConfigIsValid(const PLSR_CONFIG *config, { const PLSR_SEGMENT_CONFIG *segment = &config->segments[index]; - if ((segment->frequencyHz == 0UL) - || (segment->frequencyHz > PLSR_FREQUENCY_MAX_HZ) + if ((segment->frequencyHz > PLSR_FREQUENCY_MAX_HZ) || (segment->waitType > PLSR_EXT_OR_COMPLETE) || (segment->jumpSegment > PLSR_SEGMENT_COUNT_MAX)) { @@ -1322,20 +1292,8 @@ static uint8_t PlsrApplyFrequencyPair(uint32_t requestedFirstFrequencyHz, PlsrHandoffPlan.valid = 0U; if (haveCandidatePlan != 0U) { - PlsrHandoffPlan.magnitude = candidatePlan.magnitude; + PlsrHandoffPlan = candidatePlan; PlsrHandoffPlan.firstFrequencyHz = actualQueuedFrequencyHz; - PlsrHandoffPlan.secondFrequencyHz = - candidatePlan.secondFrequencyHz; - PlsrHandoffPlan.firstRequestedFrequencyHz = - candidatePlan.firstRequestedFrequencyHz; - PlsrHandoffPlan.secondRequestedFrequencyHz = - candidatePlan.secondRequestedFrequencyHz; - PlsrHandoffPlan.firstSetting = candidatePlan.firstSetting; - PlsrHandoffPlan.secondSetting = candidatePlan.secondSetting; - PlsrCopyShortProfile(&PlsrHandoffPlan.profile, - &candidatePlan.profile); - PlsrHandoffPlan.nextSegment = candidatePlan.nextSegment; - PlsrHandoffPlan.positive = candidatePlan.positive; PlsrHandoffPlan.valid = 1U; } } @@ -1350,76 +1308,15 @@ static uint8_t PlsrApplyFrequencyPair(uint32_t requestedFirstFrequencyHz, return 1U; } -static uint8_t PlsrApplyFrequency(uint32_t requestedFrequencyHz, - uint32_t expectedEpoch) -{ - if (PlsrFinitePulseActive != 0U) - { - PLSR_PLATFORM_TIMER_SETTING setting; - uint32_t actualFrequencyHz; - PLSR_PLATFORM_QUEUE_RESULT result; - - if ((PlsrSegmentEpoch != expectedEpoch) - || (requestedFrequencyHz == 0UL) - || (PlsrPlatformBuildTimerSetting( - (uint8_t)PlsrActiveConfig.pulseOutput, - PLSR_OUTPUT_PULSE_DIR, requestedFrequencyHz, - &setting) == 0U)) - { - return 0U; - } - result = PlsrPlatformUpdateFinitePrepared( - (uint8_t)PlsrActiveConfig.pulseOutput, &setting, - &actualFrequencyHz); - if (result == PLSR_PLATFORM_QUEUE_FAILED) - { - return 0U; - } - if (result == PLSR_PLATFORM_QUEUE_APPLIED) - { - PlsrDiagnosticRecordFrequency(requestedFrequencyHz, - setting.actualFrequencyHz, - actualFrequencyHz); - PlsrDiagnosticRecordCurveSample(setting.actualFrequencyHz, - actualFrequencyHz); - PlsrCurrentFrequencyHz = actualFrequencyHz; - PlsrQueuedFrequencyHz = actualFrequencyHz; - } - return 1U; - } - return PlsrApplyFrequencyPair(requestedFrequencyHz, - requestedFrequencyHz, - expectedEpoch); -} - -static void PlsrDiagnosticFinishFiniteSegment(uint32_t observedCycles) +static PLSR_PLATFORM_QUEUE_RESULT PlsrApplyFrequency( + uint32_t requestedFrequencyHz, + uint32_t expectedEpoch) { - uint32_t expectedCycles; - - if ((PlsrDiagnostic.flags & PLSR_DIAG_FLAG_MONITORING) == 0U) - { - return; - } -#ifdef PLSR_HOST_TEST - PlsrHostTestEventSequence++; - PlsrHostTestLastDiagnosticFinishEvent = PlsrHostTestEventSequence; -#endif - PlsrDiagnostic.observedCycles = observedCycles; - PlsrDiagnostic.flags &= (uint16_t)~PLSR_DIAG_FLAG_MONITORING; - expectedCycles = PlsrDiagnostic.expectedCycles; - PlsrDiagnostic.countError = - PlsrDifferenceI32(observedCycles, expectedCycles); - PlsrDiagnostic.flags |= PLSR_DIAG_FLAG_COUNT_CHECKED; - if (PlsrDiagnostic.countError != 0L) - { - PlsrDiagnosticFailedChecks |= PLSR_DIAG_FLAG_COUNT_PASS; - PlsrDiagnostic.flags &= (uint16_t)~PLSR_DIAG_FLAG_COUNT_PASS; - PlsrDiagnosticLatch(PLSR_DIAG_REASON_COUNT); - } - else if ((PlsrDiagnosticFailedChecks & PLSR_DIAG_FLAG_COUNT_PASS) == 0U) - { - PlsrDiagnostic.flags |= PLSR_DIAG_FLAG_COUNT_PASS; - } + return (PlsrApplyFrequencyPair(requestedFrequencyHz, + requestedFrequencyHz, + expectedEpoch) != 0U) + ? PLSR_PLATFORM_QUEUE_APPLIED + : PLSR_PLATFORM_QUEUE_FAILED; } static uint8_t PlsrStopDrainPulseCount(void) @@ -1442,12 +1339,6 @@ static uint8_t PlsrArmStopDrainLocked(void) { uint32_t drainPulses = PlsrStopDrainPulseCount(); - if (PlsrFinitePulseActive != 0U) - { - PlsrStopPulsesRemaining = (uint8_t)drainPulses; - return PlsrPlatformRetargetFiniteStop( - (uint8_t)PlsrActiveConfig.pulseOutput, drainPulses); - } PlsrStopPulsesRemaining = (uint8_t)drainPulses; return PlsrArmFinalAbBoundaryLocked(); } @@ -1489,10 +1380,7 @@ static uint8_t PlsrRampAdvance(uint32_t expectedEpoch) uint32_t toHz; uint32_t durationMs; uint32_t elapsedMs; -#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \ - && !defined(PLSR_HOST_TEST) - uint32_t startedAt = 0UL; -#endif + PLSR_PLATFORM_QUEUE_RESULT applyResult; criticalState = PlsrPlatformEnterCritical(); if ((PlsrSegmentEpoch != expectedEpoch) || (PlsrRamp.active == 0U)) @@ -1500,20 +1388,16 @@ static uint8_t PlsrRampAdvance(uint32_t expectedEpoch) PlsrPlatformExitCritical(criticalState); return 1U; } - PlsrRamp.elapsedMs++; + if (PlsrRamp.elapsedMs < PlsrRamp.durationMs) + { + PlsrRamp.elapsedMs++; + } fromHz = PlsrRamp.fromHz; toHz = PlsrRamp.toHz; durationMs = PlsrRamp.durationMs; elapsedMs = PlsrRamp.elapsedMs; PlsrPlatformExitCritical(criticalState); -#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \ - && !defined(PLSR_HOST_TEST) - if (PlsrFinitePulseActive != 0U) - { - startedAt = PlsrProfileTimingNow(); - } -#endif progress = PlsrCurveProgressQ16(elapsedMs, durationMs, PlsrActiveConfig.curveMode); @@ -1530,10 +1414,15 @@ static uint8_t PlsrRampAdvance(uint32_t expectedEpoch) - (uint32_t)(((uint64_t)gap * progress) / 65535UL); } - if (PlsrApplyFrequency(frequency, expectedEpoch) == 0U) + applyResult = PlsrApplyFrequency(frequency, expectedEpoch); + if (applyResult == PLSR_PLATFORM_QUEUE_FAILED) { return 0U; } + if (applyResult == PLSR_PLATFORM_QUEUE_STALE) + { + return 1U; + } criticalState = PlsrPlatformEnterCritical(); if (PlsrSegmentEpoch != expectedEpoch) @@ -1554,19 +1443,6 @@ static uint8_t PlsrRampAdvance(uint32_t expectedEpoch) } } PlsrPlatformExitCritical(criticalState); -#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \ - && !defined(PLSR_HOST_TEST) - if (startedAt != 0UL) - { - uint32_t elapsedCycles = PlsrProfileTimingNow() - startedAt; - - PlsrFiniteRampUpdateCount++; - if (elapsedCycles > PlsrFiniteRampUpdateMaxCycles) - { - PlsrFiniteRampUpdateMaxCycles = elapsedCycles; - } - } -#endif return 1U; } @@ -1588,6 +1464,14 @@ static uint8_t PlsrGetNextSegment(uint8_t *nextSegment) return 0U; } +static uint32_t PlsrResolvedSegmentFrequency(const PLSR_CONFIG *config, + uint8_t segmentIndex) +{ + uint32_t frequencyHz = config->segments[segmentIndex].frequencyHz; + + return (frequencyHz == 0UL) ? config->defaultSpeedHz : frequencyHz; +} + static int64_t PlsrSegmentDisplacement(uint8_t segmentNumber, int32_t referencePosition) { @@ -1649,591 +1533,37 @@ static uint64_t PlsrRampPulseEstimate(uint32_t fromHz, { uint32_t duration = PlsrRampDurationMs(fromHz, toHz); uint64_t sum = (uint64_t)fromHz + toHz; - uint64_t estimate = (sum * duration + 1999UL) / 2000UL + 2UL; - uint64_t reserve = ((uint64_t)rateBoundHz + 999UL) / 1000UL; - - if ((duration != 0UL) && (fromHz > toHz)) - { - /* A 1 ms held deceleration step can exceed the ideal ramp area. */ - reserve += ((uint64_t)(fromHz - toHz) + 999UL) / 1000UL; - } - if (UINT64_MAX - estimate < reserve) - { - return UINT64_MAX; - } - - return estimate + reserve; -} - -static uint16_t PlsrShortProfileRampTime(uint32_t fromHz, uint32_t toHz) -{ - if (toHz > fromHz) - { - return PlsrActiveConfig.accelerationTimeMs; - } - if (toHz < fromHz) - { - return PlsrActiveConfig.decelerationTimeMs; - } - return 0U; -} - -static uint64_t PlsrShortProfileRampWeight(uint32_t fromHz, - uint32_t toHz, - uint16_t timeMs) -{ - 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 * timeMs; -} - -static uint32_t PlsrIntegerSquareRoot(uint64_t value) -{ - uint64_t bit = (uint64_t)1U << 62U; - uint64_t root = 0UL; - - while (bit > value) - { - bit >>= 2U; - } - while (bit != 0UL) - { - if (value >= (root + bit)) - { - value -= root + bit; - root = (root >> 1U) + bit; - } - else - { - root >>= 1U; - } - bit >>= 2U; - } - return (uint32_t)root; -} - -static uint32_t PlsrShortProfilePeak(uint32_t startHz, - uint32_t targetHz, - uint32_t endHz, - uint16_t pulseCount) -{ - uint32_t upperEndpoint = (startHz > endHz) ? startHz : endHz; - uint32_t lowerEndpoint = (startHz < endHz) ? startHz : endHz; - uint16_t entryTime; - uint16_t exitTime; - uint32_t timeSum; - uint64_t weightedEndpoints; - uint64_t availableArea; - uint64_t peakSquared; - uint32_t peakHz; - - if (targetHz > upperEndpoint) - { - entryTime = PlsrShortProfileRampTime(startHz, targetHz); - exitTime = PlsrShortProfileRampTime(targetHz, endHz); - timeSum = (uint32_t)entryTime + exitTime; - if (timeSum == 0UL) - { - return targetHz; - } - weightedEndpoints = ((uint64_t)startHz * startHz * entryTime) - + ((uint64_t)endHz * endHz * exitTime); - availableArea = (uint64_t)2U * pulseCount - * PlsrActiveConfig.defaultSpeedHz * 1000UL; - peakSquared = (availableArea + weightedEndpoints) / timeSum; - peakHz = PlsrIntegerSquareRoot(peakSquared); - if (peakHz < upperEndpoint) - { - peakHz = upperEndpoint; - } - if (peakHz > targetHz) - { - peakHz = targetHz; - } - return peakHz; - } - - if (targetHz < lowerEndpoint) - { - entryTime = PlsrShortProfileRampTime(startHz, targetHz); - exitTime = PlsrShortProfileRampTime(targetHz, endHz); - timeSum = (uint32_t)entryTime + exitTime; - if (timeSum == 0UL) - { - return targetHz; - } - weightedEndpoints = ((uint64_t)startHz * startHz * entryTime) - + ((uint64_t)endHz * endHz * exitTime); - availableArea = (uint64_t)2U * pulseCount - * PlsrActiveConfig.defaultSpeedHz * 1000UL; - if (availableArea >= weightedEndpoints) - { - return targetHz; - } - peakSquared = (weightedEndpoints - availableArea) / timeSum; - peakHz = PlsrIntegerSquareRoot(peakSquared); - if (peakHz < targetHz) - { - peakHz = targetHz; - } - if (peakHz > lowerEndpoint) - { - peakHz = lowerEndpoint; - } - return peakHz; - } - - return targetHz; -} - -static uint64_t PlsrShortProfileRequiredSteps(uint64_t rampWeight) -{ - uint64_t denominator = (uint64_t)2U - * PlsrActiveConfig.defaultSpeedHz * 1000UL; - - if (rampWeight == 0UL) - { - return 0UL; - } - return (rampWeight + denominator - 1UL) / denominator; -} - -static uint32_t PlsrShortProfileReachableFrequency(uint32_t fromHz, - uint32_t towardHz, - uint16_t pulseCount) -{ - uint16_t baseTime = PlsrShortProfileRampTime(fromHz, towardHz); - uint64_t frequencySquared = (uint64_t)fromHz * fromHz; - uint64_t changeSquared; - uint32_t reachableHz; - - if ((baseTime == 0U) || (fromHz == towardHz)) - { - return towardHz; - } - changeSquared = (uint64_t)2U * pulseCount - * PlsrActiveConfig.defaultSpeedHz * 1000UL / baseTime; - if (towardHz > fromHz) - { - reachableHz = PlsrIntegerSquareRoot(frequencySquared + changeSquared); - return (reachableHz > towardHz) ? towardHz : reachableHz; - } - - frequencySquared = (changeSquared >= frequencySquared) - ? 0UL : (frequencySquared - changeSquared); - reachableHz = PlsrIntegerSquareRoot(frequencySquared); - if (((uint64_t)reachableHz * reachableHz) < frequencySquared) - { - reachableHz++; - } - return (reachableHz < towardHz) ? towardHz : reachableHz; -} - -static uint64_t PlsrCurveIntegralQ32(uint64_t progressQ32, - uint16_t curveMode) -{ - uint64_t scaled; - uint32_t index; - uint32_t fraction; - uint64_t first; - uint64_t second; - const uint32_t *table; - - if (progressQ32 >= PLSR_Q32_ONE) - { - return PLSR_Q32_ONE / 2ULL; - } - if (curveMode == 0U) - { - return (progressQ32 * progressQ32) >> 33U; - } - - table = (curveMode == 1U) ? PlsrSmoothIntegralQ24 - : PlsrSineIntegralQ24; - 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 PlsrRampAreaQ32(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)PlsrCurveIntegralQ32( - progressQ32, PlsrActiveConfig.curveMode); - return (uint64_t)area; -} - -static uint32_t PlsrRampInstantFrequency(uint32_t fromHz, - uint32_t toHz, - uint64_t progressQ32) -{ - uint64_t curveProgressQ32; - uint64_t scaled; - uint32_t index; - uint32_t tableSlopeQ32; - uint32_t gap; - const uint32_t *integralTable; - - if (progressQ32 >= PLSR_Q32_ONE) - { - return toHz; - } - if (PlsrActiveConfig.curveMode != 0U) - { - scaled = progressQ32 * 64ULL; - index = (uint32_t)(scaled >> 32U); - integralTable = (PlsrActiveConfig.curveMode == 1U) - ? PlsrSmoothIntegralQ24 - : PlsrSineIntegralQ24; - tableSlopeQ32 = - (integralTable[index + 1UL] - integralTable[index]) << 14U; - curveProgressQ32 = tableSlopeQ32; - } - else - { - curveProgressQ32 = progressQ32; - } - + uint64_t discreteArea = sum * duration; + uint64_t rampArea; + uint64_t pollReserve = 0ULL; + uint64_t candidateReserve = + ((uint64_t)rateBoundHz + 999UL) / 1000UL; + uint64_t endpointCorrection = 0ULL; + + /* RampAdvance applies samples at 1/duration through duration/duration. + For all supported symmetric curves this right-endpoint sum differs + from the continuous trapezoid by half the endpoint delta. */ if (toHz >= fromHz) { - gap = toHz - fromHz; - return fromHz - + (uint32_t)(((uint64_t)gap * curveProgressQ32) >> 32U); - } - gap = fromHz - toHz; - return fromHz - - (uint32_t)(((uint64_t)gap * curveProgressQ32) >> 32U); -} - -static uint64_t PlsrExactRampBoundaryQ32(uint32_t fromHz, - uint32_t toHz, - uint64_t previousBoundaryQ32, - uint64_t targetAreaQ32) -{ - uint64_t lowerQ32 = previousBoundaryQ32; - uint64_t upperQ32 = PLSR_Q32_ONE; - uint64_t middleQ32; - uint32_t iteration; - - /* The Q32 domain is 2^32 units wide, so 32 fixed bisections are exact. */ - for (iteration = 0UL; iteration < 32UL; iteration++) - { - middleQ32 = lowerQ32 + ((upperQ32 - lowerQ32) >> 1U); - if (PlsrRampAreaQ32(fromHz, toHz, middleQ32) < targetAreaQ32) - { - lowerQ32 = middleQ32; - } - else - { - upperQ32 = middleQ32; - } - } - return upperQ32; -} - -static uint32_t PlsrCountLeadingZeros32(uint32_t value) -{ -#if defined(__ICCARM__) - return __CLZ(value); -#else - uint32_t count = 0U; - - if ((value & 0xFFFF0000UL) == 0UL) - { - count += 16U; - value <<= 16U; - } - if ((value & 0xFF000000UL) == 0UL) - { - count += 8U; - value <<= 8U; - } - if ((value & 0xF0000000UL) == 0UL) - { - count += 4U; - value <<= 4U; - } - if ((value & 0xC0000000UL) == 0UL) - { - count += 2U; - value <<= 2U; - } - if ((value & 0x80000000UL) == 0UL) - { - count++; - } - return count; -#endif -} - -static uint32_t PlsrDivideU64Low(uint32_t highWord, - uint32_t lowWord, - uint32_t divisor, - uint32_t *remainder) -{ - const uint32_t halfBase = 0x10000UL; - uint32_t shift = PlsrCountLeadingZeros32(divisor); - uint32_t normalizedDivisor = divisor << shift; - uint32_t divisorHigh = normalizedDivisor >> 16U; - uint32_t divisorLow = normalizedDivisor & 0xFFFFUL; - uint32_t normalizedHigh; - uint32_t normalizedLow = lowWord << shift; - uint32_t lowHigh = normalizedLow >> 16U; - uint32_t lowLow = normalizedLow & 0xFFFFUL; - uint32_t quotientHigh; - uint32_t quotientLow; - uint32_t partialRemainder; - uint32_t middle; - uint32_t normalizedRemainder; - - if (shift == 0U) - { - normalizedHigh = highWord; - } - else - { - normalizedHigh = (highWord << shift) - | (lowWord >> (32U - shift)); - } - - quotientHigh = normalizedHigh / divisorHigh; - partialRemainder = normalizedHigh - quotientHigh * divisorHigh; - while ((quotientHigh >= halfBase) - || (quotientHigh * divisorLow - > (partialRemainder << 16U) + lowHigh)) - { - quotientHigh--; - partialRemainder += divisorHigh; - if (partialRemainder >= halfBase) - { - break; - } - } - - middle = normalizedHigh * halfBase + lowHigh - - quotientHigh * normalizedDivisor; - quotientLow = middle / divisorHigh; - partialRemainder = middle - quotientLow * divisorHigh; - while ((quotientLow >= halfBase) - || (quotientLow * divisorLow - > (partialRemainder << 16U) + lowLow)) - { - quotientLow--; - partialRemainder += divisorHigh; - if (partialRemainder >= halfBase) - { - break; - } - } - - normalizedRemainder = middle * halfBase + lowLow - - quotientLow * normalizedDivisor; - if (remainder != NULL) - { - *remainder = normalizedRemainder >> shift; - } - return quotientHigh * halfBase + quotientLow; -} - -static uint64_t PlsrDivideU64ByU32(uint64_t dividend, - uint32_t divisor, - uint32_t *remainder) -{ - uint32_t highWord = (uint32_t)(dividend >> 32U); - uint32_t lowWord = (uint32_t)dividend; - uint32_t quotientHigh = highWord / divisor; - uint32_t highRemainder = highWord - quotientHigh * divisor; - uint32_t quotientLow = PlsrDivideU64Low(highRemainder, lowWord, - divisor, remainder); - - return ((uint64_t)quotientHigh << 32U) | quotientLow; -} - -static void PlsrPrepareShortProfileBoundaries(PLSR_SHORT_PROFILE *profile) -{ - uint64_t totalAreaQ32; - uint64_t areaStepQ32; - uint64_t targetAreaQ32; - uint32_t areaRemainder; - - if (profile->entryPulses != 0U) - { - totalAreaQ32 = PlsrRampAreaQ32( - profile->startHz, profile->peakHz, PLSR_Q32_ONE); - areaStepQ32 = totalAreaQ32 / profile->entryPulses; - areaRemainder = (uint32_t)(totalAreaQ32 % profile->entryPulses); - profile->entryFirstBoundaryQ32 = PlsrExactRampBoundaryQ32( - profile->startHz, profile->peakHz, 0ULL, areaStepQ32); - if (profile->entryPulses > 1U) - { - targetAreaQ32 = areaStepQ32 * 2ULL - + (((uint64_t)areaRemainder * 2ULL) - / profile->entryPulses); - profile->entrySecondBoundaryQ32 = PlsrExactRampBoundaryQ32( - profile->startHz, profile->peakHz, - profile->entryFirstBoundaryQ32, targetAreaQ32); - } - } - if (profile->exitPulses != 0U) - { - totalAreaQ32 = PlsrRampAreaQ32( - profile->peakHz, profile->endHz, PLSR_Q32_ONE); - areaStepQ32 = totalAreaQ32 / profile->exitPulses; - areaRemainder = (uint32_t)(totalAreaQ32 % profile->exitPulses); - profile->exitFirstBoundaryQ32 = PlsrExactRampBoundaryQ32( - profile->peakHz, profile->endHz, 0ULL, areaStepQ32); - if (profile->exitPulses > 1U) - { - targetAreaQ32 = areaStepQ32 * 2ULL - + (((uint64_t)areaRemainder * 2ULL) - / profile->exitPulses); - profile->exitSecondBoundaryQ32 = PlsrExactRampBoundaryQ32( - profile->peakHz, profile->endHz, - profile->exitFirstBoundaryQ32, targetAreaQ32); - } - } -} - -static uint64_t PlsrRampBoundaryQ32(uint32_t fromHz, - uint32_t toHz, - uint64_t previousBoundaryQ32, - uint64_t targetAreaQ32, - uint64_t predictedStepQ32) -{ - uint64_t currentAreaQ32; - uint64_t candidateQ32; - uint64_t candidateAreaQ32; - uint64_t differenceQ32; - uint64_t correctionQ32; - uint32_t derivativeHz; - - currentAreaQ32 = (previousBoundaryQ32 == 0ULL) - ? 0ULL - : PlsrRampAreaQ32(fromHz, toHz, - previousBoundaryQ32); - if (targetAreaQ32 <= currentAreaQ32) - { - return previousBoundaryQ32 + 1ULL; - } - - if (predictedStepQ32 != 0ULL) - { - if (predictedStepQ32 >= PLSR_Q32_ONE - previousBoundaryQ32) - { - candidateQ32 = PLSR_Q32_ONE; - } - else - { - candidateQ32 = previousBoundaryQ32 + predictedStepQ32; - } + discreteArea += (uint64_t)toHz - fromHz; } else { - derivativeHz = PlsrRampInstantFrequency(fromHz, toHz, - previousBoundaryQ32); - if (derivativeHz == 0UL) - { - derivativeHz = 1UL; - } - differenceQ32 = targetAreaQ32 - currentAreaQ32; - correctionQ32 = - PlsrDivideU64ByU32(differenceQ32 + derivativeHz - 1UL, - derivativeHz, NULL); - if (correctionQ32 >= PLSR_Q32_ONE - previousBoundaryQ32) - { - candidateQ32 = PLSR_Q32_ONE; - } - else - { - candidateQ32 = previousBoundaryQ32 + correctionQ32; - } + discreteArea -= (uint64_t)fromHz - toHz; + endpointCorrection = + ((uint64_t)fromHz - toHz + 1999UL) / 2000UL; } + rampArea = (discreteArea + 1999UL) / 2000UL; - candidateAreaQ32 = PlsrRampAreaQ32(fromHz, toHz, candidateQ32); - derivativeHz = PlsrRampInstantFrequency(fromHz, toHz, candidateQ32); - if (derivativeHz == 0UL) - { - derivativeHz = 1UL; - } - if (candidateAreaQ32 < targetAreaQ32) + if (candidateReserve + <= rampArea / PLSR_RAMP_POLL_RESERVE_AREA_DIVISOR) { - differenceQ32 = targetAreaQ32 - candidateAreaQ32; - correctionQ32 = - PlsrDivideU64ByU32(differenceQ32 + derivativeHz - 1UL, - derivativeHz, NULL); - if (correctionQ32 >= PLSR_Q32_ONE - candidateQ32) - { - candidateQ32 = PLSR_Q32_ONE; - } - else - { - candidateQ32 += correctionQ32; - } - } - else if (candidateAreaQ32 > targetAreaQ32) - { - differenceQ32 = candidateAreaQ32 - targetAreaQ32; - correctionQ32 = PlsrDivideU64ByU32(differenceQ32, derivativeHz, - NULL); - if (correctionQ32 == 0ULL) - { - correctionQ32 = 1ULL; - } - if (correctionQ32 >= candidateQ32 - previousBoundaryQ32) - { - candidateQ32 = previousBoundaryQ32 + 1ULL; - } - else - { - candidateQ32 -= correctionQ32; - } + pollReserve = candidateReserve + endpointCorrection; } - /* The corrected phase step seeds the next pulse and keeps ISR work fixed. */ - return candidateQ32; -} - -static uint32_t PlsrRampAverageFrequency(uint64_t areaIncrementQ32, - uint64_t firstBoundaryQ32, - uint64_t secondBoundaryQ32) -{ - uint64_t denominator; - uint64_t frequencyHz; - - if (secondBoundaryQ32 <= firstBoundaryQ32) - { - return 1UL; - } - denominator = secondBoundaryQ32 - firstBoundaryQ32; - if (denominator == PLSR_Q32_ONE) - { - frequencyHz = (areaIncrementQ32 + denominator / 2ULL) >> 32U; - } - else - { - frequencyHz = PlsrDivideU64ByU32( - areaIncrementQ32 + denominator / 2ULL, - (uint32_t)denominator, NULL); - } - if (frequencyHz == 0UL) - { - return 1UL; - } - if (frequencyHz > PLSR_FREQUENCY_MAX_HZ) - { - return PLSR_FREQUENCY_MAX_HZ; - } - return (uint32_t)frequencyHz; + /* A full poll reserve on shorter ramps would dominate their area and + leave stop-speed tail pulses. */ + return rampArea + pollReserve; } static uint8_t PlsrPrepareShortProfile(PLSR_SHORT_PROFILE *profile, @@ -2242,300 +1572,173 @@ static uint8_t PlsrPrepareShortProfile(PLSR_SHORT_PROFILE *profile, uint32_t targetFrequencyHz, uint64_t pulseCount) { + PLSR_MOTION_BLOCK block; + PLSR_PLANNER_STATUS status; const PLSR_SEGMENT_CONFIG *segment; - uint32_t endFrequencyHz; - uint32_t peakFrequencyHz; - uint16_t entryTime; - uint16_t exitTime; - uint16_t totalPulses; - uint64_t entryWeight; - uint64_t exitWeight; - uint64_t entryRequired; - uint64_t exitRequired; - uint64_t totalWeight; - uint64_t scaledEntry; - - uint64_t durationWeight; - uint64_t singleFrequencyHz; - uint16_t directTime; - uint64_t directWeight; - uint64_t directRequired; + uint8_t nextSegment = 0U; + uint8_t currentPositive; + uint8_t nextPositive; (void)memset(profile, 0, sizeof(*profile)); - if ((pulseCount == 0UL) - || (pulseCount > PLSR_SHORT_PROFILE_MAX_PULSES) - || (segmentNumber != PlsrActiveConfig.segmentCount)) - { - return 0U; - } - - segment = &PlsrActiveConfig.segments[segmentNumber - 1U]; - if (segment->jumpSegment != 0U) + if ((PlsrActiveConfig.outputMode != PLSR_OUTPUT_PULSE_DIR) + || (pulseCount == 0ULL) || (pulseCount > 0xFFFFFFFFULL) + || (segmentNumber == 0U) + || (segmentNumber > PlsrActiveConfig.segmentCount)) { return 0U; } - endFrequencyHz = PlsrEffectiveStopFrequency(targetFrequencyHz); - totalPulses = (uint16_t)pulseCount; - directTime = PlsrShortProfileRampTime(startFrequencyHz, endFrequencyHz); - directWeight = PlsrShortProfileRampWeight(startFrequencyHz, - endFrequencyHz, - directTime); - directRequired = PlsrShortProfileRequiredSteps(directWeight); - if (directRequired > totalPulses) - { - profile->startHz = startFrequencyHz; - profile->peakHz = PlsrShortProfileReachableFrequency( - startFrequencyHz, endFrequencyHz, totalPulses); - profile->endHz = profile->peakHz; - profile->pulseCount = totalPulses; - profile->entryPulses = totalPulses; - profile->active = 1U; - PlsrPrepareShortProfileBoundaries(profile); - return 1U; - } + block.entryHz = startFrequencyHz; + block.cruiseHz = targetFrequencyHz; + block.exitHz = PlsrEffectiveStopFrequency(targetFrequencyHz); + block.pulseBudget = (uint32_t)pulseCount; + block.referenceSpeedHz = PlsrActiveConfig.defaultSpeedHz; + block.accelerationTimeMs = PlsrActiveConfig.accelerationTimeMs; + block.decelerationTimeMs = PlsrActiveConfig.decelerationTimeMs; + block.curveMode = PlsrActiveConfig.curveMode; + block.pulseOutput = (uint8_t)PlsrActiveConfig.pulseOutput; + block.boundary = PLSR_BOUNDARY_STOP; - peakFrequencyHz = PlsrShortProfilePeak(startFrequencyHz, - targetFrequencyHz, - endFrequencyHz, - (uint16_t)pulseCount); - entryTime = PlsrShortProfileRampTime(startFrequencyHz, peakFrequencyHz); - exitTime = PlsrShortProfileRampTime(peakFrequencyHz, endFrequencyHz); - entryWeight = PlsrShortProfileRampWeight(startFrequencyHz, - peakFrequencyHz, - entryTime); - exitWeight = PlsrShortProfileRampWeight(peakFrequencyHz, - endFrequencyHz, - exitTime); - entryRequired = PlsrShortProfileRequiredSteps(entryWeight); - exitRequired = PlsrShortProfileRequiredSteps(exitWeight); - if ((entryRequired == 0UL) && (exitRequired == 0UL)) + segment = &PlsrActiveConfig.segments[segmentNumber - 1U]; + if (segment->waitType == PLSR_EXT_OR_COMPLETE) { - return 0U; + nextSegment = (segment->jumpSegment != 0U) + ? (uint8_t)segment->jumpSegment + : ((segmentNumber < PlsrActiveConfig.segmentCount) + ? (uint8_t)(segmentNumber + 1U) : 0U); } - - profile->startHz = startFrequencyHz; - profile->peakHz = peakFrequencyHz; - profile->endHz = endFrequencyHz; - profile->pulseCount = totalPulses; - - if (totalPulses == 1U) + if (nextSegment != 0U) { - durationWeight = - (uint64_t)((peakFrequencyHz > startFrequencyHz) - ? (peakFrequencyHz - startFrequencyHz) - : (startFrequencyHz - peakFrequencyHz)) * entryTime - + (uint64_t)((peakFrequencyHz > endFrequencyHz) - ? (peakFrequencyHz - endFrequencyHz) - : (endFrequencyHz - peakFrequencyHz)) * exitTime; - if (durationWeight == 0UL) - { - return 0U; - } - singleFrequencyHz = - ((uint64_t)PlsrActiveConfig.defaultSpeedHz * 1000UL - + durationWeight / 2UL) / durationWeight; - if (singleFrequencyHz == 0UL) + currentPositive = PlsrCountPositive; + if (PlsrActiveConfig.positionMode == PLSR_POSITION_RELATIVE) { - singleFrequencyHz = 1UL; + currentPositive = (segment->pulses >= 0L) ? 1U : 0U; + nextPositive = + (PlsrActiveConfig.segments[nextSegment - 1U].pulses >= 0L) + ? 1U : 0U; } - if (singleFrequencyHz > PLSR_FREQUENCY_MAX_HZ) + else if ((segmentNumber == PlsrCurrentSegment) + && (PlsrPredictNextDirection(nextSegment, + &nextPositive) != 0U)) { - singleFrequencyHz = PLSR_FREQUENCY_MAX_HZ; + /* direction resolved from the live absolute position */ } - profile->peakHz = (uint32_t)singleFrequencyHz; - profile->steadyPulses = 1U; - profile->active = 1U; - return 1U; - } - - if ((exitRequired == 0UL) && (peakFrequencyHz != endFrequencyHz)) - { - exitRequired = 1UL; - exitWeight = (uint64_t)2U - * PlsrActiveConfig.defaultSpeedHz * 1000UL; - } - - if ((entryRequired + exitRequired) <= totalPulses) - { - profile->entryPulses = (uint16_t)entryRequired; - profile->exitPulses = (uint16_t)exitRequired; - profile->steadyPulses = - (uint16_t)(totalPulses - profile->entryPulses - - profile->exitPulses); - } - else if (entryRequired == 0UL) - { - profile->exitPulses = totalPulses; - } - else if (exitRequired == 0UL) - { - profile->entryPulses = totalPulses; - } - else - { - totalWeight = entryWeight + exitWeight; - /* Peak planning bounds both weights by this move's pulse budget. */ - scaledEntry = ((uint64_t)totalPulses * entryWeight - + totalWeight / 2UL) / totalWeight; - if (scaledEntry == 0UL) + else { - scaledEntry = 1UL; + nextPositive = (uint8_t)(currentPositive ^ 1U); } - if (scaledEntry >= totalPulses) + if (nextPositive == currentPositive) { - scaledEntry = totalPulses - 1U; + block.exitHz = targetFrequencyHz; + block.boundary = PLSR_BOUNDARY_NEXT_CARRY; } - profile->entryPulses = (uint16_t)scaledEntry; - profile->exitPulses = - (uint16_t)(totalPulses - profile->entryPulses); } - profile->active = 1U; - PlsrPrepareShortProfileBoundaries(profile); + status = PlsrPlannerBegin(&profile->planner, &block, + startFrequencyHz, 0ULL); + if ((status != PLSR_PLANNER_OK) + && (status != PLSR_PLANNER_CLIPPED)) + { + return 0U; + } + profile->startHz = profile->planner.startHz; + profile->peakHz = profile->planner.peakHz; + profile->endHz = profile->planner.endHz; + profile->pulseCount = profile->planner.block.pulseBudget; + profile->entryPulses = profile->planner.entryPulses; + profile->steadyPulses = profile->planner.steadyPulses; + profile->exitPulses = profile->planner.exitPulses; + profile->active = ((profile->startHz != profile->peakHz) + || (profile->peakHz != profile->endHz)) + ? 1U : 0U; return 1U; } static uint32_t PlsrShortProfileTakeFrequency(PLSR_SHORT_PROFILE *profile) { - uint16_t period = profile->nextPeriod; - uint16_t relativePeriod; - uint16_t rampPulseCount; - uint32_t fromHz; - uint32_t toHz; - uint64_t firstBoundaryQ32; - uint64_t secondBoundaryQ32; - uint64_t previousTargetAreaQ32; - uint64_t areaIncrementQ32; - uint64_t firstCachedBoundaryQ32; - uint64_t secondCachedBoundaryQ32; uint32_t frequencyHz; - if ((profile->active == 0U) || (period >= profile->pulseCount)) + if ((profile->active == 0U) + || (profile->nextPeriod >= profile->pulseCount)) { return (profile->endHz == 0UL) ? 1UL : profile->endHz; } - profile->nextPeriod = (uint16_t)(period + 1U); - - if (period < profile->entryPulses) - { - relativePeriod = period; - rampPulseCount = profile->entryPulses; - fromHz = profile->startHz; - toHz = profile->peakHz; - firstCachedBoundaryQ32 = profile->entryFirstBoundaryQ32; - secondCachedBoundaryQ32 = profile->entrySecondBoundaryQ32; - firstBoundaryQ32 = (relativePeriod == 0U) - ? 0ULL : profile->rampBoundaryQ32; - } - else if (period < (uint16_t)(profile->entryPulses - + profile->steadyPulses)) + if (profile->pendingRepeats == 0UL) { - profile->lastRampFrequencyHz = 0UL; - return (profile->peakHz == 0UL) ? 1UL : profile->peakHz; - } - else - { - relativePeriod = - (uint16_t)(period - profile->entryPulses - - profile->steadyPulses); - rampPulseCount = profile->exitPulses; - fromHz = profile->peakHz; - toHz = profile->endHz; - firstCachedBoundaryQ32 = profile->exitFirstBoundaryQ32; - secondCachedBoundaryQ32 = profile->exitSecondBoundaryQ32; - firstBoundaryQ32 = (relativePeriod == 0U) - ? 0ULL : profile->rampBoundaryQ32; + if (PlsrPlannerGenerate(&profile->planner, + &profile->pendingItem, 1U) == 0U) + { + profile->active = 0U; + return (profile->endHz == 0UL) ? 1UL : profile->endHz; + } + profile->pendingRepeats = profile->pendingItem.repeatCount; } - if (relativePeriod == 0U) - { - profile->rampTotalAreaQ32 = - PlsrRampAreaQ32(fromHz, toHz, PLSR_Q32_ONE); - profile->rampAreaStepQ32 = - PlsrDivideU64ByU32(profile->rampTotalAreaQ32, - rampPulseCount, - &profile->rampAreaRemainder); - profile->rampRemainderAccumulator = 0UL; - profile->rampTargetAreaQ32 = 0ULL; - profile->rampBoundaryQ32 = 0ULL; - profile->lastRampPhaseStepQ32 = 0ULL; - profile->lastRampFrequencyHz = 0UL; - } - previousTargetAreaQ32 = profile->rampTargetAreaQ32; - profile->rampTargetAreaQ32 += profile->rampAreaStepQ32; - profile->rampRemainderAccumulator += profile->rampAreaRemainder; - if (profile->rampRemainderAccumulator >= rampPulseCount) + frequencyHz = profile->pendingItem.setting.actualFrequencyHz; + profile->pendingRepeats--; + profile->nextPeriod++; + profile->lastRampFrequencyHz = frequencyHz; + if (profile->nextPeriod >= profile->pulseCount) { - profile->rampTargetAreaQ32++; - profile->rampRemainderAccumulator -= rampPulseCount; + profile->active = 0U; } - if ((uint16_t)(relativePeriod + 1U) >= rampPulseCount) + return frequencyHz; +} + +/* Caller holds the platform critical section. The currently preloaded + period is the single committed pulse; the new stream starts after it. */ +static uint8_t PlsrReplanPulseDir(uint32_t targetHz, + uint32_t totalPulses) +{ + uint32_t appliedHz; + uint32_t epoch; + uint8_t segmentNumber; + uint8_t handoffBank; + + PlsrRamp.active = 0U; + PlsrShortProfile.active = 0U; + PlsrProfileQueueInvalidateGeneration(); + PlsrHandoffPlan.valid = 0U; + if (totalPulses <= 1UL) { - profile->rampTargetAreaQ32 = profile->rampTotalAreaQ32; - secondBoundaryQ32 = PLSR_Q32_ONE; + return 1U; } - else if (relativePeriod == 0U) + + appliedHz = PlsrQueuedFrequencyHz; + if (appliedHz == 0UL) { - secondBoundaryQ32 = firstCachedBoundaryQ32; + appliedHz = PlsrCurrentFrequencyHz; } - else if (relativePeriod == 1U) + if ((appliedHz == 0UL) + || (PlsrPrepareShortProfile(&PlsrShortProfile, + PlsrCurrentSegment, + appliedHz, targetHz, + totalPulses - 1UL) == 0U)) { - secondBoundaryQ32 = secondCachedBoundaryQ32; + return 0U; } - else + PlsrRefreshCurrentHandoffPlan(PlsrShortProfile.endHz); + + epoch = PlsrSegmentEpoch; + segmentNumber = PlsrCurrentSegment; + handoffBank = PlsrPreparedHandoffBank; + (void)PlsrProfileQueueBegin(&PlsrShortProfile, epoch, + segmentNumber, handoffBank); + if (PlsrProfileQueueFill(PLSR_REPLAN_WARMUP_ITEMS) == 0U) { - secondBoundaryQ32 = PlsrRampBoundaryQ32( - fromHz, toHz, firstBoundaryQ32, - profile->rampTargetAreaQ32, - (relativePeriod < 2U) ? 0ULL - : profile->lastRampPhaseStepQ32); - } - areaIncrementQ32 = - profile->rampTargetAreaQ32 - previousTargetAreaQ32; - profile->rampBoundaryQ32 = secondBoundaryQ32; - profile->lastRampPhaseStepQ32 = - secondBoundaryQ32 - firstBoundaryQ32; - frequencyHz = PlsrRampAverageFrequency(areaIncrementQ32, firstBoundaryQ32, - secondBoundaryQ32); - if ((profile->lastRampFrequencyHz != 0UL) - && (((toHz > fromHz) - && (frequencyHz < profile->lastRampFrequencyHz)) - || ((toHz < fromHz) - && (frequencyHz > profile->lastRampFrequencyHz)))) - { - frequencyHz = profile->lastRampFrequencyHz; + PlsrProfileQueueReset(); + PlsrShortProfile.active = 0U; + return 0U; } - profile->lastRampFrequencyHz = frequencyHz; - return frequencyHz; + PlsrExecutor.generation++; + PlsrExecutor.mode = PLSR_EXEC_STREAM; + return 1U; } static void PlsrCopyShortProfile(PLSR_SHORT_PROFILE *destination, const PLSR_SHORT_PROFILE *source) { - destination->active = 0U; - destination->startHz = source->startHz; - destination->peakHz = source->peakHz; - destination->endHz = source->endHz; - destination->pulseCount = source->pulseCount; - destination->entryPulses = source->entryPulses; - destination->steadyPulses = source->steadyPulses; - destination->exitPulses = source->exitPulses; - destination->nextPeriod = source->nextPeriod; - destination->rampBoundaryQ32 = source->rampBoundaryQ32; - destination->rampTotalAreaQ32 = source->rampTotalAreaQ32; - destination->rampTargetAreaQ32 = source->rampTargetAreaQ32; - destination->rampAreaStepQ32 = source->rampAreaStepQ32; - destination->rampAreaRemainder = source->rampAreaRemainder; - destination->rampRemainderAccumulator = - source->rampRemainderAccumulator; - destination->lastRampPhaseStepQ32 = source->lastRampPhaseStepQ32; - destination->entryFirstBoundaryQ32 = source->entryFirstBoundaryQ32; - destination->entrySecondBoundaryQ32 = source->entrySecondBoundaryQ32; - destination->exitFirstBoundaryQ32 = source->exitFirstBoundaryQ32; - destination->exitSecondBoundaryQ32 = source->exitSecondBoundaryQ32; - destination->lastRampFrequencyHz = source->lastRampFrequencyHz; - destination->active = source->active; + *destination = *source; } static uint16_t PlsrProfileQueueCount(void) @@ -2629,7 +1832,7 @@ static void PlsrProfileRecordProducerCycles(uint32_t startCycles) } #endif -static uint8_t PlsrProfileQueueFill(void) +static uint8_t PlsrProfileQueueFill(uint16_t targetCount) { PLSR_SHORT_PROFILE candidate; PLSR_PROFILE_ENTRY entry; @@ -2642,12 +1845,17 @@ static uint8_t PlsrProfileQueueFill(void) uint32_t startCycles; #endif + if ((targetCount == 0U) + || (targetCount > PLSR_PROFILE_QUEUE_CAPACITY)) + { + targetCount = PLSR_PROFILE_QUEUE_CAPACITY; + } while (1) { criticalState = PlsrPlatformEnterCritical(); if ((PlsrProfileQueue.active == 0U) || (PlsrProfileQueue.generatorComplete != 0U) - || (PlsrProfileQueueCount() >= PLSR_PROFILE_QUEUE_CAPACITY)) + || (PlsrProfileQueueCount() >= targetCount)) { PlsrPlatformExitCritical(criticalState); return 1U; @@ -2710,6 +1918,10 @@ static PLSR_PLATFORM_QUEUE_RESULT PlsrProfileQueueCommitNext( if (PlsrProfileQueue.active == 0U) { + if (PlsrProfileQueue.generatorComplete != 0U) + { + PlsrShortProfile.active = 0U; + } return PLSR_PLATFORM_QUEUE_APPLIED; } readIndex = PlsrProfileQueue.readIndex; @@ -2756,6 +1968,7 @@ static PLSR_PLATFORM_QUEUE_RESULT PlsrProfileQueueCommitNext( && (PlsrProfileQueue.generatorComplete != 0U)) { PlsrProfileQueue.active = 0U; + PlsrShortProfile.active = 0U; } return PLSR_PLATFORM_QUEUE_APPLIED; } @@ -2793,8 +2006,10 @@ static void PlsrMaybePlanBoundaryRamp(uint32_t expectedEpoch) uint64_t estimate; uint64_t candidateEstimate; uint64_t remaining; + uint8_t rampActive; - if ((PlsrPulseActive == 0U) || (PlsrBoundaryRampStarted != 0U) + if ((PlsrPulseActive == 0U) + || (PlsrBoundaryRampStarted != 0U) || (PlsrShortProfile.active != 0U) || (PlsrHandoffPlan.valid != 0U) || (PlsrStopRequested != 0U) || (PlsrCurrentSegment == 0U)) @@ -2806,8 +2021,7 @@ static void PlsrMaybePlanBoundaryRamp(uint32_t expectedEpoch) hasNext = PlsrGetNextSegment(&nextSegment); targetHz = PlsrEffectiveStopFrequency(segment->frequencyHz); - if ((PlsrActiveConfig.sendMode == PLSR_SEND_SUBSEQUENT) - && (hasNext != 0U) + if ((hasNext != 0U) && (segment->waitType == PLSR_EXT_OR_COMPLETE)) { if ((PlsrPredictNextDirection(nextSegment, &nextPositive) != 0U) @@ -2880,89 +2094,41 @@ static void PlsrMaybePlanBoundaryRamp(uint32_t expectedEpoch) PlsrBoundaryRampStarted = 1U; PlsrRampStart(PlsrCurrentFrequencyHz, targetHz); + rampActive = PlsrRamp.active; PlsrPlatformExitCritical(criticalState); - if (PlsrRamp.active == 0U) + if (rampActive != 0U) + { + if (PlsrRampAdvance(expectedEpoch) == 0U) + { + PlsrEnterErrorIfEpoch(PLSR_ERROR_TIMER, expectedEpoch); + } + } + else { (void)PlsrApplyFrequency(targetHz, expectedEpoch); } } + static uint8_t PlsrBeginSegmentOutput(uint32_t startFrequencyHz) { uint32_t targetFrequencyHz = PlsrActiveConfig.segments[PlsrCurrentSegment - 1U].frequencyHz; - uint32_t stopFrequencyHz = - PlsrEffectiveStopFrequency(targetFrequencyHz); uint32_t firstFrequencyHz; uint32_t secondFrequencyHz; uint32_t profileEpoch; - uint64_t remainingPulses; - int32_t position; uint8_t profileSegment; uint8_t profileHandoffBank; - uint8_t countPositive; PlsrSegmentClockStarted = 1U; PlsrSegmentElapsedMs = 0UL; - remainingPulses = PlsrRemainingPulses; - position = PlsrPosition; - countPositive = PlsrCountPositive; - if ((PlsrActiveConfig.outputMode == PLSR_OUTPUT_PULSE_DIR) - && ((PlsrActiveConfig.sendMode == PLSR_SEND_COMPLETE) - || (PlsrActiveConfig.sendMode == PLSR_SEND_SUBSEQUENT)) - && (PlsrActiveConfig.positionMode == PLSR_POSITION_RELATIVE) - && (PlsrActiveConfig.segments[PlsrCurrentSegment - 1U].waitType - == PLSR_EXT_OR_COMPLETE) - && (startFrequencyHz == targetFrequencyHz) - && (PlsrPlatformSupportsFinitePulseTrain() != 0U) - && (PlsrBeginFiniteSequenceOutput() != 0U)) - { - return 1U; - } - if ((PlsrActiveConfig.outputMode == PLSR_OUTPUT_PULSE_DIR) - && (((PlsrActiveConfig.sendMode == PLSR_SEND_COMPLETE) - && (PlsrActiveConfig.segments[PlsrCurrentSegment - 1U].waitType - <= PLSR_WAIT_SIGNAL - || (PlsrActiveConfig.segments[ - PlsrCurrentSegment - 1U].waitType - == PLSR_EXT_OR_COMPLETE))) - || ((PlsrActiveConfig.sendMode == PLSR_SEND_SUBSEQUENT) - && (PlsrActiveConfig.segments[PlsrCurrentSegment - 1U].waitType - == PLSR_EXT_OR_COMPLETE))) - && (remainingPulses <= 0xFFFFFFFFUL) - && (((countPositive != 0U) - && (remainingPulses - <= (uint64_t)((int64_t)INT32_MAX - - (int64_t)position))) - || ((countPositive == 0U) - && (remainingPulses - <= (uint64_t)((int64_t)position - - (int64_t)INT32_MIN)))) - && (PlsrPlatformSupportsFinitePulseTrain() != 0U)) - { - if ((startFrequencyHz == targetFrequencyHz) - && (stopFrequencyHz == targetFrequencyHz)) - { - return PlsrBeginFiniteConstantOutput(targetFrequencyHz); - } - if (remainingPulses - > (PlsrRampPulseEstimate(startFrequencyHz, - targetFrequencyHz, - targetFrequencyHz) - + PlsrRampPulseEstimate(targetFrequencyHz, - stopFrequencyHz, - targetFrequencyHz))) - { - return PlsrBeginFiniteProfileOutput(startFrequencyHz, - targetFrequencyHz); - } - } if (PlsrPrepareShortProfile(&PlsrShortProfile, PlsrCurrentSegment, startFrequencyHz, targetFrequencyHz, PlsrRemainingSnapshot()) != 0U) { PlsrRamp.active = 0U; + PlsrRefreshCurrentHandoffPlan(PlsrShortProfile.endHz); firstFrequencyHz = PlsrShortProfileTakeFrequency(&PlsrShortProfile); secondFrequencyHz = @@ -2976,7 +2142,7 @@ static uint8_t PlsrBeginSegmentOutput(uint32_t startFrequencyHz) profileEpoch, profileSegment, profileHandoffBank); - if (PlsrProfileQueueFill() == 0U) + if (PlsrProfileQueueFill(PLSR_PROFILE_QUEUE_CAPACITY) == 0U) { PlsrProfileQueueReset(); PlsrShortProfile.active = 0U; @@ -3002,9 +2168,16 @@ static uint8_t PlsrBeginSegmentOutput(uint32_t startFrequencyHz) PlsrShortProfile.active = 0U; return 0U; } + PlsrExecutor.generation++; + PlsrExecutor.mode = PLSR_EXEC_STREAM; return 1U; } + if (PlsrActiveConfig.outputMode == PLSR_OUTPUT_PULSE_DIR) + { + return 0U; + } + PlsrProfileQueueReset(); PlsrRampStart(startFrequencyHz, targetFrequencyHz); @@ -3027,311 +2200,8 @@ static uint8_t PlsrBeginSegmentOutput(uint32_t startFrequencyHz) { return 0U; } - return 1U; -} - -static uint8_t PlsrBeginFiniteConstantOutput(uint32_t frequencyHz) -{ - PLSR_PLATFORM_TIMER_SETTING setting; - uint32_t actualFrequencyHz; - - if ((PlsrRemainingPulses == 0UL) - || (PlsrPlatformBuildTimerSetting( - (uint8_t)PlsrActiveConfig.pulseOutput, - PLSR_OUTPUT_PULSE_DIR, frequencyHz, &setting) == 0U) - || (PlsrPlatformStartFinitePrepared( - (uint8_t)PlsrActiveConfig.pulseOutput, &setting, - (uint32_t)PlsrRemainingPulses, &actualFrequencyHz) == 0U)) - { - PlsrDiagnosticFinishSegment(0U); - return 0U; - } - PlsrDiagnosticRecordFrequency(frequencyHz, - setting.actualFrequencyHz, - actualFrequencyHz); - PlsrDiagnosticRecordCurveSample(setting.actualFrequencyHz, - actualFrequencyHz); - PlsrDiagnosticQueuedExpectedHz = setting.actualFrequencyHz; - PlsrPulseActive = 1U; - PlsrFinitePulseActive = 1U; - PlsrFinitePublishedPulses = 0UL; - PlsrFiniteTargetPulses = (uint32_t)PlsrRemainingPulses; - PlsrCurrentFrequencyHz = actualFrequencyHz; - PlsrQueuedFrequencyHz = actualFrequencyHz; - PlsrRamp.active = 0U; - PlsrRunStatus = PLSR_STATUS_RUNNING; - return 1U; -} - -static uint8_t PlsrBeginFiniteProfileOutput(uint32_t startFrequencyHz, - uint32_t targetFrequencyHz) -{ - PLSR_PLATFORM_TIMER_SETTING setting; - uint32_t actualFrequencyHz; - uint32_t hardwareStartHz = (startFrequencyHz == 0UL) - ? 1UL : startFrequencyHz; - - if ((PlsrRemainingPulses == 0UL) - || (PlsrPlatformBuildTimerSetting( - (uint8_t)PlsrActiveConfig.pulseOutput, - PLSR_OUTPUT_PULSE_DIR, hardwareStartHz, &setting) == 0U) - || (PlsrPlatformStartFinitePrepared( - (uint8_t)PlsrActiveConfig.pulseOutput, &setting, - (uint32_t)PlsrRemainingPulses, &actualFrequencyHz) == 0U)) - { - PlsrDiagnosticFinishSegment(0U); - return 0U; - } - - PlsrDiagnosticRecordFrequency(hardwareStartHz, - setting.actualFrequencyHz, - actualFrequencyHz); - PlsrDiagnosticRecordCurveSample(setting.actualFrequencyHz, - actualFrequencyHz); - PlsrDiagnosticQueuedExpectedHz = setting.actualFrequencyHz; - PlsrPulseActive = 1U; - PlsrFinitePulseActive = 1U; - PlsrFinitePublishedPulses = 0UL; - PlsrFiniteTargetPulses = (uint32_t)PlsrRemainingPulses; - PlsrCurrentFrequencyHz = actualFrequencyHz; - PlsrQueuedFrequencyHz = actualFrequencyHz; - PlsrBoundaryRampStarted = 0U; - PlsrRampStart(startFrequencyHz, targetFrequencyHz); - if (PlsrRamp.active == 0U) - { - if ((startFrequencyHz != targetFrequencyHz) - && (PlsrApplyFrequency(targetFrequencyHz, - PlsrSegmentEpoch) == 0U)) - { - return 0U; - } - PlsrRunStatus = PLSR_STATUS_RUNNING; - } - return 1U; -} - -static uint8_t PlsrBeginFiniteSequenceOutput(void) -{ - PLSR_PLATFORM_FINITE_STEP steps[PLSR_SEGMENT_COUNT_MAX]; - uint8_t visited[PLSR_SEGMENT_COUNT_MAX] = {0U}; - uint8_t segmentNumber = PlsrCurrentSegment; - uint8_t stepCount = 0U; - uint8_t direction = PlsrCountPositive; - uint32_t actualFrequencyHz; - int64_t projectedPosition = PlsrPosition; - - while (stepCount < PLSR_SEGMENT_COUNT_MAX) - { - const PLSR_SEGMENT_CONFIG *segment = - &PlsrActiveConfig.segments[segmentNumber - 1U]; - int64_t displacement = segment->pulses; - uint64_t magnitude = (displacement < 0) - ? (uint64_t)(-displacement) - : (uint64_t)displacement; - uint8_t positive = (displacement >= 0) ? 1U : 0U; - uint8_t nextSegment; - - if ((visited[segmentNumber - 1U] != 0U) || (magnitude == 0UL) - || (magnitude > 0xFFFFFFFFUL) || (positive != direction) - || (segment->waitType != PLSR_EXT_OR_COMPLETE) - || (PlsrPlatformBuildTimerSetting( - (uint8_t)PlsrActiveConfig.pulseOutput, - PLSR_OUTPUT_PULSE_DIR, segment->frequencyHz, - &steps[stepCount].setting) == 0U)) - { - break; - } - projectedPosition += displacement; - if ((projectedPosition > (int64_t)INT32_MAX) - || (projectedPosition < (int64_t)INT32_MIN)) - { - break; - } - visited[segmentNumber - 1U] = 1U; - steps[stepCount].pulseCount = (uint32_t)magnitude; - steps[stepCount].segmentNumber = segmentNumber; - stepCount++; - - if (segment->jumpSegment != 0U) - { - nextSegment = (uint8_t)segment->jumpSegment; - } - else if (segmentNumber < PlsrActiveConfig.segmentCount) - { - nextSegment = (uint8_t)(segmentNumber + 1U); - } - else - { - break; - } - segmentNumber = nextSegment; - } - - if ((stepCount < 2U) - || (PlsrPlatformStartFiniteSequencePrepared( - (uint8_t)PlsrActiveConfig.pulseOutput, steps, stepCount, - &actualFrequencyHz) == 0U)) - { - return 0U; - } - PlsrDiagnosticRecordFrequency( - steps[0].setting.actualFrequencyHz, - steps[0].setting.actualFrequencyHz, actualFrequencyHz); - PlsrDiagnosticRecordCurveSample(steps[0].setting.actualFrequencyHz, - actualFrequencyHz); - PlsrDiagnosticQueuedExpectedHz = steps[0].setting.actualFrequencyHz; - PlsrPulseActive = 1U; - PlsrFinitePulseActive = 1U; - PlsrFiniteSequenceActive = 1U; - PlsrFinitePublishedPulses = 0UL; - PlsrFiniteTargetPulses = steps[0].pulseCount; - PlsrCurrentFrequencyHz = actualFrequencyHz; - PlsrQueuedFrequencyHz = actualFrequencyHz; - PlsrRamp.active = 0U; - PlsrRunStatus = PLSR_STATUS_RUNNING; - return 1U; -} - -static uint8_t PlsrServiceFiniteBoundaries(void) -{ - uint8_t segmentNumber; - uint8_t sequenceContinues; - uint32_t completedPulses; - uint32_t activeFrequencyHz; - uint8_t handled = 0U; - - while ((PlsrFiniteSequenceActive != 0U) - && (PlsrPlatformTakeFiniteBoundary( - (uint8_t)PlsrActiveConfig.pulseOutput, - &segmentNumber, &completedPulses, - &sequenceContinues, &activeFrequencyHz) != 0U)) - { - uint32_t delta; - uint32_t positionBits; - - if ((segmentNumber != PlsrCurrentSegment) - || (completedPulses < PlsrFinitePublishedPulses)) - { - PlsrEnterError(PLSR_ERROR_INTERNAL); - return 1U; - } - delta = completedPulses - PlsrFinitePublishedPulses; - positionBits = (uint32_t)PlsrPosition; - positionBits = (PlsrCountPositive != 0U) - ? positionBits + delta : positionBits - delta; - PlsrPosition = (int32_t)positionBits; - PlsrPositionCheckpointDirty = 1U; - PlsrDiagnosticFinishFiniteSegment(completedPulses); - handled = 1U; - - if (sequenceContinues != 0U) - { - uint8_t nextSegment; - int32_t pulses; - uint32_t magnitude; - - if (PlsrGetNextSegment(&nextSegment) == 0U) - { - PlsrEnterError(PLSR_ERROR_INTERNAL); - return 1U; - } - pulses = PlsrActiveConfig.segments[nextSegment - 1U].pulses; - magnitude = (pulses < 0) ? (uint32_t)(-pulses) - : (uint32_t)pulses; - PlsrSegmentEpoch++; - PlsrCurrentSegment = nextSegment; - PlsrRemainingPulses = magnitude; - PlsrFinitePublishedPulses = 0UL; - PlsrFiniteTargetPulses = magnitude; - PlsrCurrentFrequencyHz = activeFrequencyHz; - PlsrQueuedFrequencyHz = activeFrequencyHz; - PlsrDiagnosticBeginSegment(nextSegment, magnitude, - PlsrCountPositive); - PlsrDiagnosticRecordFrequency( - PlsrActiveConfig.segments[nextSegment - 1U].frequencyHz, - activeFrequencyHz, activeFrequencyHz); - PlsrDiagnosticRecordCurveSample(activeFrequencyHz, - activeFrequencyHz); - } - else - { - PlsrRemainingPulses = 0UL; - PlsrFinitePublishedPulses = completedPulses; - PlsrFiniteSequenceActive = 0U; - } - } - return handled; -} - -static uint8_t PlsrSyncFiniteProgress(void) -{ - uint32_t completedPulses; - uint32_t delta; - uint32_t positionBits; - - if ((PlsrFinitePulseActive == 0U) - || (PlsrPlatformFiniteProgress( - (uint8_t)PlsrActiveConfig.pulseOutput, - &completedPulses) == 0U)) - { - return 0U; - } - if (completedPulses < PlsrFinitePublishedPulses) - { - return 0U; - } - delta = completedPulses - PlsrFinitePublishedPulses; - if (delta == 0UL) - { - return 1U; - } - - positionBits = (uint32_t)PlsrPosition; - positionBits = (PlsrCountPositive != 0U) - ? positionBits + delta : positionBits - delta; - PlsrPosition = (int32_t)positionBits; - PlsrPositionCheckpointDirty = 1U; - PlsrFinitePublishedPulses = completedPulses; - PlsrRemainingPulses = (uint64_t)PlsrFiniteTargetPulses - - completedPulses; - return 1U; -} - -static uint8_t PlsrServiceFiniteCompletion(void) -{ - uint32_t completedPulses; - uint32_t positionBits; - - (void)PlsrSyncFiniteProgress(); - if ((PlsrFinitePulseActive == 0U) - || (PlsrPlatformTakeFiniteCompletion( - (uint8_t)PlsrActiveConfig.pulseOutput, - &completedPulses) == 0U)) - { - return 0U; - } - - positionBits = (uint32_t)PlsrPosition; - if (completedPulses > PlsrFinitePublishedPulses) - { - uint32_t delta = completedPulses - PlsrFinitePublishedPulses; - - positionBits = (PlsrCountPositive != 0U) - ? positionBits + delta : positionBits - delta; - PlsrPosition = (int32_t)positionBits; - } - PlsrPositionCheckpointDirty = 1U; - PlsrRemainingPulses = 0UL; - PlsrFinitePulseActive = 0U; - PlsrFiniteSequenceActive = 0U; - PlsrFinitePublishedPulses = 0UL; - PlsrFiniteTargetPulses = 0UL; - PlsrPulseActive = 0U; - PlsrDiagnosticFinishSegment(1U); - PlsrBoundaryFrequencyHz = PlsrCurrentFrequencyHz; - PlsrCurrentFrequencyHz = 0UL; - PlsrQueuedFrequencyHz = 0UL; - PlsrBoundaryPending = 1U; + PlsrExecutor.generation++; + PlsrExecutor.mode = PLSR_EXEC_AB_LEGACY; return 1U; } @@ -3507,10 +2377,6 @@ static void PlsrFinishCompleted(void) PlsrPlatformStopPulse((uint8_t)PlsrActiveConfig.pulseOutput); PlsrRemainingPulses = 0UL; PlsrPulseActive = 0U; - PlsrFinitePulseActive = 0U; - PlsrFiniteSequenceActive = 0U; - PlsrFinitePublishedPulses = 0UL; - PlsrFiniteTargetPulses = 0UL; PlsrCutRequested = 0U; PlsrBoundaryPending = 0U; PlsrBoundaryWasCut = 0U; @@ -3531,6 +2397,7 @@ static void PlsrFinishCompleted(void) PlsrHandoffPlan.valid = 0U; PlsrTimerErrorPending = 0U; PlsrRamp.active = 0U; + PlsrExecutor.mode = PLSR_EXEC_IDLE; PlsrInvalidateHandoffPlans(); PlsrRunStatus = PLSR_STATUS_COMPLETED; PlsrError = PLSR_ERROR_NONE; @@ -3544,10 +2411,6 @@ static void PlsrFinishStopped(void) PlsrPlatformStopPulse((uint8_t)PlsrActiveConfig.pulseOutput); PlsrRemainingPulses = 0UL; PlsrPulseActive = 0U; - PlsrFinitePulseActive = 0U; - PlsrFiniteSequenceActive = 0U; - PlsrFinitePublishedPulses = 0UL; - PlsrFiniteTargetPulses = 0UL; PlsrCutRequested = 0U; PlsrBoundaryPending = 0U; PlsrBoundaryWasCut = 0U; @@ -3568,6 +2431,7 @@ static void PlsrFinishStopped(void) PlsrHandoffPlan.valid = 0U; PlsrTimerErrorPending = 0U; PlsrRamp.active = 0U; + PlsrExecutor.mode = PLSR_EXEC_IDLE; PlsrInvalidateHandoffPlans(); PlsrRunStatus = PLSR_STATUS_STOPPED; PlsrError = PLSR_ERROR_NONE; @@ -3581,10 +2445,6 @@ static void PlsrEnterError(PLSR_ERROR error) PlsrPlatformStopPulse((uint8_t)PlsrActiveConfig.pulseOutput); PlsrRemainingPulses = 0UL; PlsrPulseActive = 0U; - PlsrFinitePulseActive = 0U; - PlsrFiniteSequenceActive = 0U; - PlsrFinitePublishedPulses = 0UL; - PlsrFiniteTargetPulses = 0UL; PlsrCutRequested = 0U; PlsrBoundaryPending = 0U; PlsrBoundaryWasCut = 0U; @@ -3605,6 +2465,7 @@ static void PlsrEnterError(PLSR_ERROR error) PlsrHandoffPlan.valid = 0U; PlsrTimerErrorPending = 0U; PlsrRamp.active = 0U; + PlsrExecutor.mode = PLSR_EXEC_IDLE; PlsrInvalidateHandoffPlans(); PlsrRunStatus = PLSR_STATUS_ERROR; PlsrError = error; @@ -3631,16 +2492,18 @@ static void PlsrTransitionToNext(uint8_t allowCarry) static uint8_t PlsrBuildHandoffPlan(uint8_t sourceSegment, const PLSR_CONFIG *frequencyConfig, + uint32_t carryFrequencyHz, PLSR_HANDOFF_PLAN *plan) { const PLSR_SEGMENT_CONFIG *segment; uint8_t nextSegment; int64_t displacement; + uint32_t nextFrequencyHz; uint8_t positive; + uint8_t warmupIndex; plan->valid = 0U; - if ((PlsrActiveConfig.sendMode != PLSR_SEND_SUBSEQUENT) - || (sourceSegment == 0U) + if ((sourceSegment == 0U) || (sourceSegment > PlsrActiveConfig.segmentCount)) { return 0U; @@ -3681,23 +2544,25 @@ static uint8_t PlsrBuildHandoffPlan(uint8_t sourceSegment, plan->magnitude = (displacement < 0) ? (uint64_t)(-displacement) : (uint64_t)displacement; - plan->firstFrequencyHz = - frequencyConfig->segments[nextSegment - 1U].frequencyHz; - if (PlsrPrepareShortProfile(&plan->profile, nextSegment, - plan->firstFrequencyHz, - plan->firstFrequencyHz, - plan->magnitude) != 0U) + if (carryFrequencyHz == 0UL) { - plan->firstFrequencyHz = - PlsrShortProfileTakeFrequency(&plan->profile); + carryFrequencyHz = + PlsrResolvedSegmentFrequency(frequencyConfig, sourceSegment - 1U); } - plan->secondFrequencyHz = plan->firstFrequencyHz; - if ((plan->profile.active != 0U) - && (plan->profile.nextPeriod < plan->profile.pulseCount)) + nextFrequencyHz = + PlsrResolvedSegmentFrequency(frequencyConfig, nextSegment - 1U); + if (PlsrPrepareShortProfile(&plan->profile, nextSegment, + carryFrequencyHz, nextFrequencyHz, + plan->magnitude) == 0U) { - plan->secondFrequencyHz = - PlsrShortProfileTakeFrequency(&plan->profile); + return 0U; } + plan->firstFrequencyHz = + PlsrShortProfileTakeFrequency(&plan->profile); + plan->secondFrequencyHz = + (plan->profile.nextPeriod < plan->profile.pulseCount) + ? PlsrShortProfileTakeFrequency(&plan->profile) + : plan->firstFrequencyHz; plan->firstRequestedFrequencyHz = plan->firstFrequencyHz; plan->secondRequestedFrequencyHz = plan->secondFrequencyHz; if ((PlsrPlatformBuildTimerSetting( @@ -3711,6 +2576,25 @@ static uint8_t PlsrBuildHandoffPlan(uint8_t sourceSegment, { return 0U; } + plan->warmupCount = 0U; + for (warmupIndex = 0U; + (warmupIndex < PLSR_HANDOFF_WARMUP_ITEMS) + && (plan->profile.nextPeriod < plan->profile.pulseCount); + warmupIndex++) + { + PLSR_PROFILE_ENTRY *entry = &plan->warmup[warmupIndex]; + + entry->requestedFrequencyHz = + PlsrShortProfileTakeFrequency(&plan->profile); + if (PlsrPlatformBuildTimerSetting( + (uint8_t)PlsrActiveConfig.pulseOutput, + (uint8_t)PlsrActiveConfig.outputMode, + entry->requestedFrequencyHz, &entry->setting) == 0U) + { + return 0U; + } + plan->warmupCount++; + } plan->firstFrequencyHz = plan->firstSetting.actualFrequencyHz; plan->secondFrequencyHz = plan->secondSetting.actualFrequencyHz; plan->nextSegment = nextSegment; @@ -3738,12 +2622,35 @@ static uint8_t PlsrBuildHandoffPlanBank( { destination = &PlsrPreparedHandoffPlans[buildBank][sourceSegment - 1U]; - (void)PlsrBuildHandoffPlan(sourceSegment, frequencyConfig, + (void)PlsrBuildHandoffPlan(sourceSegment, frequencyConfig, 0UL, destination); } return buildBank; } +static void PlsrRefreshCurrentHandoffPlan(uint32_t terminalFrequencyHz) +{ + PLSR_PLATFORM_TIMER_SETTING terminalSetting; + PLSR_HANDOFF_PLAN *destination; + uint8_t currentSegment = PlsrCurrentSegment; + uint8_t preparedBank = PlsrPreparedHandoffBank; + + if ((currentSegment == 0U) + || (currentSegment > PlsrActiveConfig.segmentCount) + || (PlsrPlatformBuildTimerSetting( + (uint8_t)PlsrActiveConfig.pulseOutput, + (uint8_t)PlsrActiveConfig.outputMode, + terminalFrequencyHz, &terminalSetting) == 0U)) + { + return; + } + destination = &PlsrPreparedHandoffPlans[preparedBank] + [currentSegment - 1U]; + (void)PlsrBuildHandoffPlan(currentSegment, &PlsrActiveConfig, + terminalSetting.actualFrequencyHz, + destination); +} + static uint8_t PlsrSelectPreparedHandoffPlan(PLSR_HANDOFF_PLAN *plan) { const PLSR_HANDOFF_PLAN *prepared; @@ -3753,7 +2660,6 @@ static uint8_t PlsrSelectPreparedHandoffPlan(PLSR_HANDOFF_PLAN *plan) plan->valid = 0U; currentSegment = PlsrCurrentSegment; if ((PlsrRemainingPulses != 1UL) - || (PlsrActiveConfig.sendMode != PLSR_SEND_SUBSEQUENT) || (PlsrStopRequested != 0U) || (PlsrCountOverflowPending != 0U) || (PlsrCutRequested != 0U) @@ -3771,17 +2677,7 @@ static uint8_t PlsrSelectPreparedHandoffPlan(PLSR_HANDOFF_PLAN *plan) return 0U; } - plan->magnitude = prepared->magnitude; - plan->firstFrequencyHz = prepared->firstFrequencyHz; - plan->secondFrequencyHz = prepared->secondFrequencyHz; - plan->firstRequestedFrequencyHz = prepared->firstRequestedFrequencyHz; - plan->secondRequestedFrequencyHz = prepared->secondRequestedFrequencyHz; - plan->firstSetting = prepared->firstSetting; - plan->secondSetting = prepared->secondSetting; - PlsrCopyShortProfile(&plan->profile, &prepared->profile); - plan->nextSegment = prepared->nextSegment; - plan->positive = prepared->positive; - plan->valid = 1U; + *plan = *prepared; return 1U; } @@ -3793,8 +2689,7 @@ static uint8_t PlsrPrepareFutureHandoffQueue(void) PlsrProfileQueueReset(); if ((currentSegment == 0U) - || (currentSegment > PlsrActiveConfig.segmentCount) - || (PlsrActiveConfig.sendMode != PLSR_SEND_SUBSEQUENT)) + || (currentSegment > PlsrActiveConfig.segmentCount)) { return 1U; } @@ -3808,7 +2703,7 @@ static uint8_t PlsrPrepareFutureHandoffQueue(void) PlsrSegmentEpoch + 1UL, prepared->nextSegment, preparedBank); - if (PlsrProfileQueueFill() == 0U) + if (PlsrProfileQueueFill(PLSR_PROFILE_QUEUE_CAPACITY) == 0U) { PlsrProfileQueueReset(); return 0U; @@ -3816,6 +2711,39 @@ static uint8_t PlsrPrepareFutureHandoffQueue(void) return 1U; } +/* The first two periods are already in the timer pipeline. Seed a small + precomputed tail here so the 1 ms producer has time to resume without + doing planner math in the pulse IRQ. */ +static void PlsrActivateHandoffQueueFromIrq( + const PLSR_HANDOFF_PLAN *plan, + uint32_t producerEpoch) +{ + uint8_t index; + + PlsrProfileQueue.generation++; + PlsrProfileQueue.readIndex = 0U; + PlsrProfileQueue.writeIndex = plan->warmupCount; + for (index = 0U; index < plan->warmupCount; index++) + { + PlsrProfileQueue.entries[index] = plan->warmup[index]; + } + PlsrCopyShortProfile(&PlsrProfileQueue.producerProfile, + &plan->profile); + PlsrProfileQueue.producerEpoch = producerEpoch; + PlsrProfileQueue.producerSegment = plan->nextSegment; + PlsrProfileQueue.preparedHandoffBank = PlsrPreparedHandoffBank; + PlsrProfileQueue.generatorComplete = + (plan->profile.active == 0U) ? 1U : 0U; + PlsrProfileQueue.active = + ((plan->warmupCount != 0U) || (plan->profile.active != 0U)) + ? 1U : 0U; + PlsrCopyShortProfile(&PlsrShortProfile, &plan->profile); + if (PlsrProfileQueue.active != 0U) + { + PlsrShortProfile.active = 1U; + } +} + static PLSR_PLATFORM_QUEUE_RESULT PlsrPrimeHandoff(void) { PLSR_HANDOFF_PLAN candidatePlan; @@ -3850,24 +2778,13 @@ static PLSR_PLATFORM_QUEUE_RESULT PlsrPrimeHandoff(void) PlsrDiagnosticQueuedExpectedHz = candidatePlan.firstSetting.actualFrequencyHz; PlsrQueuedFrequencyHz = actualFrequencyHz; - PlsrHandoffPlan.magnitude = candidatePlan.magnitude; + PlsrHandoffPlan = candidatePlan; PlsrHandoffPlan.firstFrequencyHz = actualFrequencyHz; - PlsrHandoffPlan.secondFrequencyHz = candidatePlan.secondFrequencyHz; - PlsrHandoffPlan.firstRequestedFrequencyHz = - candidatePlan.firstRequestedFrequencyHz; - PlsrHandoffPlan.secondRequestedFrequencyHz = - candidatePlan.secondRequestedFrequencyHz; - PlsrHandoffPlan.firstSetting = candidatePlan.firstSetting; - PlsrHandoffPlan.secondSetting = candidatePlan.secondSetting; - PlsrCopyShortProfile(&PlsrHandoffPlan.profile, - &candidatePlan.profile); - PlsrHandoffPlan.nextSegment = candidatePlan.nextSegment; - PlsrHandoffPlan.positive = candidatePlan.positive; PlsrHandoffPlan.valid = 1U; return PLSR_PLATFORM_QUEUE_APPLIED; } -static PLSR_PLATFORM_QUEUE_RESULT PlsrTrySubsequentHandoff(void) +static PLSR_PLATFORM_QUEUE_RESULT PlsrTryContinuousHandoff(void) { uint32_t actualQueuedFrequencyHz; uint32_t currentFrequencyHz; @@ -3878,7 +2795,6 @@ static PLSR_PLATFORM_QUEUE_RESULT PlsrTrySubsequentHandoff(void) PLSR_PLATFORM_QUEUE_RESULT result; if ((PlsrHandoffPlan.valid == 0U) - || (PlsrActiveConfig.sendMode != PLSR_SEND_SUBSEQUENT) || (PlsrStopRequested != 0U) || (PlsrCountOverflowPending != 0U) || (PlsrCutRequested != 0U) @@ -3889,17 +2805,6 @@ static PLSR_PLATFORM_QUEUE_RESULT PlsrTrySubsequentHandoff(void) { return PLSR_PLATFORM_QUEUE_FAILED; } - if ((PlsrHandoffPlan.profile.active != 0U) - && ((PlsrProfileQueue.active == 0U) - || (PlsrProfileQueue.producerEpoch != PlsrSegmentEpoch + 1UL) - || (PlsrProfileQueue.producerSegment != nextSegment) - || (PlsrProfileQueue.preparedHandoffBank - != PlsrPreparedHandoffBank))) - { - PlsrTimerErrorPending = 1U; - return PLSR_PLATFORM_QUEUE_FAILED; - } - PlsrDeferredFrequencyPending = 0U; result = PlsrPlatformLoadPreparedFromIrq( (uint8_t)PlsrActiveConfig.pulseOutput, @@ -3916,8 +2821,6 @@ static PLSR_PLATFORM_QUEUE_RESULT PlsrTrySubsequentHandoff(void) return result; } - PlsrCopyShortProfile(&PlsrShortProfile, - &PlsrHandoffPlan.profile); PlsrDiagnosticRecordFrequency( PlsrHandoffPlan.secondRequestedFrequencyHz, PlsrHandoffPlan.secondSetting.actualFrequencyHz, @@ -3927,6 +2830,8 @@ static PLSR_PLATFORM_QUEUE_RESULT PlsrTrySubsequentHandoff(void) PlsrQueuedFrequencyHz = actualQueuedFrequencyHz; PlsrDiagnosticFinishSegment(1U); PlsrSegmentEpoch++; + PlsrActivateHandoffQueueFromIrq(&PlsrHandoffPlan, + PlsrSegmentEpoch); PlsrCurrentSegment = nextSegment; PlsrRemainingPulses = magnitude; PlsrCountPositive = positive; @@ -4071,8 +2976,7 @@ static void PlsrHandleBoundary(uint8_t extEdge) break; case PLSR_EXT_OR_COMPLETE: - PlsrTransitionToNext( - (PlsrActiveConfig.sendMode == PLSR_SEND_SUBSEQUENT) ? 1U : 0U); + PlsrTransitionToNext(1U); break; default: @@ -4238,15 +3142,12 @@ uint8_t PlsrInit(void) (void)memset(&PlsrShortProfile, 0, sizeof(PlsrShortProfile)); (void)memset(&PlsrProfileQueue, 0, sizeof(PlsrProfileQueue)); (void)memset(&PlsrHandoffPlan, 0, sizeof(PlsrHandoffPlan)); + (void)memset(&PlsrExecutor, 0, sizeof(PlsrExecutor)); (void)memset(PlsrPreparedHandoffPlans, 0, sizeof(PlsrPreparedHandoffPlans)); PlsrPreparedHandoffBank = 0U; PlsrRemainingPulses = 0UL; PlsrPulseActive = 0U; - PlsrFinitePulseActive = 0U; - PlsrFiniteSequenceActive = 0U; - PlsrFinitePublishedPulses = 0UL; - PlsrFiniteTargetPulses = 0UL; PlsrCutRequested = 0U; PlsrBoundaryPending = 0U; PlsrBoundaryWasCut = 0U; @@ -4286,8 +3187,6 @@ uint8_t PlsrInit(void) PlsrProfileProducerItemCount = 0UL; PlsrProfileProducerTotalCycles = 0UL; PlsrProfileProducerMaxItemCycles = 0UL; - PlsrFiniteRampUpdateCount = 0UL; - PlsrFiniteRampUpdateMaxCycles = 0UL; #endif PlsrError = PLSR_ERROR_NONE; PlsrRunStatus = PLSR_STATUS_IDLE; @@ -4358,8 +3257,14 @@ static void PlsrExecuteStart(void) { uint32_t criticalState; uint8_t handoffBank; + uint8_t index; PlsrActiveConfig = PlsrCommandMailbox.startConfig; + for (index = 0U; index < PlsrActiveConfig.segmentCount; index++) + { + PlsrActiveConfig.segments[index].frequencyHz = + PlsrResolvedSegmentFrequency(&PlsrActiveConfig, index); + } handoffBank = PlsrBuildHandoffPlanBank(&PlsrActiveConfig); criticalState = PlsrPlatformEnterCritical(); PlsrPreparedHandoffBank = handoffBank; @@ -4384,6 +3289,8 @@ static uint8_t PlsrExecuteStop(void) { uint32_t criticalState; uint32_t stopTargetHz; + uint32_t appliedHz; + uint64_t drainPulses; if (PlsrIsBusy() == 0U) { @@ -4416,9 +3323,35 @@ static uint8_t PlsrExecuteStop(void) stopTargetHz = PlsrEffectiveStopFrequency( PlsrActiveConfig.segments[PlsrCurrentSegment - 1U].frequencyHz); - if (stopTargetHz > PlsrCurrentFrequencyHz) + appliedHz = (PlsrQueuedFrequencyHz != 0UL) + ? PlsrQueuedFrequencyHz : PlsrCurrentFrequencyHz; + if (stopTargetHz > appliedHz) + { + stopTargetHz = appliedHz; + } + if (PlsrActiveConfig.outputMode == PLSR_OUTPUT_PULSE_DIR) { - stopTargetHz = PlsrCurrentFrequencyHz; + drainPulses = PlsrRampPulseEstimate(appliedHz, stopTargetHz, + appliedHz) + 1ULL; + if (drainPulses > PlsrRemainingPulses) + { + drainPulses = PlsrRemainingPulses; + } + if (drainPulses == 0ULL) + { + drainPulses = 1ULL; + } + PlsrRemainingPulses = drainPulses; + if (PlsrReplanPulseDir(stopTargetHz, + (uint32_t)drainPulses) == 0U) + { + PlsrPlatformExitCritical(criticalState); + PlsrEnterError(PLSR_ERROR_TIMER); + return 1U; + } + PlsrRunStatus = PLSR_STATUS_DECELERATING; + PlsrPlatformExitCritical(criticalState); + return 1U; } PlsrRampStart(PlsrCurrentFrequencyHz, stopTargetHz); PlsrRunStatus = PLSR_STATUS_DECELERATING; @@ -4465,6 +3398,7 @@ static void PlsrExecuteClear(void) PlsrDeferredFrequencyPending = 0U; PlsrTimerErrorPending = 0U; PlsrRamp.active = 0U; + PlsrExecutor.mode = PLSR_EXEC_IDLE; PlsrError = PLSR_ERROR_NONE; PlsrRunStatus = PLSR_STATUS_IDLE; PlsrMarkPersistenceDirty(PLSR_CONFIG_SAVE_DELAY_MS); @@ -4518,6 +3452,7 @@ void PlsrPoll1ms(void) uint8_t extEdge; uint8_t activeSegmentNumber; uint8_t applyDynamicFrequency = 0U; + uint8_t pulseDirReplanFailed = 0U; PLSR_SEGMENT_CONFIG *activeSegment; uint32_t criticalState; uint32_t newTargetHz; @@ -4535,12 +3470,8 @@ void PlsrPoll1ms(void) PlsrPollPositionCheckpoint(); - (void)PlsrServiceFiniteBoundaries(); - (void)PlsrServiceFiniteCompletion(); - (void)PlsrSyncFiniteProgress(); - if ((PlsrShortProfile.active != 0U) - && (PlsrProfileQueueFill() == 0U)) + && (PlsrProfileQueueFill(PLSR_PROFILE_QUEUE_CAPACITY) == 0U)) { PlsrEnterError(PLSR_ERROR_TIMER); return; @@ -4650,14 +3581,34 @@ void PlsrPoll1ms(void) PlsrShortProfile.active = 0U; PlsrProfileQueueInvalidateGeneration(); PlsrBoundaryRampStarted = 0U; - PlsrRampStart(PlsrCurrentFrequencyHz, newTargetHz); - if (PlsrRamp.active == 0U) + if (PlsrActiveConfig.outputMode == PLSR_OUTPUT_PULSE_DIR) { - applyDynamicFrequency = 1U; + uint64_t remaining = PlsrRemainingPulses; + + if ((remaining == 0ULL) || (remaining > 0xFFFFFFFFULL) + || (PlsrReplanPulseDir(newTargetHz, + (uint32_t)remaining) == 0U)) + { + pulseDirReplanFailed = 1U; + } + } + else + { + PlsrRampStart(PlsrCurrentFrequencyHz, newTargetHz); + if (PlsrRamp.active == 0U) + { + applyDynamicFrequency = 1U; + } } } PlsrPlatformExitCritical(criticalState); + if (pulseDirReplanFailed != 0U) + { + PlsrEnterErrorIfEpoch(PLSR_ERROR_TIMER, pollEpoch); + return; + } + if (applyDynamicFrequency != 0U) { if (PlsrApplyFrequency(newTargetHz, pollEpoch) == 0U) @@ -4673,7 +3624,8 @@ void PlsrPoll1ms(void) PlsrPlatformExitCritical(criticalState); } - if ((PlsrShortProfile.active == 0U) + if ((PlsrActiveConfig.outputMode == PLSR_OUTPUT_AB) + && (PlsrShortProfile.active == 0U) && (PlsrRamp.active != 0U) && (PlsrRampAdvance(pollEpoch) == 0U)) { @@ -4687,7 +3639,8 @@ void PlsrPoll1ms(void) PlsrPlatformExitCritical(criticalState); return; } - if ((PlsrStopRequested != 0U) && (PlsrRamp.active == 0U)) + if ((PlsrActiveConfig.outputMode == PLSR_OUTPUT_AB) + && (PlsrStopRequested != 0U) && (PlsrRamp.active == 0U)) { if (PlsrStopPulsesRemaining == 0U) { @@ -4721,7 +3674,10 @@ void PlsrPoll1ms(void) return; } - PlsrMaybePlanBoundaryRamp(pollEpoch); + if (PlsrActiveConfig.outputMode == PLSR_OUTPUT_AB) + { + PlsrMaybePlanBoundaryRamp(pollEpoch); + } } void PlsrPulseTimerIrq(uint8_t pulseOutput) @@ -4787,7 +3743,7 @@ void PlsrPulseTimerIrq(uint8_t pulseOutput) { if (remainingPulses == 0UL) { - queueResult = PlsrTrySubsequentHandoff(); + queueResult = PlsrTryContinuousHandoff(); if (queueResult != PLSR_PLATFORM_QUEUE_FAILED) { return; @@ -4842,8 +3798,7 @@ void PlsrPulseTimerIrq(uint8_t pulseOutput) } if ((PlsrShortProfile.active == 0U) - && (remainingPulses == 1UL) - && (PlsrActiveConfig.sendMode == PLSR_SEND_SUBSEQUENT)) + && (remainingPulses == 1UL)) { if (PlsrPrimeHandoff() == PLSR_PLATFORM_QUEUE_STALE) { @@ -5164,8 +4119,8 @@ PLSR_MB_RESULT PlsrModbusWriteHolding(uint16_t startAddress, && (PlsrCurrentSegment != 0U) && (PlsrCurrentSegment <= PlsrCandidateConfig.segmentCount)) { - drainedSegmentTargetHz = - PlsrCandidateConfig.segments[PlsrCurrentSegment - 1U].frequencyHz; + drainedSegmentTargetHz = PlsrResolvedSegmentFrequency( + &PlsrCandidateConfig, PlsrCurrentSegment - 1U); } } PlsrShadowConfig = PlsrCandidateConfig; @@ -5175,28 +4130,11 @@ PLSR_MB_RESULT PlsrModbusWriteHolding(uint16_t startAddress, { uint8_t frequencyChanged = (PlsrActiveConfig.segments[index].frequencyHz - != PlsrCandidateConfig.segments[index].frequencyHz) + != PlsrResolvedSegmentFrequency(&PlsrCandidateConfig, index)) ? 1U : 0U; PlsrActiveConfig.segments[index].frequencyHz = - PlsrCandidateConfig.segments[index].frequencyHz; - if ((frequencyChanged != 0U) - && (PlsrFiniteSequenceActive != 0U) - && (index + 1U != PlsrCurrentSegment)) - { - PLSR_PLATFORM_TIMER_SETTING setting; - - if (PlsrPlatformBuildTimerSetting( - (uint8_t)PlsrActiveConfig.pulseOutput, - PLSR_OUTPUT_PULSE_DIR, - PlsrCandidateConfig.segments[index].frequencyHz, - &setting) != 0U) - { - (void)PlsrPlatformUpdateFiniteStep( - (uint8_t)PlsrActiveConfig.pulseOutput, - (uint8_t)(index + 1U), &setting); - } - } + PlsrResolvedSegmentFrequency(&PlsrCandidateConfig, index); if ((frequencyChanged != 0U) && (index + 1U == PlsrCurrentSegment) && (PlsrStopRequested == 0U) @@ -5204,7 +4142,8 @@ PLSR_MB_RESULT PlsrModbusWriteHolding(uint16_t startAddress, && (PlsrRunStatus != PLSR_STATUS_WAITING)) { PlsrFrequencyUpdateTargetHz = - PlsrCandidateConfig.segments[index].frequencyHz; + PlsrResolvedSegmentFrequency(&PlsrCandidateConfig, + index); PlsrFrequencyUpdateSegment = (uint8_t)(index + 1U); PlsrFrequencyUpdatePending = 1U; } @@ -5286,13 +4225,6 @@ PLSR_MB_RESULT PlsrModbusWriteHolding(uint16_t startAddress, } #ifdef PLSR_HOST_TEST -uint64_t PlsrTestDivideU64ByU32(uint64_t dividend, - uint32_t divisor, - uint32_t *remainder) -{ - return PlsrDivideU64ByU32(dividend, divisor, remainder); -} - void PlsrTestSetPosition(int32_t position, uint8_t positionValid) { uint32_t criticalState = PlsrPlatformEnterCritical(); diff --git a/PLSR/Src/plsr_internal.h b/PLSR/Src/plsr_internal.h index 044ff9f..b277314 100644 --- a/PLSR/Src/plsr_internal.h +++ b/PLSR/Src/plsr_internal.h @@ -4,11 +4,21 @@ #include #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; diff --git a/PLSR/Src/plsr_planner.c b/PLSR/Src/plsr_planner.c new file mode 100644 index 0000000..690fcfc --- /dev/null +++ b/PLSR/Src/plsr_planner.c @@ -0,0 +1,735 @@ +#include "plsr_planner.h" + +#include +#include + +#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; +} diff --git a/PLSR/Src/plsr_planner.h b/PLSR/Src/plsr_planner.h new file mode 100644 index 0000000..590e81e --- /dev/null +++ b/PLSR/Src/plsr_planner.h @@ -0,0 +1,89 @@ +#ifndef PLSR_PLANNER_H +#define PLSR_PLANNER_H + +#include "plsr_platform.h" + +#include + +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 */ diff --git a/PLSR/Src/plsr_platform.h b/PLSR/Src/plsr_platform.h index 77ca910..761e0dc 100644 --- a/PLSR/Src/plsr_platform.h +++ b/PLSR/Src/plsr_platform.h @@ -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, diff --git a/PLSR/Src/plsr_platform_f407.c b/PLSR/Src/plsr_platform_f407.c index 732250c..6980dff 100644 --- a/PLSR/Src/plsr_platform_f407.c +++ b/PLSR/Src/plsr_platform_f407.c @@ -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]; diff --git a/tests/plsr_host/run_tests.ps1 b/tests/plsr_host/run_tests.ps1 index 9c2cd76..6e55aef 100644 --- a/tests/plsr_host/run_tests.ps1 +++ b/tests/plsr_host/run_tests.ps1 @@ -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" diff --git a/tests/plsr_host/test_plsr_host.c b/tests/plsr_host/test_plsr_host.c index 85b574c..d50046c 100644 --- a/tests/plsr_host/test_plsr_host.c +++ b/tests/plsr_host/test_plsr_host.c @@ -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; } +