| 作者 | SHA1 | 備註 | 提交日期 |
|---|---|---|---|
|
|
dd4a42e3b9 |
修改了显示频谱的脚本
和s曲线 |
3 週之前 |
|
|
ee75c7e18d | 修改了上位机 | 3 週之前 |
|
|
f6d93ff341 | 修复了按键消抖 | 3 週之前 |
|
|
3478f1c15a | 加入了按键消抖 | 3 週之前 |
|
|
9e3c942171 | 优化了高频计算 | 3 週之前 |
|
|
a6c667d485 | 优化前上传一次 | 3 週之前 |
|
|
47dabdb226 | 加了点注释 | 3 週之前 |
|
|
7810b61ba7 | 改了一版上位机显示波形,mcu程序加了点注释 | 4 週之前 |
| @@ -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/ | |||
| @@ -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) | |||
| @@ -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 地址与空洞的请求返回“非法地址”。 | |||
| @@ -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 | |||
| 语义;迁移路线按"所有权 → 重规划 → 统一规划器 → 曲线校准"重排。 | |||
| @@ -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> | |||
| @@ -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__': | |||
| @@ -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() | |||
| @@ -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": | |||
| @@ -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: | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -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, | |||
| @@ -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; | |||
| @@ -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 | |||
| } | |||
| } | |||
| @@ -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); | |||