Переглянути джерело

加了点注释

codex/plsr-2026-minimal
ywh 3 тижднів тому
джерело
коміт
47dabdb226
6 змінених файлів з 159 додано та 549 видалено
  1. +128
    -0
      Document/PLSR_document/AI问询会话记录_2026.8.17-2026.8.23.md
  2. +0
    -134
      Document/PLSR_document/PLSR地址映射.md
  3. +0
    -388
      Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md
  4. +1
    -1
      PLSR/Src/plsr.c
  5. +20
    -16
      PLSR/Src/plsr_planner.c
  6. +10
    -10
      PLSR/Src/plsr_planner.h

+ 128
- 0
Document/PLSR_document/AI问询会话记录_2026.8.17-2026.8.23.md Переглянути файл

@@ -0,0 +1,128 @@
# AI 问询会话记录(2026-08-17 ~ 2026-08-23)

---

## 周一(2026-08-17):1 ms 轨迹规划审查

**问:分析一下我的轨迹规划代码。**
答:仓库里不是「一个规划器」,而是两套数学模型叠在一起,真正上硬件的只有 1 ms 时间域。运行路径是:段参数 → `MotionBlock` → `PlsrTrajectoryBegin` → `PlsrPlannerConfigureFrameProfile` → 1 ms 帧表(加速/匀速/减速)→ TIM6 每 1 ms 取下一帧写频率 → 硬件有限脉冲计数到 N 后停。`PlsrPlannerBegin()` 只做参数拷贝和合法性检查,不规划。执行器不管脉冲相位,只按 1 ms 取帧;硬件停靠的是 `pulseCount`,不是「帧全部播完」。也就是频率曲线按时间台阶走,停点按脉冲计数走。`plsr_pulse_trajectory.c` 更接近设计书里的脉冲域规划,但引擎里只拿它做 `PlsrSystemMeasurePulseTrajectory()` 计时,不参与输出。组块时 `exitHz` 只看加减时间,不看完成方式/后续方式,所以同向连续段也会先减到停止速度。

**问:什么意思。**
答:人话就是:规划器像按节拍一格一格调音量,执行器像按粒数停豆子。歌还没放到结尾,豆子可能已经数完;或者歌放完了,豆子还差几粒。长匀速、加减时间够时观感正常。短距离、刚起步、中途改速度时,尾速会偏高、加速段会缺一截、末尾会拖一小段慢爬。同向连续段也会先刹到停止速度再爬,因为组块没闻 `sendMode`。

**问:设计书写的和代码做的差在哪。**
答:设计书 `PLSR重构设计书_加减速与多段统一.md` 要的是脉冲域 `∫f dt = k`、`PlsrPlannerGenerate()` 流式吐 `STREAM_ITEM`、量化误差按真实周期闭环、多段解析 `NEXT_FRESH / NEXT_CARRY`、STOP/EXT/改频统一从 `appliedHz` 扣 `committed` 重规划。现状是时间域 1 ms 台阶,没有 `Generate` 接口,只在 1 ms 帧上用 `actualHz` 求和,`sendMode` 写进段对象后基本不用,在线改频有了但 EXT 仍是 cut,`tests/plsr_host` 不测 planner/trajectory。`PlsrPlannerBegin()` 永远只回 `OK` 或 `INVALID`,从不回 `CLIPPED`;clip 发生在 `ConfigureFrameProfile()` 之后,状态通道是死的。

**问:1 ms 规划器结构上哪些值得留,真正的问题是什么。**
答:值得留:规划/发布/执行分开(`SELECT → PREPARE → PUBLISH → RUNNING`),TIM6 只用段对象副本;轨迹是参数对象不是 N 个点的表;在线改频走 inactive bank;面积用量化后频率;短轮廓允许 clip。真正的问题:时间域曲线 vs 脉冲域停点(主矛盾);两套规划器数学不一致;S 曲线瞬时频率没有插值(64 级台阶);减速区第一帧被钉在峰值;候选搜索最后一帧估计不可靠;计算量配在 1 ms 任务里偏重;多段边界还没进规划;改频没扣 committed 预装载脉冲。

**问:看一下这个时间域求根脚本(线性 / 分段二次 S / 正弦)。**
答:脚本对每个脉冲 n 二分时间 \(t_n\),使 \(N(t)=\int_0^{t} f(\tau)\,d\tau = n\),再 \(f[n]=1000/\Delta t_n\)。这和设计书、脉冲域规划的定义一致,作为加速段连续金标这条是对的。时间折算也对:\(T=|f_{end}-f_{start}|\cdot T_{conf}/f_{default}\)。最大问题是 S 曲线不是这份固件:脚本 \(y(s)\) 是分段二次 \(2s^2\) / \(1-2(1-s)^2\),`plsr_pulse_trajectory.c` 与 `plsr_planner.c` 都是 smoothstep \(3s^2-2s^3\)。两端 \(y(0)=0,y(1)=1,\int y=1/2\) 都成立,平均脉冲数能对上,中间形状对不上。界面没有停止速度、总预算、三角/裁峰值、量化后 actualHz,不能当整段 `MotionBlock` 金标。对拍前必须先定:要对当前固件还是要对信捷/cla.py。

---

## 周二(2026-08-18):Codex 逆映射与段衔接

**问:Codex 说轨迹对象多算、128 点少算,65 点逆相位锚点 + 一次 Newton。什么意思。**
答:现在 S/正弦每个点在解逆问题:已知第 k 个脉冲,反求曲线相位,再求频率。正问题(相位→累计脉冲)容易,逆问题难,多半在 64 格积分表上二分/扫描,128 个点就炸。Codex 的招:建轨迹时先造两张 65 点锚点表(加速一张、减速一张),进度 0%、1/64、…、100% 对应曲线相位。以后每个点只做:算归一化进度 → 落在哪两个锚点 → 线性插值 → 最多 Newton 一次 → 相位→频率→`BuildTimerSetting`。从「每点大约 30 次二分」变成「查表+插值+最多一次校正」。不要直接存 128 个频率,因为 128 只是一批容量,下一批 `firstPulse` 不同、总脉冲数不同还要能算。RAM:65×4×2≈520 字节/对象。时间从实时 1 ms 挪到 OS 建段时算一次。

**问:梯形、S、正弦的轨迹对象分开了吗。**
答:算法分开了,对象没分开。当时是一个大结构体靠 `curveMode` 切换:0 线性用 \(v^2\) 递推,1/2 要逆相位,公式还不一样。但 `accelerationNumerator` 这种只有梯形才用的字段,S/正弦对象里也占着;128 点循环里还要反复 `if (curveMode)`。应先改成公共参数一份 + union{linear, smooth, sine}。好处:128 点入口按模式分派一次,循环里不再问 curveMode;以后只优化 S 不会把梯形搞坏。头文件后来已经拆过一刀,但 `.c` 里 128 点路径一度还是老写法。

**问:这个方案的话,段与段之间衔接很慢了岂不是。**
答:会,但只在一种接法下会。两件事别混:段里面每批 128 点会变快;段和段之间若下一段还没建好,电机会停。Codex 把几百微秒~1~2 ms 从出点挪到建对象。若仍在 `PREPARE_SEGMENT` 里做,S/正弦大约多等 1 拍。当前引擎本就是上一段完成后才 `RESOLVE_NEXT → SELECT → PREPARE → PUBLISH`,设计书 Bug 4 那个约 2.5 ms 低电平间隙,根子就是这个。梯形不用 65 点逆表,段间几乎不增。`WAIT_TIME`/换向方向延时、`NEXT_FRESH` 其实不在乎这 1 ms。真正在乎的是后续发送同向连续(`NEXT_CARRY`)。

**问:怎么避免衔接变慢。**
答:Codex 说「也可以只给当前段和预备段保存,压到约 1 KB」,意思就是双缓冲:当前段还在跑时,OS 空闲/提前几拍把下一段 `Begin()` + 65 点表算完;当前段最后几个脉冲时下一段对象已经热好,直接 publish / 切 cursor。交界只剩切对象、装第一拍。预计算放 Prepare、Prepare 放在段后 → 交界变厚;预计算放预备段、上一段还在跑时做完 → 交界可以更紧。你现在连 carry 都还没接上,每段还先减到停,那时候多 1 ms 建表不是主因。

**问:为什么不要直接预存 128 个频率。**
答:下一批 `firstPulse` 一变表就作废;总脉冲数一变(改频、EXT、剩余预算)又废。65 点逆表跟的是这条斜坡的形状(起速/峰值/终速/曲线),不是某一批 128 点。同向连续下一段只要参数定了,可以在上一段还没结束时就算好。这反而比「预存 128 频率」更适合衔接。

---

## 周三(2026-08-19):128 点耗时与正向表

**问:组数据说明优化只完成一半:正弦对象 352 µs、128 点 2890 µs;S 型 319 / 2821。共同瓶颈是相位插值后 Newton + 64 位除法。想把进度→频率也前移。还能怎么优化。**
答:诊断对。正弦和 S 已经打平,说明曲线公式不再是瓶颈。线性 128 点约 402 µs、每点约 3.1 µs 才是地板。热路径 `CurveFastPointHz` 每个点还在:逆表二分(512 格约 9 次比较)→ 插值出相位(一次 64 位除)→ Newton 最多 2 轮(速度、权重、有符号 64 位除)→ 两个 while 整数相位对齐 → 相位→频率 → `BuildTimerSetting`。1~4 是共同瓶颈。下一步「进度→频率」表方向对,但别用现在的每点算法去填 513 个频率锚点:22.6 µs×513≈11.6 ms,对象构建会从 0.3 ms 炸成十几毫秒。表必须正向填。头文件已经在往 `entryFrequencyHz[513]` 走,`.c` 里却还是 progressQ30 + 运行时 Newton。

**问:OS 任务改计算哪些轨迹对象参数,TIM6 的 1 ms 里要怎么计算 128 个 setting。**
答:分工一句话:OS 把轨迹做成查表就能用的对象;TIM6 每 1 ms 只抽出最多 128 个 setting,不准再解方程。OS 在 `PREPARE_SEGMENT` / 预备段 / 改频重规划时算一次:三种曲线都要的公共几何(start/peak/end、三段脉冲、时间、curveMode);梯形只填 \(v^2\) 分子,不建相位表;S/正弦正向铺进度均匀频率表(建议先 129 点)以及 `entryStepQ30`/`exitStepQ30`。TIM6 先处理 cut/完成/EXT,再读游标;入口按 curveMode 分派一次;S/正弦:`p += step`,`index = p >> shift`,插值,同频则拷贝 setting。这一拍 `n = min(128, 剩余脉冲, 队列空位)`。表还没瘦到 400 µs 之前,128 点仍放 OS 预计算,TIM6 只消费现成数组。2.8 ms 那种东西不准进 1 ms 中断。

**问:启动的时候 S 曲线和正弦的轨迹路径已经算好了吗。为什么算得这么慢。**
答:不是。启动时算好的是「这条坡长什么样」(起/峰/终、三段脉冲数、大约一百来个频率锚点),不是「每一个脉冲已经排好队」。整段几千、几万个 PSC/ARR 不会在启动时算完:脉冲总数随段变,中途改频/EXT 一变后面全作废,RAM 也扛不住。慢的不是启动把整条路算完了,是每个点还在解很难的逆问题。对象 319~352 µs 可以忽略;128 点 2821~2890 µs,每点约 22 µs,比线性贵大约 7 倍。正弦已经和 S 一样慢,正好说明瓶颈不是 sin/cos。

**问:所以这个方案是不是不好。**
答:方向对,做到现在这一半不好。停在「相位均匀进度表 + 热路径 Newton」是累赘:结构更胖、速度几乎没变。只有热路径变成「进度累加 → 移位插值频率 → 同频拷贝 / BuildTimerSetting」,并且 128 点掉到大约 400~700 µs、对象仍 <1 ms,才配进 TIM6,才配预备段。撤当前热路径里的 Newton,留对象和分派;换成正向铺进度均匀频率表(先 129 点);timer 同频拷贝,否则永远打不穿线性那 402 µs 地板。

**问:Begin 正向积分→P(x)→单调游标重采样→边界时间→直接频率表;128 点 cursor 递推→O(1) 索引→插值→timer。能用吗。你确定能满足时间要求吗。我改的代码你看一下。**
答:可以用,而且比逆相位+Newton 更接近该做的那一刀。必须写对三件事:表的横轴是脉冲进度不是相位 x(按 x 均匀建表,按脉冲查又要二分);相邻 Δt 换频率要乘「这一格有多少脉冲」,写成 \(f=1000/\Delta T\) 会偏高大约 \(N_{ramp}/M\) 倍;正向游标是 O(表长) 不是 O(总脉冲)。时间上不能 100% 打包票:128 点 400~700 µs 高把握(对照线性 402 µs 还含 64 位 sqrt);Begin <1 ms 仅当闭式/现成正弦表、M=128/256、无 RK4/libm;TIM6 整拍硬吃 128 个不确定,先 OS 预热。你这版对象已是频率表、Newton 离开 128 点、同频拷贝 setting,但 `BuildCurveMap` 对 513 点各做 RK4(约八千次 64 位除),`CurveTableHz` 还要整数 sqrt。S 还改成了 cla.py 分段二次。N=1 用了 `double`。应扳回均匀 P(x)、先 129 点、禁止热路径 sqrt。

---

## 周四(2026-08-20):架构取舍与发波分支

**问:我觉得我的架构有问题,你觉得呢。**
答:有问题,而且不是 S 曲线慢那么点事,是坐标系和节拍没对齐。电机按脉冲活,控制按 1 ms 活。规划做了两份:`plsr_trajectory`(1 ms)在跑,`plsr_pulse_trajectory` 在 Measure。TIM6 现在是 1 ms 采样器,你要的 128 个 setting 是脉冲队列生产者,两种执行器塞进一个中断。段是「做完再备课」:`RESOLVE → SELECT → PREPARE → PUBLISH` 全在上一段停了之后。`sendMode` 进了段对象,组块仍永远 `exitHz = stopSpeed`。改频有 inactive bank,没扣 committed;EXT 是切断。优化发生在错误的所有权上:热路径该是执行器装下一项,却在给测量用的 `BuildBatch` 堆 RK4/sqrt。该留:规划对象/发布副本、MotionBlock 不读现场、CLIPPED 当短轮廓、线性 \(v^2\)、硬件预装载。应在 A(承认 1 ms 命令率)和 B(改成脉冲命令率)里选一个,不要混用。

**问:现在切了一下分支,仔细分析这个分支里是怎么发波的。**
答:当前分支 `codex/plsr-2026-minimal` 已经是脉冲域规划 + 软件队列 + 硬件按 run 发波,不是 TIM6 每 1 ms 改一次频。任务态把轨迹提前切成「同频重复 N 次」的 run,推进 1024 项环形队列(双 bank)。PWM 按当前 PSC/ARR 吐方波;TIM9/TIM12 数脉冲;当前 run 结束前一拍(CC1)预装下一 run,结束时(UIF)提交。IRQ 里不算规划。`PlsrPulseTimerIrq` 在 `STREAM / STEP_TABLE / STOPPING` 下直接 return。启动时 `PrepareShortProfile` → `TakeRun` 第一 run → `QueueBegin` → Fill 到约 400 项 → `StartCountedStreamPrepared`。之后 1 ms 任务 `Fill`:水位低于 800 就补,每一拍最多 200 项。队列空且规划还没完成:借用最多 2 ms / 200 脉冲,按最后一个 setting 继续发,任务之后把同样数量的规划脉冲还债(推进 planner 但不入队)。

**问:哪种方法好一点。没听懂。**
答:这个分支更好。发波该按脉冲走,不该按 1 ms 走。上一套:规划按时间切台阶,硬件按脉冲个数停,短段/起步/改频一定会偏。现在这套:规划吐 `{setting, N}`,换挡发生在完整脉冲边界,1 ms 任务只负责把未来几毫秒填进队列,IRQ 不准做规划数学。代价:冷启动要先填约 400 项才出第一波;斜坡 100 kHz 时 1 ms 要能产约 200 项;AB 还是旧逐脉冲 IRQ。人话:好的方法是先算每个脉冲多长,再按这个发;差的方法是每毫秒改一次速度,另外再数脉冲停。菜单是 `{这一档周期, 连发 N 个}`;硬件发完这一页再翻下一页,翻页发生在一个完整波结束的时候。

**问:现在的脉冲点对应的频率是怎么算的。我问你在哪算的。拆开揉碎和我讲讲。**
答:每个脉冲频率 = 这个脉冲该占多长时间的倒数,不是「这一毫秒该跑多快」。第 k 个脉冲要满足:从开始到这个脉冲结束,速度曲线底下的面积刚好等于 k 个脉冲。\(f[k]=1/(t_k-t_{k-1})\),再收成 PSC/ARR。`Begin` 不算每个点,只切加速 N1 / 匀速 N2 / 减速 N3。斜坡上把总面积切成 N 等份;目标面积加一格;找时间边界 x(前两个脉冲二分,后面用斜率推);\(\Delta x = x - \)已经按真实频率走过的时间;\(f_{req}=\)总面积\((N\cdot\Delta x)\);`BuildTimerSetting`;用 `actualHz` 把时间账往前推(闭环,量化误差不累计)。算的地方:`plsr_planner.c` 的 `PlsrPlannerTakeRampStep()`(约 468 行)是斜坡频率核心;匀速在 `TakeStep()`(约 553 行)直接用 `peakHz`。`Generate`(约 701 行)是对外入口。`plsr.c` 的 `TakeRunLimited` / `Fill` 只取出来入队。IRQ 不算。

**问:流程和数据流。你觉得复杂吗。OS 是不是一直在算频率帧存到队列里。一拍算几个。计算速度怎么样。**
答:总流程:解析本段 → `PrepareShortProfile`/`PlannerBegin`(还没有每个点的 Hz)→ `TakeRun`/`Generate` 算出下一项 → 灌队列 → 硬件开波 → 同时 1 ms 补队列、硬件按 run 翻页。数据:配置/现场合成 `MotionBlock`(不读 Modbus)→ Begin 得到三段脉冲数和 peak → Generate 吐 `STREAM_ITEM` → 入队成 `PROFILE_ENTRY` → CC1 预装、UIF 提交。三种 Hz 别混:配置目标、requested、actual。复杂在该复杂的地方(队列/双 bank/改频/handoff),公式本身就是面积÷时间。对,OS 一直在算 `{周期, N}` 存进队列,但不是 1 ms 时间帧。一拍最多 200 项(启动 400),堆到约 800 就停;斜坡一项常是 1 个脉冲,匀速一项 N 可以很大。速度:匀速一项约 1~3 µs;线性斜坡约 5~15 µs;S/正弦约 8~25 µs。一拍 200 项线性大约 1~3 ms,可能比 1 ms 墙钟还长,所以靠 8 ms 库存和 2 ms 借债。没有板上逐项 DWT,不能装测过。

---

## 周五(2026-08-21):队列节点与补货

**问:现在是 OS 任务里算好(频率,几个点)放到 1024 的队列里,然后 UIF 不断去消费这个队列是吧。**
答:骨架对,差两点。不是每个 UIF 都从队列拿一项:计数器盯当前这一 run,还差 1 个脉冲(CC1)才取下一项并预装 PSC/ARR;这一 run 的 N 个发完(UIF)才提交、换成下一项的 N。斜坡 N=1 时几乎每个脉冲翻一页;匀速 N=200 时发 200 个相同的波才吃队列一次。IRQ 里只拿现成项,不算频率。

**问:队列的元素叫什么。算好 `{这一档频率(PSC/ARR), 连续发几个脉冲}` 怎么形容。脉冲 run 是怎么算的。**
答:每一格叫 `PLSR_PROFILE_ENTRY`。整条队列叫 `PLSR_PROFILE_QUEUE`(1024 项,双 bank)。规划器吐出来还叫 `PLSR_STREAM_ITEM`,入队时加上 `startsNextSegment`。最短说法:一项 = 一档同频脉冲,「这一档的周期,连发 N 个」。不要说频率帧,要说脉冲 run / 同频项。run 不是单独一套公式:先算出下一个脉冲该用哪个周期,再决定这个周期连发几个。斜坡 `repeatCount` 通常=1;匀速先给剩下的全部,再被 `PlsrProfileRunLimit` 切成大约最多 2 ms;入队时量化后相同的相邻项还会再合并。

**问:计算函数和塞队列函数在哪。每一个节点都放了什么。**
答:算在 `PLSR/Src/plsr_planner.c`:`Begin`(592)切三段,`StartRamp`(398)切面积,`TakeRampStep`(468)算斜坡 Hz,`TakeStep`(553)分派,`Generate`(701)对外入口,`BuildStep`(449)量化。塞在 `PLSR/Src/plsr.c`:`TakeRunLimited`(1998)调 Generate 再按约 2 ms 切 N,`Fill`(3125)1 ms 循环算+写,`AppendLocked`(2624)真正写入、同档合并。IRQ 消费:`PlsrExecTakeCountedRunIrq`(4617)只读。节点只有四样:`setting`(PSC/ARR/actualHz)、`requestedFrequencyHz`(公式 Hz,发波不用)、`repeatCount`、`startsNextSegment`。没有脉冲序号、段号、面积。

**问:计算脉冲 run 到塞进队列到消费的流程,函数调用链。**
答:启动:`BeginSegmentOutput`(3680)→ `PrepareShortProfile` → `TakeRun` 第一 run → `QueueBegin` → `Fill(400)` → `StageCountedHandoff` → 再 Fill → `StartCountedStreamPrepared`。运行:`PlsrPoll1ms`(6028)→ 水位≤100 时预算改回 400 → `Fill(800, 200或400)` → `StageCountedHandoff` → 再 Fill。Fill 每一圈:拷出 producerProfile → `TakeRunLimited` → 核对 generation/bank/epoch → 还债则推进 planner 不 Append,否则 `AppendLocked`。消费:`TIM1_BRK_TIM9` / `TIM8_BRK_TIM12` → `HandleCounterIrq` → CC1 `PrepareNextStepIrq` → `TakeCountedRunIrq` 预写 PSC/ARR;UIF `FiniteCounterIrq` 提交换档。

**问:解释 Poll1ms 里补队列那一段。后面就没有计算了是吧。**
答:那段是 1 ms 任务后半截:先处理 START/STOP/CLEAR,再 `ExecServiceCountedEvents`(段边界必须在计数同步之前消费,否则会把下一段脉冲算进旧段),再位置掉电,再 `ServiceCountedExecutor`(这段波若已经发完就 HandleBoundary,本拍不再 Fill)。还在跑才看水位:≤100 则本拍预算从 200 加到 400。第一次 Fill 补当前段到 800(要 active 且 generatorComplete==0)。第二次 `StageCountedHandoff` 把下一段预热的第一/第二 run 接到队尾,再 Fill 一次。活队列「给当前段流式补 run」到两次 Fill 基本结束。后面还有:`ServiceTimedStartPreparation`(空闲 bank 预热下一段)、延时到 0 的 `BeginSegmentOutput`、动态改频 `ReplanPulseDir`。不是空白,是改干别的计算。

---

## 周六(2026-08-22):Fill、欠债、匀速切段

**问:为什么会欠债。**
答:欠债 = 队列空了,规划还没跟上,硬件先用上一档频率多打了几下。IRQ 要下一项时 `read==write`,但 `generatorComplete` 还是 0。常见原因:斜坡+高速(100 kHz 几乎一个脉冲一项,每毫秒吃 100 项,任务一拍最多算 200 项还可能超时);这一拍先忙了改频/handoff;Fill 在门外算的时候 IRQ 把剩下的格子吃光了。空了就停波会突然断一截。所以用 `lastSetting` 再连发最多约 2 ms(或 200 个脉冲),`underrunDebtPulses += N`,`generation++`。下一拍 Fill 发现有债:照样 Generate 把规划器往前拨 N 个,不 Append,减债。还清之前不能 `generatorComplete=1`。总脉冲数仍然要准:借的是「提前用末频打了规划里后面那 N 个」,不是额外赠送。波形上可能平一段末频(最多 2 ms)。

**问:Fill 这段 while 在干什么。TakeRunLimited 在干什么。`pendingRepeats==0` 是什么情况。**
答:Fill 每圈:关中断看还要不要算(不 active / 已 complete / 深度≥目标 / 预算用尽则成功返回)→ 先扣预算 → 记下 generation/epoch/债 → 拷出 profile → 开中断 Generate → 再关门核对 bank/世代/是否已满,对不上就 continue → 写回 profile → 有债减债不入队,没债 Append → 吐完且无债则 generatorComplete=1。TakeRunLimited:从规划器手里掰下下一项。`pendingRepeats==0` 才 Generate 拿新的一档(可能 N 很大);然后 N 取 min(本档剩余, 本段剩余, maximumRepeats, 约 2 ms 上限)。还债时 maximumRepeats=当前债。人话:规划器有时一次给整根同频香肠;`pendingRepeats` 是还没切出去的长度;等于 0 才再要一根。

**问:假设加速 1000 个脉冲,匀速 5000 个,减速 1000,现象是什么样。**
答:加速 1000:几乎一格一个波,频率一格一变,`pendingRepeats` 几乎每次都是 0,每圈都 Generate,队列大约 1000 格(量化相同才会合并)。匀速 5000:频率不变,但按约 2 ms 切开——1000 Hz 时一格约 2 个,约 2500 格;8000 Hz 时一格约 16 个。Generate 只发生一次,`pendingRepeats` 从 5000 往下减。示波器是很长一串等周期方波,看不出切了多少刀。减速 1000:再变回一格一波,频率下降。高速时盘子主要被斜坡撑满;匀速反而很省格子。

**问:为什么匀速要切这么多次。是不是队列里放 8000,8000…8500 个,或 (8000,8500)、(7999,1)、(7998,1)。入队数量是动态的吗。**
答:因为硬件一旦开始打这一项,就要把这一项的 N 个全部打完,中途改频也插不进这一项的肚子里。一次 8500、1000 Hz 就是 8.5 秒同一档,改频要等 8.5 秒。所以上限约 2 ms。多次的是 TakeRunLimited 进进出出,不是规划器算 4250 遍频率——频率只算 1 次。不是 (8000,8500) 一格吃完;不是频率 7999、7998 往下掉(那是案板上 `pendingRepeats` 在减,不进队列);也不是 8500 个光秃秃的 8000。实际是很多格、频率相同、每格 N 大约是 2 ms 能打多少个,例如 8000 Hz 时 `(8000Hz, 16)` 重复约 531 格加余数。入队多少、每一格里装几个都是当场算的:min(本档剩余, 本段剩余, 2 ms 能打几个, 还债则当前欠多少);这一拍塞几格也随水位和预算变。

---

## 周日(2026-08-23):Generate、规划上下文、会话记录

**问:解释 PlsrPlannerGenerate。局部变量含义讲了吗。**
答:这是规划器对外窗口:一次最多吐 `capacity` 项 `{setting, 请求频率, N}`,能合并的叠到上一项上。`TakeRunLimited` 每次 `capacity=1`,所以几乎总是吐 1 项就回去。开头指针空/`active==0`/`capacity==0` 则返回 0。循环条件:还能写,且 `generatedPulses < pulseBudget`。每圈 `TakeStep` 要下一步;失败则 `active=0` 并 break。若本次已有上一格且 setting 与 requestedHz 都相同且 N 不溢出,则只 `repeatCount +=`;否则新开一格、`produced++`。然后 `generatedPulses += repeatCount`(按脉冲不是按项数)。吐完预算则 `active=0`。返回 `produced`。局部变量:`setting` 是量化后 PSC/ARR;`requestedFrequencyHz` 是公式 Hz;`repeatCount` 是这一档连发几个;`produced` 是本次写了几项。`capacity=1` 时匀速 5000 会整项放在 `output[0]`,切开 2 ms 是后面 TakeRunLimited 的事。

**问:STREAM_ITEM 是什么。和队列节点差在哪。**
答:`PLSR_STREAM_ITEM` 是规划器吐出来的一项,还没进 1024 队列:`setting`、`requestedFrequencyHz`、`repeatCount`。没有 `startsNextSegment`。`pendingItem` 就是一个 STREAM_ITEM。PROFILE_ENTRY 在这上面再加段边界标记,并可能把 N 切短。

**问:TakeStep 是计算核心吗。**
答:算「现在该出哪一档」的分派核心是它;算「这个斜坡脉冲多少 Hz」的核心不是它。它只看 `generatedPulses` 落在加速/匀速/减速哪一段,第一次进斜坡时调 `StartRamp`,加速/减速交给 `TakeRampStep`,匀速 `repeatCount=剩下的匀速个数` 并 `BuildStep(peakHz)`。几何核心是 `Begin`;量化是 `BuildStep`。

**问:给 PLANNER_CONTEXT 带注释(含行尾 // 格式)。**
答:按块输入、三段几何、已吐脉冲、当前斜坡账本排。`block` 是本块输入;`phasePulses` 是起点相位;`startHz/peakHz/endHz` 是裁剪后的实际端点;`entry/steady/exitPulses` 是三段脉冲数;`generatedPulses` 是已交给调用方的脉冲总数。`ramp*` 只在当前加速或减速内有效:相对序号、总脉冲、起终点、折算时间、上一档 actualHz、Q32 总面积/每格面积/目标面积/边界/已走时间/上一相位步长、第 1/2 个精确边界、余数及累加器。`rampKind`:0 无斜坡/匀速,1 加速,2 减速。`clipped` 表示预算不足已裁但仍可执行。`active` 表示是否还可 Generate。Q32 表示把斜坡归一化时间 0~1 放大到 \(2^{32}\)。

+ 0
- 134
Document/PLSR_document/PLSR地址映射.md Переглянути файл

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

`0x2100` 标志位:

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

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

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

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

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

## 6 控制区

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

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

## 7 地址边界

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

+ 0
- 388
Document/PLSR_document/PLSR重构设计书_加减速与多段统一.md Переглянути файл

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

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

---

## 1. 问题定义

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

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

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

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

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

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

### 1.2 重构原则

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

---

## 2. 目标架构总览

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

---

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

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

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

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

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

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

---

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

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

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

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

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

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

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

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

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

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

要点:

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

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

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

---

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

### 5.1 执行状态机

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

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

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

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

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

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

### 5.3 保留的硬件语义

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

---

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

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

### 6.1 统一协议

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

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

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

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

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

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

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

### 6.4 动态改频

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

---

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

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

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

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

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

---

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

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

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

---

## 9. 验收标准汇总

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

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

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

---

## 10. 风险与回退

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

---

## 版本历史

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

+ 1
- 1
PLSR/Src/plsr.c Переглянути файл

@@ -3241,7 +3241,7 @@ static uint8_t PlsrProfileQueueFill(uint16_t targetCount,
{
return 0U;
}
//正常才继续
criticalState = PlsrPlatformEnterCritical();
currentBank = PlsrProfileQueueBank;
queueActive = queue->active;


+ 20
- 16
PLSR/Src/plsr_planner.c Переглянути файл

@@ -40,9 +40,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 +214,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)
{
@@ -406,8 +407,8 @@ static void PlsrPlannerStartRamp(PLSR_PLANNER_CONTEXT *context,
context->rampPulseCount = pulseCount;
context->rampFromHz = fromHz;
context->rampToHz = toHz;
context->rampDurationMs = PlsrPlannerRampTime(&context->block,
fromHz, toHz);
context->rampDurationMs = PlsrPlannerRampTime(&context->block, fromHz, toHz);
context->rampTotalAreaQ32 = PlsrPlannerRampAreaQ32(
context, fromHz, toHz, PLSR_PLANNER_Q32_ONE);
context->rampAreaStepQ32 = context->rampTotalAreaQ32 / pulseCount;
@@ -556,22 +557,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 +582,7 @@ static uint8_t PlsrPlannerTakeStep(PLSR_PLANNER_CONTEXT *context,
requestedFrequencyHz);
}
if (context->rampKind != 2U)
{
{ // 第一次走进减速
PlsrPlannerStartRamp(context, 2U, context->peakHz,
context->endHz, context->exitPulses);
}
@@ -713,11 +714,12 @@ uint16_t PlsrPlannerGenerate(PLSR_PLANNER_CONTEXT *context,//规划账本:三
return 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)
@@ -728,6 +730,8 @@ uint16_t PlsrPlannerGenerate(PLSR_PLANNER_CONTEXT *context,//规划账本:三
<= 0xFFFFFFFFUL - repeatCount))
{
output[produced - 1U].repeatCount += repeatCount;
//不用开新格

}
else
{


+ 10
- 10
PLSR/Src/plsr_planner.h Переглянути файл

@@ -20,16 +20,16 @@ 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


Завантаження…
Відмінити
Зберегти