8 коммитов

Автор SHA1 Сообщение Дата
  ywh dd4a42e3b9 修改了显示频谱的脚本 3 недель назад
  ywh ee75c7e18d 修改了上位机 3 недель назад
  ywh f6d93ff341 修复了按键消抖 3 недель назад
  ywh 3478f1c15a 加入了按键消抖 3 недель назад
  ywh 9e3c942171 优化了高频计算 3 недель назад
  ywh a6c667d485 优化前上传一次 3 недель назад
  ywh 47dabdb226 加了点注释 3 недель назад
  ywh 7810b61ba7 改了一版上位机显示波形,mcu程序加了点注释 4 недель назад
20 измененных файлов: 2277 добавлений и 789 удалений
  1. +5
    -0
      .gitignore
  2. +44
    -9
      Core/Src/main.c
  3. +0
    -134
      Document/PLSR_document/PLSR地址映射.md
  4. +0
    -388
      Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md
  5. +274
    -3
      EWARM/Modbus.ewp
  6. Двоичные данные
      HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc
  7. Двоичные данные
      HostComputer/__pycache__/plsr_control_panel.cpython-314.pyc
  8. Двоичные данные
      HostComputer/__pycache__/plsr_modbus_product_test.cpython-314.pyc
  9. +228
    -49
      HostComputer/bin_to_time_freq.py
  10. +432
    -0
      HostComputer/bin_to_time_freq1.py
  11. +101
    -4
      HostComputer/plsr_control_panel.py
  12. +5
    -0
      HostComputer/plsr_modbus_product_test.py
  13. +7
    -4
      Middlewares/Third_Party/Micrium/Config/app_cfg.h
  14. +7
    -0
      PLSR/Inc/plsr.h
  15. +351
    -91
      PLSR/Src/plsr.c
  16. +524
    -54
      PLSR/Src/plsr_planner.c
  17. +102
    -40
      PLSR/Src/plsr_planner.h
  18. +102
    -5
      PLSR/Src/plsr_platform_f407.c
  19. +8
    -1
      tests/plsr_host/run_tests.ps1
  20. +87
    -7
      tests/plsr_host/test_plsr_host.c

+ 5
- 0
.gitignore Просмотреть файл

@@ -30,3 +30,8 @@ tmp/
.codex-tmp/
Document/PLSR_document/波形/
Document/PLSR_document/上位机图片/
.codex_docx_qa_final5/
.codex_docx_qa_final/
.codex_docx_qa_final2/
.codex_docx_qa_final4/
.codex_docx_qa_final3/

+ 44
- 9
Core/Src/main.c Просмотреть файл

@@ -47,7 +47,8 @@ DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx;
/* USER CODE BEGIN PV */

static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];
static OS_STK AppTaskPlsrStk[APP_TASK_PLSR_STK_SIZE];
static OS_STK AppTaskModbusStk[APP_TASK_MODBUS_STK_SIZE];
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
@@ -56,7 +57,8 @@ static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);
/* USER CODE BEGIN PFP */
static void AppTaskStart(void *pArg);
static void AppTaskPlsr(void *pArg);
static void AppTaskModbus(void *pArg);
extern void PlsrPlatformForceSafeOutputsFromFault(void);

/* USER CODE END PFP */
@@ -64,7 +66,31 @@ extern void PlsrPlatformForceSafeOutputsFromFault(void);
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

static void AppTaskStart(void *pArg)
static void AppTaskPlsr(void *pArg)
{
INT32U lastPollTick;
INT32U currentTick;

(void)pArg;
lastPollTick = OSTimeGet() - 1U;

while (1)
{
currentTick = OSTimeGet();
if (currentTick != lastPollTick)
{
lastPollTick = currentTick;
PlsrPoll1ms();
}

if (PlsrServiceProfileProducer(APP_PLSR_PRODUCER_BUDGET) == 0U)
{
OSTimeDly(1U);
}
}
}

static void AppTaskModbus(void *pArg)
{
(void)pArg;

@@ -76,12 +102,10 @@ static void AppTaskStart(void *pArg)
while (1)
{
ModbusSlavePoll();
PlsrPoll1ms();
if (ModbusSlaveIsIdle() != 0U)
{
PlsrServicePersistence();
}
OSTimeDly(1U);
}
}

@@ -129,10 +153,21 @@ int main(void)

OSInit();

osError = OSTaskCreateExt(AppTaskStart, 0,
&AppTaskStartStk[APP_TASK_START_STK_SIZE - 1u],
APP_TASK_START_PRIO, APP_TASK_START_PRIO,
&AppTaskStartStk[0], APP_TASK_START_STK_SIZE, 0,
osError = OSTaskCreateExt(AppTaskPlsr, 0,
&AppTaskPlsrStk[APP_TASK_PLSR_STK_SIZE - 1u],
APP_TASK_PLSR_PRIO, APP_TASK_PLSR_PRIO,
&AppTaskPlsrStk[0], APP_TASK_PLSR_STK_SIZE, 0,
(OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR));

if (osError != OS_ERR_NONE)
{
Error_Handler();
}

osError = OSTaskCreateExt(AppTaskModbus, 0,
&AppTaskModbusStk[APP_TASK_MODBUS_STK_SIZE - 1u],
APP_TASK_MODBUS_PRIO, APP_TASK_MODBUS_PRIO,
&AppTaskModbusStk[0], APP_TASK_MODBUS_STK_SIZE, 0,
(OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR));

if (osError != OS_ERR_NONE)


+ 0
- 134
Document/PLSR_document/PLSR地址映射.md Просмотреть файл

@@ -1,134 +0,0 @@
# PLSR Modbus 地址映射(2026)

> 文档版本:V2.0
> 字节序:每个 32 位量均为低 16 位字在前、高 16 位字在后
> 访问方式:配置和控制支持 FC06/FC16,状态和诊断仅支持 FC03

## 1 固定配置区 `0x1000..0x1197`

### 1.1 公共参数 `0x1000..0x1013`

| 地址 | 类型 | 名称 | 取值/单位 |
|---|---|---|---|
| `0x1000` | U16 | 脉冲输出 | `0..3`,对应 Y0..Y3 |
| `0x1001` | U16 | 方向输出 | `0..3`,对应 Y12..Y15;AB 模式忽略 |
| `0x1002` | U16 | 等待输入 | `0=X4`,`1=X5` |
| `0x1003` | U16 | 外部中断输入 | `0=X4`,`1=X5` |
| `0x1004` | U16 | 发送模式 | `0=完整发送`,`1=后续发送` |
| `0x1005` | U16 | 方向延时 | ms;AB 模式忽略 |
| `0x1006` | U16 | 方向负逻辑 | `0/1`;AB 模式忽略 |
| `0x1007` | U16 | 曲线模式 | `0=直线`,`1=S 曲线`,`2=正弦曲线` |
| `0x1008` | U16 | 位置模式 | `0=相对`,`1=绝对` |
| `0x1009` | U16 | 有效段数 | `1..10` |
| `0x100A` | U16 | 起始段 | `1..10`,启动时不得超过有效段数 |
| `0x100B..0x100C` | U32 | 默认速度 | Hz,`1..100000` |
| `0x100D..0x100E` | U32 | 起始速度 | Hz,`0..100000` |
| `0x100F` | U16 | 保留 | 读为 0,仅允许写 0 |
| `0x1010..0x1011` | U32 | 停止速度 | Hz,`0..100000` |
| `0x1012` | U16 | 加速时间 | ms |
| `0x1013` | U16 | 减速时间 | ms |

`0x1014..0x10FF` 为保留区,读为 0,仅允许写 0。32 位参数必须在同一次请求中完整访问两个寄存器。

### 1.2 分段参数 `0x1100..0x1197`

第 `n` 段(`n=1..10`)首地址为 `0x1100 + (n-1)*0x10`。

| 段内偏移 | 类型 | 名称 | 取值/单位 |
|---|---|---|---|
| `+0x0..+0x1` | U32 | 段频率 | Hz,`1..100000` |
| `+0x2..+0x3` | I32 | 位移/脉冲数 | 正数为正向,负数为反向 |
| `+0x4` | U16 | 等待类型 | `0=定时等待`,`1=信号等待`,`2=定时动作`,`3=外部信号`,`4=外部或完成` |
| `+0x5` | U16 | 等待时间 | ms |
| `+0x6` | U16 | 动作时间 | ms |
| `+0x7` | U16 | 跳转段 | `0=不跳转`,`1..10=目标段` |
| `+0x8..+0xF` | U16 | 保留 | 读为 0,仅允许写 0 |

## 2 扩展配置区 `0x1200`

| 地址 | 类型 | 名称 | 取值 |
|---|---|---|---|
| `0x1200` | U16 | 输出模式 | `0=PULSE/DIR`,`1=AB 正交脉冲` |

该参数参与持久化。AB 模式只允许 `0x1000=0`(Y0/Y1)或 `0x1000=2`(Y2/Y3);其他组合返回非法数据值。运行期间不得修改输出模式或脉冲输出。

AB 模式的频率单位是完整正交周期/秒,一个 `00` 起止的四状态周期等于一个指令脉冲:

- 正向:`00 -> 10 -> 11 -> 01 -> 00`
- 反向:`00 -> 01 -> 11 -> 10 -> 00`

方向由段位移的符号决定。独立 DIR 输出、DIR 极性和 DIR 延时在 AB 模式中不参与输出。

## 3 固定状态区 `0x2000..0x2006`(只读)

| 地址 | 类型 | 名称 | 说明 |
|---|---|---|---|
| `0x2000..0x2001` | I32 | 当前位置 | 累计指令脉冲位置 |
| `0x2002..0x2003` | U32 | 当前频率 | Hz |
| `0x2004` | U16 | 运行状态 | `0=未初始化`,`1=空闲`,`2=加速`,`3=运行`,`4=减速`,`5=等待`,`6=暂停`,`7=完成`,`8=停止`,`9=错误` |
| `0x2005` | U16 | 当前段 | `0=无`,`1..10=段号` |
| `0x2006` | U16 | 错误码 | `0=无错误`,其余见 `PLSR_ERROR` 定义 |

## 4 扩展诊断区 `0x2100..0x2119`(只读)

| 地址 | 类型 | 名称 | 说明 |
|---|---|---|---|
| `0x2100` | U16 | 诊断标志 | 位定义见下表 |
| `0x2101` | U16 | 首个锁存原因 | `0=无`,`1=计数`,`2=频率`,`3=曲线`,`4=无效采样` |
| `0x2102` | U16 | 段号 | 当前或末次诊断段 |
| `0x2103` | U16 | 模式与方向 | bit0..7=输出模式,bit8=`1` 正向/`0` 反向 |
| `0x2104..0x2105` | U32 | 期望脉冲数 | 当前或末次诊断段的完整指令脉冲数 |
| `0x2106..0x2107` | U32 | 实测脉冲数 | 当前或末次诊断段的实测值;目标板来自独立硬件计数链 |
| `0x2108..0x2109` | I32 | 计数误差 | `实测 - 期望` |
| `0x210A..0x210B` | U32 | 请求频率 | Hz,规划器本次请求值 |
| `0x210C..0x210D` | U32 | 期望定时器频率 | Hz,根据实际 PSC/ARR 可实现的量化值 |
| `0x210E..0x210F` | U32 | 活动定时器频率 | Hz,实际生效的定时器配置值 |
| `0x2110..0x2111` | I32 | 请求量化误差 | `期望定时器频率 - 请求频率`,Hz |
| `0x2112..0x2113` | U32 | 采样数 | 本次诊断累计的有效样本数 |
| `0x2114..0x2115` | U32 | 不匹配数 | 频率或曲线不匹配的累计样本数 |
| `0x2116..0x2117` | U32 | 最大频率误差 | `abs(活动定时器频率 - 期望定时器频率)`,Hz |
| `0x2118..0x2119` | U32 | 首个不匹配样本 | 无不匹配时为 `0xFFFFFFFF` |

`0x2100` 标志位:

| 位 | 名称 | 置位条件 |
|---|---|---|
| bit0 | `monitoring_active` | 正在采集诊断数据 |
| bit1 | `count_checked` | 已完成计数核对 |
| bit2 | `count_pass` | 计数核对通过 |
| bit3 | `frequency_checked` | 已执行频率核对 |
| bit4 | `frequency_pass` | 频率核对通过 |
| bit5 | `curve_checked` | 已执行加减速次序核对 |
| bit6 | `curve_pass` | 曲线核对通过 |
| bit7 | `fault_latched` | 至少一个诊断故障已锁存 |

诊断使用定时器分频值、完整周期计数和频率变化次序核对输出配置。它能发现内部计数、频率和曲线不一致,但不能代替逻辑分析仪对端子占空比、窄脉冲或电气波形的验收。

## 5 profile 队列诊断区 `0x2200..0x2208`(只读)

| 地址 | 类型 | 名称 | 说明 |
|---|---|---|---|
| `0x2200` | U16 | 队列诊断标志 | bit0=到达低水位,bit1=发生欠载,bit2=规划曲线被裁剪 |
| `0x2201` | U16 | 最小队列深度 | 本次运行期间观测到的最小 profile 项数;尚无样本时为 `0xFFFF` |
| `0x2202` | U16 | 当前队列深度 | 读取时尚未消费的 profile 项数 |
| `0x2203..0x2204` | U32 | 低水位事件数 | 队列跨入低水位的累计次数 |
| `0x2205..0x2206` | U32 | 队列欠载数 | IRQ 需要新项但生产器未完成且队列为空的次数 |
| `0x2207..0x2208` | U32 | 规划裁剪数 | 已发布但无法达到请求峰值的规划次数 |

`0x3100=1` 同时清除本区的锁存标志和计数。原有 `0x2100..0x2119` 诊断区的边界保持不变。

## 6 控制区

| 地址 | 类型 | 名称 | 写入值 |
|---|---|---|---|
| `0x3000` | U16 | PLSR 命令 | `0=无操作`,`1=START`,`2=STOP`,`4=CLEAR` |
| `0x3100` | U16 | 诊断控制 | `0=无操作`,`1=清除诊断锁存和统计` |

控制寄存器读取恒为 0。除上述单一值外的位组合均返回非法数据值。
运行期间写 `0x3100=1` 返回设备忙,避免清除正在采集的诊断上下文。

## 7 地址边界

- 原固定区 `0x1000..0x1197`、`0x2000..0x2006`、`0x3000` 保持不变。
- 扩展区增加 `0x1200`、`0x2100..0x2119`、`0x2200..0x2208`、`0x3100`。
- 完全落在 PLSR 地址之外的请求返回“不处理”,跨越 PLSR 地址与空洞的请求返回“非法地址”。

+ 0
- 388
Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md Просмотреть файл

@@ -1,388 +0,0 @@
# 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
语义;迁移路线按"所有权 → 重规划 → 统一规划器 → 曲线校准"重排。

+ 274
- 3
EWARM/Modbus.ewp Просмотреть файл

@@ -226,7 +226,7 @@
<name>CCDefines</name>
<state>USE_HAL_DRIVER</state>
<state>STM32F407xx</state>
<state>PLSR_DEBUG_TIMING=1</state>
<state>PLSR_DEBUG_TIMING=0</state>
</option>
<option>
<name>CCPreprocFile</name>
@@ -1291,7 +1291,7 @@
<name>CCDefines</name>
<state>USE_HAL_DRIVER</state>
<state>STM32F407xx</state>
<state>PLSR_DEBUG_TIMING=1</state>
<state>PLSR_DEBUG_TIMING=0</state>
</option>
<option>
<name>CCPreprocFile</name>
@@ -1580,7 +1580,7 @@
<name>CCDefines</name>
<state>USE_HAL_DRIVER</state>
<state>STM32F407xx</state>
<state>PLSR_DEBUG_TIMING=1</state>
<state>PLSR_DEBUG_TIMING=0</state>
</option>
<option>
<name>CCPreprocFile</name>
@@ -1836,5 +1836,276 @@
</settings>
</configuration>
</file>
<configuration>
<name>Modbus</name>
<settings>
<name>ICCARM</name>
<data>
<version>35</version>
<wantNonLocal>0</wantNonLocal>
<debug>1</debug>
<option>
<name>CCOptimizationNoSizeConstraints</name>
<state>0</state>
</option>
<option>
<name>CCDefines</name>
<state>USE_HAL_DRIVER</state>
<state>STM32F407xx</state>
<state>PLSR_DEBUG_TIMING=0</state>
</option>
<option>
<name>CCPreprocFile</name>
<state>0</state>
</option>
<option>
<name>CCPreprocComments</name>
<state>0</state>
</option>
<option>
<name>CCPreprocLine</name>
<state>0</state>
</option>
<option>
<name>CCListCFile</name>
<state>0</state>
</option>
<option>
<name>CCListCMnemonics</name>
<state>0</state>
</option>
<option>
<name>CCListCMessages</name>
<state>0</state>
</option>
<option>
<name>CCListAssFile</name>
<state>0</state>
</option>
<option>
<name>CCListAssSource</name>
<state>0</state>
</option>
<option>
<name>CCEnableRemarks</name>
<state>0</state>
</option>
<option>
<name>CCDiagSuppress</name>
<state></state>
</option>
<option>
<name>CCDiagRemark</name>
<state></state>
</option>
<option>
<name>CCDiagWarning</name>
<state></state>
</option>
<option>
<name>CCDiagError</name>
<state></state>
</option>
<option>
<name>CCObjPrefix</name>
<state>1</state>
</option>
<option>
<name>CCAllowList</name>
<version>1</version>
<state>11111110</state>
</option>
<option>
<name>CCDebugInfo</name>
<state>1</state>
</option>
<option>
<name>IEndianMode</name>
<state>1</state>
</option>
<option>
<name>IProcessor</name>
<state>1</state>
</option>
<option>
<name>IExtraOptionsCheck</name>
<state>0</state>
</option>
<option>
<name>IExtraOptions</name>
<state></state>
</option>
<option>
<name>CCLangConformance</name>
<state>0</state>
</option>
<option>
<name>CCSignedPlainChar</name>
<state>1</state>
</option>
<option>
<name>CCRequirePrototypes</name>
<state>0</state>
</option>
<option>
<name>CCDiagWarnAreErr</name>
<state>0</state>
</option>
<option>
<name>CCCompilerRuntimeInfo</name>
<state>0</state>
</option>
<option>
<name>IFpuProcessor</name>
<state>1</state>
</option>
<option>
<name>OutputFile</name>
<state>$FILE_BNAME$.o</state>
</option>
<option>
<name>CCLibConfigHeader</name>
<state>1</state>
</option>
<option>
<name>PreInclude</name>
<state></state>
</option>
<option>
<name>CompilerMisraOverride</name>
<state>0</state>
</option>
<option>
<name>CCIncludePath2</name>
<state>$PROJ_DIR$/../USB_DEVICE/App</state>
<state>$PROJ_DIR$/../USB_DEVICE/Target</state>
<state>$PROJ_DIR$/../Core/Inc</state>
<state>$PROJ_DIR$/../Drivers/STM32F4xx_HAL_Driver/Inc</state>
<state>$PROJ_DIR$/../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy</state>
<state>$PROJ_DIR$/../Middlewares/ST/STM32_USB_Device_Library/Core/Inc</state>
<state>$PROJ_DIR$/../Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc</state>
<state>$PROJ_DIR$/../Drivers/CMSIS/Device/ST/STM32F4xx/Include</state>
<state>$PROJ_DIR$/../Drivers/CMSIS/Include</state>
<state>$PROJ_DIR$\..\Modbus\Inc</state>
<state>$PROJ_DIR$\..\PLSR\Inc</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\Config</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Source</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Ports\ARM-Cortex-M4\IAR</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uC-CPU</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uC-CPU\ARM-Cortex-M4\IAR</state>
<state>$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uC-LIB</state>
<state>$PROJ_DIR$\..\Modbus\Inc</state>
</option>
<option>
<name>CCStdIncCheck</name>
<state>0</state>
</option>
<option>
<name>CCCodeSection</name>
<state>.text</state>
</option>
<option>
<name>IProcessorMode2</name>
<state>1</state>
</option>
<option>
<name>CCOptLevel</name>
<state>3</state>
</option>
<option>
<name>CCOptStrategy</name>
<version>0</version>
<state>2</state>
</option>
<option>
<name>CCOptLevelSlave</name>
<state>3</state>
</option>
<option>
<name>CompilerMisraRules98</name>
<version>0</version>
<state>1000111110110101101110011100111111101110011011000101110111101101100111111111111100110011111001110111001111111111111111111111111</state>
</option>
<option>
<name>CompilerMisraRules04</name>
<version>0</version>
<state>111101110010111111111000110111111111111111111111111110010111101111010101111111111111111111111111101111111011111001111011111011111111111111111</state>
</option>
<option>
<name>CCPosIndRopi</name>
<state>0</state>
</option>
<option>
<name>CCPosIndRwpi</name>
<state>0</state>
</option>
<option>
<name>CCPosIndNoDynInit</name>
<state>0</state>
</option>
<option>
<name>IccLang</name>
<state>0</state>
</option>
<option>
<name>IccCDialect</name>
<state>1</state>
</option>
<option>
<name>IccAllowVLA</name>
<state>0</state>
</option>
<option>
<name>IccStaticDestr</name>
<state>1</state>
</option>
<option>
<name>IccCppInlineSemantics</name>
<state>0</state>
</option>
<option>
<name>IccCmsis</name>
<state>1</state>
</option>
<option>
<name>IccFloatSemantics</name>
<state>0</state>
</option>
<option>
<name>CCNoLiteralPool</name>
<state>0</state>
</option>
<option>
<name>CCOptStrategySlave</name>
<version>0</version>
<state>2</state>
</option>
<option>
<name>CCEncSource</name>
<state>0</state>
</option>
<option>
<name>CCEncOutput</name>
<state>0</state>
</option>
<option>
<name>CCEncOutputBom</name>
<state>1</state>
</option>
<option>
<name>CCEncInput</name>
<state>0</state>
</option>
<option>
<name>IccExceptions2</name>
<state>0</state>
</option>
<option>
<name>IccRTTI2</name>
<state>0</state>
</option>
</data>
</settings>
</configuration>
</group>
</project>

Двоичные данные
HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc Просмотреть файл


Двоичные данные
HostComputer/__pycache__/plsr_control_panel.cpython-314.pyc Просмотреть файл


Двоичные данные
HostComputer/__pycache__/plsr_modbus_product_test.cpython-314.pyc Просмотреть файл


+ 228
- 49
HostComputer/bin_to_time_freq.py Просмотреть файл

@@ -4,31 +4,44 @@

数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点;
电平 >= 阈值(默认128) 判为高(脉冲),否则为低。
频率定义: 每脉冲频率 = 采样率 / 脉冲周期
周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点
最后一个脉冲无下一脉冲,按高电平宽度 x2 近似(50% 占空比)。
时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)
频率定义: 用连续 N 个脉冲的上升沿间隔估计频率
频率点放在 N 周期测量窗口中间
频率 = N * 采样率 /(第 N 个后续上升沿 - 当前上升沿);
时间定义: 频率点时间为测量窗口中点;高电平宽度取中心脉冲

用法:
python bin_to_time_freq.py <bin文件> [采样率Hz] [选项]
未填写采样率时默认使用 3125000 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
python bin_to_time_freq.py "Document/PLSR_document/波形/10段.bin" 3125000
python bin_to_time_freq.py xxx.bin 3125000 --ymax=12000
python bin_to_time_freq.py xxx.bin 3125000 --save=out.png
python bin_to_time_freq.py xxx.bin 3125000 --selftest
python bin_to_time_freq.py xxx.bin 3125000 --reverse --cycles=8
python bin_to_time_freq.py xxx.bin 3125000 --d1 --d2
python bin_to_time_freq.py xxx.bin 3125000 --d1 --dwindow-ms=10
python bin_to_time_freq.py xxx.bin 3125000 --d1 --d2 --d1-window-ms=5 --d2-window-ms=15

选项:
--thresh=128 电平阈值(默认128)
--cycles=8 用连续多少个周期估计频率(默认8;1为逐周期)
--reverse 反相电平后再检测脉冲(低电平作为脉冲高电平)
--ymax=12000 频率轴上限(默认自适应)
--d1 另开一张图:频率对时间的一阶导数 df/dt(红)
--d2 另开一张图:频率对时间的二阶导数 d²f/dt²(黑)
--dwindow-ms=5 一、二阶导数的公共拟合窗口,单位 ms(默认5)
--d1-window-ms 原函数 -> 一阶导数的拟合窗口(默认继承 --dwindow-ms)
--d2-window-ms 一阶导数 -> 二阶导数的拟合窗口(默认继承 --dwindow-ms)
--save=路径 保存 PNG 后退出(不弹窗)
--selftest 仅打印统计,不画图
--help, -h 显示这份中文帮助后退出

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

依赖: pip install numpy matplotlib
"""
@@ -46,13 +59,17 @@ except Exception:
pass


def load_time_freq(path, fs, threshold=128):
def load_time_freq(path, fs, threshold=128, cycles=8, reverse=False):
"""读取 bin,返回 (t_sec, freq_hz, hi_width_sec, meta)。
t_sec 以第一个脉冲为 0 时刻;hi_width 为每脉冲高电平宽度。"""
t_sec 以第一个脉冲高电平中点为 0 时刻;hi_width 为中心脉冲高电平宽度。"""
data = np.fromfile(path, dtype=np.uint8)
if data.size == 0:
raise ValueError("文件为空: %s" % path)
if not 0 <= threshold <= 255:
raise ValueError("阈值必须在0到255之间")
samples = (data >= threshold).astype(np.int8)
if reverse:
samples = 1 - samples

# run 级压缩
changes = np.flatnonzero(np.diff(samples) != 0) + 1
@@ -60,50 +77,160 @@ def load_time_freq(path, fs, threshold=128):
ends = np.concatenate((changes, [len(samples)]))
runs = np.column_stack((starts, ends, samples[starts]))

# 脉冲 = 高电平 run,且其后紧跟低电平 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]
if hi.size and runs[hi[0], 0] == 0:
hi = hi[1:]

if hi.size == 0:
raise ValueError("未检测到上升沿脉冲: %s" % path)

# 保留浮点上升沿位置:在原始电平相邻采样点之间线性插值,
# 再配合多周期测量,显著降低整数采样点带来的量化抖动。
rise_idx = runs[hi, 0].astype(np.int64)
t_start = rise_idx.astype(np.float64)
valid = rise_idx > 0
if np.any(valid):
y0 = data[rise_idx[valid] - 1].astype(np.float64)
y1 = data[rise_idx[valid]].astype(np.float64)
delta = y1 - y0
frac = np.zeros_like(y0)
np.divide(float(threshold) - y0, delta, out=frac, where=delta != 0)
t_start[valid] = rise_idx[valid] - 1.0 + np.clip(frac, 0.0, 1.0)

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

if fs <= 0:
raise ValueError("采样率必须大于0")
try:
cycles = int(cycles)
except (TypeError, ValueError):
raise ValueError("cycles 必须是正整数")
if cycles < 1:
raise ValueError("cycles 必须是正整数")
if len(t_start) < 2:
raise ValueError("脉冲不足两个,无法计算频率")

# 用下降沿的阈值 crossing 计算高电平宽度,时间单位仍为采样点。
# 文件末尾若停在高电平,频率仍可用该上升沿作为前一窗口终点;
# 该脉冲的宽度只取到文件末尾,不用于猜测频率。
has_fall = np.zeros(len(hi), dtype=bool)
adjacent = (hi + 1) < len(runs)
has_fall[adjacent] = runs[hi[adjacent] + 1, 2] == 0
fall_idx = np.where(has_fall, runs[hi, 1], len(data)).astype(np.int64)
fall_edge = fall_idx.astype(np.float64)
valid = has_fall & (fall_idx > 0) & (fall_idx < len(data))
if np.any(valid):
y0 = data[fall_idx[valid] - 1].astype(np.float64)
y1 = data[fall_idx[valid]].astype(np.float64)
delta = y1 - y0
frac = np.zeros_like(y0)
np.divide(float(threshold) - y0, delta, out=frac, where=delta != 0)
fall_edge[valid] = fall_idx[valid] - 1.0 + np.clip(frac, 0.0, 1.0)
hi_width = np.maximum(fall_edge - t_start, 0.0)
pulse_mid = t_start + hi_width / 2.0

# 单周期只看到整数采样点,周期接近半个采样点时会严重跳变。
# 跨 cycles 个周期计算总时长,再换算回单周期频率,可显著抑制量化抖动。
cycles = min(cycles, len(t_start) - 1)
left = np.arange(len(t_start) - cycles, dtype=np.int64)
right = left + cycles
period = t_start[right] - t_start[left]
freq = cycles * float(fs) / np.maximum(period, np.finfo(np.float64).eps)

# 频率点放在测量窗口的时间中心;宽度取中心附近脉冲,仅用于兼容绘图接口。
t_center = (t_start[left] + t_start[right]) / 2.0
center_idx = np.minimum(left + cycles // 2, len(hi_width) - 1)
hi_width_out = hi_width[center_idx]
# 时间原点仍为第一个脉冲的高电平中点;多周期测量点自然位于窗口中心。
t_sec = (t_center - pulse_mid[0]) / fs

meta = {
'total_samples': int(len(samples)),
'duration': len(samples) / fs,
'pulses': int(len(freq)),
'detected_pulses': int(len(t_start)),
'cycles': int(cycles),
'offset_ms': t_start[0] / fs * 1000,
'activity_duration': max(0.0, (fall_edge[-1] - pulse_mid[0]) / fs),
}
return t_sec, freq, hi_width / fs, meta
return t_sec, freq, hi_width_out / fs, meta


def selftest(path, fs, thresh):
def selftest(path, fs, thresh, cycles, reverse=False):
import statistics
t, freq, hi_w, meta = load_time_freq(path, fs, thresh)
t, freq, hi_w, meta = load_time_freq(path, fs, thresh, cycles, reverse)
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'])
print("检测到脉冲: %d,频率测量点: %d" % (
meta['detected_pulses'], 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" % (
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)
print("估计周期窗口: %d 个周期" % meta['cycles'])
print("波形活动时长: %.1f ms" % (meta['activity_duration'] * 1000))


def local_linear_derivative(t_sec, values, window_s=5e-3, min_points=7):
"""对非等间隔时间点做局部线性拟合,返回 d(values)/dt。

直接对相邻频率点差分会放大采样量化抖动;局部最小二乘斜率
使用真实时间间隔,并对这种抖动做平均。
"""
t = np.asarray(t_sec, dtype=np.float64)
y = np.asarray(values, dtype=np.float64)
if t.ndim != 1 or y.ndim != 1 or len(t) != len(y):
raise ValueError("t_sec 和 values 必须是等长一维数组")
if len(t) < 2:
raise ValueError("至少需要两个点才能计算导数")
if window_s <= 0:
raise ValueError("导数拟合窗口必须大于0")
if np.any(np.diff(t) <= 0):
raise ValueError("时间点必须严格递增")

n = len(t)
min_points = min(n, max(2, int(min_points)))
half_window = window_s / 2.0
left = np.searchsorted(t, t - half_window, side='left')
right = np.searchsorted(t, t + half_window, side='right')

# 低频段在固定时间窗口内可能点数太少,至少补足 min_points 个点。
idx = np.arange(n)
fallback_left = np.clip(idx - min_points // 2, 0, n - min_points)
fallback_right = fallback_left + min_points
too_few = (right - left) < min_points
left[too_few] = fallback_left[too_few]
right[too_few] = fallback_right[too_few]

# 移动时间原点减少长时采集时前缀和相减的精度损失。
x = t - (t[0] + t[-1]) / 2.0
sx = np.concatenate(([0.0], np.cumsum(x)))
sy = np.concatenate(([0.0], np.cumsum(y)))
sxx = np.concatenate(([0.0], np.cumsum(x * x)))
sxy = np.concatenate(([0.0], np.cumsum(x * y)))
count = (right - left).astype(np.float64)
sum_x = sx[right] - sx[left]
sum_y = sy[right] - sy[left]
sum_xx = sxx[right] - sxx[left]
sum_xy = sxy[right] - sxy[left]
denominator = count * sum_xx - sum_x * sum_x
numerator = count * sum_xy - sum_x * sum_y

derivative = np.empty(n, dtype=np.float64)
good = np.abs(denominator) > np.finfo(np.float64).eps
derivative[good] = numerator[good] / denominator[good]
derivative[~good] = np.gradient(y, t)[~good]
return derivative


def plot_show(path, fs, thresh, ymax, cycles, reverse, save_path=None,
draw_d1=False, draw_d2=False, d1_window_ms=5.0,
d2_window_ms=None):
t, freq, hi_w, meta = load_time_freq(path, fs, thresh, cycles, reverse)

import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
@@ -117,11 +244,11 @@ def plot_show(path, fs, thresh, ymax, save_path=None):
tx_ms = t * 1000
x_full = tx_ms
y_full = freq
ax.set_xlim(0.0, (t[-1] + hi_w[-1]) * 1000 * 1.02)
ax.set_xlim(0.0, meta['activity_duration'] * 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_title("%s 时频曲线(检测到脉冲 %d,频率测量点 %d,%d 周期平均,活动时长 %.0f ms)" % (
os.path.basename(path), meta['detected_pulses'], meta['pulses'], meta['cycles'],
meta['activity_duration'] * 1000))
ax.set_xlabel("时间 (ms)")
ax.set_ylabel("频率 (Hz)")
# 更密的刻度
@@ -131,10 +258,53 @@ def plot_show(path, fs, thresh, ymax, save_path=None):
ax.grid(True, which='minor', alpha=0.15)
ax.minorticks_on()

fig_der = None
draw_d1 = draw_d1 and (len(freq) >= 2)
draw_d2 = draw_d2 and (len(freq) >= 3)
if draw_d1 or draw_d2:
f64 = freq.astype(np.float64)
if d2_window_ms is None:
d2_window_ms = d1_window_ms
d1_full = local_linear_derivative(t, f64, d1_window_ms * 1e-3)
d2_full = (local_linear_derivative(t, d1_full, d2_window_ms * 1e-3)
if draw_d2 else None)
plot_count = int(draw_d1) + int(draw_d2)
fig_der, axes_der = plt.subplots(plot_count, 1, figsize=(15, 4.5 * plot_count),
sharex=True, squeeze=False)
fig_der.subplots_adjust(bottom=0.14, top=0.90)
row = 0
if draw_d1:
ax_der = axes_der[row, 0]
ax_der.plot(x_full, d1_full, '-', lw=0.9, color='red', label='df/dt')
ax_der.set_ylabel("一阶导数 (Hz/s)")
ax_der.legend(loc='upper right')
row += 1
if draw_d2:
ax_der = axes_der[row, 0]
ax_der.plot(x_full, d2_full, '-', lw=0.9, color='black', label='d²f/dt²')
ax_der.set_ylabel("二阶导数 (Hz/s²)")
ax_der.legend(loc='upper right')
for ax_der in axes_der[:, 0]:
ax_der.set_xlim(0.0, meta['activity_duration'] * 1000 * 1.02)
ax_der.grid(True, which='major', alpha=0.35)
ax_der.minorticks_on()
axes_der[-1, 0].set_xlabel("时间 (ms)")
axes_der[0, 0].set_title(
"时频曲线导数(一阶窗口 %.3g ms / 二阶窗口 %.3g ms)" %
(d1_window_ms, d2_window_ms))

if save_path:
scatter.set_data(x_full, y_full)
root, ext = os.path.splitext(save_path)
fig.savefig(save_path, dpi=130)
print("已保存: %s" % save_path)
if fig_der is not None:
der_path = root + "_d" + ext
fig_der.savefig(der_path, dpi=130)
print("已保存: %s" % der_path)
plt.close(fig)
if fig_der is not None:
plt.close(fig_der)
return

# 交互:滚轮缩放 X(Ctrl+滚轮缩放 Y)/ 左键双向拖拽平移 / 双击复位 / 悬停高亮
@@ -224,7 +394,7 @@ def plot_show(path, fs, thresh, ymax, save_path=None):
if dist[k] <= 20.0:
idx = int(np.flatnonzero(mask)[k])
hl_marker.set_data([x_full[idx]], [y_full[idx]])
hl_text.set_text("脉冲 #%d\nt = %.3f ms\nf = %.0f Hz"
hl_text.set_text("测量点 #%d\nt = %.3f ms\nf = %.0f Hz"
% (idx + 1, x_full[idx], y_full[idx]))
hl_text.set_position((x_full[idx] + (x1 - x0) * 0.01,
y_full[idx] + (ax.get_ylim()[1] - ax.get_ylim()[0]) * 0.02))
@@ -246,7 +416,7 @@ def plot_show(path, fs, thresh, ymax, save_path=None):

def on_double(event):
if event.dblclick:
ax.set_xlim(0.0, (t[-1] + hi_w[-1]) * 1000 * 1.02)
ax.set_xlim(0.0, meta['activity_duration'] * 1000 * 1.02)
ax.set_ylim(0, ymax if ymax > 0 else freq.max() * 1.08)
update_scatter()
fig.canvas.draw_idle()
@@ -259,8 +429,9 @@ def plot_show(path, fs, thresh, ymax, save_path=None):
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))
print("检测到脉冲 %d,频率测量点 %d,使用 %d 周期平均,活动时长 %.1f ms" % (
meta['detected_pulses'], meta['pulses'], meta['cycles'],
meta['activity_duration'] * 1000))
plt.show()


@@ -269,24 +440,32 @@ def main():
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:
if len(args) < 1 or '--help' in opts or '-h' in sys.argv[1:]:
print(__doc__)
sys.exit(0)

path = args[0]
fs = float(args[1]) if len(args) > 1 else 6.25e6
fs = float(args[1]) if len(args) > 1 else 3.125e6
thresh = int(opts.get('--thresh', 128))
cycles = int(opts.get('--cycles', 8))
reverse = '--reverse' in opts
ymax = float(opts.get('--ymax', 0))
save_path = opts.get('--save', '')
draw_d1 = '--d1' in opts
draw_d2 = '--d2' in opts
derivative_window_ms = float(opts.get('--dwindow-ms', 5.0))
d1_window_ms = float(opts.get('--d1-window-ms', derivative_window_ms))
d2_window_ms = float(opts.get('--d2-window-ms', derivative_window_ms))

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

if '--selftest' in opts:
selftest(path, fs, thresh)
selftest(path, fs, thresh, cycles, reverse)
else:
plot_show(path, fs, thresh, ymax, save_path or None)
plot_show(path, fs, thresh, ymax, cycles, reverse, save_path or None,
draw_d1, draw_d2, d1_window_ms, d2_window_ms)


if __name__ == '__main__':


+ 432
- 0
HostComputer/bin_to_time_freq1.py Просмотреть файл

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

数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点;
电平 >= 阈值(默认128) 判为高(脉冲),否则为低。
--reverse 时先把高低电平对调,再按同样规则找脉冲。
频率定义: 每脉冲频率 = 采样率 / 脉冲周期;
周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点;
仅使用实际检测到的相邻上升沿,不猜测末脉冲周期。
时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)。

用法:
python bin_to_time_freq1.py <bin文件> [采样率Hz] [选项]
示例:
python bin_to_time_freq1.py "Document/PLSR_document/波形/10段.bin" 6250000
python bin_to_time_freq1.py xxx.bin 6250000 --ymax=12000
python bin_to_time_freq1.py xxx.bin 6250000 --line
python bin_to_time_freq1.py xxx.bin 6250000 --reverse
python bin_to_time_freq1.py xxx.bin 6250000 --d1 --d2
python bin_to_time_freq1.py xxx.bin 6250000 --d1 --dwindow-ms=10
python bin_to_time_freq1.py xxx.bin 6250000 --d1 --d2 --d1-window-ms=5 --d2-window-ms=15
python bin_to_time_freq1.py xxx.bin 6250000 --save=out.png
python bin_to_time_freq1.py xxx.bin 6250000 --selftest

选项:
--thresh=128 电平阈值(默认128)
--ymax=12000 频率轴上限(默认自适应)
--line 将脉冲点连成折线(默认只画散点)
--reverse 高低电平对调后再算频率点(低电平当脉冲)
--d1 另开一张图:频率对时间的一阶导数 df/dt(红)
--d2 另开一张图:频率对时间的二阶导数 d²f/dt²(黑)
--dwindow-ms=5 一、二阶导数的公共拟合窗口,单位 ms(默认5)
--d1-window-ms 原函数 -> 一阶导数的拟合窗口(默认继承 --dwindow-ms)
--d2-window-ms 一阶导数 -> 二阶导数的拟合窗口(默认继承 --dwindow-ms)
--save=路径 保存 PNG 后退出(不弹窗)
--selftest 仅打印统计,不画图
--help, -h 显示这份中文帮助后退出

交互(弹窗模式):
滚轮 缩放 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, reverse=False):
"""读取 bin,返回 (t_sec, freq_hz, hi_width_sec, meta)。
t_sec 以第一个脉冲为 0 时刻;hi_width 为每脉冲高电平宽度。
reverse=True 时先把高低电平对调,再按高电平找脉冲。"""
data = np.fromfile(path, dtype=np.uint8)
if data.size == 0:
raise ValueError("文件为空: %s" % path)
if fs <= 0:
raise ValueError("采样率必须大于0")
if not 0 <= threshold <= 255:
raise ValueError("阈值必须在0到255之间")
samples = (data >= threshold).astype(np.int8)
if reverse:
samples = 1 - samples

# 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)
if hi.size and runs[hi[0], 0] == 0:
hi = hi[1:]
if hi.size < 2:
raise ValueError("至少需要两个完整的上升沿才能计算频率: %s" % path)

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

# 只用相邻实测上升沿求周期,不根据占空比猜测末脉冲频率。
period = np.diff(t_start)
freq = float(fs) / np.maximum(period, 1)

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

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


def local_linear_derivative(t_sec, values, window_s=5e-3, min_points=7):
"""对非等间隔时间点做局部线性拟合,返回 d(values)/dt。

直接对相邻单周期频率做差分会放大整数采样点带来的量化抖动;
局部最小二乘斜率使用真实时间间隔,并对这种抖动做平均。
"""
t = np.asarray(t_sec, dtype=np.float64)
y = np.asarray(values, dtype=np.float64)
if t.ndim != 1 or y.ndim != 1 or len(t) != len(y):
raise ValueError("t_sec 和 values 必须是等长一维数组")
if len(t) < 2:
raise ValueError("至少需要两个点才能计算导数")
if window_s <= 0:
raise ValueError("导数拟合窗口必须大于0")
if np.any(np.diff(t) <= 0):
raise ValueError("时间点必须严格递增")

n = len(t)
min_points = min(n, max(2, int(min_points)))
half_window = window_s / 2.0
left = np.searchsorted(t, t - half_window, side='left')
right = np.searchsorted(t, t + half_window, side='right')

# 低频段在固定时间窗口内可能点数太少,至少补足 min_points 个点。
idx = np.arange(n)
fallback_left = np.clip(idx - min_points // 2, 0, n - min_points)
fallback_right = fallback_left + min_points
too_few = (right - left) < min_points
left[too_few] = fallback_left[too_few]
right[too_few] = fallback_right[too_few]

# 前缀和使每个窗口的最小二乘斜率可以 O(1) 计算。
# 先把时间原点移到数据中心,降低长时采集时前缀和相减的精度损失。
x = t - (t[0] + t[-1]) / 2.0
sx = np.concatenate(([0.0], np.cumsum(x)))
sy = np.concatenate(([0.0], np.cumsum(y)))
sxx = np.concatenate(([0.0], np.cumsum(x * x)))
sxy = np.concatenate(([0.0], np.cumsum(x * y)))
count = (right - left).astype(np.float64)
sum_x = sx[right] - sx[left]
sum_y = sy[right] - sy[left]
sum_xx = sxx[right] - sxx[left]
sum_xy = sxy[right] - sxy[left]
denominator = count * sum_xx - sum_x * sum_x
numerator = count * sum_xy - sum_x * sum_y

derivative = np.empty(n, dtype=np.float64)
good = np.abs(denominator) > np.finfo(np.float64).eps
derivative[good] = numerator[good] / denominator[good]
derivative[~good] = np.gradient(y, t)[~good]
return derivative


def selftest(path, fs, thresh, reverse=False):
import statistics
t, freq, hi_w, meta = load_time_freq(path, fs, thresh, reverse)
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,频率测量点: %d" % (
meta['detected_pulses'], 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, draw_line=False,
reverse=False, draw_d1=False, draw_d2=False,
d1_window_ms=5.0, d2_window_ms=None):
t, freq, hi_w, meta = load_time_freq(path, fs, thresh, reverse)

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

draw_d1 = draw_d1 and (len(freq) >= 2)
draw_d2 = draw_d2 and (len(freq) >= 3)
fig, ax = plt.subplots(figsize=(15, 7))
fig.subplots_adjust(bottom=0.10, top=0.92)

# 全脉冲散点:复用 artist,重绘时按可见范围抽稀(大文件流畅)
MAX_VISIBLE_POINTS = 5000 # 可见范围内最多绘制的点数,超过则等间隔抽稀
(scatter,) = ax.plot([], [], '.', ms=1.5, color='C0', alpha=0.5, zorder=2)
line = None
if draw_line:
(line,) = ax.plot([], [], '-', lw=0.9, color='C0', alpha=0.85, zorder=1)
tx_ms = t * 1000
x_full = tx_ms
y_full = freq
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)
title_extra = ",高低对调" if reverse else ""
ax.set_title("%s 时频曲线(检测到脉冲 %d,频率测量点 %d,活动时长 %.0f ms%s)" % (
os.path.basename(path), meta['detected_pulses'], meta['pulses'],
(t[-1] + hi_w[-1] / 2) * 1000, title_extra))
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()

fig_der = None
if draw_d1 or draw_d2:
f64 = freq.astype(np.float64)
if d2_window_ms is None:
d2_window_ms = d1_window_ms
d1_full = local_linear_derivative(t, f64, d1_window_ms * 1e-3)
d2_full = (local_linear_derivative(t, d1_full, d2_window_ms * 1e-3)
if draw_d2 else None)
plot_count = int(draw_d1) + int(draw_d2)
fig_der, axes_der = plt.subplots(plot_count, 1, figsize=(15, 4.5 * plot_count),
sharex=True, squeeze=False)
fig_der.subplots_adjust(bottom=0.14, top=0.90)
row = 0
if draw_d1:
ax_der = axes_der[row, 0]
ax_der.plot(x_full, d1_full, '-', lw=0.9, color='red', label='df/dt')
ax_der.set_ylabel("一阶导数 (Hz/s)")
ax_der.legend(loc='upper right')
row += 1
if draw_d2:
ax_der = axes_der[row, 0]
ax_der.plot(x_full, d2_full, '-', lw=0.9, color='black', label='d²f/dt²')
ax_der.set_ylabel("二阶导数 (Hz/s²)")
ax_der.legend(loc='upper right')
for ax_der in axes_der[:, 0]:
ax_der.set_xlim(0.0, (t[-1] + hi_w[-1]) * 1000 * 1.02)
ax_der.grid(True, which='major', alpha=0.35)
ax_der.minorticks_on()
axes_der[-1, 0].set_xlabel("时间 (ms)")
axes_der[0, 0].set_title(
"时频曲线导数(一阶窗口 %.3g ms / 二阶窗口 %.3g ms)" %
(d1_window_ms, d2_window_ms))

if save_path:
scatter.set_data(x_full, y_full)
if line is not None:
line.set_data(x_full, y_full)
root, ext = os.path.splitext(save_path)
fig.savefig(save_path, dpi=130)
print("已保存: %s" % save_path)
if fig_der is not None:
der_path = root + "_d" + ext
fig_der.savefig(der_path, dpi=130)
print("已保存: %s" % der_path)
plt.close(fig)
if fig_der is not None:
plt.close(fig_der)
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 update_scatter():
x0, x1 = ax.get_xlim()
mask = (x_full >= x0) & (x_full <= x1)
n_vis = int(np.count_nonzero(mask))
if n_vis > MAX_VISIBLE_POINTS:
# 等间隔抽稀:取 n_vis 中的 MAX_VISIBLE_POINTS 个
step = (n_vis + MAX_VISIBLE_POINTS - 1) // MAX_VISIBLE_POINTS
idx = np.flatnonzero(mask)[::step]
else:
idx = np.flatnonzero(mask)
scatter.set_data(x_full[idx], y_full[idx])
if line is not None:
line.set_data(x_full[idx], y_full[idx])

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)
update_scatter()
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)
update_scatter()
fig.canvas.draw_idle()
return
# 悬停:找可见范围内距鼠标最近的脉冲点(屏幕像素距离 < 20px 才高亮)
x0, x1 = ax.get_xlim()
mask = (x_full >= x0) & (x_full <= x1)
if not np.any(mask):
if hover_last['visible']:
hl_marker.set_visible(False)
hl_text.set_visible(False)
fig.canvas.draw_idle()
hover_last['visible'] = False
return
px, py = ax.transData.transform(np.column_stack([x_full[mask], y_full[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([x_full[idx]], [y_full[idx]])
hl_text.set_text("脉冲 #%d\nt = %.3f ms\nf = %.0f Hz"
% (idx + 1, x_full[idx], y_full[idx]))
hl_text.set_position((x_full[idx] + (x1 - x0) * 0.01,
y_full[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)
update_scatter()
fig.canvas.draw_idle()

update_scatter()
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,频率测量点 %d,活动时长 %.1f ms" % (
meta['detected_pulses'], 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 sys.argv[1:]:
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', '')
draw_line = '--line' in opts
reverse = '--reverse' in opts
draw_d1 = '--d1' in opts
draw_d2 = '--d2' in opts
derivative_window_ms = float(opts.get('--dwindow-ms', 5.0))
d1_window_ms = float(opts.get('--d1-window-ms', derivative_window_ms))
d2_window_ms = float(opts.get('--d2-window-ms', derivative_window_ms))

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

if '--selftest' in opts:
selftest(path, fs, thresh, reverse)
else:
plot_show(path, fs, thresh, ymax, save_path or None, draw_line,
reverse, draw_d1, draw_d2, d1_window_ms, d2_window_ms)


if __name__ == '__main__':
main()

+ 101
- 4
HostComputer/plsr_control_panel.py Просмотреть файл

@@ -6,6 +6,7 @@ import argparse
import queue
import threading
import time
from datetime import datetime
import tkinter as tk
import tkinter.font as tkfont
from tkinter import messagebox, ttk
@@ -48,7 +49,9 @@ from plsr_modbus_product_test import (

BAUD_RATE = 9600
SERIAL_TIMEOUT_SECONDS = 0.8
STATUS_POLL_SECONDS = 0.25
DEFAULT_STATUS_POLL_MS = 250
MIN_STATUS_POLL_MS = 1
MAX_STATUS_POLL_MS = 3_600_000
SEGMENT_COUNT = 10
SEGMENT_STRIDE = 0x10
MAX_FREQUENCY_HZ = 100_000
@@ -171,10 +174,25 @@ class SerialWorker(threading.Thread):
self.stop_event = threading.Event()
self.client = None
self.next_status_poll = 0.0
self.polling_enabled = True
self.poll_interval_seconds = DEFAULT_STATUS_POLL_MS / 1000.0
self.log_callback = None

def submit(self, command, **payload):
self.commands.put((command, payload))

def configure_polling(self, enabled, interval_ms):
self.polling_enabled = bool(enabled)
self.poll_interval_seconds = interval_ms / 1000.0
self.next_status_poll = time.monotonic() + self.poll_interval_seconds

def set_log_callback(self, callback):
self.log_callback = callback

def _log(self, message):
if self.log_callback is not None:
self.log_callback("%s %s" % (datetime.now().strftime("%H:%M:%S.%f")[:-3], message))

def close(self):
self.stop_event.set()
self.commands.put(("shutdown", {}))
@@ -228,6 +246,7 @@ class SerialWorker(threading.Thread):
SERIAL_TIMEOUT_SECONDS,
)
client.__enter__()
client.log_callback = self._log
self.client = client
self.next_status_poll = 0.0
self._emit(
@@ -298,14 +317,14 @@ class SerialWorker(threading.Thread):
diagnostic = read_diagnostic(self._require_client())
self._emit("status", status=status)
self._emit("diagnostic", diagnostic=diagnostic)
self.next_status_poll = time.monotonic() + STATUS_POLL_SECONDS
self.next_status_poll = time.monotonic() + self.poll_interval_seconds
except (OSError, serial.SerialException) as error:
self._handle_connection_error("状态轮询", error)
except (TestFailure, ModbusException, RuntimeError) as error:
self._emit(
"error", operation="状态轮询", message=str(error), modal=False
)
self.next_status_poll = time.monotonic() + 1.0
self.next_status_poll = time.monotonic() + self.poll_interval_seconds

def run(self):
try:
@@ -322,7 +341,8 @@ class SerialWorker(threading.Thread):
self._handle_command(command, payload)

if (
self.client is not None
self.polling_enabled
and self.client is not None
and time.monotonic() >= self.next_status_poll
):
self._poll_status()
@@ -341,10 +361,15 @@ class PlsrControlPanel:
self.connected = False
self.operation_pending = False
self.worker = SerialWorker()
self.worker.set_log_callback(self._queue_log)
self.worker.start()
self.log_window = None
self.log_text = None

self.port_var = tk.StringVar()
self.slave_var = tk.StringVar(value="1")
self.polling_enabled_var = tk.BooleanVar(value=True)
self.poll_interval_var = tk.StringVar(value=str(DEFAULT_STATUS_POLL_MS))
self.connection_var = tk.StringVar(value="未连接")
self.footer_var = tk.StringVar(value="请选择串口并连接")
self.status_vars = {
@@ -410,6 +435,21 @@ class PlsrControlPanel:
connection, text="连接", command=self._toggle_connection
)
self.connect_button.grid(row=0, column=8, padx=8, pady=8)

polling = ttk.LabelFrame(connection, text="状态轮询")
polling.grid(row=1, column=0, columnspan=9, padx=8, pady=(0, 6), sticky="ew")
ttk.Checkbutton(
polling, text="启动轮询", variable=self.polling_enabled_var,
command=self._apply_polling_settings,
).grid(row=0, column=0, padx=(8, 4), pady=6)
ttk.Label(polling, text="轮询间隔").grid(row=0, column=1, padx=(12, 4), pady=6)
ttk.Entry(polling, textvariable=self.poll_interval_var, width=9).grid(
row=0, column=2, padx=4, pady=6
)
ttk.Label(polling, text="ms(最小 1 ms)").grid(
row=0, column=3, padx=(4, 8), pady=6
)

self.connection_label = ttk.Label(
connection,
textvariable=self.connection_var,
@@ -420,6 +460,10 @@ class PlsrControlPanel:
self.connection_label.grid(row=0, column=7, padx=8, pady=8, sticky="e")
self.connection_widgets = [self.port_box, refresh_button, slave_entry]

ttk.Button(connection, text="通信日志", command=self._show_log_window).grid(
row=0, column=9, padx=8, pady=8
)

self._build_runtime_status()

notebook = ttk.Notebook(self.root)
@@ -648,6 +692,44 @@ class PlsrControlPanel:
elif not ports:
self.port_var.set("")

def _queue_log(self, message):
self.worker.results.put(("log", {"message": message}))

def _show_log_window(self):
if self.log_window is not None and self.log_window.winfo_exists():
self.log_window.deiconify()
self.log_window.lift()
return
self.log_window = tk.Toplevel(self.root)
self.log_window.title("Modbus 通信日志")
self.log_window.geometry("760x420")
self.log_text = tk.Text(self.log_window, wrap="none", state="disabled")
self.log_text.pack(fill="both", expand=True, padx=8, pady=8)
ttk.Button(self.log_window, text="清空日志", command=self._clear_log).pack(pady=(0, 8))

def _clear_log(self):
if self.log_text is not None:
self.log_text.configure(state="normal")
self.log_text.delete("1.0", "end")
self.log_text.configure(state="disabled")

def _polling_settings(self):
interval_ms = parse_integer(
self.poll_interval_var.get(), "轮询间隔", MIN_STATUS_POLL_MS, MAX_STATUS_POLL_MS
)
return self.polling_enabled_var.get(), interval_ms

def _apply_polling_settings(self):
try:
enabled, interval_ms = self._polling_settings()
except ValueError as error:
messagebox.showerror("轮询设置", str(error))
return
self.worker.configure_polling(enabled, interval_ms)
self.footer_var.set(
"轮询已启动,间隔 %d ms" % interval_ms if enabled else "轮询已停止"
)

def _set_connected(self, connected, description=""):
self.connected = connected
self.connect_button.configure(
@@ -715,6 +797,15 @@ class PlsrControlPanel:
self._update_action_state()
self.connection_var.set("正在连接 %s" % port)
self.footer_var.set("正在打开串口并读取参数")
try:
enabled, interval_ms = self._polling_settings()
except ValueError as error:
self.connect_button.configure(state="normal")
self.operation_pending = False
self._update_action_state()
messagebox.showerror("轮询设置", str(error))
return
self.worker.configure_polling(enabled, interval_ms)
self.worker.submit("connect", port=port, slave=slave)

def _common_words(self):
@@ -985,6 +1076,12 @@ class PlsrControlPanel:
self._show_status(payload["status"])
elif event == "diagnostic":
self._show_diagnostic(payload["diagnostic"])
elif event == "log":
if self.log_text is not None and self.log_text.winfo_exists():
self.log_text.configure(state="normal")
self.log_text.insert("end", payload["message"] + "\n")
self.log_text.see("end")
self.log_text.configure(state="disabled")
elif event == "operation":
self._finish_operation(payload["message"])
elif event == "error":


+ 5
- 0
HostComputer/plsr_modbus_product_test.py Просмотреть файл

@@ -504,6 +504,7 @@ class RtuClient:
self.frame_gap_seconds = 3.5 * self.character_seconds
self.serial_port = None
self.last_request_finished = 0.0
self.log_callback = None

def __enter__(self):
self.serial_port = serial.Serial(
@@ -534,6 +535,8 @@ class RtuClient:
self.serial_port.reset_input_buffer()
self.serial_port.write(request)
self.serial_port.flush()
if hasattr(self, "log_callback") and self.log_callback is not None:
self.log_callback("TX %s" % request.hex(" "))

response = bytearray()
deadline = time.monotonic() + self.timeout
@@ -564,6 +567,8 @@ class RtuClient:
expected_length = 8

self.last_request_finished = time.monotonic()
if self.log_callback is not None:
self.log_callback("RX %s" % bytes(response).hex(" "))
if not response:
raise RtuTimeout("no Modbus response to %s" % request.hex(" "))
if expected_length is None:


+ 7
- 4
Middlewares/Third_Party/Micrium/Config/app_cfg.h Просмотреть файл

@@ -5,14 +5,17 @@
* uC/OS-II优先级数值越小,任务优先级越高。
* 每个任务必须使用不同的优先级;0通常留给最紧急的系统任务。
*/
#define APP_TASK_START_PRIO 5u
#define APP_TASK_TEST_PRIO 6u
#define APP_TASK_PLSR_PRIO 4u
#define APP_TASK_MODBUS_PRIO 5u

/*
* 任务栈大小的单位是OS_STK元素。当前OS_STK为32位。
*/
#define APP_TASK_START_STK_SIZE 512u
#define APP_TASK_TEST_STK_SIZE 256u
#define APP_TASK_PLSR_STK_SIZE 512u
#define APP_TASK_MODBUS_STK_SIZE 512u

/* Maximum profile items planned by one high-priority producer pass. */
#define APP_PLSR_PRODUCER_BUDGET 32u

/* The short-profile pulse IRQ needs more than the port's 128-word default. */
#define OS_CPU_EXCEPT_STK_SIZE 512u


+ 7
- 0
PLSR/Inc/plsr.h Просмотреть файл

@@ -60,6 +60,8 @@ typedef enum

uint8_t PlsrInit(void);
void PlsrPoll1ms(void);
/* Returns nonzero while another immediate bounded producer pass is useful. */
uint8_t PlsrServiceProfileProducer(uint16_t itemBudget);
void PlsrServicePersistence(void);

PLSR_MB_RESULT PlsrModbusReadHolding(uint16_t startAddress,
@@ -117,6 +119,7 @@ uint16_t PlsrTestProfileProducerNextPeriod(void);
uint16_t PlsrTestActiveProfileNextPeriod(void);
uint32_t PlsrTestLastGateEvent(void);
uint32_t PlsrTestLastDiagnosticFinishEvent(void);
uint8_t PlsrTestInitialDirectionPreparePending(void);
void PlsrTestSetPosition(int32_t position, uint8_t positionValid);
void PlsrTestClearPersistentStorage(void);
void PlsrTestResetSaveCount(void);
@@ -128,8 +131,12 @@ extern volatile uint32_t PlsrProfileProducerItemCount;
extern volatile uint32_t PlsrProfileProducerTotalCycles;
extern volatile uint32_t PlsrProfileProducerMaxItemCycles;
extern volatile uint32_t PlsrFiniteBlockIrqCount[4];
extern volatile uint32_t PlsrFiniteBlockIrqLastCycles[4];
extern volatile uint32_t PlsrFiniteBlockIrqTotalCycles[4];
extern volatile uint32_t PlsrFiniteBlockIrqMaxCycles[4];
extern volatile uint32_t PlsrFiniteFinalIrqCount[4];
extern volatile uint32_t PlsrFiniteFinalIrqLastCycles[4];
extern volatile uint32_t PlsrFiniteFinalIrqTotalCycles[4];
extern volatile uint32_t PlsrFiniteFinalIrqMaxCycles[4];
#endif



+ 351
- 91
PLSR/Src/plsr.c Просмотреть файл

@@ -56,13 +56,12 @@
|| (PLSR_PROFILE_QUEUE_CAPACITY > 65535U))
#error "PLSR profile queue capacity must be a power of two not exceeding 65535"
#endif
/* A profile item can represent only one pulse. At the 100 kHz limit these
thresholds provide 4 ms of startup data, refill toward 8 ms, and cap each
1 ms producer pass at 2 ms worth of newly planned items. */
#define PLSR_PROFILE_STARTUP_TARGET (400U)
#define PLSR_PROFILE_STARTUP_BUDGET (400U)
/* At the 100 kHz limit these thresholds prefill and refill toward 8 ms of
queued profile data. The caller bounds each producer service pass. */
#define PLSR_PROFILE_STARTUP_TARGET (800U)
#define PLSR_PROFILE_STARTUP_BUDGET (800U)
#define PLSR_PROFILE_REFILL_TARGET (800U)
#define PLSR_PROFILE_REFILL_BUDGET (200U)
#define PLSR_PROFILE_PRODUCER_BATCH_ITEMS (16U)
#define PLSR_TIMED_START_BUILD_BUDGET (32U)
#define PLSR_TIMED_START_READY_ITEMS PLSR_PROFILE_STARTUP_BUDGET
#define PLSR_PROFILE_LOW_WATER (100U)
@@ -124,9 +123,13 @@ typedef struct
typedef struct
{
PLSR_PLATFORM_TIMER_SETTING setting;
//这一档已经量化好的定时器参数,发波直接用
uint32_t requestedFrequencyHz;
//公式算出来的 Hz(量化前),给诊断/对照,硬件不拿它发波
uint32_t repeatCount;
//这一档连续发几个脉冲
uint8_t startsNextSegment;
//1 = 这项是下一段的第一拍(handoff),0 = 还在本段
} PLSR_PROFILE_ENTRY;

typedef struct
@@ -331,6 +334,9 @@ static uint8_t PlsrInitialized;
static volatile uint8_t PlsrCurrentSegment;
static uint8_t PlsrDirectionDelayActive;
static uint16_t PlsrDirectionDelayRemainingMs;
static uint8_t PlsrInitialDirectionPreparePending;
static uint8_t PlsrInitialDirectionPrepareLevel;
static uint32_t PlsrInitialDirectionPrepareEpoch;
static volatile uint8_t PlsrSegmentClockStarted;
static volatile uint32_t PlsrSegmentElapsedMs;
static volatile uint32_t PlsrWaitElapsedMs;
@@ -403,6 +409,7 @@ static uint32_t PlsrPulseDirStartFrequency(
uint32_t carryFrequencyHz);
static uint32_t PlsrEffectiveStopFrequency(uint32_t segmentFrequencyHz);
static uint8_t PlsrBeginSegmentOutput(uint32_t startFrequencyHz);
static uint8_t PlsrServiceInitialDirectionPrepare(void);
static void PlsrHandleBoundary(uint8_t extEdge);
static void PlsrTransitionToNext(uint8_t allowCarry);
static void PlsrRequestCut(uint32_t expectedEpoch);
@@ -469,6 +476,9 @@ static uint8_t PlsrProfileQueueFill(uint16_t targetCount,
static uint8_t PlsrTimerSettingsEqual(
const PLSR_PLATFORM_TIMER_SETTING *first,
const PLSR_PLATFORM_TIMER_SETTING *second);
static uint8_t PlsrProfileEntryTryMerge(
PLSR_PROFILE_ENTRY *previous,
const PLSR_PROFILE_ENTRY *entry);
static uint8_t PlsrProfileQueueAppendLocked(
PLSR_PROFILE_QUEUE *queue,
const PLSR_PROFILE_ENTRY *entry);
@@ -1995,6 +2005,35 @@ static uint32_t PlsrProfileRunLimit(uint32_t frequencyHz)
return (limit > 0xFFFFFFFFULL) ? 0xFFFFFFFFUL : (uint32_t)limit;
}

/*******************************************************************************
* 函数名称: PlsrShortProfileTakeRunLimited
* 功能描述: 从短轮廓中切出一项 counted run,写入 entry。
* 若规划器一次生成过长的同频项,则按上限将其拆成可入队的短 run。
* 输入参数: profile - 短轮廓对象,含规划上下文、已输出脉冲数、
* 尚未切完的同频项
* maximumRepeats - 本项允许的最大脉冲数;QueueFill 还债时传入
* 当前欠债脉冲数
* 输出参数: entry - 切出的队列节点
* setting : 量化后的定时器参数
* requestedFrequencyHz : 规划请求频率
* repeatCount : 本 run 连续脉冲数
* startsNextSegment : 本函数恒写 0
* 返 回 值: 1 - 切出成功,entry 有效,profile 账本已推进
* 0 - 失败,entry 无效。失败原因包括:
* 本段脉冲已全部切出、入口指针非法、maximumRepeats 为 0、
* 规划器无法再生成、或裁切后脉冲数为 0
* 注意事项: 1. 本项脉冲数取下列三者的最小值:
* 规划器剩余同频脉冲、本段尚未切出的脉冲、maximumRepeats。
* 2. maximumRepeats 还会被 PlsrProfileRunLimit() 再次压缩,
* 使单次硬件 run 不超过约 2 ms(PLSR_COUNTED_RUN_MAX_TIME_US)。
* 3. pendingRepeats 未耗尽时不调用规划器,继续切上一档同频项。
* 4. 还债路径由调用方传入欠债作为 maximumRepeats,并自行决定
* 不入队;本函数仍会填写 entry 并推进 nextPeriod。
* 5. 段边界标志 startsNextSegment 不在本函数置位,由
* counted-handoff 路径单独盖章。
* 6. 本段切尽时将 profile->active 清 0;generatorComplete
* 仍由 QueueFill 在欠债还清后置位。
******************************************************************************/
static uint8_t PlsrShortProfileTakeRunLimited(
PLSR_SHORT_PROFILE *profile,
PLSR_PROFILE_ENTRY *entry,
@@ -2002,30 +2041,36 @@ static uint8_t PlsrShortProfileTakeRunLimited(
{
uint32_t repeatCount;

/* 本段已切完、出口无效、或允许脉冲数为 0,无法再切。 */
if ((profile->nextPeriod >= profile->pulseCount)
|| (entry == NULL) || (maximumRepeats == 0UL))
{
return 0U;
}

/* 上一档同频项已切完,向规划器再取一档。 */
if (profile->pendingRepeats == 0UL)
{
if (PlsrPlannerGenerate(&profile->planner,
&profile->pendingItem, 1U) == 0U)
if (PlsrPlannerGenerate(&profile->planner, &profile->pendingItem, 1U) == 0U)
{
profile->active = 0U;
return 0U;
}
profile->pendingRepeats = profile->pendingItem.repeatCount;
}

{
uint32_t runLimit = PlsrProfileRunLimit(
profile->pendingItem.setting.actualFrequencyHz);

/* 限制单次 run 时长,避免在线改频阻塞在超长匀速之后。 */
if (maximumRepeats > runLimit)
{
maximumRepeats = runLimit;
}
}

/* 本项脉冲数 = min(本档剩余, 本段剩余, 调用方上限)。 */
repeatCount = profile->pendingRepeats;
if (repeatCount > profile->pulseCount - profile->nextPeriod)
{
@@ -2040,11 +2085,13 @@ static uint8_t PlsrShortProfileTakeRunLimited(
profile->active = 0U;
return 0U;
}

entry->setting = profile->pendingItem.setting;
entry->requestedFrequencyHz =
profile->pendingItem.requestedFrequencyHz;
entry->repeatCount = repeatCount;
entry->startsNextSegment = 0U;
entry->startsNextSegment = 0U; /* 段边界不由本函数标记 */

profile->pendingRepeats -= repeatCount;
profile->nextPeriod += repeatCount;
profile->lastRampFrequencyHz = entry->setting.actualFrequencyHz;
@@ -2052,6 +2099,7 @@ static uint8_t PlsrShortProfileTakeRunLimited(
{
profile->active = 0U;
}

return 1U;
}

@@ -2621,6 +2669,33 @@ static uint8_t PlsrTimerSettingsEqual(
&& (first->compare == second->compare)) ? 1U : 0U;
}

static uint8_t PlsrProfileEntryTryMerge(
PLSR_PROFILE_ENTRY *previous,
const PLSR_PROFILE_ENTRY *entry)
{
uint32_t runLimit;

if ((previous == NULL) || (entry == NULL)
|| (previous->startsNextSegment != 0U)
|| (entry->startsNextSegment != 0U)
|| (PlsrTimerSettingsEqual(&previous->setting,
&entry->setting) == 0U))
{
return 0U;
}
runLimit = PlsrProfileRunLimit(
previous->setting.actualFrequencyHz);
if ((entry->repeatCount > runLimit)
|| (previous->repeatCount > (runLimit - entry->repeatCount))
|| (previous->repeatCount
> (0xFFFFFFFFUL - entry->repeatCount)))
{
return 0U;
}
previous->repeatCount += entry->repeatCount;
return 1U;
}

static uint8_t PlsrProfileQueueAppendLocked(
PLSR_PROFILE_QUEUE *queue,
const PLSR_PROFILE_ENTRY *entry)
@@ -2637,20 +2712,8 @@ static uint8_t PlsrProfileQueueAppendLocked(
{
previous = &queue->entries[
(writeIndex - 1UL) & PLSR_PROFILE_QUEUE_MASK];
if ((previous->startsNextSegment == 0U)
&& (entry->startsNextSegment == 0U)
&& (PlsrTimerSettingsEqual(
&previous->setting, &entry->setting) != 0U)
&& (entry->repeatCount <= PlsrProfileRunLimit(
previous->setting.actualFrequencyHz))
&& (previous->repeatCount
<= (PlsrProfileRunLimit(
previous->setting.actualFrequencyHz)
- entry->repeatCount))
&& (previous->repeatCount
<= (0xFFFFFFFFUL - entry->repeatCount)))
{
previous->repeatCount += entry->repeatCount;
if (PlsrProfileEntryTryMerge(previous, entry) != 0U)
{
return 1U;
}
}
@@ -3103,21 +3166,33 @@ static uint32_t PlsrProfileTimingNow(void)
return *((volatile uint32_t *)0xE0001004UL);
}

static void PlsrProfileRecordProducerCycles(uint32_t startCycles)
static void PlsrProfileRecordProducerCycles(uint32_t startCycles,
uint16_t itemCount)
{
uint32_t elapsed = PlsrProfileTimingNow() - startCycles;
uint32_t total = PlsrProfileProducerTotalCycles;
uint32_t perItemCycles;

if (PlsrProfileProducerItemCount != 0xFFFFFFFFUL)
if (itemCount == 0U)
{
PlsrProfileProducerItemCount++;
return;
}
if (PlsrProfileProducerItemCount
<= (0xFFFFFFFFUL - (uint32_t)itemCount))
{
PlsrProfileProducerItemCount += itemCount;
}
else
{
PlsrProfileProducerItemCount = 0xFFFFFFFFUL;
}
PlsrProfileProducerTotalCycles =
(elapsed > (0xFFFFFFFFUL - total)) ? 0xFFFFFFFFUL
: total + elapsed;
if (elapsed > PlsrProfileProducerMaxItemCycles)
perItemCycles = (elapsed + itemCount - 1UL) / itemCount;
if (perItemCycles > PlsrProfileProducerMaxItemCycles)
{
PlsrProfileProducerMaxItemCycles = elapsed;
PlsrProfileProducerMaxItemCycles = perItemCycles;
}
}
#endif
@@ -3126,6 +3201,7 @@ static uint8_t PlsrProfileQueueFill(uint16_t targetCount,
uint16_t *itemBudget)
{
PLSR_SHORT_PROFILE candidate;
PLSR_PROFILE_ENTRY entries[PLSR_PROFILE_PRODUCER_BATCH_ITEMS];
PLSR_PROFILE_ENTRY entry;
uint32_t generation;
uint32_t producerEpoch;
@@ -3136,7 +3212,12 @@ static uint8_t PlsrProfileQueueFill(uint16_t targetCount,
uint32_t queueCount;
uint32_t currentGeneration;
uint32_t currentProducerEpoch;
uint32_t targetSpace;
PLSR_PROFILE_QUEUE *queue;
uint16_t batchLimit;
uint16_t generatedItems;
uint16_t entryCount;
uint16_t index;
uint8_t queueBank;
uint8_t currentBank;
uint8_t queueActive;
@@ -3158,6 +3239,10 @@ static uint8_t PlsrProfileQueueFill(uint16_t targetCount,
}
while (1)
{
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
startCycles = PlsrProfileTimingNow();
#endif
criticalState = PlsrPlatformEnterCritical();
queueBank = PlsrProfileQueueBank;
queue = &PlsrProfileQueues[queueBank];
@@ -3174,25 +3259,60 @@ static uint8_t PlsrProfileQueueFill(uint16_t targetCount,
PlsrPlatformExitCritical(criticalState);
return 1U;
}
(*itemBudget)--;
generation = queue->generation;
producerEpoch = queue->producerEpoch;
underrunDebt = queue->underrunDebtPulses;
payingUnderrunDebt = (underrunDebt != 0UL) ? 1U : 0U;
batchLimit = *itemBudget;
if (batchLimit > PLSR_PROFILE_PRODUCER_BATCH_ITEMS)
{
batchLimit = PLSR_PROFILE_PRODUCER_BATCH_ITEMS;
}
targetSpace = targetCount - queueCount;
if ((uint32_t)batchLimit > targetSpace)
{
batchLimit = (uint16_t)targetSpace;
}
if (payingUnderrunDebt != 0U)
{
/* Debt changes the queue generation and must be paid against the
latest published amount, so keep this exceptional path single. */
batchLimit = 1U;
}
PlsrCopyShortProfile(&candidate, &queue->producerProfile);
PlsrPlatformExitCritical(criticalState);

#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
startCycles = PlsrProfileTimingNow();
#endif
if (PlsrShortProfileTakeRunLimited(
&candidate, &entry,
(payingUnderrunDebt != 0U)
? underrunDebt : 0xFFFFFFFFUL) == 0U)
generatedItems = 0U;
entryCount = 0U;
while ((generatedItems < batchLimit)
&& (candidate.active != 0U))
{
if (PlsrShortProfileTakeRunLimited(
&candidate, &entry,
(payingUnderrunDebt != 0U)
? underrunDebt : 0xFFFFFFFFUL) == 0U)
{
return 0U;
}
generatedItems++;
if ((entryCount == 0U)
|| (PlsrProfileEntryTryMerge(
&entries[entryCount - 1U], &entry) == 0U))
{
entries[entryCount] = entry;
entryCount++;
}
if (payingUnderrunDebt != 0U)
{
break;
}
}
if ((generatedItems == 0U) || (entryCount == 0U))
{
return 0U;
}
*itemBudget = (uint16_t)(*itemBudget - generatedItems);

criticalState = PlsrPlatformEnterCritical();
currentBank = PlsrProfileQueueBank;
@@ -3206,29 +3326,38 @@ static uint8_t PlsrProfileQueueFill(uint16_t targetCount,
|| (queueActive == 0U)
|| (currentGeneration != generation)
|| (currentProducerEpoch != producerEpoch)
|| (queueCount >= PLSR_PROFILE_QUEUE_CAPACITY))
|| ((payingUnderrunDebt == 0U)
&& ((queueCount + entryCount)
> PLSR_PROFILE_QUEUE_CAPACITY)))
{
PlsrPlatformExitCritical(criticalState);
continue;
}
PlsrCopyShortProfile(&queue->producerProfile, &candidate);
if (payingUnderrunDebt != 0U)
{
underrunDebt = queue->underrunDebtPulses;
if (underrunDebt < entry.repeatCount)
if (underrunDebt < entries[0].repeatCount)
{
PlsrPlatformExitCritical(criticalState);
continue;
}
queue->underrunDebtPulses =
underrunDebt - entry.repeatCount;
underrunDebt - entries[0].repeatCount;
}
else if (PlsrProfileQueueAppendLocked(
queue, &entry) == 0U)
else
{
PlsrPlatformExitCritical(criticalState);
continue;
for (index = 0U; index < entryCount; index++)
{
if (PlsrProfileQueueAppendLocked(
queue, &entries[index]) == 0U)
{
PlsrPlatformExitCritical(criticalState);
return 0U;
}
}
}
PlsrCopyShortProfile(&queue->producerProfile, &candidate);
queueWriteIndex = queue->writeIndex;
queueReadIndex = queue->readIndex;
queueCount = queueWriteIndex - queueReadIndex;
@@ -3245,7 +3374,7 @@ static uint8_t PlsrProfileQueueFill(uint16_t targetCount,
PlsrPlatformExitCritical(criticalState);
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrProfileRecordProducerCycles(startCycles);
PlsrProfileRecordProducerCycles(startCycles, generatedItems);
#endif
}
}
@@ -3259,6 +3388,7 @@ static PLSR_PLATFORM_QUEUE_RESULT PlsrProfileQueueCommitNext(
uint32_t writeIndex;
uint32_t actualFrequencyHz;
uint32_t repeatRemaining;
uint16_t queueDepth;
PLSR_PLATFORM_QUEUE_RESULT result;

repeatRemaining = PlsrProfileQueue.repeatRemaining;
@@ -3299,10 +3429,10 @@ static PLSR_PLATFORM_QUEUE_RESULT PlsrProfileQueueCommitNext(
nextReadIndex = readIndex + 1UL;
PlsrProfileQueue.readIndex = nextReadIndex;
PlsrProfileQueue.repeatRemaining = entry->repeatCount - 1UL;
queueDepth = (uint16_t)(PlsrProfileQueue.writeIndex - nextReadIndex);
if (PlsrProfileQueue.generatorComplete == 0U)
{
PlsrProfileQueueRecordDepth(
(uint16_t)(PlsrProfileQueue.writeIndex - nextReadIndex));
PlsrProfileQueueRecordDepth(queueDepth);
}
PlsrDeferredFrequencyPending = 0U;
PlsrQueuedFrequencyHz = actualFrequencyHz;
@@ -3977,6 +4107,21 @@ static uint8_t PlsrStartSegment(uint8_t segmentNumber,
}
}

if ((PlsrActiveConfig.outputMode == PLSR_OUTPUT_PULSE_DIR)
&& (directionChanged != 0U)
&& (entryType == PLSR_SEGMENT_ENTRY_COMMAND_START))
{
/* Keep the expensive initial profile preparation and the direction
GPIO edge in different real 1 ms polls. The next poll publishes
direction and loads the full delay without decrementing it. */
PlsrInitialDirectionPrepareLevel = directionLevel;
PlsrInitialDirectionPrepareEpoch = PlsrSegmentEpoch;
PlsrInitialDirectionPreparePending = 1U;
PlsrDelayedEntryType = entryType;
PlsrRunStatus = PLSR_STATUS_ACCELERATING;
return 1U;
}

if (PlsrPlatformPrepare((uint8_t)PlsrActiveConfig.pulseOutput,
(uint8_t)PlsrActiveConfig.directionOutput,
directionLevel,
@@ -4010,6 +4155,56 @@ static uint8_t PlsrStartSegment(uint8_t segmentNumber,
return PlsrBeginSegmentOutput(startFrequencyHz);
}

static uint8_t PlsrServiceInitialDirectionPrepare(void)
{
uint64_t magnitude;
uint32_t criticalState;
uint8_t segmentNumber;
uint8_t positive;
uint8_t directionLevel;

if (PlsrInitialDirectionPreparePending == 0U)
{
return 0U;
}
if ((PlsrInitialDirectionPrepareEpoch != PlsrSegmentEpoch)
|| (PlsrCurrentSegment == 0U)
|| (PlsrCurrentSegment > PlsrActiveConfig.segmentCount)
|| (PlsrActiveConfig.outputMode != PLSR_OUTPUT_PULSE_DIR)
|| (PlsrRunStatus != PLSR_STATUS_ACCELERATING))
{
PlsrInitialDirectionPreparePending = 0U;
PlsrEnterError(PLSR_ERROR_INTERNAL);
return 1U;
}

directionLevel = PlsrInitialDirectionPrepareLevel;
segmentNumber = PlsrCurrentSegment;
positive = PlsrCountPositive;
PlsrInitialDirectionPreparePending = 0U;
if (PlsrPlatformPrepare((uint8_t)PlsrActiveConfig.pulseOutput,
(uint8_t)PlsrActiveConfig.directionOutput,
directionLevel,
(uint8_t)PlsrActiveConfig.outputMode,
positive) == 0U)
{
criticalState = PlsrPlatformEnterCritical();
PlsrInvalidateTimedStartLocked();
PlsrPlatformExitCritical(criticalState);
PlsrEnterError(PLSR_ERROR_INVALID_RESOURCE);
return 1U;
}

magnitude = PlsrRemainingSnapshot();
PlsrDiagnosticBeginSegment(segmentNumber, magnitude, positive);
PlsrLastDirectionValid = 1U;
PlsrLastDirectionOutput = (uint8_t)PlsrActiveConfig.directionOutput;
PlsrLastDirectionLevel = directionLevel;
PlsrDirectionDelayRemainingMs = PlsrEffectiveDirectionDelayMs();
PlsrDirectionDelayActive = 1U;
return 1U;
}

static void PlsrMarkPersistenceDirty(uint16_t delayMs)
{
PlsrPersistenceDirty = 1U;
@@ -4065,6 +4260,7 @@ static void PlsrFinishCompleted(void)
PlsrSegmentClockStarted = 0U;
PlsrDirectionDelayActive = 0U;
PlsrDirectionDelayRemainingMs = 0U;
PlsrInitialDirectionPreparePending = 0U;
PlsrExtCaptureSegment = 0U;
PlsrExtEdgePending = 0U;
PlsrStopRequested = 0U;
@@ -4107,6 +4303,7 @@ static void PlsrFinishStopped(void)
PlsrSegmentClockStarted = 0U;
PlsrDirectionDelayActive = 0U;
PlsrDirectionDelayRemainingMs = 0U;
PlsrInitialDirectionPreparePending = 0U;
PlsrExtCaptureSegment = 0U;
PlsrExtEdgePending = 0U;
PlsrStopRequested = 0U;
@@ -4149,6 +4346,7 @@ static void PlsrEnterError(PLSR_ERROR error)
PlsrSegmentClockStarted = 0U;
PlsrDirectionDelayActive = 0U;
PlsrDirectionDelayRemainingMs = 0U;
PlsrInitialDirectionPreparePending = 0U;
PlsrExtCaptureSegment = 0U;
PlsrExtEdgePending = 0U;
PlsrStopRequested = 0U;
@@ -5651,6 +5849,9 @@ uint8_t PlsrInit(void)
PlsrCurrentSegment = 0U;
PlsrDirectionDelayActive = 0U;
PlsrDirectionDelayRemainingMs = 0U;
PlsrInitialDirectionPreparePending = 0U;
PlsrInitialDirectionPrepareLevel = 0U;
PlsrInitialDirectionPrepareEpoch = 0UL;
PlsrSegmentClockStarted = 0U;
PlsrExtPreviousLevel = 0U;
PlsrExtEdgePending = 0U;
@@ -5677,6 +5878,9 @@ uint8_t PlsrInit(void)
PlsrProfileProducerItemCount = 0UL;
PlsrProfileProducerTotalCycles = 0UL;
PlsrProfileProducerMaxItemCycles = 0UL;
#if !defined(PLSR_HOST_TEST)
PlsrPlannerTimingReset();
#endif
#endif
PlsrError = PLSR_ERROR_NONE;
PlsrRunStatus = PLSR_STATUS_IDLE;
@@ -5765,6 +5969,7 @@ static void PlsrExecuteStart(void)
PlsrShortProfile.active = 0U;
PlsrError = PLSR_ERROR_NONE;
PlsrLastDirectionValid = 0U;
PlsrInitialDirectionPreparePending = 0U;
PlsrDelayedEntryType = PLSR_SEGMENT_ENTRY_COMMAND_START;
PlsrPulseDirMotionStarted = 0U;
PlsrCheckpointPosition(1U);
@@ -5893,6 +6098,7 @@ static void PlsrExecuteClear(void)
PlsrBoundaryWasCut = 0U;
PlsrDirectionDelayActive = 0U;
PlsrDirectionDelayRemainingMs = 0U;
PlsrInitialDirectionPreparePending = 0U;
PlsrExtCaptureSegment = 0U;
PlsrExtEdgePending = 0U;
PlsrShortProfile.active = 0U;
@@ -6025,78 +6231,127 @@ static uint8_t PlsrServiceCountedExecutor(void)
return 1U;
}

uint8_t PlsrServiceProfileProducer(uint16_t itemBudget)
{
uint8_t executorMode;

if ((PlsrInitialized == 0U) || (itemBudget == 0U))
{
return 0U;
}

executorMode = PlsrExecutor.mode;
if (((executorMode == PLSR_EXEC_STEP_TABLE)
|| (executorMode == PLSR_EXEC_STREAM)
|| (executorMode == PLSR_EXEC_AB_LEGACY))
&& (PlsrProfileQueue.active != 0U)
&& (PlsrProfileQueue.generatorComplete == 0U)
&& (PlsrProfileQueueFill(PLSR_PROFILE_REFILL_TARGET,
&itemBudget) == 0U))
{
PlsrEnterError(PLSR_ERROR_TIMER);
return 0U;
}

executorMode = PlsrExecutor.mode;
if (((executorMode == PLSR_EXEC_STEP_TABLE)
|| (executorMode == PLSR_EXEC_STREAM))
&& ((PlsrStageCountedHandoff() == 0U)
|| (PlsrProfileQueueFill(PLSR_PROFILE_REFILL_TARGET,
&itemBudget) == 0U)))
{
PlsrEnterError(PLSR_ERROR_TIMER);
return 0U;
}

executorMode = PlsrExecutor.mode;
if (((executorMode == PLSR_EXEC_STEP_TABLE)
|| (executorMode == PLSR_EXEC_STREAM)
|| (executorMode == PLSR_EXEC_AB_LEGACY))
&& (PlsrProfileQueue.active != 0U)
&& (PlsrProfileQueue.generatorComplete == 0U)
&& (PlsrProfileQueueCount() < PLSR_PROFILE_REFILL_TARGET))
{
return 1U;
}

return 0U;
}

void PlsrPoll1ms(void)
{
uint8_t extLevel;
uint8_t extEdge;
uint8_t activeSegmentNumber;
uint8_t applyDynamicFrequency = 0U;
uint8_t pulseDirReplanRequested = 0U;
uint8_t pulseDirReplanFailed = 0U;
PLSR_SEGMENT_CONFIG *activeSegment;
uint32_t criticalState;
uint32_t newTargetHz;
uint32_t pulseDirReplanPulses = 0UL;
uint32_t pollEpoch;
uint16_t fillBudget = PLSR_PROFILE_REFILL_BUDGET;
uint8_t extLevel;//这一拍读到的 EXT 引脚电平。后面才用,这段没用
uint8_t extEdge;//这一拍有没有上升沿。后面才用
uint8_t activeSegmentNumber;//当前段号的快照
uint8_t applyDynamicFrequency = 0U;//要不要改目标频率。0 = 先当不用改
uint8_t pulseDirReplanRequested = 0U;//要不要对 PULSE/DIR 整段重规划。0 = 不要
uint8_t pulseDirReplanFailed = 0U;//重规划失败标志。先清零
PLSR_SEGMENT_CONFIG *activeSegment;//指向当前段配置。后面才赋值
uint32_t criticalState;//进临界区前的中断状态,退出时原样恢复。这段还没进临界区
uint32_t newTargetHz;//改频后的新目标。后面才用
uint32_t pulseDirReplanPulses = 0UL;//重规划还剩多少脉冲。先 0
uint32_t pollEpoch;//这一拍看到的段世代号,防止用过期段的 EXT/改频
PLSR_PLATFORM_SERVICE_RESULT pulseDirReplanResult =
PLSR_PLATFORM_SERVICE_READY;
PLSR_PLATFORM_SERVICE_READY;//重规划结果,先当成功

if (PlsrInitialized == 0U)
{
return;
}

#if !defined(PLSR_HOST_TEST)
/* Run the target throughput benchmark only while idle. It blocks task
context while measuring but never starts pulse output. */
if ((PlsrPlannerBenchmarkRequest != 0UL)
&& (PlsrIsBusy() == 0U))
{
PlsrPlannerBenchmarkService();
return;
}
#endif

if (PlsrPollCommandMailbox() != 0U)
{
/*看邮箱有没有 START / STOP / CLEAR。
有命令:当场执行(启动、停止、清位置)。
返回 非 0:这拍到此结束(常见是 STOP 认为后面不用再 poll)。
返回 0:没命令,或 STOP 说还可以继续 → 往下走。*/
return;
}

if (PlsrServiceInitialDirectionPrepare() != 0U)
{
/* Direction changed in this poll. Leave the complete configured
delay untouched until the following real 1 ms poll. */
return;
}

/* 段边界事件必须在任何计数同步之前消费:快照是 IRQ 边界时刻的
硬件计数,晚消费会把下一 run 的脉冲算进旧段。 */
PlsrExecServiceCountedEvents();
PlsrExecServiceCountedEvents();//把 IRQ 丢进边界事件环的东西在任务里消化:切段、记账。只做账,不算频率
if (PlsrTimerErrorPending != 0U)
{
/*IRQ 或平台已经锁存了定时器故障。
进错误态,停后续。故障了还 Fill 会越帮越忙*/
PlsrEnterError(PLSR_ERROR_TIMER);
return;
}

PlsrPollPositionCheckpoint();
PlsrPollPositionCheckpoint();//该把位置/配置写到掉电保存就写。和发波、队列无关,夹在中间是因为 1ms 顺手做

if (PlsrServiceCountedExecutor() != 0U)
{
/*给正在按队列发波 的执行器做服务:用硬件计数更新剩余脉冲和位置;
若 整段已经发完(硬件 completion),清队列、置 BoundaryPending,返回 1。
还在跑 → 返回 0,if 不进。
已经发完 → 进大括号*/
if (PlsrBoundaryPending != 0U)
{
PlsrHandleBoundary(0U);
//有段边界待处理:接下一段、等待、完成发送等。参数 0 = 不是从脉冲 IRQ 里来的
}
PlsrPollPersistenceDelay();
return;
}

if (PlsrProfileQueueCount() <= PLSR_PROFILE_LOW_WATER)
{
fillBudget = PLSR_PROFILE_STARTUP_BUDGET;
}

if (((PlsrExecutor.mode == PLSR_EXEC_STEP_TABLE)
|| (PlsrExecutor.mode == PLSR_EXEC_STREAM)
|| (PlsrExecutor.mode == PLSR_EXEC_AB_LEGACY))
&& (PlsrProfileQueue.active != 0U)
&& (PlsrProfileQueue.generatorComplete == 0U)
&& (PlsrProfileQueueFill(PLSR_PROFILE_REFILL_TARGET,
&fillBudget) == 0U))
{
PlsrEnterError(PLSR_ERROR_TIMER);
return;
}
if (((PlsrExecutor.mode == PLSR_EXEC_STEP_TABLE)
|| (PlsrExecutor.mode == PLSR_EXEC_STREAM))
&& ((PlsrStageCountedHandoff() == 0U)
|| (PlsrProfileQueueFill(PLSR_PROFILE_REFILL_TARGET,
&fillBudget) == 0U)))
{
PlsrEnterError(PLSR_ERROR_TIMER);
return;
PlsrPollPersistenceDelay();//掉电保存的延时到了就存
return;//这段波已经结束,本拍不要再补队列。Fill 是给还在跑的段用的
}

/* This work is bounded and targets only the inactive bank. It is done
@@ -7082,4 +7337,9 @@ uint32_t PlsrTestLastDiagnosticFinishEvent(void)
{
return PlsrHostTestLastDiagnosticFinishEvent;
}

uint8_t PlsrTestInitialDirectionPreparePending(void)
{
return PlsrInitialDirectionPreparePending;
}
#endif

+ 524
- 54
PLSR/Src/plsr_planner.c Просмотреть файл

@@ -5,34 +5,208 @@

#define PLSR_PLANNER_Q32_ONE (4294967296ULL)

static const uint32_t PlsrPlannerSmoothIntegralQ24[65] =
#if !defined(PLSR_HOST_TEST)
#define PLSR_PLANNER_BENCHMARK_DEMCR_ADDRESS (0xE000EDFCUL)
#define PLSR_PLANNER_BENCHMARK_DWT_CTRL (0xE0001000UL)
#define PLSR_PLANNER_BENCHMARK_DWT_CYCCNT (0xE0001004UL)
#define PLSR_PLANNER_BENCHMARK_TRCENA (1UL << 24U)
#define PLSR_PLANNER_BENCHMARK_CYCCNTENA (1UL << 0U)
#define PLSR_PLANNER_BENCHMARK_PULSES (100000UL)
#define PLSR_PLANNER_BENCHMARK_BATCH_CALLS (128UL)
#define PLSR_PLANNER_BENCHMARK_TARGET_HZ (100000UL)

volatile uint32_t PlsrPlannerBenchmarkRequest;
volatile uint32_t PlsrPlannerBenchmarkRunning;
volatile uint32_t PlsrPlannerBenchmarkRunCount;
volatile uint32_t PlsrPlannerBenchmarkCoreClockHz = 168000000UL;
volatile PLSR_PLANNER_BENCHMARK_RESULT PlsrPlannerBenchmarkResults[3];
#endif

#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
#define PLSR_PLANNER_CYCCNT_ADDRESS (0xE0001004UL)

volatile PLSR_PLANNER_TIMING PlsrPlannerTiming;

static uint32_t PlsrPlannerTimingNow(void)
{
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
return *((volatile uint32_t *)PLSR_PLANNER_CYCCNT_ADDRESS);
}

static void PlsrPlannerTimingRecord(
volatile PLSR_PLANNER_TIMING_SAMPLE *sample,
uint32_t startedAt)
{
uint32_t elapsed = PlsrPlannerTimingNow() - startedAt;
uint32_t total = sample->totalCycles;

sample->lastCycles = elapsed;
if ((sample->callCount == 0UL) || (elapsed < sample->minCycles))
{
sample->minCycles = elapsed;
}
if (sample->callCount != 0xFFFFFFFFUL)
{
sample->callCount++;
}
sample->totalCycles = (elapsed > (0xFFFFFFFFUL - total))
? 0xFFFFFFFFUL : total + elapsed;
if (elapsed > sample->maxCycles)
{
sample->maxCycles = elapsed;
}
}

void PlsrPlannerTimingReset(void)
{
(void)memset((void *)&PlsrPlannerTiming, 0, sizeof(PlsrPlannerTiming));
}
#endif

#define PLSR_PLANNER_CURVE_TABLE_BITS (9U)
#define PLSR_PLANNER_CURVE_TABLE_INTERVALS (1UL << PLSR_PLANNER_CURVE_TABLE_BITS)
#define PLSR_PLANNER_CURVE_TABLE_SIZE (PLSR_PLANNER_CURVE_TABLE_INTERVALS + 1UL)
#define PLSR_PLANNER_CURVE_DERIVATIVE_SHIFT (8U + PLSR_PLANNER_CURVE_TABLE_BITS)

/* curveMode 1: fixed seven-section jerk profile. Each entry/exit ramp
uses a 1:2:1 constant-jerk/constant-acceleration/constant-jerk ratio.
The table stores the integral of the normalized frequency blend. */
static const uint32_t PlsrPlannerSmoothIntegralQ24[PLSR_PLANNER_CURVE_TABLE_SIZE] =
{
0UL, 0UL, 1UL, 3UL, 7UL, 14UL, 24UL, 38UL,
57UL, 81UL, 111UL, 148UL, 192UL, 244UL, 305UL, 375UL,
455UL, 546UL, 648UL, 762UL, 889UL, 1029UL, 1183UL, 1352UL,
1536UL, 1736UL, 1953UL, 2187UL, 2439UL, 2710UL, 3000UL, 3310UL,
3641UL, 3993UL, 4367UL, 4764UL, 5184UL, 5628UL, 6097UL, 6591UL,
7111UL, 7658UL, 8232UL, 8834UL, 9465UL, 10125UL, 10815UL, 11536UL,
12288UL, 13072UL, 13889UL, 14739UL, 15623UL, 16542UL, 17496UL, 18486UL,
19513UL, 20577UL, 21679UL, 22820UL, 24000UL, 25220UL, 26481UL, 27783UL,
29127UL, 30514UL, 31944UL, 33418UL, 34937UL, 36501UL, 38111UL, 39768UL,
41472UL, 43224UL, 45025UL, 46875UL, 48775UL, 50726UL, 52728UL, 54782UL,
56889UL, 59049UL, 61263UL, 63532UL, 65856UL, 68236UL, 70673UL, 73167UL,
75719UL, 78330UL, 81000UL, 83730UL, 86521UL, 89373UL, 92287UL, 95264UL,
98304UL, 101408UL, 104577UL, 107811UL, 111111UL, 114478UL, 117912UL, 121414UL,
124985UL, 128625UL, 132335UL, 136116UL, 139968UL, 143892UL, 147889UL, 151959UL,
156103UL, 160322UL, 164616UL, 168986UL, 173433UL, 177957UL, 182559UL, 187240UL,
192000UL, 196840UL, 201761UL, 206763UL, 211847UL, 217014UL, 222264UL, 227598UL,
233017UL, 238521UL, 244110UL, 249785UL, 255545UL, 261390UL, 267321UL, 273337UL,
279438UL, 285625UL, 291897UL, 298254UL, 304697UL, 311225UL, 317838UL, 324537UL,
331321UL, 338190UL, 345145UL, 352185UL, 359310UL, 366521UL, 373817UL, 381198UL,
388665UL, 396217UL, 403854UL, 411577UL, 419385UL, 427278UL, 435257UL, 443321UL,
451470UL, 459705UL, 468025UL, 476430UL, 484921UL, 493497UL, 502158UL, 510905UL,
519737UL, 528654UL, 537657UL, 546745UL, 555918UL, 565177UL, 574521UL, 583950UL,
593465UL, 603065UL, 612750UL, 622521UL, 632377UL, 642318UL, 652345UL, 662457UL,
672654UL, 682937UL, 693305UL, 703758UL, 714297UL, 724921UL, 735630UL, 746425UL,
757305UL, 768270UL, 779321UL, 790457UL, 801678UL, 812985UL, 824377UL, 835854UL,
847417UL, 859065UL, 870798UL, 882617UL, 894521UL, 906510UL, 918585UL, 930745UL,
942990UL, 955321UL, 967737UL, 980238UL, 992825UL, 1005497UL, 1018254UL, 1031097UL,
1044025UL, 1057038UL, 1070137UL, 1083321UL, 1096590UL, 1109945UL, 1123385UL, 1136910UL,
1150521UL, 1164217UL, 1177998UL, 1191865UL, 1205817UL, 1219854UL, 1233977UL, 1248185UL,
1262478UL, 1276857UL, 1291321UL, 1305870UL, 1320505UL, 1335225UL, 1350030UL, 1364921UL,
1379897UL, 1394958UL, 1410105UL, 1425337UL, 1440654UL, 1456057UL, 1471545UL, 1487118UL,
1502777UL, 1518521UL, 1534350UL, 1550265UL, 1566265UL, 1582350UL, 1598521UL, 1614777UL,
1631118UL, 1647545UL, 1664057UL, 1680654UL, 1697337UL, 1714105UL, 1730958UL, 1747897UL,
1764921UL, 1782030UL, 1799225UL, 1816505UL, 1833870UL, 1851321UL, 1868857UL, 1886478UL,
1904185UL, 1921977UL, 1939854UL, 1957817UL, 1975865UL, 1993998UL, 2012217UL, 2030521UL,
2048910UL, 2067385UL, 2085945UL, 2104590UL, 2123321UL, 2142137UL, 2161038UL, 2180025UL,
2199097UL, 2218254UL, 2237497UL, 2256825UL, 2276238UL, 2295737UL, 2315321UL, 2334990UL,
2354745UL, 2374585UL, 2394510UL, 2414521UL, 2434617UL, 2454798UL, 2475065UL, 2495417UL,
2515854UL, 2536377UL, 2556985UL, 2577678UL, 2598457UL, 2619321UL, 2640270UL, 2661305UL,
2682425UL, 2703630UL, 2724921UL, 2746297UL, 2767758UL, 2789305UL, 2810937UL, 2832654UL,
2854457UL, 2876345UL, 2898318UL, 2920377UL, 2942521UL, 2964750UL, 2987065UL, 3009465UL,
3031950UL, 3054521UL, 3077177UL, 3099918UL, 3122745UL, 3145657UL, 3168654UL, 3191737UL,
3214905UL, 3238158UL, 3261497UL, 3284921UL, 3308430UL, 3332025UL, 3355705UL, 3379470UL,
3403321UL, 3427257UL, 3451278UL, 3475385UL, 3499577UL, 3523854UL, 3548217UL, 3572665UL,
3597198UL, 3621817UL, 3646521UL, 3671310UL, 3696185UL, 3721145UL, 3746190UL, 3771321UL,
3796537UL, 3821838UL, 3847225UL, 3872697UL, 3898254UL, 3923897UL, 3949625UL, 3975438UL,
4001337UL, 4027321UL, 4053390UL, 4079545UL, 4105785UL, 4132110UL, 4158521UL, 4185017UL,
4211598UL, 4238265UL, 4265017UL, 4291854UL, 4318777UL, 4345785UL, 4372878UL, 4400057UL,
4427321UL, 4454670UL, 4482104UL, 4509622UL, 4537223UL, 4564907UL, 4592673UL, 4620520UL,
4648448UL, 4676456UL, 4704543UL, 4732709UL, 4760953UL, 4789274UL, 4817672UL, 4846146UL,
4874695UL, 4903319UL, 4932017UL, 4960788UL, 4989632UL, 5018548UL, 5047535UL, 5076593UL,
5105721UL, 5134918UL, 5164184UL, 5193518UL, 5222919UL, 5252387UL, 5281921UL, 5311520UL,
5341184UL, 5370912UL, 5400703UL, 5430557UL, 5460473UL, 5490450UL, 5520488UL, 5550586UL,
5580743UL, 5610959UL, 5641233UL, 5671564UL, 5701952UL, 5732396UL, 5762895UL, 5793449UL,
5824057UL, 5854718UL, 5885432UL, 5916198UL, 5947015UL, 5977883UL, 6008801UL, 6039768UL,
6070784UL, 6101848UL, 6132959UL, 6164117UL, 6195321UL, 6226570UL, 6257864UL, 6289202UL,
6320583UL, 6352007UL, 6383473UL, 6414980UL, 6446528UL, 6478116UL, 6509743UL, 6541409UL,
6573113UL, 6604854UL, 6636632UL, 6668446UL, 6700295UL, 6732179UL, 6764097UL, 6796048UL,
6828032UL, 6860048UL, 6892095UL, 6924173UL, 6956281UL, 6988418UL, 7020584UL, 7052778UL,
7084999UL, 7117247UL, 7149521UL, 7181820UL, 7214144UL, 7246492UL, 7278863UL, 7311257UL,
7343673UL, 7376110UL, 7408568UL, 7441046UL, 7473543UL, 7506059UL, 7538593UL, 7571144UL,
7603712UL, 7636296UL, 7668895UL, 7701509UL, 7734137UL, 7766778UL, 7799432UL, 7832098UL,
7864775UL, 7897463UL, 7930161UL, 7962868UL, 7995584UL, 8028308UL, 8061039UL, 8093777UL,
8126521UL, 8159270UL, 8192024UL, 8224782UL, 8257543UL, 8290307UL, 8323073UL, 8355840UL,
8388608UL
};

static const uint32_t PlsrPlannerSineIntegralQ24[65] =
static const uint32_t PlsrPlannerSineIntegralQ24[PLSR_PLANNER_CURVE_TABLE_SIZE] =
{
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
0UL, 0UL, 1UL, 3UL, 7UL, 13UL, 22UL, 35UL,
53UL, 75UL, 103UL, 137UL, 178UL, 226UL, 282UL, 347UL,
421UL, 505UL, 599UL, 705UL, 822UL, 951UL, 1094UL, 1250UL,
1420UL, 1604UL, 1805UL, 2021UL, 2254UL, 2503UL, 2771UL, 3057UL,
3362UL, 3687UL, 4032UL, 4398UL, 4785UL, 5194UL, 5626UL, 6081UL,
6560UL, 7063UL, 7592UL, 8146UL, 8726UL, 9333UL, 9967UL, 10630UL,
11321UL, 12041UL, 12791UL, 13571UL, 14382UL, 15225UL, 16100UL, 17008UL,
17949UL, 18923UL, 19933UL, 20977UL, 22057UL, 23173UL, 24325UL, 25516UL,
26744UL, 28010UL, 29316UL, 30661UL, 32046UL, 33472UL, 34939UL, 36449UL,
38000UL, 39595UL, 41233UL, 42915UL, 44642UL, 46414UL, 48232UL, 50096UL,
52007UL, 53966UL, 55972UL, 58027UL, 60131UL, 62284UL, 64487UL, 66742UL,
69047UL, 71404UL, 73813UL, 76275UL, 78790UL, 81359UL, 83982UL, 86660UL,
89393UL, 92182UL, 95028UL, 97930UL, 100890UL, 103908UL, 106984UL, 110119UL,
113314UL, 116568UL, 119882UL, 123258UL, 126695UL, 130194UL, 133755UL, 137379UL,
141066UL, 144817UL, 148632UL, 152512UL, 156457UL, 160468UL, 164545UL, 168688UL,
172899UL, 177177UL, 181523UL, 185937UL, 190421UL, 194973UL, 199596UL, 204289UL,
209052UL, 213886UL, 218792UL, 223770UL, 228820UL, 233943UL, 239140UL, 244409UL,
249753UL, 255172UL, 260665UL, 266234UL, 271878UL, 277599UL, 283396UL, 289270UL,
295221UL, 301250UL, 307358UL, 313543UL, 319808UL, 326151UL, 332575UL, 339078UL,
345662UL, 352326UL, 359072UL, 365899UL, 372808UL, 379799UL, 386873UL, 394029UL,
401269UL, 408592UL, 416000UL, 423491UL, 431068UL, 438729UL, 446475UL, 454307UL,
462225UL, 470229UL, 478320UL, 486497UL, 494762UL, 503114UL, 511554UL, 520082UL,
528698UL, 537403UL, 546197UL, 555080UL, 564053UL, 573116UL, 582268UL, 591511UL,
600845UL, 610269UL, 619785UL, 629392UL, 639090UL, 648881UL, 658763UL, 668739UL,
678806UL, 688967UL, 699221UL, 709568UL, 720008UL, 730543UL, 741171UL, 751894UL,
762711UL, 773623UL, 784629UL, 795731UL, 806928UL, 818220UL, 829608UL, 841092UL,
852672UL, 864348UL, 876121UL, 887990UL, 899955UL, 912018UL, 924178UL, 936435UL,
948789UL, 961241UL, 973790UL, 986438UL, 999183UL, 1012026UL, 1024968UL, 1038007UL,
1051146UL, 1064383UL, 1077718UL, 1091153UL, 1104686UL, 1118319UL, 1132051UL, 1145882UL,
1159812UL, 1173841UL, 1187971UL, 1202200UL, 1216528UL, 1230956UL, 1245485UL, 1260113UL,
1274841UL, 1289669UL, 1304597UL, 1319626UL, 1334754UL, 1349983UL, 1365312UL, 1380742UL,
1396271UL, 1411902UL, 1427632UL, 1443464UL, 1459395UL, 1475428UL, 1491560UL, 1507793UL,
1524127UL, 1540561UL, 1557096UL, 1573732UL, 1590467UL, 1607304UL, 1624240UL, 1641278UL,
1658415UL, 1675654UL, 1692992UL, 1710431UL, 1727970UL, 1745610UL, 1763349UL, 1781189UL,
1799129UL, 1817169UL, 1835309UL, 1853548UL, 1871888UL, 1890328UL, 1908867UL, 1927505UL,
1946244UL, 1965082UL, 1984019UL, 2003055UL, 2022190UL, 2041425UL, 2060758UL, 2080191UL,
2099722UL, 2119351UL, 2139080UL, 2158906UL, 2178831UL, 2198854UL, 2218974UL, 2239193UL,
2259509UL, 2279923UL, 2300434UL, 2321042UL, 2341747UL, 2362550UL, 2383449UL, 2404444UL,
2425536UL, 2446724UL, 2468008UL, 2489388UL, 2510864UL, 2532435UL, 2554101UL, 2575863UL,
2597719UL, 2619670UL, 2641715UL, 2663855UL, 2686088UL, 2708416UL, 2730837UL, 2753351UL,
2775958UL, 2798659UL, 2821451UL, 2844337UL, 2867314UL, 2890384UL, 2913545UL, 2936797UL,
2960141UL, 2983575UL, 3007100UL, 3030716UL, 3054421UL, 3078216UL, 3102101UL, 3126075UL,
3150138UL, 3174290UL, 3198530UL, 3222858UL, 3247274UL, 3271777UL, 3296368UL, 3321045UL,
3345809UL, 3370659UL, 3395595UL, 3420617UL, 3445724UL, 3470915UL, 3496192UL, 3521552UL,
3546997UL, 3572525UL, 3598137UL, 3623831UL, 3649608UL, 3675467UL, 3701408UL, 3727430UL,
3753534UL, 3779718UL, 3805983UL, 3832327UL, 3858752UL, 3885255UL, 3911838UL, 3938498UL,
3965237UL, 3992054UL, 4018948UL, 4045919UL, 4072966UL, 4100090UL, 4127289UL, 4154564UL,
4181913UL, 4209337UL, 4236836UL, 4264407UL, 4292052UL, 4319770UL, 4347560UL, 4375422UL,
4403356UL, 4431361UL, 4459436UL, 4487581UL, 4515797UL, 4544081UL, 4572435UL, 4600857UL,
4629347UL, 4657904UL, 4686529UL, 4715220UL, 4743977UL, 4772800UL, 4801688UL, 4830641UL,
4859658UL, 4888739UL, 4917883UL, 4947090UL, 4976359UL, 5005690UL, 5035082UL, 5064536UL,
5094050UL, 5123623UL, 5153256UL, 5182948UL, 5212698UL, 5242506UL, 5272372UL, 5302294UL,
5332273UL, 5362308UL, 5392398UL, 5422543UL, 5452742UL, 5482995UL, 5513301UL, 5543660UL,
5574071UL, 5604534UL, 5635047UL, 5665612UL, 5696227UL, 5726891UL, 5757604UL, 5788366UL,
5819175UL, 5850032UL, 5880936UL, 5911886UL, 5942882UL, 5973923UL, 6005009UL, 6036139UL,
6067312UL, 6098529UL, 6129787UL, 6161088UL, 6192430UL, 6223813UL, 6255236UL, 6286698UL,
6318200UL, 6349740UL, 6381317UL, 6412933UL, 6444585UL, 6476273UL, 6507997UL, 6539755UL,
6571549UL, 6603376UL, 6635236UL, 6667129UL, 6699054UL, 6731011UL, 6762999UL, 6795017UL,
6827065UL, 6859142UL, 6891247UL, 6923381UL, 6955542UL, 6987730UL, 7019944UL, 7052183UL,
7084448UL, 7116737UL, 7149050UL, 7181386UL, 7213745UL, 7246126UL, 7278528UL, 7310951UL,
7343394UL, 7375857UL, 7408339UL, 7440839UL, 7473358UL, 7505893UL, 7538445UL, 7571012UL,
7603596UL, 7636194UL, 7668806UL, 7701431UL, 7734070UL, 7766721UL, 7799383UL, 7832057UL,
7864741UL, 7897435UL, 7930138UL, 7962850UL, 7995570UL, 8028297UL, 8061031UL, 8093771UL,
8126517UL, 8159267UL, 8192022UL, 8224781UL, 8257543UL, 8290307UL, 8323073UL, 8355840UL,
8388608UL
};

static uint32_t PlsrPlannerAbsDifference(uint32_t first, uint32_t second)
@@ -40,9 +214,9 @@ 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)
static uint32_t PlsrPlannerRampTime(const PLSR_MOTION_BLOCK *block,uint32_t fromHz, uint32_t toHz)
{
uint32_t baseTimeMs;
uint64_t durationMs;
@@ -214,7 +388,8 @@ static uint32_t PlsrPlannerPeak(const PLSR_MOTION_BLOCK *block,
}
return (peakHz > lowerEndpoint) ? lowerEndpoint : peakHz;
}

//C(x)函数
//瞬时速度=delta*C(x)
static uint64_t PlsrPlannerCurveIntegralQ32(uint64_t progressQ32,
uint16_t curveMode)
{
@@ -235,7 +410,7 @@ static uint64_t PlsrPlannerCurveIntegralQ32(uint64_t progressQ32,
}
table = (curveMode == 1U) ? PlsrPlannerSmoothIntegralQ24
: PlsrPlannerSineIntegralQ24;
scaled = progressQ32 * 64ULL;
scaled = progressQ32 * PLSR_PLANNER_CURVE_TABLE_INTERVALS;
index = (uint32_t)(scaled >> 32U);
fraction = (uint32_t)scaled;
first = (uint64_t)table[index] << 8U;
@@ -306,10 +481,11 @@ static uint32_t PlsrPlannerInstantFrequency(
table = (context->block.curveMode == 1U)
? PlsrPlannerSmoothIntegralQ24
: PlsrPlannerSineIntegralQ24;
scaled = progressQ32 * 64ULL;
scaled = progressQ32 * PLSR_PLANNER_CURVE_TABLE_INTERVALS;
index = (uint32_t)(scaled >> 32U);
curveProgressQ32 =
(uint64_t)(table[index + 1UL] - table[index]) << 14U;
(uint64_t)(table[index + 1UL] - table[index])
<< PLSR_PLANNER_CURVE_DERIVATIVE_SHIFT;
}
if (context->rampToHz >= context->rampFromHz)
{
@@ -401,38 +577,86 @@ static void PlsrPlannerStartRamp(PLSR_PLANNER_CONTEXT *context,
uint32_t toHz,
uint32_t pulseCount)
{
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
uint32_t totalStartedAt = PlsrPlannerTimingNow();
uint32_t phaseStartedAt;
#endif

context->rampKind = rampKind;
context->rampRelativePulse = 0UL;
context->rampPulseCount = pulseCount;
context->rampFromHz = fromHz;
context->rampToHz = toHz;
context->rampDurationMs = PlsrPlannerRampTime(&context->block,
fromHz, toHz);
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
phaseStartedAt = PlsrPlannerTimingNow();
#endif
context->rampDurationMs = PlsrPlannerRampTime(&context->block, fromHz, toHz);
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampTime, phaseStartedAt);
phaseStartedAt = PlsrPlannerTimingNow();
#endif
context->rampTotalAreaQ32 = PlsrPlannerRampAreaQ32(
context, fromHz, toHz, PLSR_PLANNER_Q32_ONE);
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampTotalArea,
phaseStartedAt);
phaseStartedAt = PlsrPlannerTimingNow();
#endif
context->rampAreaStepQ32 = context->rampTotalAreaQ32 / pulseCount;
context->rampAreaRemainder =
(uint32_t)(context->rampTotalAreaQ32 % pulseCount);
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampAreaSplit,
phaseStartedAt);
#endif
context->rampRemainderAccumulator = 0UL;
context->rampTargetAreaQ32 = 0ULL;
context->rampBoundaryQ32 = 0ULL;
context->rampActualTimeQ32 = 0ULL;
context->rampLastPhaseStepQ32 = 0ULL;
context->lastRampHz = 0UL;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
phaseStartedAt = PlsrPlannerTimingNow();
#endif
context->rampFirstBoundaryQ32 = PlsrPlannerExactBoundaryQ32(
context, 0ULL, context->rampAreaStepQ32);
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampFirstBoundary,
phaseStartedAt);
#endif
if (pulseCount > 1UL)
{
uint64_t secondTargetAreaQ32 = context->rampAreaStepQ32 * 2ULL
uint64_t secondTargetAreaQ32;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
phaseStartedAt = PlsrPlannerTimingNow();
#endif
secondTargetAreaQ32 = context->rampAreaStepQ32 * 2ULL
+ ((uint64_t)context->rampAreaRemainder * 2ULL) / pulseCount;
context->rampSecondBoundaryQ32 = PlsrPlannerExactBoundaryQ32(
context, context->rampFirstBoundaryQ32,
secondTargetAreaQ32);
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampSecondBoundary,
phaseStartedAt);
#endif
}
else
{
context->rampSecondBoundaryQ32 = PLSR_PLANNER_Q32_ONE;
}
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.startRamp, totalStartedAt);
#endif
}

static uint8_t PlsrPlannerSameSetting(
@@ -451,6 +675,12 @@ static uint8_t PlsrPlannerBuildStep(PLSR_PLANNER_CONTEXT *context,
PLSR_PLATFORM_TIMER_SETTING *setting,
uint32_t *normalizedRequestedHz)
{
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
uint32_t startedAt;
uint8_t result;
#endif

if (requestedHz == 0UL)
{
requestedHz = 1UL;
@@ -460,9 +690,19 @@ static uint8_t PlsrPlannerBuildStep(PLSR_PLANNER_CONTEXT *context,
requestedHz = PLSR_FREQUENCY_MAX_HZ;
}
*normalizedRequestedHz = requestedHz;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
startedAt = PlsrPlannerTimingNow();
result = PlsrPlatformBuildTimerSetting(context->block.pulseOutput,
PLSR_OUTPUT_PULSE_DIR,
requestedHz, setting);
PlsrPlannerTimingRecord(&PlsrPlannerTiming.timerSetting, startedAt);
return result;
#else
return PlsrPlatformBuildTimerSetting(context->block.pulseOutput,
PLSR_OUTPUT_PULSE_DIR,
requestedHz, setting);
#endif
}

static uint8_t PlsrPlannerTakeRampStep(
@@ -475,6 +715,11 @@ static uint8_t PlsrPlannerTakeRampStep(
uint64_t denominator;
uint64_t requestedHz;
uint64_t actualDeltaQ32;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
uint32_t totalStartedAt = PlsrPlannerTimingNow();
uint32_t phaseStartedAt;
#endif

context->rampTargetAreaQ32 += context->rampAreaStepQ32;
context->rampRemainderAccumulator += context->rampAreaRemainder;
@@ -498,8 +743,17 @@ static uint8_t PlsrPlannerTakeRampStep(
}
else
{
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
phaseStartedAt = PlsrPlannerTimingNow();
#endif
nextBoundaryQ32 = PlsrPlannerPredictedBoundaryQ32(
context, context->rampTargetAreaQ32);
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampPredictedBoundary,
phaseStartedAt);
#endif
}

desiredDeltaQ32 = (nextBoundaryQ32 > context->rampActualTimeQ32)
@@ -508,11 +762,20 @@ static uint8_t PlsrPlannerTakeRampStep(
/* 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. */
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
phaseStartedAt = PlsrPlannerTimingNow();
#endif
denominator = (uint64_t)context->rampPulseCount * desiredDeltaQ32;
requestedHz = (denominator == 0ULL)
? context->rampToHz
: (context->rampTotalAreaQ32
+ denominator / 2ULL) / denominator;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampRequestedDivide,
phaseStartedAt);
#endif
if (requestedHz == 0ULL)
{
requestedHz = 1ULL;
@@ -532,21 +795,39 @@ static uint8_t PlsrPlannerTakeRampStep(
if (PlsrPlannerBuildStep(context, (uint32_t)requestedHz, setting,
requestedFrequencyHz) == 0U)
{
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampStep,
totalStartedAt);
#endif
return 0U;
}

#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
phaseStartedAt = PlsrPlannerTimingNow();
#endif
denominator = (uint64_t)context->rampPulseCount
* setting->actualFrequencyHz;
actualDeltaQ32 = (denominator == 0ULL)
? desiredDeltaQ32
: (context->rampTotalAreaQ32
+ denominator / 2ULL) / denominator;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampActualDivide,
phaseStartedAt);
#endif
context->rampActualTimeQ32 += actualDeltaQ32;
context->rampLastPhaseStepQ32 =
nextBoundaryQ32 - context->rampBoundaryQ32;
context->rampBoundaryQ32 = nextBoundaryQ32;
context->lastRampHz = setting->actualFrequencyHz;
context->rampRelativePulse++;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.rampStep, totalStartedAt);
#endif
return 1U;
}

@@ -556,22 +837,22 @@ static uint8_t PlsrPlannerTakeStep(PLSR_PLANNER_CONTEXT *context,
uint32_t *repeatCount)
{
uint32_t entryEnd = context->entryPulses;
//加速段结束门槛。已经吐出的脉冲 < entryEnd 还在加速。例如加速 1000,这里就是 1000
uint32_t steadyEnd = entryEnd + context->steadyPulses;
//匀速段结束门槛。< steadyEnd 且 ≥ entryEnd 就是匀速。例如再加 5000 匀速,这里就是 6000。再往后是减速
*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);
{ // 第一次走进加速
PlsrPlannerStartRamp(context, 1U, context->startHz,context->peakHz, context->entryPulses);
}
return PlsrPlannerTakeRampStep(context, setting,
requestedFrequencyHz);
return PlsrPlannerTakeRampStep(context, setting,requestedFrequencyHz);
}
if (context->generatedPulses < steadyEnd)
{
@@ -581,7 +862,7 @@ static uint8_t PlsrPlannerTakeStep(PLSR_PLANNER_CONTEXT *context,
requestedFrequencyHz);
}
if (context->rampKind != 2U)
{
{ // 第一次走进减速
PlsrPlannerStartRamp(context, 2U, context->peakHz,
context->endHz, context->exitPulses);
}
@@ -601,6 +882,10 @@ PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *context,
uint64_t exitWeight;
uint64_t totalWeight;
uint64_t scaledEntry;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
uint32_t startedAt = PlsrPlannerTimingNow();
#endif

if ((context == NULL) || (block == NULL)
|| (block->pulseBudget == 0UL)
@@ -613,6 +898,10 @@ PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *context,
|| (appliedHz > PLSR_FREQUENCY_MAX_HZ)
|| (block->curveMode > 2U) || (block->pulseOutput > 3U))
{
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.begin, startedAt);
#endif
return PLSR_PLANNER_INVALID;
}

@@ -639,6 +928,10 @@ PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *context,
context->entryPulses = block->pulseBudget;
context->clipped = 1U;
context->active = 1U;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.begin, startedAt);
#endif
return PLSR_PLANNER_CLIPPED;
}

@@ -694,42 +987,55 @@ PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *context,
context->clipped = 1U;
}
context->active = 1U;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.begin, startedAt);
#endif
return (context->clipped != 0U) ? PLSR_PLANNER_CLIPPED
: PLSR_PLANNER_OK;
}

uint16_t PlsrPlannerGenerate(PLSR_PLANNER_CONTEXT *context,
PLSR_STREAM_ITEM *output,
uint16_t capacity)
//返回值:实际写了几项(合并后的项数,不是脉冲数)。0 = 没吐出任何东西
uint16_t PlsrPlannerGenerate(PLSR_PLANNER_CONTEXT *context,//规划账本:三段脉冲、已经吐了多少、斜坡面积指针
PLSR_STREAM_ITEM *output,//输出数组,调用方准备好的格子
uint16_t capacity)//这一次最多往 output 里写几项
{
PLSR_PLATFORM_TIMER_SETTING setting;
uint32_t requestedFrequencyHz;
uint32_t repeatCount;
uint16_t produced = 0U;
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
uint32_t startedAt = PlsrPlannerTimingNow();
#endif

if ((context == NULL) || (output == NULL) || (capacity == 0U)
|| (context->active == 0U))
{
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.generate, startedAt);
#endif
return 0U;
}
while ((produced < capacity)
&& (context->generatedPulses < context->block.pulseBudget))
{
if (PlsrPlannerTakeStep(context, &setting, &requestedFrequencyHz,
&repeatCount) == 0U)
while ((produced < capacity)&& (context->generatedPulses < context->block.pulseBudget))
{ //输出数组还没写满&&本段预算还没全部交给外面。generatedPulses 按脉冲个数计
if (PlsrPlannerTakeStep(context, &setting, &requestedFrequencyHz, &repeatCount) == 0U)
//本圈 TakeStep 算出的量化后 PSC/ARR/actualHz,先放栈上
{
context->active = 0U;
context->active = 0U;//规划器关掉
break;
}
if ((produced != 0U)
&& (PlsrPlannerSameSetting(&output[produced - 1U].setting,
&setting) != 0U)
&& (PlsrPlannerSameSetting(&output[produced - 1U].setting,&setting) != 0U)
&& (output[produced - 1U].requestedFrequencyHz
== requestedFrequencyHz)
&& (output[produced - 1U].repeatCount
<= 0xFFFFFFFFUL - repeatCount))
{
output[produced - 1U].repeatCount += repeatCount;
//不用开新格

}
else
{
@@ -744,9 +1050,173 @@ uint16_t PlsrPlannerGenerate(PLSR_PLANNER_CONTEXT *context,
{
context->active = 0U;
}
#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
PlsrPlannerTimingRecord(&PlsrPlannerTiming.generate, startedAt);
#endif
return produced;
}

#if !defined(PLSR_HOST_TEST)
static uint32_t PlsrPlannerBenchmarkCyclesNow(void)
{
return *((volatile uint32_t *)PLSR_PLANNER_BENCHMARK_DWT_CYCCNT);
}

static void PlsrPlannerBenchmarkEnableCounter(void)
{
*((volatile uint32_t *)PLSR_PLANNER_BENCHMARK_DEMCR_ADDRESS) |=
PLSR_PLANNER_BENCHMARK_TRCENA;
*((volatile uint32_t *)PLSR_PLANNER_BENCHMARK_DWT_CYCCNT) = 0UL;
*((volatile uint32_t *)PLSR_PLANNER_BENCHMARK_DWT_CTRL) |=
PLSR_PLANNER_BENCHMARK_CYCCNTENA;
}

static uint32_t PlsrPlannerBenchmarkCyclesQ16(uint32_t cycles,
uint32_t pulses)
{
if (pulses == 0UL)
{
return 0UL;
}
return (uint32_t)((((uint64_t)cycles << 16U) + pulses / 2UL)
/ pulses);
}

static void PlsrPlannerBenchmarkMode(uint16_t curveMode)
{
PLSR_MOTION_BLOCK block;
PLSR_PLANNER_CONTEXT context;
PLSR_STREAM_ITEM item;
PLSR_PLANNER_BENCHMARK_RESULT result;
PLSR_PLANNER_STATUS status;
uint64_t totalCycles = 0ULL;
uint32_t minimumBlockQ16 = 0xFFFFFFFFUL;
uint32_t maximumBlockQ16 = 0UL;
uint32_t generateCalls = 0UL;
uint8_t failed = 0U;

(void)memset(&block, 0, sizeof(block));
(void)memset(&context, 0, sizeof(context));
(void)memset(&item, 0, sizeof(item));
(void)memset(&result, 0, sizeof(result));

block.entryHz = 1UL;
block.cruiseHz = PLSR_PLANNER_BENCHMARK_TARGET_HZ;
block.exitHz = 1UL;
block.pulseBudget = PLSR_PLANNER_BENCHMARK_PULSES;
block.referenceSpeedHz = PLSR_PLANNER_BENCHMARK_TARGET_HZ;
block.accelerationTimeMs = 1000U;
block.decelerationTimeMs = 1000U;
block.curveMode = curveMode;
block.pulseOutput = 0U;
block.boundary = PLSR_BOUNDARY_STOP;

status = PlsrPlannerBegin(&context, &block, 0UL, 0ULL);
result.curveMode = curveMode;
result.beginStatus = (uint32_t)status;
if (status == PLSR_PLANNER_OK)
{
while (context.active != 0U)
{
uint32_t batchCalls = 0UL;
uint32_t startedAt = PlsrPlannerBenchmarkCyclesNow();
uint32_t elapsed;
uint32_t blockQ16;

while ((batchCalls < PLSR_PLANNER_BENCHMARK_BATCH_CALLS)
&& (context.active != 0U))
{
if (PlsrPlannerGenerate(&context, &item, 1U) == 0U)
{
failed = 1U;
break;
}
batchCalls++;
}
elapsed = PlsrPlannerBenchmarkCyclesNow() - startedAt;
if (batchCalls != 0UL)
{
blockQ16 = PlsrPlannerBenchmarkCyclesQ16(elapsed,
batchCalls);
totalCycles += elapsed;
generateCalls += batchCalls;
if (blockQ16 < minimumBlockQ16)
{
minimumBlockQ16 = blockQ16;
}
if (blockQ16 > maximumBlockQ16)
{
maximumBlockQ16 = blockQ16;
}
}
if (failed != 0U)
{
break;
}
}
}

result.plannedPulses = context.generatedPulses;
result.generateCalls = generateCalls;
result.totalCycles = totalCycles;
result.minimumBlockCyclesPerPulseQ16 =
(minimumBlockQ16 == 0xFFFFFFFFUL) ? 0UL : minimumBlockQ16;
result.maximumBlockCyclesPerPulseQ16 = maximumBlockQ16;
if ((context.generatedPulses != 0UL) && (totalCycles != 0ULL))
{
uint64_t averageQ16 = ((totalCycles << 16U)
+ context.generatedPulses / 2UL)
/ context.generatedPulses;
uint64_t estimatedHz =
((uint64_t)PlsrPlannerBenchmarkCoreClockHz
* context.generatedPulses + totalCycles / 2ULL)
/ totalCycles;
uint64_t targetCyclesQ16 =
((uint64_t)PlsrPlannerBenchmarkCoreClockHz << 16U)
/ PLSR_PLANNER_BENCHMARK_TARGET_HZ;

result.averageCyclesPerPulseQ16 =
(averageQ16 > 0xFFFFFFFFULL) ? 0xFFFFFFFFUL
: (uint32_t)averageQ16;
result.estimatedPulsesPerSecond =
(estimatedHz > 0xFFFFFFFFULL) ? 0xFFFFFFFFUL
: (uint32_t)estimatedHz;
result.passes100k = (averageQ16 <= targetCyclesQ16) ? 1UL : 0UL;
}
result.completed = ((failed == 0U)
&& (context.generatedPulses
== PLSR_PLANNER_BENCHMARK_PULSES)
&& (context.active == 0U)) ? 1UL : 0UL;
PlsrPlannerBenchmarkResults[curveMode] = result;
}

void PlsrPlannerBenchmarkService(void)
{
uint16_t curveMode;

if ((PlsrPlannerBenchmarkRequest == 0UL)
|| (PlsrPlannerBenchmarkRunning != 0UL))
{
return;
}
PlsrPlannerBenchmarkRequest = 0UL;
PlsrPlannerBenchmarkRunning = 1UL;
(void)memset((void *)PlsrPlannerBenchmarkResults, 0,
sizeof(PlsrPlannerBenchmarkResults));
PlsrPlannerBenchmarkEnableCounter();
for (curveMode = 0U; curveMode < 3U; curveMode++)
{
PlsrPlannerBenchmarkMode(curveMode);
}
if (PlsrPlannerBenchmarkRunCount != 0xFFFFFFFFUL)
{
PlsrPlannerBenchmarkRunCount++;
}
PlsrPlannerBenchmarkRunning = 0UL;
}
#endif

static uint64_t PlsrPlannerRampDurationUs(uint32_t pulseCount,
uint32_t fromHz,
uint32_t toHz)


+ 102
- 40
PLSR/Src/plsr_planner.h Просмотреть файл

@@ -20,23 +20,23 @@ typedef enum
/* 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;
uint32_t entryHz; // 本块起点频率:启动速度,或上一段 carry 过来的速度
uint32_t cruiseHz; // 本块目标巡航频率(段频率)
uint32_t exitHz; // 本块终点频率:停止速度,或下一段 cruise(同向连续)
uint32_t pulseBudget; // 本块要发的脉冲总数
uint32_t referenceSpeedHz; // 默认速度,用来折算加减速时间 T∝|Δf|/f_ref
uint16_t accelerationTimeMs; // 从 0 爬到默认速度所配的加速时间(ms)
uint16_t decelerationTimeMs; // 从默认速度降到 0 所配的减速时间(ms)
uint16_t curveMode; // 0 线性,1 S 曲线,2 正弦
uint8_t pulseOutput; // 脉冲输出通道 0..3
PLSR_BOUNDARY_ACTION boundary; // 块结束后怎么走:停 / 下一段重起 / carry / WAIT / 跳转 / EXT切断
} PLSR_MOTION_BLOCK;

typedef struct
{
PLSR_PLATFORM_TIMER_SETTING setting;
uint32_t requestedFrequencyHz;
uint32_t repeatCount;
uint32_t requestedFrequencyHz;//用于诊断的频率
uint32_t repeatCount;//这一档连续多少个脉冲。斜坡多半 1;匀速可以是剩下的全部(例如 5000)
} PLSR_STREAM_ITEM;

typedef enum
@@ -65,36 +65,98 @@ typedef struct

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_MOTION_BLOCK block; // 本块输入:起/巡/终速度、预算、加减速时间、曲线、通道
uint64_t phasePulses; // 起点相位,已实际输出的脉冲累计,冷启动为 0
uint32_t startHz; // 实际起始频率(appliedHz,0 抬成 1)
uint32_t peakHz; // 实际峰值,预算不足时低于 cruise
uint32_t endHz; // 实际终点:停止速度、carry 或 clip 后可达值
uint32_t entryPulses; // 加速段脉冲数
uint32_t steadyPulses; // 匀速段脉冲数,三角轮廓为 0
uint32_t exitPulses; // 减速段脉冲数
uint32_t generatedPulses; // 已交给调用方的脉冲总数(按脉冲计,非项数)
uint32_t rampRelativePulse; // 当前斜坡内已走脉冲序号,0 为斜坡第一项
uint32_t rampPulseCount; // 当前斜坡总脉冲数
uint32_t rampFromHz; // 当前斜坡起点频率
uint32_t rampToHz; // 当前斜坡终点频率
uint32_t rampDurationMs; // 当前斜坡折算持续时间(ms)
uint32_t lastRampHz; // 上一斜坡脉冲量化后的实际频率,禁止加减速倒退
uint64_t rampTotalAreaQ32; // 当前斜坡 x=0~1 的总面积(Q32)
uint64_t rampAreaStepQ32; // 每个脉冲的面积整数商:totalArea / N
uint64_t rampTargetAreaQ32; // 到当前脉冲结束必须达到的累计面积
uint64_t rampBoundaryQ32; // 上一脉冲结束时的归一化时间边界 x
uint64_t rampActualTimeQ32; // 已按量化后 actualHz 走过的归一化时间(闭环)
uint64_t rampLastPhaseStepQ32; // 上一脉冲相位步长,供后续预测边界
uint64_t rampFirstBoundaryQ32; // 第 1 个斜坡脉冲的精确边界(二分)
uint64_t rampSecondBoundaryQ32; // 第 2 个斜坡脉冲的精确边界(二分),其后改预测
uint32_t rampAreaRemainder; // totalArea % N,余数往各脉冲面积上摊
uint32_t rampRemainderAccumulator; // 摊余数累加器,满 N 则目标面积 +1
uint8_t rampKind; // 0 无斜坡/匀速,1 加速,2 减速
uint8_t clipped; // 预算不足已裁峰/终点,结果仍可执行
uint8_t active; // 是否还可 Generate,吐完或失败清 0
} PLSR_PLANNER_CONTEXT;

#if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \
&& !defined(PLSR_HOST_TEST)
typedef struct
{
volatile uint32_t callCount;
volatile uint32_t lastCycles;
volatile uint32_t totalCycles;
volatile uint32_t minCycles;
volatile uint32_t maxCycles;
} PLSR_PLANNER_TIMING_SAMPLE;

typedef struct
{
PLSR_PLANNER_TIMING_SAMPLE begin;
PLSR_PLANNER_TIMING_SAMPLE generate;
PLSR_PLANNER_TIMING_SAMPLE startRamp;
PLSR_PLANNER_TIMING_SAMPLE rampTime;
PLSR_PLANNER_TIMING_SAMPLE rampTotalArea;
PLSR_PLANNER_TIMING_SAMPLE rampAreaSplit;
PLSR_PLANNER_TIMING_SAMPLE rampFirstBoundary;
PLSR_PLANNER_TIMING_SAMPLE rampSecondBoundary;
PLSR_PLANNER_TIMING_SAMPLE rampPredictedBoundary;
PLSR_PLANNER_TIMING_SAMPLE rampRequestedDivide;
PLSR_PLANNER_TIMING_SAMPLE timerSetting;
PLSR_PLANNER_TIMING_SAMPLE rampActualDivide;
PLSR_PLANNER_TIMING_SAMPLE rampStep;
} PLSR_PLANNER_TIMING;

extern volatile PLSR_PLANNER_TIMING PlsrPlannerTiming;
void PlsrPlannerTimingReset(void);
#endif

#if !defined(PLSR_HOST_TEST)
/* Target-board throughput benchmark. One benchmark run exercises the exact
runtime calling convention (Generate capacity == 1) for a 100000-pulse
triangular profile in each curve mode. Q16 cycle values retain fractional
cycles without using floating point in the target. */
typedef struct
{
uint32_t curveMode;
uint32_t beginStatus;
uint32_t completed;
uint32_t plannedPulses;
uint32_t generateCalls;
uint64_t totalCycles;
uint32_t averageCyclesPerPulseQ16;
uint32_t minimumBlockCyclesPerPulseQ16;
uint32_t maximumBlockCyclesPerPulseQ16;
uint32_t estimatedPulsesPerSecond;
uint32_t passes100k;
} PLSR_PLANNER_BENCHMARK_RESULT;

extern volatile uint32_t PlsrPlannerBenchmarkRequest;
extern volatile uint32_t PlsrPlannerBenchmarkRunning;
extern volatile uint32_t PlsrPlannerBenchmarkRunCount;
extern volatile uint32_t PlsrPlannerBenchmarkCoreClockHz;
extern volatile PLSR_PLANNER_BENCHMARK_RESULT
PlsrPlannerBenchmarkResults[3];

void PlsrPlannerBenchmarkService(void);
#endif

PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *context,
const PLSR_MOTION_BLOCK *block,
uint32_t appliedHz,


+ 102
- 5
PLSR/Src/plsr_platform_f407.c Просмотреть файл

@@ -1373,6 +1373,7 @@ uint8_t PlsrTestFlashNeedsStartupRecovery(
#define PLSR_FREQUENCY_VERIFY_AB_AUX_IRQ (2U)
#define PLSR_QUEUE_WRITE_GUARD_COUNTS (64UL)
#define PLSR_FINITE_WRITE_GUARD_COUNTS (128UL)
#define PLSR_INPUT_LOW_REARM_MS (5UL)
#define PLSR_FLASH_ERASE_NONE (0U)
#define PLSR_FLASH_ERASE_SECTOR_A (1U)
#define PLSR_FLASH_ERASE_SECTOR_B (2U)
@@ -1581,6 +1582,9 @@ static volatile uint16_t PlsrFiniteStepIndex[4];
static volatile uint16_t PlsrFiniteBoundaryReadIndex[4];
static volatile uint16_t PlsrFiniteCompletedStepCount[4];
static volatile uint16_t PlsrPlatformFaultPending;
static volatile uint32_t PlsrInputLowSinceTick[2];
static volatile uint8_t PlsrInputAcceptedValidMask;
static volatile uint8_t PlsrInputLowSeenMask;

static void PlsrHandleTimerIrq(uint8_t pulseOutput);
static void PlsrCounterSuspend(uint8_t pulseOutput);
@@ -1611,8 +1615,12 @@ volatile uint32_t PlsrFinalArmJobMaxCycles[4];
volatile uint32_t PlsrFinalArmQueueToStopLastCycles[4];
volatile uint32_t PlsrFinalArmQueueToStopMaxCycles[4];
volatile uint32_t PlsrFiniteBlockIrqCount[4];
volatile uint32_t PlsrFiniteBlockIrqLastCycles[4];
volatile uint32_t PlsrFiniteBlockIrqTotalCycles[4];
volatile uint32_t PlsrFiniteBlockIrqMaxCycles[4];
volatile uint32_t PlsrFiniteFinalIrqCount[4];
volatile uint32_t PlsrFiniteFinalIrqLastCycles[4];
volatile uint32_t PlsrFiniteFinalIrqTotalCycles[4];
volatile uint32_t PlsrFiniteFinalIrqMaxCycles[4];
volatile uint32_t PlsrAbReloadCounterBefore[4];
volatile uint32_t PlsrAbReloadCounterArmed[4];
@@ -2910,6 +2918,22 @@ uint8_t PlsrPlatformInit(void)
sizeof(PlsrFinalArmQueuedAt));
(void)memset((void *)PlsrFinalArmQueueTimingPending, 0,
sizeof(PlsrFinalArmQueueTimingPending));
(void)memset((void *)PlsrFiniteBlockIrqCount, 0,
sizeof(PlsrFiniteBlockIrqCount));
(void)memset((void *)PlsrFiniteBlockIrqLastCycles, 0,
sizeof(PlsrFiniteBlockIrqLastCycles));
(void)memset((void *)PlsrFiniteBlockIrqTotalCycles, 0,
sizeof(PlsrFiniteBlockIrqTotalCycles));
(void)memset((void *)PlsrFiniteBlockIrqMaxCycles, 0,
sizeof(PlsrFiniteBlockIrqMaxCycles));
(void)memset((void *)PlsrFiniteFinalIrqCount, 0,
sizeof(PlsrFiniteFinalIrqCount));
(void)memset((void *)PlsrFiniteFinalIrqLastCycles, 0,
sizeof(PlsrFiniteFinalIrqLastCycles));
(void)memset((void *)PlsrFiniteFinalIrqTotalCycles, 0,
sizeof(PlsrFiniteFinalIrqTotalCycles));
(void)memset((void *)PlsrFiniteFinalIrqMaxCycles, 0,
sizeof(PlsrFiniteFinalIrqMaxCycles));
#endif

HAL_GPIO_WritePin(GPIOH, GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8
@@ -2921,7 +2945,7 @@ uint8_t PlsrPlatformInit(void)
gpio.Alternate = 0U;
HAL_GPIO_Init(GPIOH, &gpio);

gpio.Mode = GPIO_MODE_IT_RISING;
gpio.Mode = GPIO_MODE_IT_RISING_FALLING;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
gpio.Alternate = 0U;
@@ -2929,6 +2953,10 @@ uint8_t PlsrPlatformInit(void)
HAL_GPIO_Init(GPIOB, &gpio);
gpio.Pin = GPIO_PIN_12;
HAL_GPIO_Init(GPIOG, &gpio);
PlsrInputLowSinceTick[0] = 0UL;
PlsrInputLowSinceTick[1] = 0UL;
PlsrInputAcceptedValidMask = 0U;
PlsrInputLowSeenMask = 0U;
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_5 | GPIO_PIN_12);
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 2U, 0U);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
@@ -4421,6 +4449,52 @@ uint8_t PlsrPlatformReadInput(uint8_t inputSelection)
return 0U;
}

static uint8_t PlsrInputExtiAccept(uint8_t inputSelection)
{
uint32_t now;
uint8_t bit;
uint8_t level;

if (inputSelection > 1U)
{
return 0U;
}
now = HAL_GetTick();
bit = (uint8_t)(1U << inputSelection);
level = PlsrPlatformReadInput(inputSelection);

if (level == 0U)
{
/* Falling edges never reach the motion state machine. They only
begin a possible stable-low rearm interval. */
PlsrInputLowSinceTick[inputSelection] = now;
PlsrInputLowSeenMask |= bit;
return 0U;
}

if ((PlsrInputAcceptedValidMask & bit) == 0U)
{
/* Preserve the required first-edge-immediate behavior. */
PlsrInputAcceptedValidMask |= bit;
PlsrInputLowSeenMask &= (uint8_t)~bit;
return 1U;
}

if (((PlsrInputLowSeenMask & bit) == 0U)
|| ((uint32_t)(now - PlsrInputLowSinceTick[inputSelection])
< PLSR_INPUT_LOW_REARM_MS))
{
/* Any early rising edge breaks the stable-low interval. A new
falling edge must start the full interval again. */
PlsrInputLowSeenMask &= (uint8_t)~bit;
return 0U;
}

PlsrInputLowSeenMask &= (uint8_t)~bit;
PlsrInputAcceptedValidMask |= bit;
return 1U;
}

uint8_t PlsrPlatformTakeInputExtiPending(uint8_t inputSelection)
{
uint16_t pin;
@@ -4442,7 +4516,7 @@ uint8_t PlsrPlatformTakeInputExtiPending(uint8_t inputSelection)
return 0U;
}
__HAL_GPIO_EXTI_CLEAR_IT(pin);
return 1U;
return PlsrInputExtiAccept(inputSelection);
}

uint8_t PlsrPlatformLoad(PLSR_PERSIST_PAYLOAD *payload)
@@ -5062,7 +5136,10 @@ void EXTI9_5_IRQHandler(void)
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_5) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_5);
PlsrWaitInputExtiIrq(0U);
if (PlsrInputExtiAccept(0U) != 0U)
{
PlsrWaitInputExtiIrq(0U);
}
}
}

@@ -5071,7 +5148,10 @@ void EXTI15_10_IRQHandler(void)
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_12) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_12);
PlsrWaitInputExtiIrq(1U);
if (PlsrInputExtiAccept(1U) != 0U)
{
PlsrWaitInputExtiIrq(1U);
}
}
}

@@ -5110,10 +5190,19 @@ static void PlsrHandleCounterIrq(uint8_t counterIndex)
#if PLSR_DEBUG_TIMING
{
uint32_t elapsedCycles = DWT->CYCCNT - startedAt;
uint32_t totalCycles;

if (PlsrFiniteCompletionPending[owner] != 0U)
{
totalCycles = PlsrFiniteFinalIrqTotalCycles[owner];
if (PlsrFiniteFinalIrqCount[owner] != 0xFFFFFFFFUL)
{
PlsrFiniteFinalIrqCount[owner]++;
}
PlsrFiniteFinalIrqLastCycles[owner] = elapsedCycles;
PlsrFiniteFinalIrqTotalCycles[owner] =
(elapsedCycles > (0xFFFFFFFFUL - totalCycles))
? 0xFFFFFFFFUL : totalCycles + elapsedCycles;
if (elapsedCycles > PlsrFiniteFinalIrqMaxCycles[owner])
{
PlsrFiniteFinalIrqMaxCycles[owner] = elapsedCycles;
@@ -5121,7 +5210,15 @@ static void PlsrHandleCounterIrq(uint8_t counterIndex)
}
else
{
PlsrFiniteBlockIrqCount[owner]++;
totalCycles = PlsrFiniteBlockIrqTotalCycles[owner];
if (PlsrFiniteBlockIrqCount[owner] != 0xFFFFFFFFUL)
{
PlsrFiniteBlockIrqCount[owner]++;
}
PlsrFiniteBlockIrqLastCycles[owner] = elapsedCycles;
PlsrFiniteBlockIrqTotalCycles[owner] =
(elapsedCycles > (0xFFFFFFFFUL - totalCycles))
? 0xFFFFFFFFUL : totalCycles + elapsedCycles;
if (elapsedCycles > PlsrFiniteBlockIrqMaxCycles[owner])
{
PlsrFiniteBlockIrqMaxCycles[owner] = elapsedCycles;


+ 8
- 1
tests/plsr_host/run_tests.ps1 Просмотреть файл

@@ -1,3 +1,5 @@
param([string]$Filter)

$ErrorActionPreference = "Stop"

$gcc = "D:\Dev-Cpp\MinGW64\bin\gcc.exe"
@@ -32,7 +34,12 @@ try {
$exitCode = $LASTEXITCODE
}
else {
& $temporaryExe
if ([string]::IsNullOrEmpty($Filter)) {
& $temporaryExe
}
else {
& $temporaryExe $Filter
}
$exitCode = $LASTEXITCODE
}
}


+ 87
- 7
tests/plsr_host/test_plsr_host.c Просмотреть файл

@@ -2,6 +2,7 @@

#include <stdint.h>
#include <stdio.h>
#include <string.h>

#define PLSR_WAIT_TIME (0U)
#define PLSR_WAIT_SIGNAL (1U)
@@ -237,6 +238,13 @@ static PLSR_MB_RESULT SendCommand(uint16_t command)
if (result == PLSR_MB_OK)
{
PlsrPoll1ms();
if ((command == PLSR_COMMAND_START)
&& (PlsrTestInitialDirectionPreparePending() != 0U))
{
/* Most tests begin at the direction-delay state. Preserve that
abstraction while dedicated tests inspect the isolated poll. */
PlsrPoll1ms();
}
}
return result;
}
@@ -2417,22 +2425,87 @@ static void TestStopAtPendingBoundary(void)
}

static void TestOneMillisecondDirectionDelay(void)
{
static const uint16_t delays[] = {1U, 2U, 10U};
uint8_t segmentCount;
uint8_t delayIndex;

for (segmentCount = 1U; segmentCount <= 2U; segmentCount++)
{
for (delayIndex = 0U;
delayIndex < (uint8_t)(sizeof(delays) / sizeof(delays[0]));
delayIndex++)
{
uint16_t tick;
uint16_t delayMs = delays[delayIndex];

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, segmentCount,
PLSR_SEND_COMPLETE, 1000UL, 1000UL,
100UL, 0U, 0U);
EXPECT_U(PLSR_MB_OK, WriteWord(0x1005U, delayMs));
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 1000UL, 10L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));
if (segmentCount > 1U)
{
EXPECT_U(PLSR_MB_OK,
SetSegment(2U, 1000UL, 10L,
PLSR_EXT_OR_COMPLETE, 0U, 0U, 0U));
}

EXPECT_U(PLSR_MB_OK, QueueCommand(PLSR_COMMAND_START));
PlsrPoll1ms();
EXPECT_U(1U, PlsrTestInitialDirectionPreparePending());
EXPECT_U(0UL, PlsrTestDirectionWriteCount(0U));
EXPECT_U(0U, PlsrTestPulseIsActive());

PlsrPoll1ms();
EXPECT_U(0U, PlsrTestInitialDirectionPreparePending());
EXPECT_U(1UL, PlsrTestDirectionWriteCount(0U));
EXPECT_U(0U, PlsrTestPulseIsActive());

for (tick = 1U; tick < delayMs; tick++)
{
PlsrPoll1ms();
EXPECT_U(0U, PlsrTestPulseIsActive());
}
PlsrPoll1ms();
EXPECT_U(1U, PlsrTestPulseIsActive());
EXPECT_U(1000UL, PlsrTestOutputFrequency());
StopAndSettle();
}
}
}

static void TestStopCancelsInitialDirectionPrepare(void)
{
ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 1U, PLSR_SEND_COMPLETE,
1000UL, 1000UL, 100UL, 0U, 0U);
EXPECT_U(PLSR_MB_OK, WriteWord(0x1005U, 1U));
EXPECT_U(PLSR_MB_OK, WriteWord(0x1005U, 5U));
EXPECT_U(PLSR_MB_OK,
SetSegment(1U, 1000UL, 10L, PLSR_EXT_OR_COMPLETE,
0U, 0U, 0U));

EXPECT_U(PLSR_MB_OK, SendCommand(PLSR_COMMAND_START));
EXPECT_U(PLSR_MB_OK, QueueCommand(PLSR_COMMAND_START));
PlsrPoll1ms();
EXPECT_U(1U, PlsrTestInitialDirectionPreparePending());
EXPECT_U(0UL, PlsrTestDirectionWriteCount(0U));
EXPECT_U(0U, PlsrTestPulseIsActive());

EXPECT_U(PLSR_MB_OK, QueueCommand(PLSR_COMMAND_STOP));
PlsrPoll1ms();
EXPECT_U(1U, PlsrTestPulseIsActive());
EXPECT_U(1000UL, PlsrTestOutputFrequency());
StopAndSettle();
EXPECT_U(0U, PlsrTestInitialDirectionPreparePending());
EXPECT_U(PLSR_STATUS_STOPPED, ReadStatus());
EXPECT_U(0UL, PlsrTestDirectionWriteCount(0U));
EXPECT_U(0U, PlsrTestPulseIsActive());

PlsrPoll1ms();
EXPECT_U(PLSR_STATUS_STOPPED, ReadStatus());
EXPECT_U(0U, PlsrTestPulseIsActive());
}

static void ExpectDirectionPinLevels(uint8_t selectedOutput,
@@ -2507,6 +2580,7 @@ static void TestDirectionSameAndReverseSegments(void)
uint8_t output;

ResetCore();
PlsrTestEnableFinitePulseTrain(1U);
ConfigureCommon(0U, PLSR_POSITION_RELATIVE, 3U,
PLSR_SEND_COMPLETE, 1000UL, 1000UL, 1000UL,
0U, 0U);
@@ -4442,7 +4516,8 @@ static const TEST_CASE TestCases[] =
{"self_jump_stop", TestSelfJumpCanStop},
{"stop_deceleration", TestStopDeceleratesBeforeCut},
{"stop_pending_boundary", TestStopAtPendingBoundary},
{"direction_delay_one_ms", TestOneMillisecondDirectionDelay},
{"direction_delay_one_ms", TestOneMillisecondDirectionDelay},
{"initial_direction_prepare_stop", TestStopCancelsInitialDirectionPrepare},
{"direction_output_polarity_matrix", TestDirectionOutputPolarityMatrix},
{"direction_same_reverse_segments", TestDirectionSameAndReverseSegments},
{"direction_output_change", TestDirectionOutputCanChangeBetweenCommands},
@@ -4514,7 +4589,7 @@ static const TEST_CASE TestCases[] =
{"position_overflow", TestPositionOverflowStopsWithError}
};

int main(void)
int main(int argc, char *argv[])
{
unsigned int index;

@@ -4524,6 +4599,11 @@ int main(void)
{
unsigned int failuresBefore = FailureCount;

if ((argc > 1) && (strcmp(TestCases[index].name, argv[1]) != 0))
{
continue;
}

CurrentTest = TestCases[index].name;
(void)printf("RUN %s\n", CurrentTest);
(void)fflush(stdout);


Загрузка…
Отмена
Сохранить