diff --git a/Document/PLSR_document/AI问询会话记录_2026.8.17-2026.8.23.md b/Document/PLSR_document/AI问询会话记录_2026.8.17-2026.8.23.md deleted file mode 100644 index fa4d3b0..0000000 --- a/Document/PLSR_document/AI问询会话记录_2026.8.17-2026.8.23.md +++ /dev/null @@ -1,128 +0,0 @@ -# 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}\)。 diff --git a/PLSR/Inc/plsr.h b/PLSR/Inc/plsr.h index f62a69a..b029ab9 100644 --- a/PLSR/Inc/plsr.h +++ b/PLSR/Inc/plsr.h @@ -119,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); diff --git a/PLSR/Src/plsr.c b/PLSR/Src/plsr.c index 88ba80e..e88d74d 100644 --- a/PLSR/Src/plsr.c +++ b/PLSR/Src/plsr.c @@ -334,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; @@ -406,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); @@ -4103,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, @@ -4136,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; @@ -4191,6 +4260,7 @@ static void PlsrFinishCompleted(void) PlsrSegmentClockStarted = 0U; PlsrDirectionDelayActive = 0U; PlsrDirectionDelayRemainingMs = 0U; + PlsrInitialDirectionPreparePending = 0U; PlsrExtCaptureSegment = 0U; PlsrExtEdgePending = 0U; PlsrStopRequested = 0U; @@ -4233,6 +4303,7 @@ static void PlsrFinishStopped(void) PlsrSegmentClockStarted = 0U; PlsrDirectionDelayActive = 0U; PlsrDirectionDelayRemainingMs = 0U; + PlsrInitialDirectionPreparePending = 0U; PlsrExtCaptureSegment = 0U; PlsrExtEdgePending = 0U; PlsrStopRequested = 0U; @@ -4275,6 +4346,7 @@ static void PlsrEnterError(PLSR_ERROR error) PlsrSegmentClockStarted = 0U; PlsrDirectionDelayActive = 0U; PlsrDirectionDelayRemainingMs = 0U; + PlsrInitialDirectionPreparePending = 0U; PlsrExtCaptureSegment = 0U; PlsrExtEdgePending = 0U; PlsrStopRequested = 0U; @@ -5777,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; @@ -5894,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); @@ -6022,6 +6098,7 @@ static void PlsrExecuteClear(void) PlsrBoundaryWasCut = 0U; PlsrDirectionDelayActive = 0U; PlsrDirectionDelayRemainingMs = 0U; + PlsrInitialDirectionPreparePending = 0U; PlsrExtCaptureSegment = 0U; PlsrExtEdgePending = 0U; PlsrShortProfile.active = 0U; @@ -6242,6 +6319,13 @@ void PlsrPoll1ms(void) 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();//把 IRQ 丢进边界事件环的东西在任务里消化:切段、记账。只做账,不算频率 @@ -7253,4 +7337,9 @@ uint32_t PlsrTestLastDiagnosticFinishEvent(void) { return PlsrHostTestLastDiagnosticFinishEvent; } + +uint8_t PlsrTestInitialDirectionPreparePending(void) +{ + return PlsrInitialDirectionPreparePending; +} #endif diff --git a/PLSR/Src/plsr_platform_f407.c b/PLSR/Src/plsr_platform_f407.c index 646ed59..6eb0863 100644 --- a/PLSR/Src/plsr_platform_f407.c +++ b/PLSR/Src/plsr_platform_f407.c @@ -1373,7 +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_RETRIGGER_BLOCK_MS (5UL) +#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) @@ -1582,8 +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 uint32_t PlsrInputLastAcceptedTick[2]; -static uint8_t PlsrInputAcceptedValidMask; +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); @@ -2944,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; @@ -2952,9 +2953,10 @@ uint8_t PlsrPlatformInit(void) HAL_GPIO_Init(GPIOB, &gpio); gpio.Pin = GPIO_PIN_12; HAL_GPIO_Init(GPIOG, &gpio); - PlsrInputLastAcceptedTick[0] = 0UL; - PlsrInputLastAcceptedTick[1] = 0UL; + 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); @@ -4451,6 +4453,7 @@ static uint8_t PlsrInputExtiAccept(uint8_t inputSelection) { uint32_t now; uint8_t bit; + uint8_t level; if (inputSelection > 1U) { @@ -4458,13 +4461,36 @@ static uint8_t PlsrInputExtiAccept(uint8_t inputSelection) } now = HAL_GetTick(); bit = (uint8_t)(1U << inputSelection); - if (((PlsrInputAcceptedValidMask & bit) != 0U) - && ((uint32_t)(now - PlsrInputLastAcceptedTick[inputSelection]) - < PLSR_INPUT_RETRIGGER_BLOCK_MS)) + 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; } - PlsrInputLastAcceptedTick[inputSelection] = now; + + PlsrInputLowSeenMask &= (uint8_t)~bit; PlsrInputAcceptedValidMask |= bit; return 1U; } diff --git a/tests/plsr_host/run_tests.ps1 b/tests/plsr_host/run_tests.ps1 index 6e55aef..b2ef2aa 100644 --- a/tests/plsr_host/run_tests.ps1 +++ b/tests/plsr_host/run_tests.ps1 @@ -1,3 +1,5 @@ +param([string]$Filter) + $ErrorActionPreference = "Stop" $gcc = "D:\Dev-Cpp\MinGW64\bin\gcc.exe" @@ -32,7 +34,12 @@ try { $exitCode = $LASTEXITCODE } else { - & $temporaryExe + if ([string]::IsNullOrEmpty($Filter)) { + & $temporaryExe + } + else { + & $temporaryExe $Filter + } $exitCode = $LASTEXITCODE } } diff --git a/tests/plsr_host/test_plsr_host.c b/tests/plsr_host/test_plsr_host.c index d50046c..9a7a287 100644 --- a/tests/plsr_host/test_plsr_host.c +++ b/tests/plsr_host/test_plsr_host.c @@ -2,6 +2,7 @@ #include #include +#include #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);