ソースを参照

改了一版上位机显示波形,mcu程序加了点注释

codex/plsr-2026-minimal
ywh 4週間前
コミット
7810b61ba7
6個のファイルの変更574行の追加147行の削除
  1. バイナリ
      HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc
  2. +113
    -47
      HostComputer/bin_to_time_freq.py
  3. +293
    -0
      HostComputer/bin_to_time_freq1.py
  4. +129
    -59
      PLSR/Src/plsr.c
  5. +9
    -11
      PLSR/Src/plsr_planner.c
  6. +30
    -30
      PLSR/Src/plsr_planner.h

バイナリ
HostComputer/__pycache__/bin_to_time_freq.cpython-314.pyc ファイルの表示


+ 113
- 47
HostComputer/bin_to_time_freq.py ファイルの表示

@@ -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__':


+ 293
- 0
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 <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()

+ 129
- 59
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;
}



+ 9
- 11
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


+ 30
- 30
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,


読み込み中…
キャンセル
保存