手写一个电流环采样时序校核工具:为什么你的 FOC 电流总差几个百分点
FOC 的电流环,给定 10A,实测 9.4A。不是环路带宽不够,也不是 PI 参数没调好------是采样时刻偏了 1.2µs。
在 24V / 0.5mH 的电机上,1µs 的时间偏移等于 48mA 的电流误差。1.2µs 就是 58mA,6 个点。而且它只在特定占空比下出现,所以调试的时候经常"调好了又坏"。
这篇写一个能算的工具:给定 PWM 参数和采样窗口,直接告出"你的采样时刻允许偏多少"。
场景
某电动叉车的行走驱动,48V 母线,BLDC 电机配 FOC 控制。
控制板用的是 AQMD6030BLS-E2 无刷直流电机驱动器 ,电机控制方式为 FOC 磁场定向控制(40A 峰值 / 25A 额定,适配 12V-180W 至 60V-1250W,能耗制动 12A,带刹车电阻制动、四象限运行、485/CAN 隔离,136×82×45mm 铝合金机壳)。
现象:空载电流很准,加载到 20A 以上,电流采样值比钳形表读数低 4~6%。低速大转矩时最明显,高速时反而好转。
一、先搞清楚:为什么采样时刻这么关键
FOC 的电流环需要知道三相电流的瞬时值 。但 ADC 采样不是瞬间完成的,它有窗口宽度;而且采样必须避开开关动作,因为开关瞬间的 dv/dt 会把采样结果污染。
标准做法是中心对齐 PWM + 在计数器峰值处采样(SOP,Single-Once Sampling at the Peak):
txt
PWM 计数器:
/\ /\ /\
/ \ / \ / \
/ \ / \ / \
____/ \__/ \__/ \____
↑ ↑ ↑ ↑
谷底 峰值 谷底 峰值
↑
在这里采样(中心对齐,上下桥臂都关断的瞬间?不对)
等等,这里有个常见的误解。中心对齐 PWM 的峰值处,上桥臂是全开的,不是全关。
关键在于:三相里总有一相的占空比最大、一相最小、一相居中 。电流采样通常只在下桥臂导通的那两相上做(低边采样),因为低边采样电阻的共模电压接近 GND,好处理。
所以真正决定采样窗口的,是占空比最小那一相的下桥臂导通时间:
txt
T_window = (1 - D_max) / f_sw
D_max 是三相中最大的占空比,f_sw 是开关频率。
二、窗口里要装下什么
这个窗口不是全都能用,得扣掉三段时间:
| 项 | 典型值 | 说明 |
|---|---|---|
| 死区时间 | 500 ns | 防止上下桥臂直通,两端都要留 |
| 电流建立时间 | 800 ns | 采样电阻 + 运放的建立,取决于运放压摆率 |
| ADC 采样保持 | 300 ns | ADC 的 S/H 电路充电时间 |
可用窗口:
txt
T_usable = T_window - T_dead - T_settle - T_adc
如果 T_usable 小于 0,说明低边采样在这个占空比下根本做不了,必须改用别的方案(相电流重构、或者上桥臂采样、或者三电阻采样)。
三、再算误差:时间偏移怎么变成电流误差
这是核心公式。电感上的电流变化率:
txt
di/dt = V / L
在电流纹波的上升段,V ≈ V_bus - V_bemf,简化为 V_bus:
txt
ΔI = (V_bus / L) × Δt
采样时刻每偏移 1µs,电流就偏差 V_bus/L × 1µs。
48V / 0.5mH = 96 A/ms = 96mA/µs。
这比我前面说的 24V 情况(48mA/µs)更严重------电压越高,采样时刻越敏感。这也是为什么 48V、60V 平台对采样时序的要求比 24V 苛刻得多。
四、写一个校核工具
下面这个脚本,输入 PWM 参数 + 电机参数 + ADC 参数,输出三件事:
- 各占空比下可用采样窗口还剩多少;
- 采样时刻允许的最大偏移;
- 电流环的理论误差上界。
python
"""
FOC 电流采样时序校核工具
依赖:numpy(只用基础运算)
"""
import numpy as np
from dataclasses import dataclass
@dataclass
class PwmConfig:
f_sw: float # 开关频率 Hz
t_dead: float # 死区时间 s
t_settle: float # 采样电路建立时间 s
t_adc: float # ADC 采样保持时间 s
t_jitter: float # 触发抖动 s(定时器+驱动延迟的不确定性)
@dataclass
class MotorConfig:
v_bus: float # 母线电压 V
l_phase: float # 相电感 H
r_phase: float # 相电阻 Ω
i_rated: float # 额定电流 A
def sampling_window(duty_max, pwm: PwmConfig):
"""返回 (总窗口, 可用窗口),单位秒"""
t_win = (1.0 - duty_max) / pwm.f_sw
t_usable = t_win - pwm.t_dead - pwm.t_settle - pwm.t_adc
return t_win, t_usable
def current_slope(motor: MotorConfig, duty):
"""电流纹波斜率 A/s(上升段近似,忽略反电动势)"""
# 一个开关周期内,导通段 di/dt = (V_bus - I*R - V_bemf)/L
# 这里取最坏情况:反电动势为 0(堵转/低速大转矩)
return motor.v_bus / motor.l_phase
def error_from_offset(dt: float, motor: MotorConfig):
"""采样时刻偏移 dt 引起的电流误差 A"""
return current_slope(motor, 0) * abs(dt)
def check(pwm: PwmConfig, motor: MotorConfig, duty_list=None):
if duty_list is None:
duty_list = np.arange(0.05, 0.96, 0.05)
# 允许误差:额定电流的 1%
i_tol = motor.i_rated * 0.01
dt_allowed = i_tol / current_slope(motor, 0)
# A/s -> A/ms 除以 1e3;A/s -> mA/µs 除以 1e3(1 A/µs = 1000 A/s)
dt_slope = current_slope(motor, 0)
print(f"电流纹波斜率 di/dt = {dt_slope/1e3:.1f} A/ms "
f"= {dt_slope/1e3:.1f} mA/µs")
print(f"允许误差 {i_tol*1000:.0f} mA ({1.0:.1f}% of rated) "
f"-> 允许偏移 Δt ≤ {dt_allowed*1e6:.2f} µs")
print(f"触发抖动 {pwm.t_jitter*1e9:.0f} ns,占允许值的 {pwm.t_jitter/dt_allowed:.1%}")
print()
print(f"{'占空比':>8}{'总窗口':>11}{'可用窗口':>12}{'状态':>9}")
print("-" * 40)
worst = None
for d in duty_list:
t_win, t_usable = sampling_window(d, pwm)
ok = t_usable > 0
flag = "OK" if ok else "不可用"
print(f"{d:>7.2f}{t_win*1e9:>9.0f}ns{t_usable*1e9:>10.0f}ns{flag:>8}")
if worst is None or t_usable < worst[1]:
worst = (d, t_usable)
print()
print(f"最恶劣占空比 D={worst[0]:.2f},剩余可用窗口 {worst[1]*1e9:.0f} ns")
if worst[1] <= 0:
print(" -> 该占空比下低边采样不可行,必须改方案")
elif worst[1] < pwm.t_jitter:
print(" -> 窗口小于触发抖动,采样时刻不可控")
else:
print(f" -> 窗口内有 {worst[1]/pwm.t_jitter:.1f} 个抖动周期的余量")
if __name__ == "__main__":
pwm = PwmConfig(f_sw=16e3, t_dead=500e-9, t_settle=800e-9,
t_adc=300e-9, t_jitter=150e-9)
motor = MotorConfig(v_bus=48.0, l_phase=0.5e-3, r_phase=0.15, i_rated=25.0)
check(pwm, motor)
跑出来:
txt
电流纹波斜率 di/dt = 96.0 A/ms = 96.0 mA/µs
允许误差 250 mA (1.0% of rated) -> 允许偏移 Δt ≤ 2.60 µs
触发抖动 150 ns,占允许值的 5.8%
占空比 总窗口 可用窗口 状态
----------------------------------------
0.05 59375ns 57775ns OK
0.10 56250ns 54650ns OK
0.15 53125ns 51525ns OK
0.20 50000ns 48400ns OK
0.25 46875ns 45275ns OK
0.30 43750ns 42150ns OK
0.35 40625ns 39025ns OK
0.40 37500ns 35900ns OK
0.45 34375ns 32775ns OK
0.50 31250ns 29650ns OK
0.55 28125ns 26525ns OK
0.60 25000ns 23400ns OK
0.65 21875ns 20275ns OK
0.70 18750ns 17150ns OK
0.75 15625ns 14025ns OK
0.80 12500ns 10900ns OK
0.85 9375ns 7775ns OK
0.90 6250ns 4650ns OK
0.95 3125ns 1525ns OK
最恶劣占空比 D=0.95,剩余可用窗口 1525 ns
-> 窗口内有 10.2 个抖动周期的余量
看这个结果,一切都很健康------0.95 占空比下还有 1525ns 窗口。
那为什么现场数据会差 6%?
五、因为工具算的是"最理想的采样点",而实际采样点会跑
问题在触发抖动 和PWM 更新时刻 上。工具的 t_jitter 只考虑了定时器抖动,但真实系统里还有:
- PWM 比较值更新延迟:如果 PI 环算完新占空比后,等到下一个周期才生效,那么"你以为的占空比"和"实际的占空比"差一个周期;
- ADC 触发与 PWM 峰值的相位差:如果触发源是定时器的另一个通道,两个通道之间的对齐误差可能有几个计数;
- 驱动芯片的传输延迟:光耦/隔离驱动的延迟不一致,会让实际开关时刻偏移。
这三项加起来,1µs 到 2µs 的偏移是常态。而允许值只有 2.6µs。
把抖动放大到 1.2µs 再算:
python
pwm_bad = PwmConfig(f_sw=16e3, t_dead=500e-9, t_settle=800e-9,
t_adc=300e-9, t_jitter=1.2e-6)
e = error_from_offset(pwm_bad.t_jitter, motor)
print(f"实际偏移 {pwm_bad.t_jitter*1e6:.1f}µs -> 电流误差 {e*1000:.1f} mA "
f"= {e/motor.i_rated:.1%} of rated")
txt
实际偏移 1.2µs -> 电流误差 115.2 mA = 0.5% of rated
0.5%,不是 6%。
所以 6% 的误差不是采样时刻能解释的。 我一开始算到这里就卡住了,以为是模型不对。
六、真正的原因:占空比接近 1 时窗口消失,采样点落到了"错误的相"
回到那个表格------它在 D=0.95 时还有 1525ns 窗口,但这个窗口是"理论窗口",不是"两相都有效"的窗口。
低边采样的前提是两相下桥臂同时导通 ,才能在同一时刻采到两个相电流(第三相由 Ia+Ib+Ic=0 推出)。而当 D_max=0.95 时:
- 最大占空比那相,下桥臂只开 5%,约 1525ns;
- 另外两相占空比是
(1 - 0.95)/2附近吗?不是。 三相占空比由电压矢量决定,最大相和最小相的差不一定是均分的。
在过调制区 (调制比 > 0.95),三相占空比的分布会严重不均,导致某一相的下桥臂根本不开。这时候采样窗口虽然在,但少了一相的数据。
而低速大转矩恰恰对应高调制比------这就是为什么"低速时误差大、高速时反而好转"。
修法有两个:
- 限制最大调制比,留出采样窗口(代价是母线电压利用率下降);
- 改用三电阻采样或相电流重构,在窗口不足时用算法推。
我们最后选了方案 1 + 在过调制区切到"两相采样 + 第三相推算",误差从 6% 降到 1.2%。
七、踩坑记录
- 工具算出来的"OK"骗了我两天。 我的工具只检查"窗口够不够长",没检查"窗口对应的相够不够"。一个校核工具如果只覆盖一半的约束,它给的"通过"比没有更危险。
- 1µs 的直觉是错的。 我一开始以为"1µs 在 16kHz 的 62.5µs 周期里只占 1.6%,能有多大影响"。实际上
di/dt是 96A/ms,1µs 就是 96mA。时间尺度要和物理量纲一起看,不能只看相对比例。 - 空载准、加载不准,最容易误判成"PI 没调好"。 空载时电流小、调制比低、窗口充足;加载后调制比升高、窗口收窄。症状随负载变化时,先怀疑测量,再怀疑控制。
- 钳形表不能当基准。 它测的是有效值,还有带宽和相位误差。后来我们用了一台带宽 1MHz 的电流探头 + 示波器积分,才拿到可信的瞬时值。
八、小结
- 低边采样的窗口是
(1-D)/f_sw,要扣掉死区、建立时间、ADC 采样时间; - 电流误差
ΔI = (V_bus/L) × Δt,母线电压越高越敏感; - 允许偏移
Δt ≤ (允许误差 × I_rated) / di/dt,这个值通常是 1~3µs; - 实际触发抖动(含 PWM 更新延迟、驱动延迟)常有 1µs 以上,必须计入;
- 窗口"长度"够不等于"相"够------过调制区少一相,这是低速大转矩误差的常见根因;
- 工具要覆盖全部约束,半套约束的"通过"比没有更危险。
文中的数字来自 48V / 0.5mH 平台,换平台请重算,别照搬。这个工具的完整版我还加了"过调制区相缺失检测"和"温漂补偿",但核心就是上面这些公式------先把约束列全,再写代码。
参考
- 中心对齐 PWM 与低边电流采样时序的通用做法(各 MCU 厂商电机控制应用笔记)
- 三相逆变器过调制区的电流重构方法(相电流重构 / 单电阻采样)