ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

电机控制开源固件源码阅读路线:从SimpleFOC到VESC的入门方法

电机控制开源固件源码阅读路线:从SimpleFOC到VESC的入门方法

想读电机控制开源固件的源码,最纠结的往往不是"怎么读",而是"从哪个项目开始"。市面上能叫得上名字的项目一只手数不过来,每个都有人吹,每个看着都很有道理,结果就是收藏了一堆仓库链接,哪个都没真正翻过。这篇就把我的真实路线和取舍逻辑说清楚:先看圈子版图,再给一条我觉得最适合入门的主线,然后拆几个你必须先补的底层关节,最后把我踩过的一些坑直接摆出来,能帮你省下不少弯路。

1. 开源固件这个圈子的真实版图

先把市面上主流的电机控制开源项目捋一遍。这个领域不像Web框架那样百花齐放,真正值得读的、社区活跃的、代码有学习价值的,其实就四个方向:SimpleFOC、VESC、ODrive,以及ST官方的Motor Control SDK。每个我都实际跑过或者至少仔细翻过核心代码,下面的对比有我的个人判断,不是单纯堆参数。

项目硬件平台代码规模核心风格适合谁
SimpleFOCArduino、STM32、ESP32等小(几个核心类)极简、教学感强、算法与硬件分层清晰刚接触FOC、想快速跑通并看懂每一行的新手
VESC自研硬件为主,STM32F4/F7大(十万行级)工程级、功能完整、有RTOS和复杂通信协议做过几个项目、想深入工程细节的人
ODrive自研硬件,STM32F4/F7中大高性能伺服,代码紧凑,异步和实时控制混合对伺服性能有要求、想研究高功率密度控制的人
ST Motor Control SDKSTM32全系较大工业风格、CubeMX生成、偏工程模板做产品化、需要快速出原型并保持代码整饬的人

先说SimpleFOC。这个项目最初是Arduino生态系统里为无刷电机FOC控制做的一个库,后来扩展到了STM32、ESP32、Teensy这些更主流的平台。它的最大特点是代码量不大——整个核心库加起来可能也就二十来个文件,但每个文件职责非常单一:你看BLDCMotor这个类就知道它负责电机控制逻辑,MagneticSensorSPI负责编码器读取,InlineCurrentSense负责电流采样,driver相关文件负责PWM输出。算法代码和硬件抽象层做了非常清晰的隔离,这在开源项目里算是相当难得的。

VESC就不一样了。它本质上是一套完整的电动工具方案:固件里有电源管理、蓝牙通信、转速观测、电流控制、各种保护逻辑,还有配套的上位机软件VESC Tool。代码是典型的工程代码,很多状态机、协议解析、回调函数,初次打开容易有坠入迷宫的感觉。但如果你想理解一个产品级的电机控制器是怎么组织代码的,VESC绝对是绕不过去的标本。

ODrive走的是另一条路:高性能伺服。它的代码里大量使用实时控制和异步事件处理的混合架构,讲究的是低延迟、高带宽。里面还有完整的控制环设计文档,包括位置环、速度环、电流环的参数整定方法。这个项目建议至少读完一个规划文件再碰它的代码,否则很容易被里面的索引、状态机绕晕。

至于ST的Motor Control SDK,它在工程圈口碑一直有点两极分化:一方面它的代码确实严谨,各种保护逻辑、状态机设计非常完整;另一方面它和ST的硬件绑定得很深,CubeMX的生成代码一旦改动就很难升级,很多关键算法藏在库里只给你接口调用。这决定了它适合做产品参考,拿来做源码学习其实挺吃亏。

综合这些特点,我给的建议很明确:如果你是为了"学懂电机控制"而不是"学懂某个产品",从SimpleFOC开始是最合理的。它代码少、注释全、无抽象负担,你可以在一个周末里把FOC的核心链路从电流采样读到PWM输出。这篇后面绝大部分内容,也都围绕这一条主线来讲。

2. 为什么我建议第一条主线选SimpleFOC而不是VESC

我见过太多人一上来就开VESC,结果在第一周就萌生了弃坑的念头,原因不是他们不够聪明,而是VESC的开局门槛根本不在于算法,而在于它的复杂度。

VESC的代码量不是SimpleFOC能比的。它有一个完整的RTOS调度机制(早期是ChibiOS,新版本还有自己的调度方式),有概率性的时序错误排查需求,有庞大的commands.c里几百个COMMAND命令处理分支,还有配套的JSON配置系统。这些全部是为了在真实产品里能跑得稳、能被上位机配置、能OTA升级、能蓝牙通信。问题在于,这些功能对"理解FOC原理"这件事毫无帮助,反而会消耗你大量精力在无关的代码上。你打开VESC,第一眼看到的是mcpwm_foc.c文件里几千行的函数,其中混着电流控制器、速度控制器、负载角观测器、死区补偿、时序保护……你根本不知道该从哪个函数入手。

SimpleFOC就不一样。拿Arduino版本的SimpleFOC举例,整个电机控制的核心就集中在BLDCMotor这个类的几个方法里。Arduino的工程结构天然简单——setup()里做初始化,loop()里做周期性调用,你不需要弄懂RTOS调度,不需要理解消息队列,只要你会写Arduino的LED闪烁程序,就能开始用这个库点电机。而它内部的控制逻辑也极其清晰:loopFOC()做电流环和电压环,move()做外环目标追踪,函数名称就是它的行为,不需要看注释都能猜到七八成。

还有一个很现实的因素是社区和教程的可获取性。SimpleFOC在YouTube、CSDN、GitHub上有大量的DRV8316、L6234等驱动板跑通的例子,很多是中文的。文档站点也写得相当好,有完整的电机接线教学、参数选择建议、控制模式讲解。VESC的教程也不少,但更多是"怎么调整某个参数让车跑得更好",而不是"这里代码为什么会这样写"。

所以我的路线建议是这样:第一优先级是SimpleFOC,花一个月左右时间把它读完、读懂,期间配合一块便宜的开发板(比如Arduino Nano + L6234驱动板 + MT6701磁编码器,几十块钱就能搞定)跑通电机。等你能闭着眼画出电流采样到PWM输出的信号流了,第二优先级才是VESC。读VESC的时候你已经有了一个参考坐标系,知道"哦这个函数相当于SimpleFOC里的哪个环节",入门曲线会平缓太多。

如果你做产品的目标非常明确,比如就是要基于STM32出工业级驱动器,那可以在SimpleFOC之后直接切入ST Motor Control SDK,但心态要调整好:它是工程模板,不是教学材料,很多算法你只能通过接口调用去理解,没法像SimpleFOC那样一行行抠。

3. 读源码前必补的三个数学关节

很多人浮躁地跳过数学,直接硬看代码,结果在核心变换处彻底卡住。我只能说,FOC的数学真的没有想象中那么高不可攀,你不需要会证明,但你必须能看懂代码里那几行矩阵/复数运算是在干嘛。这里我提炼三个最关键的关节,每个都用最直白的方式解释,配合代码定位,帮你建立和源码之间的对应关系。

3.1 电流环:Clarke变换和Park变换到底是什么

FOC的核心思想是让三相电流变成两个可控的直流分量。三相电流是三个正弦波,相位各差120度,这没法直接用PID去控制好;但如果我们把坐标系变换一下,让坐标轴跟着转子磁极位置旋转,那么原本交变的量就变成了近似直流的量,就可以用经典的PID控得很稳。

这个变换分两步。第一步是Clarke变换:把三相坐标系(a、b、c)映射到两相静止坐标系(α、β)。代码里一般就是三个乘加操作,没什么神秘。第二步是Park变换:把α、β坐标系旋转到转子磁场坐标系(d、q)。这一步需要转子的电角度,所以编码器读数的准确性直接影响控制效果。

在SimpleFOC源码里,这两个变换不在一个地方,但它们都围绕BLDCMotor的运行逻辑展开。loopFOC()里会先做电流采样,然后做坐标变换,得到current.q和current.d,后面PID控制的输入就是这两个量。至于电压,则是先把PID输出的voltage.q和voltage.d做逆Park变换到αβ,再送SVPWM输出。你如果能从代码里找到_parkTransform、_clarkeTransform这组函数,并把它们的输入输出对着文档里的定义看一遍,基本就打通了一半。

3.2 PID在控制什么:电流环是绝对内环

FOC的PID是分层嵌套的:最外层是位置环或速度环,中间是速度环(如果你在外面还有位置环),最内层则是电流环。这个嵌套关系你必须在代码里找到对应的函数调用顺序,否则就会把三个PID糊在一起。

以SimpleFOC的move()和loopFOC()为例,控制流程大致是:move()负责目标角度或转速的处理,通过速度PID得到voltage.q的目标值;loopFOC()则是电流采样、电流PID到电压输出的通。这里有个反直觉的点:实际上SimpleFOC的电流环在用无感/有感模式时实现程度不一样,它有current_limit和 PI 电流控制器,但默认情况下你可能感觉不到电流环的存在,因为它不是总是被启用。这是许多初学者会困惑的地方。

我的建议是:先别纠结电流环在哪一层,先在代码里确认三个PID对象(PID速度环、PID角度环、PI电流环)各自被谁调用,输入量是什么,输出量去哪了。这时候把图景画出来,基本就理解了内环和外环的关系。

3.3 SVPWM:从电压矢量到PWM开关时间的最后一站

SVPWM(空间矢量脉宽调制)是FOC的最终输出级,它把d、q轴电压逆变换成α、β电压后,再合成出六个扇区对应的PWM波。理解它不需要会推导扇区判断,只需要看懂代码里是怎么根据电压矢量角度算出三个桥臂的占空比。

在SimpleFOC里,PWM输出实际是通过Driver类实现的,比如stm32_mcu.cpp里的setPwm,它根据phaseA、phaseB、phaseC电压产生对应占空比。SVPWM的算法细节分别在svm.h、utils.h等文件里,核心就是一个扇区判断加上占空比计算。如果只是看代码,你会看到一堆三角函数和判断分支;但你要做的不是逐行推,而是理解"电压矢量在空间里有六个扇区,每个扇区由两个相邻基础矢量合成"。

准备一个小工具会非常高效:用Python写一个简单的SVPWM可视化脚本,输入d/q电压和电角度,输出PWM波形,即时验证你对代码的理解。这个不用多复杂,几十行就能跑,比干看代码有效十倍。

3.4 锁定目标:带着电路图和时序图去映射代码

除了上述三个数学关节,我还强烈建议你在读代码之前先看硬件原理图。FOC的代码和硬件强耦合的地方特别多:PWM引脚的极性有没有翻转、ADC采样通道有没有接对、编码器的SPI时序能不能对得上,这些在代码里常常是通过一堆寄存器配置体现的,你不看原理图根本不知道为什么是那个值。

比如你用的SimpleFOC稳定版本里,驱动板的PWM引脚在BLDCMotor初始化时会被指定,而每个引脚对应的定时器和通道,其实是由MCI或底层stm32_mcu完成的。这些配置不是凭空来的,都是在参考原理图时确定的。如果你手里的板子文档不清晰,务必画出三相输出和采样电阻的对应关系图,否则调试时你会有种错觉:代码明明没抄错,电机怎么就是转不匀呢。

4. 从main()到PWM波:一条主线走通SimpleFOC

我接下来把SimpleFOC读源码的具体路径完整走一遍。这个项目在GitHub上的主库是simplefoc/Arduino-FOC,我个人建议直接读release版本而不是最新main分支,因为main分支有时候处于半重构状态,文档和数据手册不一定对得上。读的时候配合官方文档和原理图,按下面这条主线走。

4.1 先看定义:BLDCMotor类是怎么串起整个控制链的

打开src/BLDCMotor.cpp,对着头文件看它的成员变量和构造函数。你会看到Motor类里有sensor、driver、current_sense这些指针成员,都是在init()里被链接进来的。这个设计好就好在,它把"电机本体"和"感知、驱动"解耦了,你换驱动板不用改算法代码。

然后看init()方法,它做的事情大概包括:连接driver和sensor、设置PWM频率、初始化ADC、校准编码器。这是整个代码里最容易让人走神的部分,因为内容很多,但每一块都值得认真看。比如编码器校准,它在initFOC()里会通过sensor->init()启动一轮开环对齐,让转子转到电角度的零位置,这个操作你会实际观察到电机动一下就是它干的。

4.2 跟着loop走:FOC控制周期的骨架

Arduino的loop()循环里,典型写法是:

void loop() { motor.loopFOC(); motor.move(target); }

这两个就是核心。loopFOC()顺序大致是:读取电角度 -> 电流采样 -> Clarke/Park变换 -> 电流PID -> 逆Park -> 设置PWM。你要做的不是读完整个函数,而是把里面每一个子步骤对应的变量变化用手写/画图的方式捋一遍。

我的具体做法是给这段代码加大量printf调试输出,打印当前编码器角度、采样的三相电流、变换后的Id/Iq、PID输出值。在电机开环转起来(没转也行,用手慢慢转动电机)时观察这些数值的变化趋势,比盯着代码想象要直观太多。

move()的逻辑相对简单:根据选择的位置/速度控制模式,调用对应的PID控制器算目标电压。它本身不涉及电流采样,只负责目标跟随。这两个函数就是你画信号流图的骨架。

4.3 电流采样到坐标系变换:代码里别放过这几个函数

在loopFOC()里,当配置了电流传感器时,current_sense->getPhaseCurrents()返回三相电流值。接着调clarkeTransform和parkTransform,得到current.q和current.d。这两步变换的代码很短,但值得你逐行注释一遍。多花点时间,因为后面你读其他项目(比如VESC的mcpwm_foc.c)时,会发现同样的变换换了不同的变量名写在不同的位置,但本质完全一样。

4.4 PWM输出:从电压矢量到门上管的占空比

当loopFOC()计算出期望的Uα、Uβ后,下一步会调用driver->setPwm。SimpleFOC默认用的是BLDCDriver3PWM,它内部会调stm32_mcu.cpp或Arduino对应的硬件层函数。如果用的是STM32版,你会看到定时器PWM输出、互补输出、死区设置这些关键配置。死区这个参数特别重要,它防止上下桥臂直通短路,在真实硬件上不设够是会炸管或者发热的。

从这个输出链路再往细里走,就是SVPWM和死区的具体实现了。暂时不用继续深入寄存器,能把代码和波形对应上就完成了我说的第一阶段。

4.5 验证理解的三个小实验

光读不练很容易虚假繁荣,我建议你按顺序做三个实验来校验理解:

  1. 开环匀速:暂时不闭合角度,通过设置Uq为固定值、Ud=0,直接强制电机转动,观察PWM波形和电机转速关系。
  2. 闭环速度:让电机用速度PID自己稳住目标转数,打印出voltage.q的变化曲线,验证PID到底是怎么修正的。
  3. 负载扰动:用手轻轻捏住电机轴(扭矩别太大),观察电流和电压的变化,这一步能让你非常直观地理解电流环的作用。

这三个实验做完,你对SimpleFOC的信心会比读十遍代码强得多。

5. 换到STM32生态后,VESC的打开方式

当你把SimpleFOC吃透,再回头打开VESC,你的感觉会是"这里面的函数我虽然看不到,但信号流我已经能猜个大概了"。这时候再谈读VESC,就有了基础和底气。

5.1 先看VESC的整体分层:别被commands.c吓住

VESC源码里最有名的文件就是commands.c,它是通信命令的主处理入口,从UART、CAN、USB传来的指令都会汇聚到这里,然后分发到对应的控制模块。很多人第一次打开这个文件就被几百行switch/case劝退了。

但请你绕过它,直接进mcpwm_foc.c。这个文件里有两个核心函数你需要重点研究:mcpwm_foc_control()和mcpwm_foc_set_configuration()。前者是FOC控制主循环,后者是用来给控制参数赋值的。你会在mcpwm_foc_control()里看到速度控制环、电流控制环、电压输出、死区补偿、过流保护等一圈流程,调度周期很紧,时序要求高,这也是VESC能做高性能驱动的原因之一。

5.2 先抓到配置结构体,再顺着配置逻辑走

VESC的设计哲学是"上位机改配置,代码不动":几乎所有的控制参数都可以运行时修改,都塞在一个庞大的motor_config(mcconf)结构体里。你必须在读代码之前搞清楚这个结构体里到底有哪些字段,不然很容易看到一个函数调用链里塞了20多个参数就懵了。

我的建议是:用VESC Tool连接一块真实的VESC硬件,导出完整的.xml配置文件,然后对着代码把配置文件里的每个字段逐个映射到结构体成员。这个过程虽然慢,但极其见功底,因为你在做的其实是"逆向理解整个配置系统",搞懂一次之后,你再去看别的电机控制项目的配置方案(比如ODrive的odrive-*.json),会比其他人大脑清晰几个level。

5.3 VESC里值得重点读的三个模块

第一个是电流控制器。VESC的电流环控制在mcpwm_foc.c里会调用一个叫mcpwm_foc_control_loop的函数,里面包括了PI控制器、一阶滤波、弱磁控制等。和SimpleFOC不同,它对时序的要求非常严格,控制周期是微秒级的,这让你意识到工程代码和教学代码的差距。

第二个是观测器。VESC支持无感模式,里面有很多关于转子角度、转速估算的算法,代码在mcpwm_foc_obs.c里。无感控制的数学其实不简单,但读代码时可以先把重点放在"输入信号是什么(反电动势)、输出信号是什么(转子角度估算)",不必纠结具体的推导过程。

第三个是保护逻辑。VESC非常重视过流、过热、欠压保护,这部分逻辑星罗棋布地散在控制周期里。你如果从这学起,会对"控制器不仅仅是控制算法,更是一个安全系统"有直观认识。

5.4 为什么我不建议用ST MC SDK入门

ST的Motor Control SDK对很多人来说有种"官方 = 标准"的错觉,但它的架构并不适合初学者做源码阅读。它的代码很大一部分是自动生成的,算法模块以库的形式提供(比如MC_StateMachine、STM32MotorControl_ALgo),你没有源码就去抠封装好的API,可读性很差。而且它的配置依赖CubeMX图形界面,改一个细节几乎要把代码重新生成一遍,这对源码阅读者来说非常痛苦。

它最大的价值在于:你把它和SimpleFOC对照着看,能明白"工程经理在组织代码时为什么选择这种结构"。产品化过程中,状态机设计、安全监控、参数版本管理这些可维护性诉求远比算法本身重要。等你有能力评价这个取舍时,再看ST的上手收益才开始显现。

6. 读代码初期最容易陷进去的三个坑

到这一部分,我讲的全是自己或者身边做电机控制的朋友实际踩过的坑。这些坑的共性在于:不是算法太难,而是读的方式错了。希望你能绕过去。

6.1 坑一:一头扎进数学推导,出不来就弃坑

现在很多资料一讲到FOC就疯狂堆公式:傅里叶、旋转磁场、空间矢量的几何证明……如果你是非科班背景,很容易在这里产生"我还没准备好"的错觉,于是去啃理论,一两周后发现还是写不出一个PWM,就放弃了。

我的建议是彻底反过来:先用代码驱动你学数学。你不需要推导出Clarke/Park变换,只需要知道它把一个量弄到了哪个坐标轴、PID接下来要控制的是哪个量。具体操作就是拿着代码,把关键中间变量的数值打出来,用实际数字去感受变换的意义。等你在工程里遇到"为什么电感会引发电流滞后"这类问题时,再回来看数学推导,效率会高出几个档次。

6.2 坑二:低估了硬件差异对代码行为的影响

同一套SimpleFOC代码,在官方推荐的Arduino Nano + L6234驱动板上跑得很丝滑,换到你自己手工焊接的板子上就可能电机啸叫、电流波形奇怪甚至直接烧驱动。这不是代码问题,而是硬件电路差异导致的:采样电阻的位置、栅极驱动器的死区设置、PWM引脚的滤波电容都会影响控制品质。

读源码时一定要养成"结合原理图分析"的习惯。任何PWM 출력引脚映射、ADC采样通道配置都必须对着器件的Datasheet看,不动硬件原理就读代码,很容易在人云亦云中失去自己的判断力。尤其是你在做电流采样时,会看到很多滤波器的截止频率设置,这些值不是随手来的,而是根据驱动板硬件参数传上去的。

6.3 坑三:目标漂移,把"学习和理解"变成了"搭一个能跑的项目"

很多人一开始说"我想读源码",读着读着就变成了"我想让电机在我的板子上转起来"。这个目标转换未必是坏事,但如果它让你直接从源码读飘到了查驱动手册、配CubeMX逻辑分析仪,那你的原始"读源码"计划就会不了了之,哪怕最终电机真的转起来了,你还是不了解控制内部发生了什么。

我给自己定的规矩是:每次打开源码,必须有具体问题在手,比如"角度环到速度环的串联是哪里发生的?"或"限幅在这个项目中是哪个语句实现的?"带着问题找答案,会比你通读一遍后说"好像都懂了但什么都没懂"强得多。这个问题清单可以是你调试时遇到的真实bug,也可以是从文档里看到但存疑的某个说明。当你发现自己代码越读越深,而且已经能用源码里的机制解释一个非debug驱动板的现象时,恭喜你,这才算真正入门了。

如果你也准备走这条路线,我的建议就是这一句话:不要从最"出名"的项目开始,要从最"薄"的项目开始。SimpleFOC之后是VESC,VESC之后你自然会有能力判断要不要碰ODrive或ST的库。这个顺序看起来慢,但实际是最快的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进