简介:AN5327 应用笔记面向汽车电机控制方向的嵌入式工程师与学习者,围绕基于 MC9S12ZVM 的 3 相无传感器单分流电流采样 PMSM 矢量控制方案展开,重点解决低成本驱动器中转子位置/速度估算、相电流重构与 FOC 闭环调校等工程问题。压缩包仅含 1 个 PDF 文件,约 2.58MB,正文按系统概念、PMSM 磁场定向控制、MC9S12ZVML128 软件实现、FreeMASTER 与 MCAT 界面等章节组织,配有 dq 轴模型、单电阻电流测量、扩展反电动势观测器等关键框图与流程说明。已有 212 人学习,适合查阅 NXP 官方参考设计细节、理解无传感器 FOC 从理论到代码落地的读者;也可作为电机控制项目选型、算法对照与调试工具使用的技术底稿。
1. 板子上只有一颗分流电阻,位置传感器也被砍掉了
去年帮朋友看一块三相电机驱动板,原理图上数来数去只有一颗 DC-Bus 分流电阻,位置传感器接口是空的,六颗 MOS 管由一颗 MC9S12ZVM 片内的门极驱动单元直接推。这种“三相无传感器单电阻 PMSM 矢量控制”就是 AN5327 这份应用笔记要讲的东西:10 kHz 电流环、100 μs 采样周期、速度闭环、双向旋转,转子位置和转速全靠扩展反电动势观测器算出来。它面向的是汽车小功率泵、风扇、执行器这类场景——BOM 要压到最低,但不想退回方波六步换相那种粗糙控制。做嵌入式编程的同行都知道,这套方案真正的门槛不在 FOC 那几行公式,而在单电阻电流重构的采样窗口:窗口不够宽的那一刻,你手上的“三相电流”其实是错的。
2. 系统总览:从 DC-Bus 分流电阻到 100 μs 控制周期的分工
2.1 硬件链路:六颗管子加一颗电阻,电流信息全靠开关组合
三相电压源逆变器是标准的六管拓扑,上桥三颗、下桥三颗,由 MC9S12ZVM 内部集成的门极驱动单元(GDU)直推,省掉外部预驱芯片,这是这颗器件做电机控制最大的成本优势。采样部分只保留了串在直流母线负极回路里的那一颗分流电阻,另外两相的分流电阻位置在原理图上直接空着。分流电阻上的压降经过片内运放的增益网络和 RC 滤波,抬到 ADC 能接受的输入范围内。
问题的本质在这里:分流电阻串在直流母线上,它看到的电流不是某一相电流,而是“当前开关状态组合下,流过母线的那个电流”。六个非零电压矢量各自对应一组可以观测到的相电流,零矢量期间母线上根本没有相电流信息。这个对应关系必须背熟,后面所有代码都围着它转。
| 有效矢量(A/B/C 上桥) | 母线电流流向 | 分流电阻上等效电流 |
|---|---|---|
| 100 | 下桥 B、C 导通 | +i_a |
| 110 | 下桥 C 导通 | −i_c |
| 010 | 下桥 A、C 导通 | +i_b |
| 011 | 下桥 A 导通 | −i_a |
| 001 | 下桥 A、B 导通 | +i_c |
| 101 | 下桥 B 导通 | −i_b |
符号正负取决于板上电流正方向的约定,实测必须拿电流钳校一遍,别信纸面。表里这六行是后面重构代码唯一的依据:一个 PWM 周期里只要采到其中两个不同矢量的母线电流,再套基尔霍夫电流定律i_a + i_b + i_c = 0,第三相就出来了。
提示:零矢量 000 和 111 期间分流电阻上没有相电流,任何在这个时刻触发的 ADC 采样拿到都是噪声,不是电流。
2.2 软件分层:从外设寄存器到 FreeMASTER 变量表
工程包的组织方式通常按职责切四层,这个项目场景下最常见的分法是:外设驱动层负责 PMF(带故障保护的脉宽调制模块)、列表式 ADC、GDU 与时钟;算法层放 FOC 变换、观测器、电流重构;应用层是速度指令、启停状态机、保护;调试层是 FreeMASTER 通信和 MCAT 参数页。拿到工程后按功能对号入座即可,不必纠结文件名。
| 模块 | 职责 | 关键点 |
|---|---|---|
| 初始化 | 时钟、GDU、PMF、ADC 配置 | 顺序错了会烧管子 |
| PWM 中断服务 | 每 100 μs 跑一次 FOC | 执行时间必须留余量 |
| 电流重构 | 采样时刻判定、三相还原 | 盲区补偿在这里 |
| 观测器 | 扩展反电动势估角度与转速 | 低速段基本不可信 |
| 状态机 | INIT / ALIGN / RUN / FAULT | 启动流程全靠它 |
| 保护 | 过流、过压、欠压、堵转、启动失败 | 硬件比较器 + 软件双保险 |
| 调试 | 变量表、recorder、MCAT 页 | 变量地址别被优化掉 |
这份应用笔记配套的 FreeMASTER 界面之所以好用,是因为它把“期望转速、实际转速、母线电压、相电流、系统状态”这几组量都挂在了固定的变量表上,MCAT 插件再把这些变量组织成电机参数页、电流环页、无感参数页、速度环页四个标签。调参的时候不用改代码重烧,直接在图形界面上拖滑块看波形,这套流程对调试效率的提升比想象中大得多。
2.3 一个 PWM 周期里的时间预算
10 kHz 开关频率意味着每个控制周期只有 100 μs。这 100 μs 里要完成:两次 ADC 触发采样、Clarke/Park 变换、两路 PI 运算、逆变换、SVPWM 占空比计算、观测器更新、保护判断,最后把新的占空比写进 PMF 的影子寄存器。变慢了会抖,写早了会把采样窗口打碎,这是硬时序。
| 时间点 | 动作 | 注意事项 |
|---|---|---|
| 周期起点 | PMF 计数器启动 | 占空比用影子寄存器,本周期中途不许改 |
| 第一个有效矢量中点 | 触发 ADC 采第一路 | 偏离中点会吃到开关振铃 |
| 第二个有效矢量中点 | 触发 ADC 采第二路 | 窗口不足时这一路直接作废 |
| 周期后段 | FOC 计算 + 占空比更新 | 必须在下一个周期起点前写完 |
| 任意时刻 | 故障输入中断 | 优先级最高,直接封锁 GDU |
初始化顺序尤其讲究,功率级必须全程处于关断状态,直到 PWM 配置完成、占空比全部写成 0 才允许使能输出,否则第一次上电就是一次直通。
/* 上电初始化:先让功率级绝对安全,再配置会产出 PWM 的外设,最后才开中断 */ void mc_init(void) { DisableInterrupts; /* 全程关中断,避免半配置状态被 ISR 打断 */ GDU_Init(); /* 门极驱动单元:配置死区、故障输入极性 */ GDU_DisableOutputs(); /* 保险:PWM 配置完成前绝不导通任何桥臂 */ Clock_Init(); /* 总线时钟决定 PWM 分辨率与 ADC 转换时间 */ PMF_Init(PWM_FREQ_HZ, DEADTIME_NS); /* 中心对齐 PWM,死区按 MOS 关断时间给,宁大勿小 */ PMF_SetDuty(0, 0, 0); /* 三相占空比先归零,写进影子寄存器 */ ADC12B_LBA_Init(); /* 列表式 ADC,按 PMF 硬件触发的时刻装载转换命令 */ ADC_Calibrate(); /* 上电自校准,增益与偏置误差直接进电流重构 */ OpAmp_Init(OPAMP_GAIN); /* 片内运放增益要和分流电阻阻值、额定电流匹配 */ Fault_Init(); /* 过流比较器、母线分压采样、堵转计时 */ FreeMASTER_Init(); /* 调试通信最后起,别让它抢时序 */ EnableInterrupts; MotorState = STATE_IDLE; /* 停在 IDLE,等上位机下发启动命令 */ }这段的关键不在语法,在顺序。GDU_DisableOutputs()必须出现在PMF_Init()之前,因为 PMF 一配置完就可能输出有效电平;ADC_Calibrate()必须在使用任何电流值之前跑完,校准系数偏一点,重构出来的三相电流就整体带偏置,后面的观测器跟着一起飘。至于OpAmp_Init()的增益,常见做法是按“额定电流 × 分流电阻阻值 × 增益 ≈ ADC 满量程的 80%”来定,留 20% 给过流冲击,别顶到满量程。
3. 控制环拆解:Clarke/Park 变换、d-q 电流环 PI 与扩展反电动势观测器
3.1 两次变换:把交流量搬进直流坐标系
FOC 的核心思路是让定子电流里产生磁通的分量和产生转矩的分量解耦。三相电流先经 Clarke 变换压到静止的两相 αβ 坐标系,再经 Park 变换旋转到与转子磁链同步的 dq 坐标系。到了 dq 系里,两个电流分量都是直流量,用最普通的 PI 控制器就能无静差跟踪,这就是矢量控制比六步换相好调的根本原因。
/* 输入:重构得到的三相电流;sin/cos 由观测器给的角度预先查表算好 */ void foc_forward(mc_t *m) { /* Clarke:等幅值变换。i_alpha 直接等于 i_a,省一次加法,这是常用写法 */ m->i_alpha = m->i_a; m->i_beta = (m->i_a + 2.0f * m->i_b) * 0.57735027f; /* 1/√3 */ /* Park:旋转到与转子磁链对齐的 dq 坐标系 */ m->i_d = m->i_alpha * m->cos_th + m->i_beta * m->sin_th; m->i_q = -m->i_alpha * m->sin_th + m->i_beta * m->cos_th; }参数只有三个:i_a、i_b、i_c(i_c其实是-i_a - i_b算出来的,代码里往往不用显式传),以及角度theta对应的sin_th、cos_th。Park 变换最怕角度不准,角度差 10 度,d 轴和 q 轴之间的串扰就明显起来了,表现出来就是“给 q 轴电流,转速上去了但母线电流虚高”。所以后面观测器那一段的收敛质量,直接决定这段代码拿到的输入干不干净。
逆变换要注意一点:dq 电压算完之后不能直接线性映射成占空比,得先做零序注入(等效 SVPWM),把母线利用率榨出来,否则同样的母线电压下最高转速要打折扣。
void foc_inverse(mc_t *m) { /* 逆 Park:dq 电压指令回到静止坐标系 */ m->u_alpha = m->u_d * m->cos_th - m->u_q * m->sin_th; m->u_beta = m->u_d * m->sin_th + m->u_q * m->cos_th; /* 逆 Clarke,得到三相电压的相对值 */ float ua = m->u_alpha; float ub = -0.5f * m->u_alpha + 0.86602540f * m->u_beta; float uc = -0.5f * m->u_alpha - 0.86602540f * m->u_beta; /* 零序注入:把三相电压整体压到 [-Udc/2, +Udc/2] 区间内,等效 SVPWM */ float umax = fmaxf(ua, fmaxf(ub, uc)); float umin = fminf(ua, fminf(ub, uc)); float uoff = 0.5f * (umax + umin); m->duty_a = 0.5f + (ua - uoff) / m->u_dcb; m->duty_b = 0.5f + (ub - uoff) / m->u_dcb; m->duty_c = 0.5f + (uc - uoff) / m->u_dcb; }u_dcb是实时采到的母线电压,不能用额定值代替。母线电压跌 10%,用固定值算出来的占空比就偏高 10%,电流环会把它当成电机参数变化去补偿,结果就是母线一跌转速就抖。这个细节在台架上不明显,装车之后才知道疼。
3.2 d-q 轴电压方程与交叉耦合补偿
同步旋转坐标系下的电压方程是这样的:
u_d = R·i_d + L_d·(di_d/dt) − ω_e·L_q·i_q u_q = R·i_q + L_q·(di_q/dt) + ω_e·L_d·i_d + ω_e·ψ_PM两个方程结构完全一样,但各带一个交叉项:d 轴方程里的−ω_e·L_q·i_q和 q 轴方程里的+ω_e·L_d·i_d + ω_e·ψ_PM。高速段这两项数值不小,不补偿的话,你给 q 轴 PI 一个阶跃指令,d 轴电流会跟着乱跳,两个环互相打架。常见做法是在 PI 输出之后直接把这部分前馈加回去,让 PI 只负责“剩下那点误差”。
/* 电流环:PI + 交叉耦合前馈,前馈用本拍的实测电流而不是给定值 */ void current_loop(mc_t *m, float ts) { /* d 轴 PI,给定通常为 0(表贴式电机不做弱磁时 i_d 不产生转矩) */ float ed = m->i_d_ref - m->i_d; m->i_d_int += m->ki_d * ed * ts; m->i_d_int = clampf(m->i_d_int, -m->u_max, m->u_max); /* 抗积分饱和 */ float ud_pi = m->kp_d * ed + m->i_d_int; /* q 轴 PI,给定来自速度环输出 */ float eq = m->i_q_ref - m->i_q; m->i_q_int += m->ki_q * eq * ts; m->i_q_int = clampf(m->i_q_int, -m->u_max, m->u_max); float uq_pi = m->kp_q * eq + m->i_q_int; /* 交叉耦合前馈 + 反电动势前馈,补偿掉方程里的非线性项 */ m->u_d = ud_pi - m->we_est * L_Q * m->i_q; m->u_q = uq_pi + m->we_est * L_D * m->i_d + m->we_est * PSI_PM; /* 电压幅值限幅,防止过调制把占空比顶出可行域 */ float umag = sqrtf(m->u_d * m->u_d + m->u_q * m->u_q); if (umag > m->u_max) { m->u_d *= m->u_max / umag; m->u_q *= m->u_max / umag; } }u_max不是母线电压的一半,得再打个折扣:调制比留 5% 余量,再扣掉死区造成的电压损失,工程上常取0.9 × Udc / √3这个量级,具体值上板实测过调制点之后再定。we_est用观测器给的电角速度,这里千万别直接用速度环的给定值——加减速时两者差得很多,前馈用错源反而会加剧震荡。
3.3 PI 增益:极点配置法给的初值,实测还得自己收
d 轴和 q 轴被控对象都是“一阶惯性 + 积分”的形式,G(s) = 1 / (L·s + R)。用标准 PI 控制器配极点配置,让闭环变成二阶系统,阻尼比 ξ、自然频率 ω0 由你定:
Kp = 2·ξ·ω0·L − R Ki = ω0²·Lω0 一般取电流环期望带宽,比开关频率低一个数量级比较稳妥;ξ 取 0.707 到 1.0 之间,取太小阶跃响应超调明显,取太大响应发肉。这两个公式给的是理论初值,因为实际系统里还有 PWM 更新延迟(通常按 1.5 个采样周期折算)、采样滤波延迟,实际能用的带宽会比算出来的低 20% 到 30%。
/* 离散实现里 Ki 要乘采样周期,别把连续域的 Ki 直接搬进代码 */ void current_pi_tune(pi_t *pi, float L, float R, float w0, float xi, float ts) { pi->kp = 2.0f * xi * w0 * L - R; /* R 小的时候可以忽略,但热态电阻会漂,别真删 */ pi->ki = w0 * w0 * L * ts; /* 乘 ts 之后才和积分累加式的量纲对上 */ pi->out_max = U_MAX; pi->int_max = U_MAX; /* 积分限幅和输出限幅分开设,抗饱和更干净 */ }L 和 R 从哪来?MCAT 的电机参数页让你填极对数、相电阻、d/q 轴电感、永磁磁链这几个量,填完它自动算好两套 PI 系数。手边没 LCR 表的话,可以让 MCAT 用直流注入法测电阻、用高频注入测电感,精度够调参用。
3.4 扩展反电动势观测器:角度和转速都从电流误差里抠出来
无传感器方案的核心是一个电流观测器。思路是:先用估计角度把实测电流折算到估计的 dq 轴,跑一个电流模型,模型输出和实测值之间的误差拿来修正反电动势估计值,反电动势再折算成角度和转速。这套东西本质是个龙伯格观测器,工程上习惯叫扩展反电动势观测器。
/* 观测器:一拍更新一次,输入是本拍的电压指令与实测电流 */ void bemf_observer(mc_t *m, float ts) { /* 1. 电流观测器:dI/dt = (U − R·I − E)/L + K·(I_meas − I_est) */ m->i_d_est += ts * ((m->u_d - R_S * m->i_d_est - m->e_d_est) / L_D + m->k_obs * (m->i_d - m->i_d_est)); m->i_q_est += ts * ((m->u_q - R_S * m->i_q_est - m->e_q_est) / L_Q + m->k_obs * (m->i_q - m->i_q_est)); /* 2. 反电动势积分项:电流误差持续存在,说明模型里的 E 估错了,往这个方向修 */ m->e_d_est += ts * m->ki_obs * (m->i_d - m->i_d_est); m->e_q_est += ts * m->ki_obs * (m->i_q - m->i_q_est); /* 3. 角度:把 d 轴反电动势压到 0,估计角就和真实转子角对齐 */ float dtheta = atan2f(-m->e_d_est, m->e_q_est); m->theta_est += dtheta + m->we_est * ts; /* 锁相环比裸 atan2 平滑得多 */ /* 4. 转速:q 轴反电动势幅值除以永磁磁链得到电角速度,再折成机械转速 */ m->we_est = m->e_q_est / PSI_PM; m->speed_est = m->we_est / POLE_PAIRS; }三个参数要盯住:k_obs是电流观测器的比例校正增益,太大噪声进角度、太小收敛慢;ki_obs是反电动势的积分增益,它决定了观测器带宽;POLE_PAIRS填错,转速显示直接差整数倍。这套观测器的软肋在低速:反电动势幅值和转速成正比,转速掉到额定的 5% 以下,反电动势只有几十毫伏,被采样噪声和电阻温漂一淹,估出来的角度就完全是玄学了。所以工程上低速段一律切开环强拉,等转速上来、观测器收敛判据满足之后再切闭环,这个切换逻辑写不好就是启动抖动和反转。
4. 单电阻电流重构:采样窗口计算、盲区判定与占空比钳位
4.1 一个 PWM 周期里挖出两路电流
两相电流采样能成立的前提是:一个 PWM 周期内至少有两个非零电压矢量各自持续了足够长的时间。七个段式的中心对齐 PWM 在多数扇区里天然满足这个条件——占空比排在最前和最后的那两相各贡献一个有效矢量窗口。真正要做的三件事是:判定当前扇区、算出两个窗口的宽度、把 ADC 触发点放在各自窗口的正中间。
/* 按占空比大小排序,只记下标,后面查表选采样相位 */ static void sort_duty(const float *d, uint8_t *ord) { ord[0] = 0; ord[1] = 1; ord[2] = 2; for (int i = 0; i < 2; i++) for (int j = 0; j < 2 - i; j++) if (d[ord[j]] > d[ord[j + 1]]) { uint8_t t = ord[j]; ord[j] = ord[j + 1]; ord[j + 1] = t; } } /* 每周期算一次:两个采样点分别落在最短和次短占空比区间里 */ void shunt_schedule(mc_t *m) { uint8_t ord[3]; sort_duty(m->duty, ord); float w1 = m->duty[ord[0]] * m->t_pwm; /* 最短窗口 */ float w2 = (m->duty[ord[1]] - m->duty[ord[0]]) * m->t_pwm; /* 次短窗口 */ m->win1_ok = (w1 > m->t_min_ns); m->win2_ok = (w2 > m->t_min_ns); if (!m->win1_ok || !m->win2_ok) { shunt_force_window(m, ord, m->t_min_ns / m->t_pwm); /* 窗口不够,进补偿 */ } m->adc_trig1 = (uint16_t)(0.5f * w1); /* 采样点放窗口正中 */ m->adc_trig2 = (uint16_t)(w1 + 0.5f * w2); }t_min_ns这个值是整段代码的命门,它由三部分叠加:ADC 采样保持时间、运放从上一状态建立到稳定的时间、以及开关沿振铃的避让时间。常见量级在 1 到 2 微秒,占空比越低越吃紧。有人图省事把它写死成 500 ns,结果就是低速轻载时电流波形上一条一条的毛刺——那是采样点落在了振铃尾巴上。
4.2 盲区:三个占空比凑得太近的时候
盲区的成因很直白:两个非零矢量之间的切换沿挨得太近,分流电阻上的电流脉冲还没成形就结束了,ADC 采到的是一个半死不活的中间值。最恶劣的情况是三相占空比几乎相等,两个窗口同时消失,这一拍的三相电流信息全部作废。
/* 窗口不足时的钳位补偿:把相邻两个占空比的间距硬撑到最小要求 */ void shunt_force_window(mc_t *m, const uint8_t *ord, float dt_min) { float d0 = m->duty[ord[0]]; float d1 = m->duty[ord[1]]; if ((d1 - d0) < dt_min) { float half = 0.5f * (dt_min - (d1 - d0)); m->d <p> <a href="https://download.csdn.net/download/qq_38397578/87856561" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>