| @@ -4,21 +4,25 @@ | |||
| 数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点; | |||
| 电平 >= 阈值(默认128) 判为高(脉冲),否则为低。 | |||
| 频率定义: 每脉冲频率 = 采样率 / 脉冲周期; | |||
| 周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点; | |||
| 最后一个脉冲无下一脉冲,按高电平宽度 x2 近似(50% 占空比)。 | |||
| 时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)。 | |||
| 频率定义: 用连续 N 个脉冲的上升沿间隔估计频率; | |||
| 频率点放在 N 周期测量窗口中间; | |||
| 频率 = N * 采样率 /(第 N 个后续上升沿 - 当前上升沿); | |||
| 时间定义: 频率点时间为测量窗口中点;高电平宽度取中心脉冲。 | |||
| 用法: | |||
| python bin_to_time_freq.py <bin文件> [采样率Hz] [选项] | |||
| 未填写采样率时默认使用 3125000 Hz。 | |||
| 示例: | |||
| python bin_to_time_freq.py "Document/PLSR_document/波形/10段.bin" 6250000 | |||
| python bin_to_time_freq.py xxx.bin 6250000 --ymax=12000 | |||
| python bin_to_time_freq.py xxx.bin 6250000 --save=out.png | |||
| python bin_to_time_freq.py xxx.bin 6250000 --selftest | |||
| python bin_to_time_freq.py "Document/PLSR_document/波形/10段.bin" 3125000 | |||
| python bin_to_time_freq.py xxx.bin 3125000 --ymax=12000 | |||
| python bin_to_time_freq.py xxx.bin 3125000 --save=out.png | |||
| python bin_to_time_freq.py xxx.bin 3125000 --selftest | |||
| python bin_to_time_freq.py xxx.bin 3125000 --reverse --cycles=8 | |||
| 选项: | |||
| --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__': | |||
| @@ -0,0 +1,293 @@ | |||
| # -*- coding: utf-8 -*- | |||
| """ | |||
| .bin 波形文件 -> 时频曲线查看/出图工具(PLSR 项目用) | |||
| 数据格式: 逻辑分析仪导出的单通道数字采样,每字节 1 个采样点; | |||
| 电平 >= 阈值(默认128) 判为高(脉冲),否则为低。 | |||
| 频率定义: 每脉冲频率 = 采样率 / 脉冲周期; | |||
| 周期 = 本脉冲高电平起点 -> 下一脉冲高电平起点; | |||
| 最后一个脉冲无下一脉冲,按高电平宽度 x2 近似(50% 占空比)。 | |||
| 时间定义: 脉冲时间 = 高电平中点(与低电平/空闲段无关)。 | |||
| 用法: | |||
| python bin_to_time_freq.py <bin文件> [采样率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() | |||
| @@ -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; | |||
| } | |||
| @@ -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 | |||
| @@ -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, | |||