From 7810b61ba76d8f1e152491a29a90d4324afcb84f Mon Sep 17 00:00:00 2001 From: ywh <2227158009@qq.com> Date: Fri, 21 Aug 2026 17:17:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E4=BA=86=E4=B8=80=E7=89=88=E4=B8=8A?= =?UTF-8?q?=E4=BD=8D=E6=9C=BA=E6=98=BE=E7=A4=BA=E6=B3=A2=E5=BD=A2=EF=BC=8C?= =?UTF-8?q?mcu=E7=A8=8B=E5=BA=8F=E5=8A=A0=E4=BA=86=E7=82=B9=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bin_to_time_freq.cpython-314.pyc | Bin 18091 -> 22489 bytes HostComputer/bin_to_time_freq.py | 160 +++++++--- HostComputer/bin_to_time_freq1.py | 293 ++++++++++++++++++ PLSR/Src/plsr.c | 188 +++++++---- PLSR/Src/plsr_planner.c | 20 +- PLSR/Src/plsr_planner.h | 60 ++-- 6 files changed, 574 insertions(+), 147 deletions(-) create mode 100644 HostComputer/bin_to_time_freq1.py diff --git a/HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc b/HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc index d31ac38ef3a8d540d8fa4d378a5dd444f8d4af49..5189aeeeb822d84d8f80a09405c67b85c20ee499 100644 GIT binary patch delta 9304 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 delta 5778 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 NRU7Qgm23p?{{Y!R$4&qM diff --git a/HostComputer/bin_to_time_freq.py b/HostComputer/bin_to_time_freq.py index 4f26e4f..eecbece 100644 --- a/HostComputer/bin_to_time_freq.py +++ b/HostComputer/bin_to_time_freq.py @@ -4,21 +4,25 @@ 数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点; 电平 >= 阈值(默认128) 判为高(脉冲),否则为低。 -频率定义: 每脉冲频率 = 采样率 / 脉冲周期; - 周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点; - 最后一个脉冲无下一脉冲,按高电平宽度 x2 近似(50% 占空比)。 -时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)。 +频率定义: 用连续 N 个脉冲的上升沿间隔估计频率; + 频率点放在 N 周期测量窗口中间; + 频率 = N * 采样率 /(第 N 个后续上升沿 - 当前上升沿); +时间定义: 频率点时间为测量窗口中点;高电平宽度取中心脉冲。 用法: python bin_to_time_freq.py [采样率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 选项: --thresh=128 电平阈值(默认128) + --cycles=8 用连续多少个周期估计频率(默认8;1为逐周期) + --reverse 反相电平后再检测脉冲(低电平作为脉冲高电平) --ymax=12000 频率轴上限(默认自适应) --save=路径 保存 PNG 后退出(不弹窗) --selftest 仅打印统计,不画图 @@ -28,7 +32,7 @@ Ctrl+滚轮 缩放 Y 轴(以鼠标 Y 位置为中心) 左键拖拽 双向平移(上下左右跟随鼠标) 双击 复位到全图 - 鼠标悬停 高亮最近的点并显示其脉冲序号、时间与频率 + 鼠标悬停 高亮最近的点并显示其测量点序号、时间与频率 依赖: pip install numpy matplotlib """ @@ -46,13 +50,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 +68,106 @@ 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)) + print("估计周期窗口: %d 个周期" % meta['cycles']) + print("波形活动时长: %.1f ms" % (meta['activity_duration'] * 1000)) -def plot_show(path, fs, thresh, ymax, save_path=None): - t, freq, hi_w, meta = load_time_freq(path, fs, thresh) +def plot_show(path, fs, thresh, ymax, cycles, reverse, save_path=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 +181,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 周期平均,活动时长 %.0f ms)" % ( + os.path.basename(path), meta['pulses'], meta['cycles'], + meta['activity_duration'] * 1000)) ax.set_xlabel("时间 (ms)") ax.set_ylabel("频率 (Hz)") # 更密的刻度 @@ -224,7 +288,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 +310,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 +323,8 @@ 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 周期平均,活动时长 %.1f ms" % ( + meta['pulses'], meta['cycles'], meta['activity_duration'] * 1000)) plt.show() @@ -269,13 +333,15 @@ 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', '') @@ -284,9 +350,9 @@ def main(): 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) if __name__ == '__main__': diff --git a/HostComputer/bin_to_time_freq1.py b/HostComputer/bin_to_time_freq1.py new file mode 100644 index 0000000..4f26e4f --- /dev/null +++ b/HostComputer/bin_to_time_freq1.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +""" +.bin 波形文件 -> 时频曲线查看/出图工具(PLSR 项目用) + +数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点; + 电平 >= 阈值(默认128) 判为高(脉冲),否则为低。 +频率定义: 每脉冲频率 = 采样率 / 脉冲周期; + 周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点; + 最后一个脉冲无下一脉冲,按高电平宽度 x2 近似(50% 占空比)。 +时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)。 + +用法: + python bin_to_time_freq.py [采样率Hz] [选项] +示例: + python bin_to_time_freq.py "Document/PLSR_document/波形/10段.bin" 6250000 + python bin_to_time_freq.py xxx.bin 6250000 --ymax=12000 + python bin_to_time_freq.py xxx.bin 6250000 --save=out.png + python bin_to_time_freq.py xxx.bin 6250000 --selftest + +选项: + --thresh=128 电平阈值(默认128) + --ymax=12000 频率轴上限(默认自适应) + --save=路径 保存 PNG 后退出(不弹窗) + --selftest 仅打印统计,不画图 + +交互(弹窗模式): + 滚轮 缩放 X 轴(向上放大/向下缩小,以鼠标位置为中心) + Ctrl+滚轮 缩放 Y 轴(以鼠标 Y 位置为中心) + 左键拖拽 双向平移(上下左右跟随鼠标) + 双击 复位到全图 + 鼠标悬停 高亮最近的点并显示其脉冲序号、时间与频率 + +依赖: pip install numpy matplotlib +""" + +import sys +import os + +import numpy as np + +try: + import matplotlib + matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei'] + matplotlib.rcParams['axes.unicode_minus'] = False +except Exception: + pass + + +def load_time_freq(path, fs, threshold=128): + """读取 bin,返回 (t_sec, freq_hz, hi_width_sec, meta)。 + t_sec 以第一个脉冲为 0 时刻;hi_width 为每脉冲高电平宽度。""" + data = np.fromfile(path, dtype=np.uint8) + if data.size == 0: + raise ValueError("文件为空: %s" % path) + samples = (data >= threshold).astype(np.int8) + + # run 级压缩 + changes = np.flatnonzero(np.diff(samples) != 0) + 1 + starts = np.concatenate(([0], changes)) + ends = np.concatenate((changes, [len(samples)])) + runs = np.column_stack((starts, ends, samples[starts])) + + # 脉冲 = 高电平 run,且其后紧跟低电平 run + hi = np.flatnonzero(runs[:, 2] == 1) + hi = hi[hi + 1 < len(runs)] + lo_ok = runs[hi + 1, 2] == 0 + hi = hi[lo_ok] + + t_start = runs[hi, 0].astype(np.int64) + hi_width = (runs[hi, 1] - runs[hi, 0]).astype(np.int64) + + # 周期:高到高;末脉冲按 2x 高电平宽度近似 + next_start = np.append(t_start[1:], [t_start[-1] + 2 * hi_width[-1]]) + period = next_start - t_start + freq = fs / np.maximum(period, 1) + + # 时间:高电平中点,去起始偏移 + t_sec = (t_start + hi_width / 2.0) / fs + t_sec = t_sec - t_sec[0] + + meta = { + 'total_samples': int(len(samples)), + 'duration': len(samples) / fs, + 'pulses': int(len(freq)), + 'offset_ms': t_start[0] / fs * 1000, + } + return t_sec, freq, hi_width / fs, meta + + +def selftest(path, fs, thresh): + import statistics + t, freq, hi_w, meta = load_time_freq(path, fs, thresh) + print("文件: %s" % path) + print("总采样: %d (%.3f s @ %.2f MS/s)" % (meta['total_samples'], meta['duration'], fs / 1e6)) + print("起始采集偏移: %.1f ms" % meta['offset_ms']) + print("脉冲总数: %d" % meta['pulses']) + if meta['pulses']: + print("频率 min=%.0f max=%.0f 中位=%.0f Hz" % ( + freq.min(), freq.max(), statistics.median(freq))) + print("末脉冲: t=%.3f ms, 高电平 %.3f ms, %.0f Hz" % ( + t[-1] * 1000, hi_w[-1] * 1000, freq[-1])) + print("波形活动时长: %.1f ms" % ((t[-1] + hi_w[-1] / 2) * 1000)) + + +def plot_show(path, fs, thresh, ymax, save_path=None): + t, freq, hi_w, meta = load_time_freq(path, fs, thresh) + + import matplotlib.pyplot as plt + from matplotlib.ticker import MaxNLocator + + fig, ax = plt.subplots(figsize=(15, 7)) + fig.subplots_adjust(bottom=0.10, top=0.92) + + # 全脉冲散点:复用 artist,重绘时按可见范围抽稀(大文件流畅) + MAX_VISIBLE_POINTS = 5000 # 可见范围内最多绘制的点数,超过则等间隔抽稀 + (scatter,) = ax.plot([], [], '.', ms=1.5, color='C0', alpha=0.5, 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) + ax.set_title("%s 时频曲线(%d 脉冲,活动时长 %.0f ms)" % ( + os.path.basename(path), meta['pulses'], + (t[-1] + hi_w[-1] / 2) * 1000)) + ax.set_xlabel("时间 (ms)") + ax.set_ylabel("频率 (Hz)") + # 更密的刻度 + ax.xaxis.set_major_locator(MaxNLocator(nbins=20)) + ax.yaxis.set_major_locator(MaxNLocator(nbins=15)) + ax.grid(True, which='major', alpha=0.35) + ax.grid(True, which='minor', alpha=0.15) + ax.minorticks_on() + + if save_path: + fig.savefig(save_path, dpi=130) + print("已保存: %s" % save_path) + plt.close(fig) + return + + # 交互:滚轮缩放 X(Ctrl+滚轮缩放 Y)/ 左键双向拖拽平移 / 双击复位 / 悬停高亮 + state = {'press_x': None, 'press_y': None, + 'press_xlim': None, 'press_ylim': None} + + # 悬停高亮:一个红点标记 + 一个带框文本 + (hl_marker,) = ax.plot([], [], 'o', ms=9, mfc='red', mec='white', + mew=1.0, zorder=5, visible=False) + hl_text = ax.text(0, 0, '', fontsize=10, color='black', + bbox=dict(boxstyle='round,pad=0.3', fc='yellow', ec='red', alpha=0.9), + zorder=6, visible=False) + hover_last = {'idx': -1, 'visible': False} + + # 可见范围内抽稀绘制散点(大文件性能优化) + def 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]) + + 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 个,活动时长 %.1f ms" % ( + meta['pulses'], (t[-1] + hi_w[-1] / 2) * 1000)) + plt.show() + + +def main(): + args = [a for a in sys.argv[1:] if not a.startswith('--')] + opts = {a.split('=', 1)[0]: (a.split('=', 1)[1] if '=' in a else True) + for a in sys.argv[1:] if a.startswith('--')} + + if len(args) < 1 or '--help' in opts or '-h' in opts: + print(__doc__) + sys.exit(0) + + path = args[0] + fs = float(args[1]) if len(args) > 1 else 6.25e6 + thresh = int(opts.get('--thresh', 128)) + ymax = float(opts.get('--ymax', 0)) + save_path = opts.get('--save', '') + + if not os.path.isfile(path): + print("错误:文件不存在 - %s" % path) + sys.exit(1) + + if '--selftest' in opts: + selftest(path, fs, thresh) + else: + plot_show(path, fs, thresh, ymax, save_path or None) + + +if __name__ == '__main__': + main() diff --git a/PLSR/Src/plsr.c b/PLSR/Src/plsr.c index 6307ab1..0d7b0af 100644 --- a/PLSR/Src/plsr.c +++ b/PLSR/Src/plsr.c @@ -124,9 +124,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 @@ -1995,6 +1999,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 +2035,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 +2079,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 +2093,7 @@ static uint8_t PlsrShortProfileTakeRunLimited( { profile->active = 0U; } + return 1U; } @@ -3125,23 +3167,23 @@ static void PlsrProfileRecordProducerCycles(uint32_t startCycles) static uint8_t PlsrProfileQueueFill(uint16_t targetCount, uint16_t *itemBudget) { - PLSR_SHORT_PROFILE candidate; - PLSR_PROFILE_ENTRY entry; + PLSR_SHORT_PROFILE candidate;//规划账本的工作副本。在临界区外算,算完再写回 + PLSR_PROFILE_ENTRY entry;//这一圈要入队的那一项 uint32_t generation; - uint32_t producerEpoch; - uint32_t criticalState; - uint32_t underrunDebt; + uint32_t producerEpoch;//拷出去时记下的版本,写回来要对得上 + uint32_t criticalState;//开关中断用 + uint32_t underrunDebt;//硬件已经「借用」了多少还没规划的脉冲 uint32_t queueReadIndex; uint32_t queueWriteIndex; - uint32_t queueCount; + uint32_t queueCount;//当前深度 uint32_t currentGeneration; uint32_t currentProducerEpoch; - PLSR_PROFILE_QUEUE *queue; + PLSR_PROFILE_QUEUE *queue;//队列指针 uint8_t queueBank; - uint8_t currentBank; + uint8_t currentBank;//当前活队列 uint8_t queueActive; uint8_t generatorComplete; - uint8_t payingUnderrunDebt; + uint8_t payingUnderrunDebt;//这一圈是还债还是真入 #if defined(PLSR_DEBUG_TIMING) && (PLSR_DEBUG_TIMING != 0) \ && !defined(PLSR_HOST_TEST) uint32_t startCycles; @@ -3149,47 +3191,53 @@ static uint8_t PlsrProfileQueueFill(uint16_t targetCount, if (itemBudget == NULL) { + //没有预算指针,没法扣次数,当失败 return 0U; } if ((targetCount == 0U) || (targetCount > PLSR_PROFILE_QUEUE_CAPACITY)) { + //非法目标就按整队列来。正常调用是 400/800,走不到这里 targetCount = PLSR_PROFILE_QUEUE_CAPACITY; } while (1) { - criticalState = PlsrPlatformEnterCritical(); - queueBank = PlsrProfileQueueBank; - queue = &PlsrProfileQueues[queueBank]; - queueActive = queue->active; - generatorComplete = queue->generatorComplete; - queueWriteIndex = queue->writeIndex; - queueReadIndex = queue->readIndex; - queueCount = queueWriteIndex - queueReadIndex; - if ((queueActive == 0U) - || (generatorComplete != 0U) - || (queueCount >= targetCount) - || (*itemBudget == 0U)) - { + criticalState = PlsrPlatformEnterCritical();//进入临界区 + queueBank = PlsrProfileQueueBank;//现在活着的那一套队列是 0 还是 1。先记下来 + queue = &PlsrProfileQueues[queueBank];//拿到这套队列的指针。后面 queue->xxx 都是这套 + queueActive = queue->active;//这套还在给当前运动供货吗。段停完/作废会变成 0 + generatorComplete = queue->generatorComplete;//规划器是不是已经把这段所有脉冲都吐完了 + queueWriteIndex = queue->writeIndex;//任务下一次要写的位置(累计值,不是 0~1023 的下标) + queueReadIndex = queue->readIndex;//IRQ 下一次要读的位置 + queueCount = queueWriteIndex - queueReadIndex;//当前有多少格。无符号减法,环形也能得到深度 + if ((queueActive == 0U)//不供货了,不用再算 + || (generatorComplete != 0U)//这段已经吐完,不用再算 + || (queueCount >= targetCount)//已经够深(调用方一般是 800 或 400) + || (*itemBudget == 0U))//这一拍允许新算的次数用光了 + + { + //关闭临界区 PlsrPlatformExitCritical(criticalState); return 1U; } + /*先扣一次「本拍还能算几项」。 + 哪怕后面这项被扔掉,次数也花掉了,防止一拍里死循环*/ (*itemBudget)--; - generation = queue->generation; - producerEpoch = queue->producerEpoch; - underrunDebt = queue->underrunDebtPulses; - payingUnderrunDebt = (underrunDebt != 0UL) ? 1U : 0U; + generation = queue->generation;//记下现在的世代。改频、借债会 generation++ + producerEpoch = queue->producerEpoch;//记下队列现在的世代。改频、借债会 generation++ + underrunDebt = queue->underrunDebtPulses;//硬件已经用末频「预支」了多少脉冲,规划还没跟上 + payingUnderrunDebt = (underrunDebt != 0UL) ? 1U : 0U;//这一圈是 还债 还是 入队。有债=1 PlsrCopyShortProfile(&candidate, &queue->producerProfile); + //把规划账本拷到本地 candidate。等会儿在门外改它,不能边算边让 IRQ 看到半成品 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) + if (PlsrShortProfileTakeRunLimited( &candidate, &entry, + (payingUnderrunDebt != 0U) ? underrunDebt : 0xFFFFFFFFUL) == 0U) + { return 0U; } @@ -6027,20 +6075,20 @@ static uint8_t PlsrServiceCountedExecutor(void) 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/改频 + uint16_t fillBudget = PLSR_PROFILE_REFILL_BUDGET;//这段会用。 本拍最多往队列里新算多少项。宏是 200 PLSR_PLATFORM_SERVICE_RESULT pulseDirReplanResult = - PLSR_PLATFORM_SERVICE_READY; + PLSR_PLATFORM_SERVICE_READY;//重规划结果,先当成功 if (PlsrInitialized == 0U) { @@ -6049,53 +6097,75 @@ void PlsrPoll1ms(void) if (PlsrPollCommandMailbox() != 0U) { + /*看邮箱有没有 START / STOP / CLEAR。 + 有命令:当场执行(启动、停止、清位置)。 + 返回 非 0:这拍到此结束(常见是 STOP 认为后面不用再 poll)。 + 返回 0:没命令,或 STOP 说还可以继续 → 往下走。*/ 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; + PlsrPollPersistenceDelay();//掉电保存的延时到了就存 + return;//这段波已经结束,本拍不要再补队列。Fill 是给还在跑的段用的 } - if (PlsrProfileQueueCount() <= PLSR_PROFILE_LOW_WATER) + if (PlsrProfileQueueCount() <= PLSR_PROFILE_LOW_WATER)//当前队列还剩多少 格子 { + //浅到 ≤100,本拍预算从 200 改成 400。急救,多炒一倍菜。 + //没浅,仍是 200 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)) + || (PlsrExecutor.mode == PLSR_EXEC_AB_LEGACY))//这三种才会用 profile 队列。IDLE 不补 + && (PlsrProfileQueue.active != 0U)//当前 bank 还在给这段供货。段停完后 active 会被清掉 + && (PlsrProfileQueue.generatorComplete == 0U)//本段规划还没把所有脉冲吐完。吐完了第一次 Fill 没意义 + && (PlsrProfileQueueFill(PLSR_PROFILE_REFILL_TARGET,&fillBudget) == 0U)) + /*去算、去塞,直到格子到 800 或预算用完。返回 0 = Generate/入队失败*/ + { PlsrEnterError(PLSR_ERROR_TIMER); - return; + return;//活队列补失败,当定时器/规划故障停机 } if (((PlsrExecutor.mode == PLSR_EXEC_STEP_TABLE) - || (PlsrExecutor.mode == PLSR_EXEC_STREAM)) + || (PlsrExecutor.mode == PLSR_EXEC_STREAM))//这里 没有 AB_LEGACY。PULSE/DIR 的 counted 下一段才走这条 && ((PlsrStageCountedHandoff() == 0U) + /*本段已经 generatorComplete 时,把下一段预热的第一/第二 run 接到队尾。 + 还不到接的时候它返回 1(成功但空操作)。 + 返回 0 才是真失败(队列塞不下、handoff 非法等*/ || (PlsrProfileQueueFill(PLSR_PROFILE_REFILL_TARGET, &fillBudget) == 0U))) + /*handoff 成功或空操作之后,再用 剩下的预算 再填一次。 + handoff 刚占了格子,或第一次没填满。 + + 两个子条件用 或:handoff 失败 或者 第二次 Fill 失败,都进错误。 + 短路:handoff 已经失败,第二次 Fill 不会跑。*/ { - PlsrEnterError(PLSR_ERROR_TIMER); + PlsrEnterError(PLSR_ERROR_TIMER);//接不上下一段或再补失败,同样停 return; } diff --git a/PLSR/Src/plsr_planner.c b/PLSR/Src/plsr_planner.c index 8f60348..4e7c744 100644 --- a/PLSR/Src/plsr_planner.c +++ b/PLSR/Src/plsr_planner.c @@ -697,10 +697,10 @@ PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *context, 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; @@ -712,18 +712,16 @@ uint16_t PlsrPlannerGenerate(PLSR_PLANNER_CONTEXT *context, { 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)) + { + if (PlsrPlannerTakeStep(context, &setting, &requestedFrequencyHz, &repeatCount) == 0U) + //本圈 TakeStep 算出的量化后 PSC/ARR/actualHz,先放栈上 { 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 diff --git a/PLSR/Src/plsr_planner.h b/PLSR/Src/plsr_planner.h index e521e71..b35608a 100644 --- a/PLSR/Src/plsr_planner.h +++ b/PLSR/Src/plsr_planner.h @@ -35,8 +35,8 @@ typedef struct 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,34 +65,34 @@ 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; PLSR_PLANNER_STATUS PlsrPlannerBegin(PLSR_PLANNER_CONTEXT *context,