本站为个人非经营性网站,仅用于技术学习与分享,不提供任何商品、服务、报价或收费咨询。 合规信息

三相变频驱动:
从正弦表到欠压降速

这是我第一次自己做变频驱动:220 V 交流输入整流成母线,三相逆变桥带动一台泵类负载, 主控是一颗 Cortex-M0,不跑 RTOS。我在这块板子上把正弦表查表、相位偏移、分数分频、 V/f 低频补偿、位置式 PI 电流环和“欠压降速”一样一样试了过来。 这篇记录写的是每一件事为什么这么做,以及我在这个工程里踩到的几个坑——包括一个 光看文件夹名永远发现不了的坑。

Cortex-M0 SPWM V/f 控制 位置式 PI 欠压降速 已发布
已发布 · 个人学习项目

三相异步电机变频驱动与欠压降速控制(Cortex-M0 / 无 RTOS)

做这块板子的起因很简单:我想弄明白“变频器到底是怎么把 50 Hz 的市电变成任意频率的三相电”, 顺便解决一个真实的小需求——加注计量设备上的泵在低频启动时容易转矩不足、在母线电压被拉低时又容易停机。 于是我用一颗 Cortex-M0 画了一块驱动板:主循环是一个状态调度,实时性交给三个定时器中断, 用一张正弦表加相位偏移生成三相互补 PWM,用位置式 PI 做电流闭环, 再把“欠压”和“高温”做成两级降速策略。结论是功能都实现了、泵能按预期调频调压, 但这套代码里有几处我当时没意识到的问题,其中“目录名与实际功能不符”这一条, 让我后来养成了先核算、再相信名字的习惯。

PLATFORMCortex-M0,48 MHz
STACK裸机 + 高级定时器互补 PWM + 位置式 PI
TOOLSCubeMX + Keil MDK + 示波器
STATUS个人学习项目 · 功能已实现

一、这块板要驱动什么

被驱动的对象是一台三相异步电机,它带着一台泵,负载属于典型的“平方转矩”类型: 转速降一半,轴功率大约只剩八分之一。这类负载对变频器其实相当友好, 因为它不需要低速大转矩,但有两个要求是硬的:

  • 频率和电压都要能连续调:泵的流量靠转速调,转速靠频率调;频率一降,电压也得跟着降, 否则电机磁通饱和、电流白白发热。
  • 电流不能失控:泵在管路背压大的时候会憋住,这时候频率指令再往上加, 电流会直接顶到堵转电流。板子必须有闭环把电流拉回来。

供电是 220 V 交流输入,先整流成直流母线,再由三相逆变桥输出。 这也决定了后面所有“电压”相关逻辑的量纲:代码里看到的电压数值, 说的是整流后的母线电压,不是电机的相电压,更不是输入的交流有效值。 这一点后来坑了我一次,细节写在第六节。

不跑 RTOS,实时性交给三个中断

这颗 Cortex-M0 跑在 48 MHz,没有 FPU,RAM 也不宽裕,我一开始就没打算上 RTOS。 整个固件的时间结构是这样的:

时间基准承担什么为什么放在这里
高级定时器 TIM1 的周期中断 查表刷新三相占空比、频率斜坡推进 它和 PWM 本身同一个定时器,节拍天然与载波同步,抖动最小
一个慢节拍定时器 各种计时:故障确认、降速允许时间、指示灯 这些量都是百毫秒级的,放在快中断里纯属浪费
另一个定时器中断 ADC 采集与电流环计算 电流环要求等间隔执行,周期必须稳定
主循环 一个 switch 状态调度:初始化 → 等待 → 运行 → 故障 → 参数设置 没有复杂任务依赖,一条直线排下去就够,多一层调度器反而是负担

这种“硬节拍 + 主循环软处理”的写法我在 《不用 RTOS 的固件怎么写:时间片轮询 + 消息队列》 里单独整理过。这里只强调一点:中断里做的事情越少,控制周期的抖动就越小, 而电流环最怕的就是周期抖动。可惜我在这块板子上没有完全做到,见第八节。

主回路由整流、母线电容、三相逆变桥、电机组成,控制部分标出电流采样、PWM 驱动、反馈回路
图 1 · 驱动主回路与控制框图

二、三相波形怎么产生:正弦表、相位偏移与分数分频

三相 SPWM 的核心问题是:怎么用一颗没有 FPU 的 M0,在每个 PWM 周期里算出三个相位相差 120° 的正弦值,还要能随时改频率。我的答案是三件东西:一张常量正弦表、 一个索引偏移、一个分数分频。

为什么是查表,不是算 sin()

在 48 MHz 的 M0 上调用一次浮点 sin(),开销是几百个周期起步, 而 PWM 周期中断每隔几十微秒就要进来一次。三个相位各算一次, 光是三角函数就能把中断占满,还不算浮点库带来的代码体积和周期抖动。 查表就完全不一样:一次数组下标访问,时间是常数, 而且占空比的后续运算全部可以用整数乘除完成。

表本身是“一整个电周期等分成 N 份”的正弦值常量数组,N 取多少属于工程参数, 本文不列出具体点数。这里只说方法层面我确认过的三件事:

  1. 三相不是三张表。三相相差 120°,在表里就是索引相差表长的三分之一。 所以我只有一张表,取三次下标、加一个偏移就得到三个相位。
  2. 变频靠“份数”而不是“改表”。当前频率决定一个电周期里要输出多少个 PWM 周期, 这个总份数再折算成表索引的步进量。频率低的时候一个周期分成很多份、索引走得慢; 频率高的时候份数少、索引走得快。这就是所谓的分数分频: 步进量不是整数,靠一个累加量取整来逼近。
  3. 调压靠一个乘系数。表里存的是归一化的正弦形状, 实际占空比 = 表值 × 当前的电压系数 ÷ 归一化基数。电压系数变了,波形形状不变、幅值变。
/* 伪代码骨架:只保留控制流形状,表长、系数、归一化基数全部用占位符,不含真实取值 */
u16 idx_u = phase;                                      /* 当前相的表索引 */
u16 idx_v = (phase + TABLE_LEN / 3) % TABLE_LEN;         /* 滞后 120° */
u16 idx_w = (phase + TABLE_LEN * 2 / 3) % TABLE_LEN;     /* 滞后 240° */

duty_u = (u32)sine_tab[idx_u] * volt_coef / NORM_BASE;   /* 先乘后除,减少整数截断 */
duty_v = (u32)sine_tab[idx_v] * volt_coef / NORM_BASE;
duty_w = (u32)sine_tab[idx_w] * volt_coef / NORM_BASE;

pwm_set_duty(PHASE_U, duty_u);   /* 三相占空比写进三个比较寄存器 */
pwm_set_duty(PHASE_V, duty_v);   /* 每相的下桥由定时器硬件按互补方式生成, */
pwm_set_duty(PHASE_W, duty_w);   /* 不需要软件去翻转两个 IO */

下面这张表是我对这三件东西的对照理解,也是我后来给别人讲这段代码时的顺序:

要素做法解决的是哪个问题
正弦表 把一个电周期的正弦形状固化成常量数组,取值归一化 把三角函数从“每个中断算一次”变成“查一次表”,时间可预期
索引偏移 三相共用一张表,第二、第三相分别偏移表长的 1/3、2/3 省掉两张表,也保证三相严格相差 120°,不会因为三份代码写得不一致而失真
分数分频 用当前的频率换算出“一个电周期分多少份”,再由份数求索引步进 让频率连续可调,而不是只能输出表长的整数分频那几个频点
电压系数 占空比 = 表值 × 电压系数 ÷ 归一化基数 把“调频”和“调压”解耦:改频率不动幅值,改电压不动频率

互补输出与上电状态

三路半桥的六路门极由高级定时器的互补通道驱动。我的工程里能直接确认的两组是 U 相上桥 PA8 / 下桥 PA7、V 相上桥 PA9 / 下桥 PB0, 第三相用同一个高级定时器的其余互补通道。互补输出的意义在于: 上下桥的开关由硬件保证互斥,软件只需要管一个占空比, 不用担心自己写出“上下桥同时导通”这种直接炸管的组合。

上电时我给三相一个相同的、居中的固定占空比,让电机在启动前处于“三相电压对称”的状态。 这一点看起来很琐碎,但如果上电时三个比较寄存器的初值不一致, 电机会在还没收到启动指令时就受到一个直流分量,表现为轻微抖动甚至哼声。 这个经验是我在示波器上看三相波形时发现的。

三、变频与调压:V/f 补偿为什么必须做

异步电机的转矩来自气隙磁通,而磁通近似正比于“定子电压 ÷ 频率”。 所以变频器最基本的控制律就是 V/f = 常数:频率降一半,电压也降一半, 磁通保持不变,转矩能力就不变。

但这条直线在低频段会失效。原因不复杂:定子绕组有电阻, 低频时反电动势很小,定子电阻上的压降在总电压里的占比变得不可忽略, 真正用来建立磁通的电压被吃掉了;同时低频时漏抗变小、励磁电流增大。 结果就是:按纯 V/f 直线走,电机在低频段转矩不足、启动困难、电流反而偏大。

我的对策是在 V/f 直线上叠加一个低频抬压量:频率越低,抬得越多; 随着频率升高,抬压量线性衰减,到某个频率以上就完全回到纯 V/f 直线。 抬多少、从哪个频率开始衰减,是按实测定的—— 我在测试台上从最低频往上慢慢加,一边看电流一边看泵能不能带起来, 取一个“带得动、电流又不过分”的点。这个量本质上是 用电流换转矩,抬过头电机会发热,所以我不建议照抄任何人的经验值。

另一端还有一个容易被忽略的问题:母线电压偏高时输出会过压。 调制比是“输出相电压 ÷ 母线电压”,母线一高,同样的占空比对应的实际输出电压就上去了。 我的做法是让占空比上限随母线电压反比压低:母线越高,允许的最大占空比越小。 这样即使输入电压在宽范围内变化,输出到电机的电压也不会跟着一起飘。

机制解决什么问题我的取值依据
基准 V/f 直线 让磁通在额定频率以下基本恒定 按电机的额定电压与额定频率确定,属于电机参数
低频抬压 补偿定子电阻压降,让低频段带得动负载 按实测定:从最低频往上加,看到电流与带载能力都合适为止
母线电压反比限幅 输入电压偏高时不让输出过压 按母线电压范围定,上限曲线由输入电压的允许区间反推

四、电流环:位置式 PI 与抗积分饱和

电流环是这个工程里唯一一个“闭环”,也是我花了最多时间才想明白的一段。 它要解决的问题是:频率指令是由上层给的(比如泵需要多大流量), 但真正决定电机能不能承受的是电流。所以中间必须有一层: 拿额定电流当目标,拿实测电流当反馈,用 PI 算出一个“当前允许的频率”, 再把这个频率当上限去限制频率指令。

为什么用位置式,而不是增量式

位置式 PI 的输出就是控制量本身(在这里是目标频率), 每一项都由当前的偏差和累积的积分项重新算出来;增量式输出的是“这一次比上一次加多少”。 我选位置式的理由很实际:

  • 输出直接是频率量纲,后面接限幅和归一化都很自然,不用再维护一个累加器;
  • 停机、切换工作模式的时候,可以直接把输出置 0 或置成某个固定值,不用担心“上一次的增量还留着”;
  • 缺点是积分项会一直累积,必须自己做抗积分饱和——这也是这段代码里最值得写下来的部分。

抗积分饱和:夹两次

积分饱和是这样发生的:只要偏差一直存在(比如电机堵转,实测电流始终到不了目标), 积分项就会一路累加。等偏差终于反号的时候,积分项已经大得离谱, 输出要过很久才能退回来——表现就是“反应迟钝、超调、来回振荡”。

我用了两段限幅:先把积分项本身夹在一个上下限之间,再对最终输出夹一次。 第一段限制“记忆”能有多深,第二段限制“输出”能有多大。两段都必要,缺一段都还会留一个饱和的入口。

/* 伪代码骨架:位置式 PI 的形状,系数与上下限全部是占位符,不含真实取值 */
err       = current_set - current_measured;    /* 偏差 = 额定电流 - 实测电流 */
integral += err;                               /* 位置式:积分项持续累积 */

if (integral < INTEGRAL_MIN) integral = INTEGRAL_MIN;   /* 抗积分饱和第一段:夹积分项 */
if (integral > INTEGRAL_MAX) integral = INTEGRAL_MAX;

out = KP * err + (KI * integral) / NORM_BASE;           /* 比例项 + 积分项 */
if (out < OUT_MIN) out = OUT_MIN;                       /* 抗积分饱和第二段:夹输出 */
if (out > OUT_MAX) out = OUT_MAX;

target_freq = out * MAX_FREQ / current_set;             /* 归一化到频率量纲 */
if (target_freq > freq_limit) target_freq = freq_limit; /* 上层给出的频率上限 */
if (target_freq < MIN_RUN_FREQ) target_freq = MIN_RUN_FREQ;
if (freq_limit == 0) target_freq = 0;                   /* 上限为 0 就是要停机 */

这段骨架里我特意保留了三个“下限”的存在,因为它们各自防的是不同的东西: INTEGRAL_MIN 防积分项往负方向跑飞, OUT_MIN 防输出出现无意义的负频率, MIN_RUN_FREQ 则是工艺下限—— 频率低到一定程度泵就带不动了,与其让它半死不活地转,不如给一个明确的最低运行频率。 至于这三个下限具体取多少,全部按电机与泵的实测特性定,本文不给数值。

五、加减速自适应:把冲击电流压下去

频率指令不会一步跳到目标值。原因很直接:电机是一个惯性系统, 频率突变相当于给转子一个阶跃,转差瞬间变大,电流会冲到一个很高的值, 轻则过流保护动作,重则把逆变桥的余量吃光。

我用的方法是频率斜坡:在每个 PWM 周期中断里,让“当前频率”朝“目标频率” 走一小步,升频和降频分开处理,步长由斜坡时间换算而来。 真正有意思的是这个斜坡时间不是常数,而是随电流偏差自适应的:

  1. 算出“实测电流与额定电流的偏差”,并把它折算成一个正数(只看偏差大小,不看方向);
  2. 偏差越大,说明离目标越远,斜坡给得越慢;偏差越小,斜坡可以给快一点;
  3. 于是加速过程是这样的:起步慢(偏差大)→ 中间快(偏差小)→ 接近目标时再收敛。

这一步的收益在示波器上能直接看到:没有斜坡的时候,升频瞬间的直流母线电流有一个很尖的峰; 加上斜坡之后,同样的负载下这个峰明显变矮、变宽。斜坡时间的具体区间按实测定, 我的经验是“能压住冲击电流”和“响应别太肉”这两头都要试, 太慢的话负载需求上来时频率跟不上,泵会先掉速再补上,感觉更差。

环节做法不做会怎样
频率斜坡 当前频率按步长逼近目标频率,升频降频分别处理 升频瞬间电流冲击,容易触发过流保护;降频太猛会出现母线泵升电压
自适应斜坡时间 斜坡时间随电流偏差变化,偏差大就慢、偏差小就快 固定斜坡时间只能在某一个负载点上合适,换个工况不是太慢就是太冲
堵转保护计数 对“电流持续顶在额定值附近”的状态计数,超时判定为憋住并停机 泵憋住时电流会一直顶在保护边缘,长时间运行就是发热和损耗

顺带说一句堵转保护:它的判据不是“电流超了”,而是“电流长时间贴着上限下不来”。 因为堵转那一瞬间电流确实会超,但很快会被电流环压回去; 真正要防的是“压回去以后还是下不来”这个持续状态。 这个“看持续时间而不是看瞬时值”的思路,和后面欠压那节的计时器是同一套逻辑。

六、欠压降速:区分“软启动被拉低”与“母线真的撑不住”

这一节是这块板子上我认为最值得写的东西。所谓欠压降速,就是 母线电压低了,主动把输出频率降下来,减小功率需求,让母线电压回升。 听起来很简单,但我在调试时遇到了一个把人绕进去的问题:同样是母线电压低,原因完全可能不同。

两种完全不同的“电压被拉低”

工况现象我的判据对策
情况一:母线真的撑不住 长时间运行后电压缓慢下滑,越跑越低 电压低于降速点,并且当前频率已经达到了设定频率(说明系统已经进入稳态) 按线性降速曲线限制最高频率:电压越低,允许的最高频率越低,并保留一个地板频率
情况二:软启动阶段被拉低 刚上电、电机一起转,电压瞬间塌一下,随后回升 电压低于降速点,但尚未脱离上电初始阶段,且不属于情况一 只把上限压到“当前实际频率”(只降不升),等电压回升再自然放开,不套用降速曲线

为什么必须分开?因为我第一版是“一欠压就套降速曲线”,结果每次上电都被坑: 电机刚起转、母线电容还在充电、软启动电流又大,电压瞬间被拉低一下, 程序立刻按降速曲线把频率压到一个很低的点;等电压回升了,频率却要从很低的地方慢慢爬回去, 表现为“每次开机都要磨蹭很久才上得来”。而真正的母线耗尽完全不是这个形状—— 它是缓慢的、持续的、不会自己回升的。

区分这两个工况,我用的是一个“是否已经脱离上电初始阶段”的标记加上 “当前频率是否已达到设定频率”的组合判断。第一个条件把“上电那一小段时间”圈出来, 第二个条件保证情况一只在稳态下才成立。两个条件都不涉及具体电压数值, 所以换一个电源系统也不用重新推。

降速曲线:门槛 + 斜率 + 地板

情况一用的是一条线性曲线,形状是“某个电压门槛以上不限制,门槛以下每低一点、允许频率就低一点, 但有一个地板频率”。地板的存在很重要:降到零就等于停机, 而欠压降速的目的恰恰是“别停、先保命运行”。同时我用的是跟踪到的最低电压 而不是瞬时电压去查这条曲线,这样曲线只会往下走、不会因为电压抖动来回弹, 避免频率跟着噪声上下摆。

生效方式上,我把欠压降速的优先级放在电流环之上:只要欠压状态非零, 就用降速曲线算出的上限直接覆盖 PI 的输出。理由是这两个环的目标本来就冲突—— 电流环想“把频率顶上去把电流压下来”,欠压逻辑想“把频率降下来把电压抬起来”, 同时生效会互相打架。让保护类的逻辑优先,是我在这块板子上定下的规矩。

横轴为母线电压,纵轴为允许的最高运行频率,曲线随电压下降而下降,末端是一条水平的地板频率线
图 2 · 欠压降速曲线图

七、高温降速与分级允许运行时间

欠压之外还有一条降速来源:温度。功率器件和电机的温度上来了, 继续满频运行只会越来越热,所以需要“温度越高、允许的频率越低”。 我在这块板子上把温度和欠压做成了同一套结构:分档 + 限频 + 允许时间。

用历史最高温做判据

温度判据我用的是采样以来的历史最高温,而不是当前瞬时温度。 原因是温度有惯性:负载一轻,散热器温度会立刻开始回落, 如果按瞬时值判断,就会出现“降速 → 温度掉一点 → 恢复满频 → 温度又上去”的来回振荡。 用历史最高温,降速只会越降越多,直到温度真正稳定下来。代价是恢复慢, 需要有一个明确的复位条件(我放在停机/卸载的时刻)。

分档降速与“允许运行时间”

温度每升高一档就降一档频率,超过某一档之后直接把最高频率钉在更低的水平上; 再往上则按温区给出不同的允许维持时间:温度越高,允许维持的时间越短, 到点后放开到一个固定的低频保命运行。每一档的起点、档距、允许时间都按实测定, 本文不列出这些数值。

欠压这一侧也有类似的“允许运行时间阶梯”,但它分的是电压等级: 把母线电压分成若干级,每一级给一个允许运行时长,电压越低,允许的时间越短; 任何一级超时,就强制把频率降到地板频率保命运行。 计时本身由一个慢节拍定时器累加,不在快中断里做。

降速来源分档依据到点动作复位条件
高温 历史最高温,按温区划分档位,温度越高允许维持时间越短 超时后放开到一个固定的低频运行点 停机或负载卸载后清除历史最高温
欠压 母线电压等级,每一级一个允许运行时长,电压越低时间越短 任一级超时,强制降到地板频率 泵停止工作(阀门全关)时清零全部计时器与欠压状态

为什么要做“允许时间”而不是一超限就停机?因为这是产线上的设备, 一次误停机的代价可能比多跑两分钟高得多。分级允许时间是一个折中: 短暂的低电压或高温,允许设备继续把当前这一罐料处理完; 但如果迟迟不恢复,就必须牺牲产量换设备安全。 这句话说起来很顺,真正难的是每一级到底给多长时间—— 这个只能按母线的放电特性、泵的允许工况和自己的风险偏好来定。

复位条件是我在调试后期才补上的。最早的版本里,计时器和降速状态一旦置起来就只等超时, 结果出现了一个很隐蔽的现象:上一轮的欠压记忆会带到下一次工作循环里, 明明这次电压正常,频率上限却已经被压住了。现在的做法是泵一停(阀门全关), 所有计时器、欠压状态、跟踪到的最低电压和算出来的频率上限全部清零。

八、采样与滤波:为什么要去极值

这块板子一共采四路模拟量:母线电压、两路温度(散热器与电机)、直流母线电流。 采集放在一个定时器中断里,每次采一轮,连续采若干轮之后做滤波。 滤波用的是最朴素也最有效的办法:排序后去掉最大和最小,对中间几点求平均。

为什么不去掉最大的,或者干脆取平均

  • 直接取平均:逆变桥的开关动作会在采样线上耦合出尖峰。 哪怕只有一轮采样撞上尖峰,均值也会被明显拉偏,而这个偏差会直接进电流环。
  • 只丢最大值:噪声是双向的,也会有偶发的偏低值(比如转换被中断打断)。 只丢一头,另一头照样能穿过去。
  • 去极值平均:丢掉两端各若干个,既保留了平均的平滑性, 又对脉冲型干扰有抵抗力。代价是参与平均的点变少,平滑程度略降。
/* 去极值平均的通用写法(教科书级算法,与任何具体产品判据无关)
   n = 一个通道连续采样的轮数,drop = 每一端丢掉的个数,两个值都按需要的
   滤波深度与响应速度现场定,因此这里只作为参数传入,不写死任何数字 */
static u16 trim_mean(u16 *buf, u8 n, u8 drop)
{
    u16 t; u8 i, j; u32 sum = 0;

    for (i = 0; i < n - 1; i++) {                 /* 点数不多,冒泡排序的开销可以接受 */
        for (j = 0; j < n - 1 - i; j++) {
            if (buf[j] > buf[j + 1]) {
                t = buf[j]; buf[j] = buf[j + 1]; buf[j + 1] = t;
            }
        }
    }
    for (i = drop; i < n - drop; i++) sum += buf[i];   /* 只累加中间的一段 */
    return (u16)(sum / (n - 2 * drop));
}

采样轮数与电流环的带宽是一对矛盾:轮数越多,滤波越平,但这个通道的更新就越慢。 电流环拿到的是一份“迟到”的测量值,环路稳定性会变差。 我在调试时把轮数改大过,波形确实更干净,但泵在负载突变时会有一下明显的顿挫, 最后还是把轮数调回了一个折中值。这个折中值按实测的噪声水平和环路响应定, 没有一个可以套用的数字。

电流换算链:这是一条标定链

直流母线电流不是直接测出来的,而是经过一条换算链估出来的。这条链的每一步都值得写清楚, 但每一步的系数都属于标定参数,本文只讲方法:

  1. ADC 码值 → 引脚电压:按 ADC 位数与参考电压换算,得到采样点上的电压。
  2. 引脚电压 → 传感器输出电流:电流经传感器/变压器转换后落到采样电阻上, 这一步的系数是在实验台上打点拟合出来的线性关系,不是算出来的。
  3. 由频率折算电机电压:既然输出是 V/f 控制,那么当前频率就对应一个电机端电压, 可以按额定点比例折算。
  4. 按功率守恒估算电机电流:母线侧功率除以电机电压,得到电机电流的估计值。
  5. 加一项损耗修正:逆变桥和线路上有损耗,用一个小修正项补偿, 让估计值在高频段更接近实测。
  6. 小信号截零:低于某个码值时直接当 0,避免把噪声放大成假的电流读数。

九、参数存储与三重授权校验

这块板子的参数(设定频率、额定电流、最大频率、堵转超时等)需要掉电保存, 但硬件上没有外挂 EEPROM,所以我把片内 Flash 的最后一页当 EEPROM 用。 参数区的起始地址由分区方案决定,与代码区严格分开,具体地址属于工程里的分区设定, 本文不列出。

写一页参数的完整流程

  1. 判断目标地址当前是不是空白(读出全 1)——是空白就说明这是首次上电,需要写入出厂默认值;
  2. 解锁 Flash 控制器;
  3. 关闭中断:整页擦除期间如果被中断打断去取指令,会拿到非法数据;
  4. 整页擦除;
  5. 按字把参数逐个编程写入;
  6. 重新上锁,恢复中断。

这套流程本身没什么特别的,值得写的是我在批量写函数上犯的一个边界错误: 循环条件写成了闭区间,于是“写 N 个参数”实际会写 N+1 个。

/* 错误示范:批量写入的循环边界写错,会多写一个元素 —— 这是错误写法,不要照抄 */
static void flash_write_multi(u32 addr, const u32 *buf, u32 size)
{
    u32 i;
    for (i = 0; i <= size; i++) {                 /* ← 问题在这里:用了闭区间 */
        flash_program_word(addr + i * 4, buf[i]);
    }
}
/* 正确写法是把条件改成 i < size,或者由调用方把 size 减一;
   但更稳妥的是让函数自己校验边界,不要依赖调用者的习惯。 */

这个 bug 的可怕之处在于它不会报错。多写的那一个元素落在参数区的下一个字段上, 程序照常运行,直到某一天你读到那个字段,发现它莫名其妙变成了别的值。 我的做法是给批量写函数加了两道保障:入口先检查 addr 与 size 是否落在参数区范围内, 结束时再回读校验一次。多花几十微秒,换来的是“写坏了能立刻知道”。

三重授权校验:只讲机制

这块板子最终要装到设备上使用,所以固件里做了一层授权校验,由三个互相独立的判据组成, 三者全部通过才算授权有效:

校验项数据来源通过条件失败后果
管脚电压判据 某个专用引脚上的 ADC 读数 读数落在预先标定的区间内(区间按实测定,本文不给数值) 授权标志不置位
自定义口令比对 参数区里保存的一段口令值 与固件内的期望值一致(口令本身不写进本文) 授权标志不置位
芯片唯一 ID 比对 MCU 出厂固化的 96 位唯一 ID 与参数区里预先写入的三段副本 三段逐段一致 授权标志不置位

三段比对的意义在于:授权是绑在这一颗芯片上的, 把固件拷到另一块板子上,第三项就过不了。前面的管脚电压与口令是“防随手拷贝”的软门槛, 最后这一项才是真正的绑定。这套机制的完整讨论, 包括它挡不住什么、以及产线换板时有多麻烦, 我在《用 MCU 唯一 ID 做程序绑定:能防什么、防不了什么》里写过。

校验不通过时:停机,而不是死循环

这是我在这块板子上做的一个明确取舍。校验不通过时,我把目标频率置 0—— 电机不转,但程序继续跑:通信还活着、状态还能读、故障码还能显示, 产线或售后有重新写入授权数据的机会。

/* 伪代码骨架:授权校验失败时的处置,只保留控制流形状,不含任何真实判据 */
if (!authorized) {
    target_freq = 0;        /* 明确停机:设备不转,但通信与状态显示仍然可用 */
}

/* 我不采用的写法(反例,只作为对照写在注释里):
   while (!authorized) { }   ← 原地等待会让看门狗、通信和诊断一起失效 */

为什么“停机”比“死循环”好?我在 那篇笔记的第三节里对比过三种处理方式, 结论很清楚:死循环看起来最“安全”(什么功能都不启动),但它的代价是 现场完全无法判断发生了什么——屏幕不亮、通信不应答, 和“程序烧坏了”“电源坏了”表现一模一样; 更糟的是如果校验代码跑在通信初始化之前,想通过总线把授权数据重新写进去都没有机会, 只能拆机用编程器处理。而“置 0 停机”虽然也是不能用,但设备是“救得回来”的。

十、问题与解决:现象、原因、办法

这一节把我在这个工程里实际处理过的问题逐条列出来。前六条是控制逻辑上的, 后四条更多是“读代码和写代码的习惯”问题——但后面这几条给我的教训反而更深。

现象原因办法
每次上电,电机刚起转就被判定欠压,频率被压得很低,很久才爬上来 把“上电软启动阶段母线被拉低”和“长期运行后母线真的撑不住”当成了同一种工况,都套了降速曲线 用一个“是否已脱离上电初始阶段”的标记把两种工况分开;软启动阶段只按当前实际频率限幅,不套曲线
低频段电机转矩不足、启动困难,电流反而偏大 定子电阻压降在低频段占比变大,纯 V/f 直线建立不起足够磁通 在 V/f 直线上叠加低频抬压量,随频率升高线性衰减到零;抬压量按实测电流与带载能力定
输入电压偏高时,输出到电机的电压跟着一起高 调制比固定时,母线电压直接决定输出电压 让占空比上限随母线电压反比压低:母线越高,允许的最大占空比越小
升频瞬间母线电流有很尖的冲击峰 频率指令阶跃,转差瞬间变大 加频率斜坡,并让斜坡时间随电流偏差自适应:偏差大就慢,偏差小就快
泵被管路憋住时电流长时间贴着保护边缘 只看瞬时电流判定,堵转瞬间会被电流环压回去,但持续状态没人管 用“电流持续顶在额定值附近”的计数超时来判定憋住,而不是看瞬时值
电流读数偶发跳变,进电流环后引起频率抖动 逆变桥开关噪声耦合进采样线,偶发尖峰被均值滤波放大 连续多轮采样后排序去极值,只对中间一段求平均;采样轮数按噪声与环路响应的折中定
欠压逻辑怎么都找不到,怀疑功能没实现 目录名里标着一个很低的电压数值,与代码里实际的量纲(整流母线电压)完全不是一回事 不信名字,从电压换算链反推满量程与量纲;把核算结果写下来,避免自己再被骗一次
注释里写的载波频率,和按定时器参数实算出来的对不上 注释是在早期版本写的,后来改了定时器参数却没人改注释 注释只写“为什么”,具体数值一律从参数现场核算;定期用清单核对定时器换算
参数批量写入后,相邻字段偶发变成莫名其妙的值 批量写函数的循环条件用了闭区间,实际多写了一个元素 循环条件改成半开区间,函数入口校验地址与长度范围,写完回读校验一次
改一个 PI 系数要重新编译、下载、上电试 PI 系数在函数里直接赋值,没有放进参数区 计划把系数挪进参数区并加“仅停机可改”的限制(还没改完)

这十条里,我认为最值得反复讲的是第七条和第八条。它们的共同点是: 错误信息来自“我以为的事实”,而不是来自代码本身。 目录名和注释都是人写的,人写的东西会过期、会写错、会被复制粘贴带走; 而换算链和定时器参数是代码里真正生效的东西。 从那以后我给自己定了一条规矩:凡是涉及数值的判断,先算一遍再说。

十一、局限与还没做完的部分

这个工程的功能是实现了的,泵能调频、电流能被限制住、欠压和高温都会降速。 但下面这些我都清楚是欠的,只是还没动手:

问题现状我打算怎么改
中断里做 ADC 轮询 启动转换和等待完成都在定时器中断里,中断执行时间不可控 改成“中断只启动转换、主循环或完成回调取结果”,或者直接用 DMA 搬运
PI 系数不可在线整定 系数在函数里直接赋值,每次调用重写一遍 挪进参数区,和别的参数一起保存,加“仅停机可改”的限制
参数区没有备份 只有一份、只有一个起始地址,没有双区备份,也没有版本迁移 按《嵌入式参数存储设计:Flash 分区、双区备份与 CRC 校验》的思路做双区 + 版本号
硬件抽象层名存实亡 抽象层目录里有一批模块文件,其中两个文件里只有一行 #include,实际逻辑仍写在功能文件里 要么把这两个文件删掉(承认没有抽象),要么把对应逻辑真的挪进去;我倾向后者
电流是估算的 用母线电流、频率和功率守恒折算电机电流,没有真正测相电流 先做一轮对比实验:用钳形表在同一工况下逐点核对估算值与实测值,量化这条链的误差
欠压降速只有定性观察 我只在台架上看了几次波形,没有做长时间老化,也没有覆盖电源波动的边界 按可靠性测试那个实验里的做法,先定义“什么叫做稳定”,再设计能必然触发的用例
互补输出的死区没有系统测过 死区由定时器配置决定,我只在示波器上抽查了几处波形 把死区时间、母线电压、负载电流做成一张对照表,逐个工况记录上下桥波形
状态调度写在主循环的 switch 里 初始化 / 等待 / 运行 / 故障 / 参数设置五个状态平铺在一个函数里 抽成显式的状态机表,让每个状态的进入条件、退出条件、错误出口都能单独看

写“还没做完”比写“已经实现”要难,因为前者需要承认自己在哪些地方是含糊过去的。 但这块板子给我的最大收获恰好就在这些含糊的地方:功能跑通只需要几天, 把每个数值的来龙去脉弄清楚要久得多。上面这八条,我打算先从“中断里的 ADC 轮询”开始改, 因为它影响的是控制周期的确定性,这比参数存储的优雅程度重要得多。

参考资料与说明

  • Cortex-M0 主控芯片参考手册(高级定时器 TIM1 的互补输出、死区与重复计数器章节;ADC 章节), 用于确认互补 PWM 的配置方式与 ADC 转换时序。
  • Cortex-M0 内核技术参考手册(NVIC 与中断优先级章节),用于确认中断嵌套与优先级安排。
  • 三相异步电机 V/f 恒磁通控制与低频转矩补偿的通用公开资料,用于确认低频抬压的作用机理。
  • 三相电压型逆变桥与 SPWM 调制的通用教材资料,用于确认查表法与调制比的定义。
  • IPC 关于印制板设计的通用规范(Generic Standard on Printed Board Design) 中电气间隙与爬电距离的一般要求,用于规划强电区域的布局间距; 本文不展开 PCB 布局细节,仅作为公开参考列出。
  • STMicroelectronics STM32CubeMX 与 HAL 库公开文档,用于确认工程配置与外设初始化流程。
  • 文中涉及的芯片型号、工具与封装名称仅用于说明技术方案,与相关厂商无隶属或授权关系。
  • 本文不列出任何产品特定参数:电压/温度/频率阈值、PI 系数、斜坡时间、载波频率、 PWM 周期值、参数区地址与出厂默认值均已省略,只保留方法与结构。
  • 示例代码为按个人理解重写的最小片段,不代表任何产品或交付代码; 其中标注为“错误示范”的片段仅用于说明问题,请勿照抄。
  • 出于对个人项目实现细节的保护,本文只公开方法与思路; 涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。