简介:《AN5327-3相无传感器单分流电流感应PMSM电机控制》是一份面向汽车电机控制、嵌入式软件与电力电子方向读者的NXP应用笔记,重点解决低成本三相永磁同步电机在无位置传感器条件下的矢量控制驱动设计问题。内容从系统概念入手,依次讲解PMSM场定向控制基本原理、正交相位同步参考坐标系下的电机模型、输出电压作用与单分流电阻相电流测量,以及基于扩展反电动势观测器的转子位置和速度估算;随后落到MC9S12ZVML128的关键模块、设备初始化与软件架构,并说明FreeMASTER调试界面和MCAT工具在参数设置、运行监控与性能调校中的用法,涉及电机控制编程与方案验证的多个环节。资源包内含1个PDF文件,约2.58MB,章节组织清晰,便于按控制算法、软件实现和调校工具分块查阅。已有212人学习下载,适合作为汽车PMSM无传感器单分流控制方案的设计参考与调试对照材料。
1. 一块板子、六颗 MOS、一颗分流电阻
手上只有一块三相逆变板、六颗 MOS、一颗几毫欧的分流电阻,却要把 3 相无传感器 PMSM 转到额定转速——这是这份车规 MCU 参考设计最反直觉的地方:不测三相电流,只在直流母线负极串一颗电阻;不装编码器,靠扩展反电动势观测器把转子位置算出来。配套的 MCAT 调参工具把电机参数、电流环、观测器和速度环分成四个页签,改完参数直接下发到板子上运行。
它面向的是汽车电子里成本敏感、空间紧张、又要求宽转速范围的泵、风机、执行器类负载,也适合正准备从方波六步换相迁到正弦 FOC 的团队。手上有那块 S12ZVM 评估板,照着流程能跑起来;没板子,也能把它当单电阻采样和无感启动的实现范本读。
2. 系统方案拆解:单电阻采样加无感 FOC 的完整信号链路
这套方案的性能指标写在应用笔记的 System concept 一节里:三相 PMSM 的 FOC、无位置传感器、闭环速度控制、双向旋转、闭环电流控制、磁链与转矩解耦、扩展反电动势观测器估算位置与转速、对齐启动、单电阻重构三相电流、100 μs 采样周期,外加直流母线过压欠压、过流、过载、启动失败五类保护。把这些指标翻译成工程语言,就是一条从功率级到 ADC 再到中断服务程序的固定链路,环环相扣,任何一环时间不够,后面的算法全部白算。
先把链路摊开看:PWM 定时器产生中心对齐的三相占空比 → 逆变桥把母线电压斩成三个矢量 → 母线电流在特定开关组合下等于某一相电流 → 分流电阻把电流转成毫伏级压差 → 片内运放放大并抬升到 ADC 量程中点 → ADC 在窗口中央采样 → 中断里重构出 ia/ib/ic → Clarke/Park → 双 PI → 反 Park → SVPWM。下面按硬件、信号调理、时序预算三块拆。
2.1 功率级拓扑:只保留一颗分流电阻的取舍
传统做法是三电阻(每相下管源极各一颗)或两电阻,采样时刻自由、软件简单,代价是三路运放、三路 ADC、三块布板面积和对应的 BOM 成本。单电阻把采样点挪到直流母线负端与桥臂公共源极之间,整机只留一路调理电路。
代价是:任意时刻母线电流只反映“当前开关组合下导通的那一相”,必须在一个 PWM 周期里凑出两个不同的有效矢量,才能拿到两个相电流,第三个用基尔霍夫电流定律 ia+ib+ic=0 算出来。六个非零矢量和可测电流的对应关系如下(记上管导通为 1):
| 有效矢量 (A/B/C 上管) | 母线电流 | 可测相电流 |
|---|---|---|
| 100 | iA | +ia |
| 110 | -iC | -ic |
| 010 | iB | +ib |
| 011 | -iA | -ia |
| 001 | iC | +ic |
| 101 | -iB | -ib |
这张表就是后面所有采样代码的依据。注意符号:110 和 011、101 三个矢量测到的是相电流的相反数,重构时必须乘 -1,否则 Park 变换出来的 iq 符号会反,电机一给转矩指令就反转或直接过流。我在早期版本上就因为漏了这个负号,台架上电三秒就触发过流保护,查了半天才回到这张表。
单电阻方案的另一个硬约束是窗口宽度。采样必须在有效矢量作用期间进行,而且不能在开关沿上:要避开死区时间、避开 MOS 开关振铃、等运放建立、再留给 ADC 采样保持。常见做法是把最小采样窗口卡在 1.5~2 μs,低于这个值就认为该次采样无效。PWM 载波 16~20 kHz 时,一个周期 50~62 μs,除去死区和两段窗口,真正留给矢量作用的时间并不宽裕,这也是为什么低调制区(占空比接近 50%,两个有效矢量被零矢量挤到两边)是单电阻方案公认的难点。
2.2 信号调理与 ADC 量程换算
片内集成了两路运放,正好用于单电阻方案的一路差分放大。分流电阻上的压差是双极性的(电流有正负,对应电动和制动),所以调理电路除了放大,还要把零点抬到 ADC 量程中点:3.3 V 参考下偏置 1.65 V,电流为零时 ADC 读到 2048 附近。
量程要倒着算。假设最大相电流 ±20 A、分流电阻 5 mΩ,则压差 ±100 mV;取增益 10,输出 ±1 V;叠加 1.65 V 偏置后落在 0.65~2.65 V,安全地在 0~3.3 V 之内,还留了 0.65 V 的过流余量。分辨率也要算:12 位 ADC、3.3 V 参考,1 LSB ≈ 0.806 mV,折算回电流是 0.806 mV / (10 × 5 mΩ) ≈ 16 mA。对于额定十几安的电机,这个分辨率做电流环够用,但如果电机只有 1~2 A,就必须提高增益或换更大的分流电阻,否则量化噪声会把速度环抖得厉害。
换算代码通常写成下面这样,注意偏置值和增益都要做成可标定的常量,因为电阻容差和运放失调是真实存在的:
/* 单电阻电流换算:ADC 码值 -> 安培 */ #define SHUNT_MOHM 5.0f /* 分流电阻,单位毫欧,实测值 */ #define AMP_GAIN 10.0f /* 运放增益,由反馈电阻比决定 */ #define ADC_VREF 3.3f /* ADC 参考电压 */ #define ADC_FULL 4096.0f /* 12 位 */ #define ADC_MID 2048.0f /* 零电流偏置码值,上电后需重新标定 */ /* 码值转安培:先转电压,再减偏置,再除以 (增益 * 电阻) */ static inline float AdcToAmp(uint16_t raw) { float v = (float)raw * ADC_VREF / ADC_FULL; /* 引脚电压 */ float vshunt = (v - ADC_MID * ADC_VREF / ADC_FULL) / AMP_GAIN; return vshunt / (SHUNT_MOHM * 0.001f); /* 毫欧转欧 */ } /* 上电标定:逆变桥全关、电机不转时连续采样求平均,得到真实零点 */ uint16_t CalibOffset(void) { uint32_t sum = 0; for (int i = 0; i < 256; i++) sum += AdcReadBlocking(); return (uint16_t)(sum / 256); }AdcToAmp的每一步都能对上硬件:先还原引脚电压,减掉零电流时的偏置,再除以放大倍数得到分流电阻上的真实压差,最后除以阻值。CalibOffset这一步不要省,运放失调和电阻容差叠加起来,零点偏移几十个码值是常态,不标定的话低速时电流环会一直带着一个直流偏置,表现为电机静止时也有一股持续的 d 轴电流,发热且浪费。
2.3 100 μs 的时序预算怎么分
应用笔记给的采样周期是 100 μs,也就是控制环 10 kHz。PWM 载波可以和它同频(中心对齐、单次更新),也可以两倍频(双次更新,等效采样率翻倍但中断更密)。常见做法是 PWM 载波 16~20 kHz、控制环 100 μs 一拍,重载中断里只做与安全相关的比较和状态更新,FOC 主体放在 ADC 完成中断里。
一拍的算力大致这么分:
| 任务 | 执行位置 | 大致占比 |
|---|---|---|
| 采样结果读取、电流重构 | ADC 完成中断 | 15% |
| Clarke/Park、双 PI、反 Park | ADC 完成中断 | 35% |
| 反电动势观测器更新 | ADC 完成中断 | 20% |
| SVPWM 占空比计算与写入 | ADC 完成中断 | 15% |
| 状态机、保护判定、慢速任务 | 1 ms 定时中断 | 15% |
这个分配不是死的,但有一条原则:观测器和电流环必须在同一拍里算完,不能让观测器用上一拍的电流、电流环用这一拍的角度,否则角度滞后一拍,高速时等效于给 dq 轴之间加了一个随转速增大的耦合项,表现为转速越高电流越大、效率越差。这条在台架上不容易看出来,只有对比不同转速下的相电流有效值才会发现。
3. 控制算法主线:从 Clarke/Park 到扩展反电动势观测器
算法部分在这份应用笔记里占了最大篇幅:FOC 基本原理、dq 同步旋转坐标系下的电机模型、输出电压激励与相电流测量、转子位置与转速估算。落到代码上其实是四个函数:坐标变换、两个电流 PI、观测器、SVPWM。这一章按数据流顺序讲,每个环节都给出可以照抄的实现和参数说明。
3.1 dq 模型与电流环 PI 的极点配置整定
PMSM 在同步旋转坐标系下的电压方程是这套控制的地基:
- d 轴:ud = Rs·id + Ld·did/dt − ωe·Lq·iq
- q 轴:uq = Rs·iq + Lq·diq/dt + ωe·Ld·id + ωe·ψPM
两个方程结构完全一致,所以 d 轴和 q 轴可以用同一套 PI 结构,只是增益里的电感值不同。方程里的交叉项(−ωe·Lq·iq 和 +ωe·Ld·id)以及 q 轴的反电动势项 ωe·ψPM,属于“已知扰动”,做前馈补偿掉之后,被控对象退化成一阶惯性环节,PI 参数就能用极点配置直接算出来:Kp = 2ξω0L − Rs,Ki = ω0²L。ω0 是期望的闭环自然频率,ξ 是阻尼比。
/* ---------- 电流环 PI:极点配置整定 ---------- */ #define RS_OHM 0.35f /* 定子相电阻,毫欧计实测,别抄手册 */ #define LD_H 0.00042f /* d 轴电感,LCR 表测或 MCAT 辨识得到 */ #define LQ_H 0.00051f #define TS_S 0.0001f /* 控制周期 100 us */ #define W0_RAD 2513.0f /* 期望带宽 2*pi*400 Hz,一般取开关频率的 1/10~1/20 */ #define XI 0.707f /* 阻尼比,0.7 左右超调小、响应快 */ typedef struct { float kp; /* 比例增益 */ float ki; /* 积分增益 */ float integ; /* 积分累加器 */ float u_max, u_min; /* 输出限幅,单位与电压标幺一致 */ } PI_t; void PI_Init(PI_t *p, float L, float Rs) { p->kp = 2.0f * XI * W0_RAD * L - Rs; /* 比例项,注意 L 用对应轴 */ p->ki = W0_RAD * W0_RAD * L; /* 积分项 */ p->integ = 0.0f; p->u_max = 0.95f; /* 归一化到 Vdc/sqrt(3) */ p->u_min = -0.95f; } float PI_Run(PI_t *p, float err) { float u = p->kp * err + p->integ; /* 先限幅输出,再决定积分器是否继续累加,抑制 windup */ if (u > p->u_max) { u = p->u_max; } else if (u < p->u_min) { u = p->u_min; } else { p->integ += p->ki * err * TS_S; } return u; }PI_Init里的三个参数决定一切:Rs必须是实测值,手册给的典型值和实物差 30% 很常见,用错了 kp 直接偏;L用对应轴的电感,d 轴和 q 轴的 kp 不一样,这是很多人直接复制粘贴一个 PI 结构然后 dq 两轴共用一个增益导致 q 轴响应慢半拍的原因;W0_RAD决定带宽,取开关频率的 1/10 到 1/20 是通行经验,取太大电流环会跟着 PWM 噪声一起振。
PI_Run里的限幅顺序有讲究:先算输出并限幅,只有在没饱和时才累加积分,这是最基本的位置式抗饱和。更严谨的做法是反算抗饱和(把超出限幅的部分按 Ki 折算回去减掉),在 100 μs 这个节拍上两者差别不大,但如果你的母线电压低、调制比长期接近 1,建议上反算,否则退出饱和会有明显滞后。
3.2 单电阻相电流重构:最小窗口与扇区边界处理
重构逻辑的核心是时序,不是数学。每个 PWM 周期里,硬件在重载后按预设的两个时刻各采一次母线电流,软件根据当前扇区判断这两次采到的分别对应哪一相、什么符号,再用 ia+ib+ic=0 补齐第三相。
typedef struct { float ia, ib, ic; /* 重构出的三相电流 */ uint8_t valid; /* 0 = 本拍窗口不足,数据不可信 */ } Phase3_t; /* 扇区 -> 第一次采样对应的相与符号(依据 2.1 节的矢量表) */ static const int8_t kPhaseOfSample[6] = { 0, 2, 1, 0, 2, 1 }; /* A,C,B,A,C,B */ static const int8_t kSignOfSample [6] = { +1, -1, +1, -1, +1, -1 }; /* adc1/adc2 为两个采样时刻的母线电流,单位安培;sector 为当前扇区 0..5 */ Phase3_t Reconstruct(float adc1, float adc2, uint8_t sector, uint8_t win_ok) { Phase3_t out = {0}; if (!win_ok) { out.valid = 0; return out; } /* 窗口不足直接丢弃,由上层保持 */ float i1 = adc1 * kSignOfSample[sector]; float i2 = adc2 * kSignOfSample[(sector + 1) % 6]; float ph[3] = {0}; ph[kPhaseOfSample[sector]] = i1; ph[kPhaseOfSample[(sector + 1) % 6]] = i2; ph[3 - kPhaseOfSample[sector] - kPhaseOfSample[(sector + 1) % 6]] = -(i1 + i2); /* 基尔霍夫补第三相 */ out.ia = ph[0]; out.ib = ph[1]; out.ic = ph[2]; out.valid = 1; return out; }三个要点。第一,kPhaseOfSample和kSignOfSample这两张表必须对着自己硬件实际接法核一遍,不同的桥臂编号、不同的运放极性都会让表不一样,抄别人的表是最常见的翻车来源。第二,win_ok由 PWM 驱动在生成占空比时算出来——两个有效矢量作用时间都大于最小窗口才置 1,否则置 0。第三,窗口不足时不要硬用坏数据,业内常见的处理是“保持上一拍电流 + 观测器继续空跑”,或者用对称 PWM 的移相技巧强行撑出窗口,前者实现简单,低速大负载时电流波形会有一点畸变但不影响运行。
还有一处容易被忽视:两段采样在时间上相隔几微秒,这期间电流其实在变化,严格来说重构出来的三相电流不是同一时刻的值。转速高时这个误差会体现为 iq 上叠加一个与转速同频的波动,观测器会把它当成反电动势的一部分,导致估算角度有周期性抖动。缓解办法是把两次采样尽量安排在 PWM 周期的对称位置(中心对齐模式天然满足),或者在重构后加一个一阶低通,代价是相位滞后。
3.3 扩展反电动势观测器与三段式启动
无传感器部分用的是扩展反电动势观测器。思路是:把电流作为状态量建一个和真实电机同参数的电流模型,用实际采到的 iα/iβ 与模型输出的差去修正反电动势估计,收敛后从 eα/eβ 的反正切拿到电角度。离散化后就是一个积分加一个增益,代码量比想象中小:
typedef struct { float i_alpha, i_beta; /* 观测器内部电流状态 */ float e_alpha, e_beta; /* 反电动势估计 */ float theta; /* 估算电角度,rad */ float omega; /* 估算电角速度,rad/s */ float g; /* 观测器增益,决定收敛速度与噪声敏感度 */ } Obs_t; /* ua/ub: alpha-beta 电压指令;ia/ib: 实测电流;每 100 us 调一次 */ void Obs_Update(Obs_t *o, float ua, float ub, float ia, float ib, float Rs, float Ls) { float ea_err = ia - o->i_alpha; float eb_err = ib - o->i_beta; /* 电流模型:di/dt = (u - e - Rs*i) / Ls,欧拉离散 */ o->i_alpha += TS_S / Ls * (ua - o->e_alpha - Rs * o->i_alpha); o->i_beta += TS_S / Ls * (ub - o->e_beta - Rs * o->i_beta); /* 用电流误差积分修正反电动势,增益越大收敛越快、噪声越大 */ o->e_alpha += o->g * ea_err * TS_S; o->e_beta += o->g * eb_err * TS_S; /* 角度由反电动势矢量的正交分量得到,符号约定随 Clarke 变换定义而变 */ float theta_raw = atan2f(-o->e_alpha, o->e_beta); /* 一阶锁相环平滑,同时输出转速,避免直接对角度差分放大噪声 */ float err = theta_raw - o->theta; while (err > PI_F) err -= 2.0f * PI_F; while (err < -PI_F) err += 2.0f * PI_F; o->omega += 2000.0f * err * TS_S; /* 锁相环积分增益,需整定 */ o->theta += o->omega * TS_S + 1.2f * err; }g是这套观测器唯一需要现场调的参数,物理意义相当于龙伯格观测器的修正强度:太小则低速时反电动势幅度只有几十毫伏、信噪比差,角度跟不上;太大则把 PWM 噪声和电流采样噪声直接放大到角度上,表现为电机在中高速时有周期性“顿挫”。经验是先让电机在开环下转到额定转速的 10%~15%,再从小往大调g,直到估算角度与实测相电压过零点对齐。
观测器在零速和极低速下不可用,因为反电动势与转速成正比,转速为零时反电动势为零,无从估算。所以启动必须分三段:对齐、开环加速、切闭环。对齐段在 d 轴注入一个固定电流(幅值取额定电流的 30%~50%,持续 200~500 ms),把转子强行拉到已知角度;开环段以固定加速度斜坡给出角度和电压,让电机转起来;当估算转速超过设定阈值(通常是额定转速的 5%~10%)且观测器角度与开环角度误差小于 20° 并持续若干拍,再切闭环。
切换那一拍是最危险的时刻。角度突变会引起电流突变,常见做法是在切换前几十毫秒就开始把观测器角度往开环角度上靠(加权混合),切过去之后再逐步撤掉开环分量。直接硬切的后果是电流冲击触发过流保护,或者干脆反转一下再转回来。
4. 软件架构:S12ZVM 上的中断时序、状态机与外设初始化
这套参考设计的软件架构是典型的“前后台 + 状态机”:中断里跑时间关键的控制算法,主循环里跑状态机、通信和慢速任务,两者通过一组全局结构体交换数据。它跑在 MC9S12ZVML128 上,用到的关键模块包括 PWM 发生器、ADC、定时器、片内运放和看门狗,这些外设的配置顺序直接决定了采样能不能落在正确的时间点上。
4.1 三个中断的时序关系
整个系统的时间基准来自 PWM 重载中断。中心对齐模式下,计数器到周期值时重载,这一时刻下发新的占空比,同时触发 ADC;ADC 转换完成中断里读取结果并执行 FOC,算出的新占空比等到下一个重载才生效。慢速任务(速度环、保护计数、状态机跳转)挂在 1 ms 定时中断上。
/* PWM 重载中断:只做最轻的事,触发采样、更新占空比 */ void PWM_Reload_ISR(void) { PWM_SetDuty(g_duty.a, g_duty.b, g_duty.c); /* 上一拍算出的占空比 */ Adc_StartConversion(); /* 硬件触发,不用软件等 */ g_tick_100us++; /* 供慢任务做时间戳 */ } /* ADC 完成中断:一整拍 FOC 都在这里,注意执行时间必须留足余量 */ void ADC_Done_ISR(void) { uint16_t raw1 = Adc_GetResult(SAMPLE1); uint16_t raw2 = Adc_GetResult(SAMPLE2); Phase3_t i3 = Reconstruct(AdcToAmp(raw1), AdcToAmp(raw2), g_sector, g_win_ok); if (!i3.valid) { i3 = g_last_i3; } /* 窗口不足,沿用上一拍 */ g_last_i3 = i3; ClarkePark(i3.ia, i3.ib, g_theta, &g_id, &g_iq); float ud = PI_Run(&g_pi_d, g_id_ref - g_id) - g_omega * LQ_H * g_iq; float uq = PI_Run(&g_pi_q, g_iq_ref - g_iq) + g_omega * (LD_H * g_id + PSI_PM); InvPark(ud, uq, g_theta, &g_ualpha, &g_ubeta); Svpwm(g_ualpha, g_ubeta, g_vdc, &g_duty, &g_sector, &g_win_ok); Obs_Update(&g_obs, g_ualpha, g_ubeta, i3.ia, i3.ib, RS_OHM, LD_H); g_theta = g_obs.theta; /* 闭环时用估算角 */ g_omega = g_obs.omega; }两个中断的分工是硬性的。重载中断里绝不能做浮点运算或调用 PI,否则占空比下发时刻被推迟,等效于给控制环加了一个随机延迟。ADC 完成中断里的执行时间要留 30% 以上余量,因为这段代码挂在最紧的时间点上,一旦超时,下一拍的采样窗口就被挤掉,表现出来是电流波形上周期性出现毛刺。g_win_ok和g_sector是由Svpwm在上一拍算出来的,这里体现了一个前后依赖:本拍能不能采到有效电流,取决于上一拍算出的矢量分布,所以Svpwm里必须顺带把窗口判断做掉。
Obs_Update放在最后、在g_theta更新之前使用上一拍的角度做 Park,这是有意为之——本拍的角度要等观测器算完才更新,用旧角度做变换再加一拍补偿,比用未收敛的新角度更稳妥。
4.2 应用状态机:从 INIT 到 FAULT 的完整跳转
状态机把这套算法串成可运行的流程,每个状态职责单一,跳转条件明确。典型的状态集合是 INIT → ALIGN → OPENLOOP → RUN → STOP,外加一个任何状态都能跳进去的 FAULT。
typedef enum { ST_INIT = 0, /* 时钟、外设、参数、偏置标定 */ ST_ALIGN, /* d 轴注入电流,把转子拉到已知角度 */ ST_OPENLOOP, /* 固定加速度斜坡,强制换相 */ ST_RUN, /* 观测器闭环,正常 FOC */ ST_STOP, /* 主动关断,占空比归零,等待下一次启动 */ ST_FAULT /* 故障锁存,需上位机或断电复位 */ } AppState_t; /* 1 ms 定时中断里调用,状态跳转全部在这里发生 */ void App_StateMachine(void) { switch (g_state) { case ST_INIT: if (Init_All() == 0) { g_state = ST_ALIGN; g_timer = 0; } else { g_fault = F_INIT; g_state = ST_FAULT; } break; case ST_ALIGN: g_id_ref = ALIGN_CURRENT; g_iq_ref = 0.0f; /* 只有 d 轴电流 */ if (++g_timer > ALIGN_MS) { g_state = ST_OPENLOOP; g_timer = 0; } break; case ST_OPENLOOP: g_speed_ref += RAMP_STEP; /* 斜坡加速 */ g_id_ref = OPENLOOP_ID; g_iq_ref = 0.0f; if (g_obs.omega > SWITCH_OMEGA && fabsf(Obs_AngleErr()) < 0.35f) { g_state = ST_RUN; /* 观测器收敛,切闭环 */ } if (g_timer++ > OPENLOOP_TIMEOUT) { g_fault = F_START; g_state = ST_FAULT; } break; case ST_RUN: g_id_ref = 0.0f; /* 表贴式电机 id 给 0 */ g_iq_ref = SpeedLoop(g_speed_ref - g_obs.omega); if (g_stop_req) g_state = ST_STOP; if (g_fault) g_state = ST_FAULT; break; case ST_STOP: g_duty.a = g_duty.b = g_duty.c = 0.5f; /* 上三管全关 */ g_id_ref = g_iq_ref = 0.0f; if (!g_stop_req) { g_state = ST_INIT; } break; case ST_FAULT: g_duty.a = g_duty.b = g_duty.c = 0.5f; g_id_ref = g_iq_ref = 0.0f; break; } }跳转条件里的每个常量都要落到具体值上:ALIGN_CURRENT取额定电流的 30%~50%,ALIGN_MS取 200~500,RAMP_STEP由期望的启动加速度反算(100 μs 一拍、1 ms 更新一次速度指令),SWITCH_OMEGA取额定电角速度的 5%~10%。OPENLOOP_TIMEOUT是兜底,防止观测器一直不收敛而电机长期开环堵转发热。Obs_AngleErr()是估算角与开环角之差,做 ±π 归一化后取绝对值。
STOP 和 FAULT 里把三个占空比都写 0.5,是因为中心对齐模式下 0.5 意味着上管全关、下管全开,此时电机三相被短接,反电动势产生的制动电流会在下管续流,属于安全的停机状态。如果写成 0,会变成上管全开、母线直通,这是教科书级的灾难。
4.3 关键外设初始化顺序
S12ZVM 的外设初始化有严格顺序,顺序错了不会报错,只会让采样点偏几十微秒,现象是电流波形上看不出问题,但转矩明显偏小。合理的顺序是:时钟 → 看门狗 → GPIO → PWM → ADC → 定时器 → 运放 → 中断使能。
int Init_All(void) { Clk_Init(); /* 先锁定总线时钟,所有分频都基于它 */ Wdg_Init(); /* 看门狗尽早开,但要留够初始化时间 */ Gpio_Init(); /* 桥臂使能脚先拉低,防止上电直通 */ Pwm_Init(PWM_FREQ_HZ, DEADTIME_NS, PWM_CENTER_ALIGNED); Adc_Init(ADC_TRIG_PWM_RELOAD, ADC_12BIT); /* 触发源必须指向 PWM 重载 */ Adc_ConfigChannel(CH_SAMPLE1, SAMPLE_DELAY1_US); Adc_ConfigChannel(CH_SAMPLE2, SAMPLE_DELAY2_US); Timer_Init(1000); /* 1 ms 慢任务 */ Opamp_Init(AMP_GAIN); /* 片内运放,增益要与 2.2 节算的一致 */ g_adc_mid = CalibOffset(); /* 桥臂未使能时标定零点 */ if (g_adc_mid < 1900 || g_adc_mid > 2200) return -1; /* 偏置异常,判硬件故障 */ Int_Enable(); return 0; }几个参数是关键。ADC_TRIG_PWM_RELOAD决定采样基准时刻,必须和 PWM 重载事件绑定,不能改成软件触发。SAMPLE_DELAY1_US和SAMPLE_DELAY2_US是相对重载沿的延时,作用是把采样点推到有效矢量的中央,避开死区和振铃,这两个值要拿示波器对着相电流波形调,通常在 1~3 μs 量级。CalibOffset必须在桥臂使能之前调用,否则电机可能已经被驱动,标出来的零点带着电流分量。
最后的偏置合法性检查很值得抄。它把“板子焊错、运放没供电、分流电阻虚焊”这类硬件问题在上电瞬间就拦住,比等电机转起来过流保护要省事得多。
4.4 保护策略:硬件快断加软件确认
应用笔记列出了五类保护:直流母线过压、欠压、过流、过载、启动失败。工程上通常做两层:硬件比较器直接封波,软件在中断里做二次确认和故障分类。
硬件层的过流比较器接在分流电阻调理输出上,阈值设到量程的 90%,一旦触发直接把 PWM 输出拉到安全状态,不经过任何软件路径,响应时间在微秒级。软件层在 1 ms 任务里做窗口判断和计数:
- 母线过压/欠压:连续 N 次采样超阈值才判定,避免母线电容充放电引起的瞬时误报;
- 过流:中断里读到的瞬时电流有效值超限,累计计数到阈值后锁存;
- 过载:iq 的平方在一段时间内积分,超过 I²t 曲线就降额或停机;
- 启动失败:OPENLOOP 超时未切换到 RUN,或切换后转速不跟随。
故障锁存之后不要自动恢复。电机类负载的故障往往有物理原因(堵转、接线错、母线异常),自动重启会反复冲击功率器件。常见做法是锁存后等上位机清除,或者断电复位,同时在 FreeMASTER 里把故障码和故障时刻的关键变量(母线电压、三相电流、估算转速)一起存下来,方便回溯。
5. 避坑与排查:单电阻采样最容易翻车的五个现场
这套方案的坑集中在“时间”和“符号”两件事上。下面五条是我在台架上真实遇到或者见别人遇到过的,每条都按现象、原因、解决写。
5.1 电机转起来但转矩明显偏小、电流偏大
现象:能闭环运行,转速也跟得上,但同样负载下相电流比预期高 20% 以上,电机温升快。
原因:两次采样点落在死区或开关振铃区,采到的是失真值,重构出来的三相电流幅值偏小、相位偏一点。控制器为了让实际转矩达标,只能加大 iq,于是电流虚高。
解决:把电流探头夹在任意一相上,同时用示波器双通道看 PWM 驱动和相电流,确认采样点是否落在矢量平台中央。调整SAMPLE_DELAY1_US/SAMPLE_DELAY2_US,让采样点离两侧开关沿都至少有 0.5 μs 余量。同时检查运放的 RC 建立时间,滤波电容太大也会让采样值偏低。
5.2 上电第一次给转矩就过流保护
现象:对齐段正常,一进闭环或者一给速度指令立刻过流,复位后再来还是同样位置。
原因:相电流符号表配错。前面 2.1 节那张表里,110、011、101 三个矢量测到的是相电流的相反数,如果kSignOfSample全部写成 +1,Park 变换出来的 iq 符号翻转,电流环变成正反馈。
解决:不要凭记忆填表,用调试器在静止状态下给定一个已知方向的 d 轴电流,观察三相电流的正负,逐个扇区核对。核对完之后在 RUN 状态下手动给一个小的 iq 指令,确认电机转向与指令符号一致。
5.3 低速段电流波形周期性畸变、转速抖动
现象:转速低于额定 10% 时,相电流上能看到明显的周期性塌陷,电机有“一顿一顿”的感觉,转速越高越不明显。
原因:低调制区盲区。占空比接近 50% 时,两个有效矢量的作用时间被零矢量挤到极短,采样窗口不足,win_ok频繁置 0,重构值长时间沿用上一拍,等效于电流反馈被“冻住”。
解决:三条路可以组合用。一是在窗口不足时不要死等,用观测器的电流估计值临时顶上,本拍只做电流环不做积分累加;二是启用移相策略,在窗口不足的扇区把 PWM 相位整体平移半个窗口宽度,强行撑出采样时间;三是低速段适当降低开关频率或提高母线电压利用率。哪条路都得实测,不能只在纸面上推。
5.4 观测器在台架上收敛,装到整机上就切不进闭环
现象:实验室空载能顺利从 OPENLOOP 切到 RUN,装到带负载的整机上,切换条件一直不满足,最后超时进 FAULT。
原因:切换阈值写死成固定电角速度,没有考虑负载下的实际电流和反电动势波形畸变。带载时电流谐波让估算角度有周期性误差,Obs_AngleErr()的瞬时值偶尔超阈值,条件判定用的是瞬时值而不是持续计数。
解决:把切换条件改成“连续 M 拍满足”而不是单拍判定,阈值适当放宽;同时把切换前的位置混合时间拉长到 50~100 ms,让观测器角度逐步接管。整机调试时优先看 FreeMASTER 的高速记录仪,把估算角度、开环角度、实际相电流一起录下来,能直接看出误差是稳态偏置还是周期性抖动。
5.5 参数辨识做完,电流环反而振了
现象:用 MCAT 走完电机参数辨识,把新的 Rs、Ld、Lq 填进去,原本平稳的电流环开始高频振荡,电机发出啸叫。
原因:辨识得到的电感是增量电感,在特定电流点测得,而 PI 整定用的是这个值;如果辨识时电流点选得偏小,L 偏小,按 Kp = 2ξω0L 算出来的增益就偏小而不是偏大——但很多人会顺手把带宽 ω0 也一起调大,两个动作叠加就过头了。
解决:改参数一次只改一组,改完必须重新跑一遍阶跃响应看超调。带宽 ω0 从开关频率的 1/20 起,逐步往上加,每次加完给一个 iq 阶跃,观察响应时间和超调量,超调超过 20% 就退回去。另外辨识电感的电流点要选在实际工作电流附近,空载辨识出来的值在满载时不准。
6. 二次开发:把参考设计改成自己板子上的东西
移植这套代码到自己板子上,最容易犯的错是从算法开始改。正确顺序反过来:先把硬件抽象层对齐,再调采样时序,最后才碰算法参数。
第一步是硬件层。桥臂编号、运放极性、ADC 通道、PWM 定时器、死区时间这五样必须逐一核对,做一张“参考板 vs 我的板”的对照表,把每个引脚和外设实例对应上。核对完先不转电机,只跑 PWM,用示波器看三相占空比、死区、中心对齐是否都正确,确认之后再接电机。
第二步是采样链路。用可调直流源在分流电阻上注入已知电流,从 ADC 码值一路标到AdcToAmp的输出,误差控制在 2% 以内再往下走。然后空载跑一次对齐,录下三相电流波形,确认重构出来的波形是干净的正弦。
第三步才是调参,顺序不能乱:电机参数(Rs、Ld、Lq、ψPM)→ 电流环带宽 → 观测器增益 → 速度环。MCAT 的四步正好对应这个顺序,每一步都有独立的页签,改完立刻能通过 FreeMASTER 下发并观察。
| 调参步骤 | 观察量 | 合格标准 |
|---|---|---|
| 电机参数 | 辨识出的 Rs/Ld/Lq | 与 LCR 表实测偏差 < 15% |
| 电流环 | iq 阶跃响应 | 上升时间符合带宽预期,超调 < 20% |
| 观测器 | 估算角 vs 实测过零点 | 稳态误差 < 10° 电角度 |
| 速度环 | 转速阶跃 | 无持续振荡,恢复时间可接受 |
改造时优先动的是三处:速度环输出限幅(按你自己电机的额定电流设)、保护阈值(母线电压范围、过流点、I²t 曲线)、启动参数(对齐电流、斜坡加速度、切换阈值)。这三处不动算法结构,改起来风险最低。真正需要改算法结构的场景只有两个:电机是内嵌式(凸极)需要 id ≠ 0 的 MTPA,或者要求零速大转矩必须上高频注入。这两种都不属于这套参考设计的覆盖范围,得另找方案。
最后说个习惯。这套代码里我最信任的不是哪个算法,而是CalibOffset里那个偏置合法性检查,以及每次上电必做的三件事:量偏置电压、看 PWM 死区、录一段空载相电流。有一次赶进度跳过了偏置标定,电机空载转了半小时才开始发烫,回头查是零点偏了 60 个码值,折合 1 A 的直流偏置一直加在 d 轴上。从那以后我每次上电都强制走一遍这三步,再急也不省。希望帮到你。
本文还有配套的精品资源,点击获取