上一次帮一个小批量工厂做电动三轮车控制器升级,真正触动我换方案的画面,是客户在试车时反复问:为什么起步时整个车厢嗡嗡响?为什么拉满货爬坡感觉后劲不足?那台车用的是最常见的方波六步驱动,代码简单、成本低,但在低速扭矩、噪音和续航上的牺牲,对于每天拉货跑街的代步车来说,已经是明显短板。于是我整理出了一套基于C语言的FOC电动车控制器源代码方案,目标很明确:无刷电机驱动顺滑、低速大扭矩、还能带刹车回馈和保护功能。这套方案主要面向电动三轮车、代步电动车这类300W到1500W功率段的车辆,代码按量产思路设计,既有完整的磁场定向控制算法实现,也包含了转把、仪表、故障保护等外围功能。如果你在折腾电动车控制器、搞BLDC或者PMSM驱动,或者正在学FOC想找一份能落地的工程参考,这篇把从选型、代码到调试的整个设计和踩坑过程都写给你。
1. 为什么代步车控制器值得从方波换到FOC
1.1 三种驱动方式的直观差异
市面上的低成本电动车控制器,绝大多数还是方波六步驱动。原理是在固定电角度区间内,把三相桥中的两相导通、一相关断,转子转60度就换一次相。这种驱动便宜、好写,但有一个天生缺点:相电流接近于方波,谐波含量大,转矩脉动明显。反映到车上就是低速起步时“嗡嗡响”,车架共振,满载爬坡时电机发热快,坡道上稍微重载就容易感觉没力。
正弦波驱动比它进了一步,让相电流更接近正弦,噪音和转矩脉动都有改善。但很多所谓的“正弦波控制器”实际只是开环VF拖动,电流并没有真正闭环,动态响应一般,堵转保护也做不到精细。也就是说,它改善了舒适性,却没有解决“电流可控”这个最核心的问题。
FOC则完全换了一种玩法。它不直接盯三相电压波形,而是站在转子坐标系上看定子电流,把电流矢量分解成d轴励磁分量和q轴转矩分量,用两个PI环分别控制。这样励磁和转矩彻底解耦,电流响应快,低速力矩可以做得很足。三种方式放在一起对比,区别非常清楚:
| 驱动方式 | 相电流形态 | 转矩脉动 | 噪音 | 低速大扭矩 | 代码复杂度 |
|---|---|---|---|---|---|
| 方波六步 | 方波/梯形 | 大 | 明显 | 一般 | 低 |
| 开环正弦波 | 正弦 | 中 | 中 | 中 | 中 |
| FOC闭环 | 受控正弦 | 小 | 小 | 好 | 高 |
1.2 FOC的本质:把三相交流折算成可控制的两个直流分量
第一次接触FOC的人容易被各种变换公式吓到,其实核心逻辑特别朴素:站在电机外面看,三相电流是三个随时间变化的交流量,又要算幅值又要算相位,很麻烦。但站在转子上看,电流矢量就像静止一样,只有两个固定分量,一个是顺着转子磁极方向的d轴,一个是垂直于它的q轴。控制交流问题就变成了控制两个直流分量。
把三相交流电流折算到转子坐标系靠的是Clark变换和Park变换。Clark变换先把ia、ib、ic变成两相静止坐标系的α、β电流,Park变换再利用当前电角度把α、β旋转到d、q坐标系。控制完id、iq之后,再做反变换回到α、β,最后用SVPWM输出三相占空比。这就是整个控制环路的全部内容,翻来覆去就是这五步。
对代步车来说,FOC不是炫技,而是真能解决实际问题。电动三轮经常重载爬坡,需要低速大扭矩;日常在街道穿梭,需要低噪音;电池电压从满电到欠压跨度很大,FOC在宽电压范围内都能稳定输出力矩;更重要的是,q轴电流可控之后,刹车回馈、堵转保护、限速这些功能全都变得好做了。当然,FOC对MCU算力、电流采样、转子位置反馈的要求都比方波高,这也是为什么过去只用在高端伺服和电车上,最近几年MCU算力上来了,才逐步进入民用代步车领域。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先盘硬件:FOC控制器电源链、采样链与位置反馈方案
2.1 系统架构与功率链路
FOC写的再好,硬件不给力也白搭。代步车控制器的功率链路基本是一套固定结构:电池正极经过预充电阻和接触器,进入母线电容组,再供给三相全桥MOSFET,最后接电机三相线。控制板上则是MCU、栅极驱动芯片、电流采样运放、霍尔或编码器接口,以及转把、刹车、档位这些开关量输入。
这里最容易被低估的是母线电容。FOC的相电流不是纯直流,逆变器会从母线上抽取高频脉动能量,如果母线电容容量不足,电压会出现明显纹波,严重时电流环反馈会跟着抖。按我实测的经验,48V系统800W到1000W功率,电解电容一般选1000μF到2200μF,再并一个几十微法的高频薄膜电容吸收尖峰。耐压至少要留1.3到1.5倍余量,48V系统不要用63V以下档位。
MCU选型时,除了看主频和Flash,更要看外设是否齐整。要做FOC,至少需要满足下面这些条件:
- 高级PWM定时器,支持互补输出、死区可调,最好能够硬件刹车;
- ADC要有三路以上采样通道,能被定时器事件触发,保证和PWM中点同步;
- 有霍尔接口或者正交编码器接口,能自动捕捉位置边沿;
- 至少一路UART给仪表通讯,一路ADC通道给温度采样。
我习惯用某Cortex-M4F内核的国产MCU,带FPU和DSP指令,三角函数开销也不大,关键是外设够用,成本压在可控范围。
2.2 三电阻电流采样与单电阻的取舍
电流采样方案直接决定FOC能不能稳。常见做法有两种:三电阻采样和单电阻采样。
三电阻采样是在三相下桥功率管源极各串一个毫欧级采样电阻,通过差分运放放大后送给ADC。FOC需要知道三相电流,实际采两相、第三相用KCL算出来,三电阻方案则是三相都采,信息冗余,抗错了也容易诊断。它的核心采样窗口是在PWM中心对齐的载波顶点附近,那时候下桥导通电流正好流过采样电阻,可以采到相电流平均值。缺点是占空比过小时导通时间太短,采样窗口不够,需要做电流重构或者限制最小占空比。
单电阻采样成本最低,只在母线串一个采样电阻,利用一个PWM周期内不同开关状态下电流流经母线的路径不同,分时采两相再重构第三相。但采样点紧贴开关边沿,噪声大,重构算法复杂,弱磁区和极端占空比工况下容易失败。所以载重变化大、工况比较恶劣的电动三轮车,我的建议很明确:老老实实选三电阻,控制逻辑简单一个量级,长期跑下来稳定性也更好。
2.3 转子位置反馈:用霍尔如何获得连续角度
FOC需要连续的电角度,但代步车几乎不可能装昂贵的光电编码器,量产方案最常用的是三颗开关型霍尔。霍尔安装在定子上,转子磁极扫过时会输出高低电平,三个霍尔组合出6个状态,每个状态对应一个60度电角度扇区。
问题来了:霍尔只能告诉你转子在哪个60度扇区,但FOC要求每毫秒都知道精确电角度,不可能只靠霍尔状态。工程上的解法是“扇区内插值”:认为电机在一个霍尔扇区内近似匀速,用上次霍尔跳变到当前时刻的累计时间和估算出的电角速度,在霍尔基准角度上累加增量角度。每次霍尔跳变时再把估算角度修正到新的霍尔基准位置,防止积分漂移。
c复制uint16_t rotor_angle = hall_base_angle + (int32_t)(speed_elec * dt);
if (rotor_angle >= 65536) rotor_angle -= 65536;
这里angle用0到65535映射0到360度电角度,配合查表法做三角函数,C语言的实现会非常干净。启动阶段霍尔信号还不能可靠估算速度,一般先做预定位,给一个固定d轴电流把转子拉到已知位置,再开环拖动一阵子,等速度估算稳定之后切闭环。低速大扭矩起步的问题,就靠这套“霍尔扇区定位+速度内插”的组合解决。
3. C语言代码架构:让FOC算法在单片机上有序运转
3.1 中断、状态机与模块划分
刚接触FOC时很多人容易犯一个错:把所有计算都塞进主循环。主循环里还有仪表通讯、按键扫描,一旦某帧处理卡顿,电流环执行时间就会抖动。电流环抖动意味着PWM占空比更新时间抖动,表现出来就是电机运行不平稳、力矩波动。所以代码架构一开始就要定好层次。
我把整个系统分成三级:主循环做慢速任务,大约10ms周期处理仪表通讯、按键、故障显示;1kHz定时器中断做速度环和转把值更新;PWM同步中断触发的ADC完成中断里跑20kHz电流环。电流环从采样到变换、PI、反变换、SVPWM,全部在中断里顺序完成,不允许被其他东西打断。
状态机能帮我们把复杂的启动过程管明白。我这里定义了一组状态:待机、预定位、开环拖动、闭环运行、故障保护。预定位就是给d轴固定电流让转子吸到已知位置,开环拖动让转子跟着旋转磁场走,等速度估算稳定之后切入闭环运行。任何一环出错,直接进故障状态,PWM刹车输出,绝不留隐患。
3.2 用结构体管理电机参数
想让FOC源代码可移植、可复用,最关键的一点是所有电机参数不要散落在各处全局变量里。我习惯用配置结构体和运行数据把电机封装起来,换一套电机只改配置,算法代码完全不动。
c复制typedef enum {
MOTOR_IDLE,
MOTOR_ALIGN,
MOTOR_OPENLOOP,
MOTOR_CLOSEDLOOP,
MOTOR_FAULT
} MotorState;
typedef struct {
uint8_t pole_pairs;
uint16_t rated_voltage_mv;
uint16_t rated_current_ma;
float current_kp;
float current_ki;
float speed_kp;
float speed_ki;
int16_t hall_offset; /* 霍尔安装角度补偿 */
} MotorConfig;
typedef struct {
MotorState state;
uint16_t rotor_angle; /* 0~65535 -> 0~360度电角度 */
float rotor_speed_rpm;
float speed_ref_rpm;
int16_t id_ref;
int16_t iq_ref;
int16_t id_fb;
int16_t iq_fb;
uint16_t duty_a;
uint16_t duty_b;
uint16_t duty_c;
uint16_t bus_voltage_mv;
int16_t ia_offset;
int16_t ib_offset;
int16_t ic_offset;
} MotorData;
angle用0到65535循环表示,是为了避免角度无限增长和浮点溢出。电流反馈用ADC码值,duty用定时器比较值,这些本质都是“标幺化”,在一个有限范围内循环,C语言处理起来又稳又快。
3.3 电流环中断的C程序骨架
电流环的骨架其实非常固定,照着这个顺序写不会有遗漏。我放在这里作为模板,实际项目可以直接抄:
c复制void CurrentLoop_ISR(void)
{
int16_t ia, ib, ic;
float ialpha, ibeta;
float id, iq;
float vd, vq;
float ualpha, ubeta;
float cos_a, sin_a;
/* 1. 读取三相电流,扣零漂 */
ia = ADC_Get(CH_IA) - motor.ia_offset;
ib = ADC_Get(CH_IB) - motor.ib_offset;
ic = ADC_Get(CH_IC) - motor.ic_offset;
/* 2. 硬件过流保护快速锁定 */
if (Hardware_Overcurrent_Flag()) {
ForcePwmOff();
motor.state = MOTOR_FAULT;
return;
}
/* 3. 查表获得当前电角度正余弦 */
uint16_t ang = motor.rotor_angle;
cos_a = CosTable(ang);
sin_a = SinTable(ang);
/* 4. Clark 变换 */
ialpha = ia;
ibeta = (ia + 2.0f * ib) * 0.5773503f;
/* 5. Park 变换 */
id = ialpha * cos_a + ibeta * sin_a;
iq = -ialpha * sin_a + ibeta * cos_a;
/* 6. 电流 PI 控制 */
vd = PI_Update(¤t_d_pi, motor.id_ref, id);
vq = PI_Update(¤t_q_pi, motor.iq_ref, iq);
/* 7. 反 Park 变换 */
ualpha = vd * cos_a - vq * sin_a;
ubeta = vd * sin_a + vq * cos_a;
/* 8. SVPWM 输出 */
SVPWM_Update(ualpha, ubeta);
}
速度环速度比较低,在1kHz中断里算完,把结果写入motor.iq_ref,电流环中断负责消费这个目标值。这样两个环之间只通过公共变量耦合,逻辑清爽,调试时也能单独看哪个环出问题。
4. 核心环路代码拆解:同步采样、变换、SVPWM与调参
4.1 PWM中点同步采样与电流重构
FOC对电流采样的要求不是“随便采一下”,而是要采到每个PWM周期的平均相电流。最稳妥的做法是中心对齐PWM,在计数器的谷底或峰顶触发ADC转换。这个时刻所有上桥刚切换完毕,振荡已经平息,采样电阻上的信号对应真正的相电流,而不是开关尖峰。
三电阻方案在占空比很小的极限工况下会出问题。下桥导通时间太短,采样信号还没有稳定,ADC转换窗口就过去了,采到的电流不准确。解决思路有两个方向:一是限制每相占空比不低于某个最小值,比如PWM周期5%以下时做相位补偿;二是做单电阻式的电流重构,在一个周期内换其他采样点补偿。小功率代步车上我一般用最小占空比限制加轻微过调制,已经能覆盖常见运行范围。
4.2 Clark与Park变换的工程化写法
数学公式和C代码写起来是两码事。Clark变换用等幅值约定,我实际使用的代码是:
c复制ialpha = ia;
ibeta = (ia + 2.0f * ib) * 0.5773503f;
这个0.577其实是1除以根号3。Park变换需要电角度的正余弦值,我建议用uint16角度直接查表,而不是用float存弧度然后再调库函数。原因很简单:uint16的角度天然会回绕,查表也避免浮点三角函数的不一致误差,整个环路代码更接近“输入角度、输出结果”的纯函数结构,方便离线仿真验证。
c复制id = ialpha * cos_a + ibeta * sin_a;
iq = -ialpha * sin_a + ibeta * cos_a;
反变换同理,把vd、vq旋转回α、β轴。整套变换代码不过十几行,却是FOC全部的精髓所在。
4.3 SVPWM七段式调制的实现
SVPWM看起来有很多扇区判断、矢量作用时间计算,实际工程里有一个非常简洁的等价写法。把Uα、Uβ先折算成三相电压指令va、vb、vc,再取三相中最大值和最小值的平均值作为零序分量,每个通道减去这个零序分量,加上0.5偏置,就得到0到1标幺化的占空比命令,乘上定时器周期得到比较值:
c复制va = ualpha;
vb = -0.5f * ualpha + 0.8660254f * ubeta;
vc = -0.5f * ualpha - 0.8660254f * ubeta;
voff = 0.5f * (fmax_v3(va, vb, vc) + fmin_v3(va, vb, vc));
ta = va - voff + 0.5f;
tb = vb - voff + 0.5f;
tc = vc - voff + 0.5f;
TIM->CCR[1] = (uint16_t)(ta * PWM_PERIOD);
TIM->CCR[2] = (uint16_t)(tb * PWM_PERIOD);
TIM->CCR[3] = (uint16_t)(tc * PWM_PERIOD);
这种“中心偏置SVPWM”和经典扇区法的输出波形完全等价,但代码量少很多,也不容易踩扇区判断的坑,适合快速落地。需要注意逆变器在线性区的最大相电压是母线电压除以根号3,一旦电压指令超过这个范围,必须做饱和限制,否则电流环会出现不可控的畸变。
4.4 从电流环到速度环的参数设计
FOC调试最容易迷路的环节是PID参数。电流环被控对象可以近似成一阶RL模型,阻抗是R加电感L,PI参数可以用一个很经典的零极点对消公式做初始化:
c复制current_kp = L * bandwidth;
current_ki = R * bandwidth;
这里bandwidth用弧度每秒。开关频率20kHz时,电流环带宽建议取1kHz到2kHz,也就是开关频率的1/20到1/10。L和R用LCR表实测电机相线,没有设备就查电机手册。按这个公式算出来的KI和KP通常已经很接近可行区,后面再根据阶跃响应微调增益。
速度环带宽要低得多,取1到10Hz就够。速度环输出限幅到额定电流的1.2倍左右,防止限幅时电流环疯涨。速度环这里一定要加积分分离或者防积分饱和,不然堵转时积分会堆到非常大,恢复瞬间产生可怕的电流冲击。我的调试顺序是先电流环,再速度环,电流环没稳之前不要去碰速度环。
5. 功能丰富究竟丰富在哪:转把、仪表、刹车与安全保护
5.1 转把处理、油门标定与防抖
市面上几乎所有的电动车转把都是霍尔型,有三根线,输出0.8V到3.6V左右的模拟电压,0.8V以下代表零位,3.6V以上代表满油门,部分转把带倒车或巡航开关。程序里首先要对模拟量做软件滤波,取多次采样中值或者滑动平均值,避免转把触点磨损后的抖动毛刺直接触发加速。
油门标定不能只做一次映射,我习惯在初始化时记录一次零位电压,运行中用相对量做归一化处理,这样转把个体差异、温漂都会被抵消掉。还有一个很重要的安全逻辑:上电后必须检测到转把回零位,才允许电机进入待命状态,否则直接故障,这是防止“一上电就飞车”的保命逻辑。目标转速发生变化时,不要一步跳变,用一个速率限制器把加速斜率压下来,加减速会平顺很多,机械冲击也小。
5.2 仪表通讯与故障上报
代步车的仪表一般是一个简单的数码管或者LCD,控制器通过UART上报数据。协议不需要多复杂,但要可靠。我用的帧结构是帧头、命令字、长度、数据体、CRC8校验,约50ms上报一次速度、母线电压、温度和故障码。
注意这里校验我只用CRC8,不用简单异或和校验和。转把上的噪声、高温环境下的丢帧,简单校验很容易漏。CRC8代价极低,查表表256字节,可靠性高很多。故障码报文设计上,每bit代表一类故障,比如bit0过流、bit1过温、bit2欠压、bit3转把失效,仪表收到后直接驱动报警灯,用户能直观看到问题。
5.3 再生制动与驻坡防溜
FOC让再生制动变得很自然:刹车信号触发以后,把q轴电流参考值平滑地变成负值,电机从电动状态转为发电状态,能量回充母线电容和电池。但要注意母线电压会因为这个过程升高,我做了限制逻辑,当母线电压超过额定电压的1.15倍时阶梯式减弱回馈电流,防止高压击穿功率管和电解电容。
驻坡防溜是电动三轮特别需要的功能。满载停在斜坡上,一旦松开转把车辆会后溜。实现方式是在零速维持模式下,检测转子转速反馈出现负值即判断后溜,给一个小的正q轴电流产生驻坡力矩,把车拉住。驻坡力矩不需要很大,但响应要快。机械鼓刹依然承担主要制动,再生制动只是辅助,软件里要把这两个逻辑协调好,避免出现“一边踩刹车一边给电”的矛盾工况。
5.4 堵转保护、防飞车与过温保护
功能丰富的控制器,保护逻辑一定不是靠硬件保险丝裸奔。我整理的这套方案里,保护分三层:硬件比较器最快,微秒级关断PWM;软件逐周期限流,每个PWM周期检查电流反馈并限幅;应用层做温和降额,比如逐步减小最大允许电流。
| 保护类型 | 判定条件 | 处理动作 |
|---|---|---|
| 堵转保护 | 转速低于阈值,且q轴电流大于80%额定,持续2秒 | 关PWM并上报故障 |
| 软件过流 | 三相电流任一大于软件限幅 | 当前周期削顶,连续超限关断 |
| 硬件过流 | 比较器触发 | 立即关PWM,进入故障锁存 |
| 过温保护 | NTC温度超过85℃ | 阶梯降额,95℃强制关断 |
| 欠压保护 | 母线电压低于欠压点 | 限制功率,深度欠压关断 |
堵转保护要特别小心:电动三轮车满载起步瞬间很容易出现短时堵转,如果保护太灵敏,车根本没法起步。所以要允许短时间大力矩,只是超过时间窗口才报故障。防飞车检测则是把转把零位信号、MOST温度、速度反馈三路互相比对,一旦转把霍尔失效导致信号漂移,立刻关输出。
6. 实测调试:现场最容易翻车的地方与排查经验
6.1 转子位置与相序不对,电机“抬轭”抖动
第一次给电机通电FOC,最常见的现象是电机不流畅转动,而是原地剧烈抖动,嗒嗒响甚至发生“抬轭”,此时电流飙升。大多数情况是位置反馈和三相相序没对齐。霍尔状态对应一个扇区,但霍尔线接到MCU的顺序和三相线接到逆变器的顺序如果不匹配,电流矢量方向就是错的。
排查链路不要一上来就调PID,先把位置基准搞对。我习惯先写一个开环拖动模式,给定一个慢速旋转的电角度,让电机像步进电机一样被“拖着转”。如果拖动时电机卡顿、反转或者抖动,说明霍尔顺序或相序有问题。这时候用手慢慢转动电机,观察三路霍尔状态跳变顺序,把霍尔状态编码和扇区基准表对齐。还有一个经常踩的坑:霍尔安装分布有60度和120度两种,代码里必须明确配置,否则插值出来的基准角度全错。
有时三相和霍尔都接对了,依然效率不高,这是因为霍尔安装存在机械偏差,需要一个角度补偿值。调试方法很简单:闭环空载旋转,从小到大调整hall_offset,观察相电流反馈中d轴电流分量,调到id_fb最接近0的位置,基本就是最佳补偿角。
6.2 电流环噪声、零漂与采样窗口
电流反馈上出现密密麻麻的毛刺,是FOC调试中绕不开的一关。很多人第一反应是PI参数太大,把增益调小,结果发现电流环变得更软,反而更难稳定。正确思路是先把噪声来源找出来。
第一个要查的是零漂。运放即使没有电流输入,输出也不是绝对的零电压,ADC读回来的码值里带了一个固定偏置。上电后电机不运行,先采样三相电流若干次做平均,保存为offset值,在电流环中断里第一时间扣掉。这个动作不做,id和iq反馈在零点附近都会有一个恒定偏差,轻则电流静态误差,重则零速时电机发热。
第二个要紧盯采样窗口。示波器探头接到运放输出端,如果毛刺正好出现在PWM边沿,说明ADC触发时刻没有避开开关噪声。把ADC触发点从PWM中点前后微调几百纳秒,观察电流反馈的离散程度。如果运放带宽不够,换增益带宽积更高的运放或者适当调整电阻网络,让信号建立时间小于采样保持时间。
6.3 简单的验证流程:从开环到闭环
很多新手调FOC喜欢一步到位直接把速度环闭环,然后一推电门就过流保护,完全不知道问题出在哪。我强烈建议按下面这套顺序来验证,每一步都有明确的通过标准,不通过就不往下走。
第一步是预定位测试。给一个固定d轴电流,电机应该发出清脆的“咔哒”声,然后定住,必须没有明显抖动。如果持续震动,检查位置反馈和d轴方向。第二步是开环拖动测试。给定线性递增电角度,电机应该像步进电机一样平稳旋转,空载电流保持在额定值10%以下。第三步是q轴电流闭环测试。给一个小幅iq参考值,用串口或调试器观察iq反馈能否在几十毫秒内跟随参考值,不能出现振荡或者等幅波动。第四步才轮到速度闭环,测试目标转速阶跃时,速度超调控制在10%以内,加速过程不出现明显的顿挫感。
这套流程看起来繁琐,但每次都能把问题隔离得很干净。做过几次之后你就明白,FOC调试的难点很多时候根本不是算法,而是位置反馈、采样窗口和噪声这些底层问题。我个人的习惯是,永远先确认开环拖动真的平稳,再碰电流环,不然把PI参数调乱了之后,你根本分不清到底是位置反馈有问题还是电流环参数有问题。这个顺序帮我省掉了至少一周的反复排查时间。
