如果你搜“电机控制开源固件源码”之后,在 GitHub 上同时打开了三四个仓库却不知道该先点开哪一个,我可以肯定地告诉你:你遇到的问题不是资料太少,而是项目选错了。我第一次下 VESC 源码的时候,对着那个几千行的mcpwm_foc.c看了整整一个晚上,连主循环在哪儿都没找到。后来换了个思路,从一个看起来更“初级”的项目入手,两天就把 FOC 的主干理清了。这篇文章就是把我验证过的选择顺序和阅读路线分享出来,给准备读电机控制开源固件源码的人一条可复用的路径。
1. 读源码前先对号入座:你的目标决定第一个项目
很多人打开仓库的第一反应是“哪个 star 多就读哪个”,这其实是最大的误区。电机控制固件不像普通 Web 项目,项目与项目之间的定位差异非常大。先想清楚你读源码到底是为了什么,再往下选,能帮你至少省掉两周的弯路。
1.1 三种典型诉求,对应三个完全不同的切入点
我把想读源码的人分成三类,你可以自己对号入座。
第一类,纯好奇型。你听过 FOC、BLDC、PID,想搞明白从传感器读数到 PWM 占空比之间到底发生了什么,不急着做产品,手头大概率只有一块开发板。这类人的首选是 SimpleFOC,原因是它把 FOC 主轴暴露得非常干净,电机换相、坐标变换、电流采样这些步骤都以最少的封装放在你眼前。
第二类,项目驱动型。你在做平衡车、机械臂、轮式机器人,需要一套可以真跑起来的固件,并且要能根据机械结构去改参数甚至改逻辑。这类人的首选是 ODrive,它的状态机、参数系统、通信协议都非常接近一个真实产品,源码组织也明显比 VESC 有章法。
第三类,产品研发型。你就是要做量产电机控制器,关心的是过流保护怎么设计、启动失败怎么恢复、量产标定怎么做。这类人绕不开 ST MCSDK 和 VESC,它们才是“能把电机控制在复杂工况下运行”的代表。
三者的阅读方式完全不同:第一类适合从头到尾读主干,第二类适合带着需求去读模块,第三类适合当成“字典”按图索骥。
1.2 先认清电机类型,再谈固件范围
电机控制开源固件这个名字听起来范围很大,但实际上值得花时间去读的存量项目,绝大多数都围绕一个核心:BLDC 和 PMSM 的无感或带感 FOC。
原因很容易理解:有刷电机用 PWM 调速加一个 PID 就够用了,逻辑太简单,没有太多源码可读;步进电机虽然也有开环/闭环控制的说法,但主流开源项目里针对它做全功能固件的不多;真正让项目复杂度飙升、让全世界工程师反复折腾的,是 BLDC/PMSM 的 FOC 控制——坐标变换、电流环、速度环、位置环、编码器校准、死区补偿、无感观测器,这些内容堆在一起,才构成了“值得读的可源代码”。
所以如果你问我“读电机控制开源固件从哪儿开始”,本质上问题已经被自动收敛成:“读 FOC 固件从哪儿开始”。想明白这一点,你就不会再浪费时间去纠结要不要先学有刷电机的库。
1.3 四个快速判断源码是否适合你当前阶段的指标
我后来养成了习惯:面对任何不熟悉的开源固件,先看四个指标,再决定要不要投入时间。
- README 是否说清硬件要求。一个 README 里写“需要 VESC6 硬件,请参考 hw/ 目录”的项目,和另一个写“Arduino 或 STM32 均可,默认引脚如下”的项目,面对的人群完全不同。说不清硬件要求的,默认你已经有它那套板子。
- 依赖是否克制。SimpleFOC 的核心依赖几乎就是标准 Arduino/STM32 外设库;Moteus 则依赖自己的异步框架;VESC 直接绑定特定 PCB 的 IO 映射。依赖越重的项目,纯读码的成本越高。
- 入口是否好找。一个项目的
main()或者核心循环是藏在三层回调里,还是在examples/里就能直接看到,决定了你前三天能不能建立起“代码在干什么”的心理模型。 - 活跃度与历史包袱。star 数量高不等于适合读。真正要关注的是这个项目是否长期有 commit,以及它是否为了兼容老硬件而堆积了大量分支判断。历史包袱越重的代码,越不适合当教材。
用这四个指标去过滤,你会发现每个项目适合的阶段其实是清晰的:SimpleFOC 适合当第一课,ODrive 适合当进阶课,VESC/Moteus/ST MCSDK 适合当你已经有了判断力之后再上手的对象。
2. 最热门的 VESC 为什么是最不适合新手的第一站
我知道这个结论有点反直觉,毕竟 VESC 是电动滑板、电动自行车圈子里被反复提及的明星项目,star 高、社区活跃、资料多。但正因为它太“真实”了,反而不适合零基础读源码的人。
2.1 VESC 的源码仓库到底长什么样
VESC 的固件主要集中在两个仓库:老一些的bldc和面向 VESC6 的vesc_fw。打开之后你先面临的是一堆顶层目录:app/是应用逻辑,hw/是硬件抽象,lib/是滤波器和算法库,recovery/是恢复固件,还有一大堆与具体板卡绑定的配置文件。
核心的 FOC 逻辑大部分集中在一个叫mcpwm_foc.c的文件里。这个文件我在不同阶段打开过很多次,第一次见它时,我的感觉是“这不像一个可以读的教程,更像一块经历了无数次迭代的化石”。它把 ADC 采样配置、Clarke 变换、Park 变换、反 Park、SVPWM、死区补偿、弱磁控制、故障保护全部糅合在一个大文件里,函数与函数之间还频繁通过全局变量和宏定义来传递数据。
作为对比,SimpleFOC 会把这些步骤分成清清楚楚的类和文件:电流采样在 CurrentSense,坐标变换函数单独存在mod_utils.h,PID 在PIDController.cpp。VESC 不是故意写得乱,而是它的代码是从真实硬件迭代中“长”出来的,而不是从教学需求中“设计”出来的。
2.2 对新手劝退的三个客观事实
第一,硬件绑定极其严重。你打开 VESC6 的固件,能看到大量诸如IO_MC_xxx_GPIO_Port、ADC_CHANNEL_xxx这样的宏,它们直接对应 VESC6 那块具体 PCB 的引脚。如果你想移植到另一个芯片上,第一周基本就是跟这些宏搏斗。
第二,历史兼容代码占了大量篇幅。VESC 要同时支持 VESC4/VESC6 以及各种衍生硬件,所以你在主文件里会看到大量“根据硬件版本选择行为”的分支。这些并非核心控制逻辑,但会严重干扰阅读。
第三,调用链埋在多层回调里。VESC 的实时控制并不是一个简单的主循环,而是分布在中断、DMA 回调、任务切换之间。对于刚接触电机控制的人来说,你甚至很难回答一个最基本的问题:“这个周期里电流环在哪里被调用?”而在 SimpleFOC 里,这个答案就是一行loopFOC(),清清楚楚。
2.3 VESC 不是不能读,而是要等到这个阶段
我并不是说 VESC 不能读,而是它应该被排在某个更后面的位置。当你已经在 SimpleFOC 里理解了 FOC 的基本链路,又在 ODrive 里理解了状态机和工程化框架之后,VESC 才会变成一个巨大的“宝库”——并且它最值得读的不是整体流程,而是细节处理。
比如,VESC 在处理电流采样延迟、谐波抑制、弱磁控制、参数辨识这些真实工况问题时,有很多非常成熟的做法。到那个阶段,你是带着明确问题去查mcpwm_foc.c的某个具体段落的,而不是试图把几万行从头读到尾。读源码最怕的是“线性阅读”,读 VESC 尤其不能这么干,必须“目标导向”。
3. 第一站选 SimpleFOC:两小时理清 FOC 主干的源码工程
如果你接受了前面的逻辑,接下来就是对的项目了。SimpleFOC 是我目前见过的、最适合作为第一个完整读下来的 FOC 开源固件。它不追求极致的代码性能,但追求“把自己讲明白”。
3.1 SimpleFOC 的仓库结构:它到底“小”在哪
打开 SimpleFOC 的仓库,核心代码在src/下面,里面包含了BLDCMotor、StepperMotor、FOCMotor这些电机类,以及MagneticSensor、CurrentSense、PIDController、LowPassFilter这类支撑模块。初次看目录,你不会像看 VESC 那样感到不知所措。
要理解 SimpleFOC 的“小”,不妨把它和 VESC 做个对比。VESC 一个mcpwm_foc.c承担的角色,在 SimpleFOC 里被拆成了几个实体:BLDCMotor负责整体电机状态和控制模式,BLDCDriver负责 PWM 输出,CurrentSense负责采样电流并完成 Clarke/Park 变换前的原始数据获取,MagneticSensor负责从编码器读取角度。这种“一个类只干一件事”的结构,正是初学者最喜欢的。
3.2 核心阅读路线:从 loopFOC 到电流环和编码器
我不建议你从main()开始读。SimpleFOC 的推荐路径是,先打开examples/里任意一个“速度控制”的示例,看清楚用户程序如何初始化、如何调用loopFOC()和move(),然后顺着这两个入口去追代码。
先说loopFOC(),这是 SimplicityFOC 把 FOC 主轴暴露得最干净的地方,它做的事情可以概括为三步:读取编码器角度、读取电流传感器的原始值并做坐标变换、把当前角度和电流交给 FOC 算法计算出三相电压矢量。你顺着这个函数看进去,就能把 Clarke/Park/反 Park 变换在一小时内串起来。
// 简化示意:看清 loopFOC 行为 void BLDCMotor::loopFOC() { // 1. 获取当前电角度(传感器数据 + 电气角度偏移) // 2. 读取电流,执行 Clarke 变换和Park 变换 // 3. 调用 FOC 算法(电压/电流指令 -> 目标 Iq/Id) // 4. 反 Park 变换 + SVPWM -> 更新三相占空比 }然后是PIDController.cpp。大部分人对 PID 的理解停留在“P、I、D 三个参数”,但读完 SimpleFOC 的 PID 实现,你会看到更关键的东西:输出限幅、积分分离、防积分饱和。这些在教科书里往往一笔带过,但在真实电机上,积分饱和才是最常见的问题。
编码器部分我会建议你优先看MagneticSensor的实现。无刷电机 FOC 里“电角度怎么得到”是非常关键的一环,SimpleFOC 的 SPI/I2C 磁性编码器实现足够短、也足够完整,涉及原点校准、角度换算、方向判断,这些细节在其他项目里往往被封装得更深。
3.3 边读边转:在真实电机上验证的硬件组合
纯读代码有一个天花板:你不把代码跑起来,就无法真正理解为什么“角度要减去偏移量”,为什么“电流采样要等一个 ADC 周期才能用”。
所以我给你一个可以在一天内搭出来的验证组合:STM32F103 或 Arduino 开发板,一块支持 SimpleFOC 的 BLDC 驱动板(比如基于 DRV8301 或类似芯片的板子),一个 AS5600 磁编码器,一个低压云台无刷电机。用串口打印角度和电流值,观察电机在不同电压、不同 P/I 参数下的反应。
这套硬件在开源社区里的资料足够多,踩坑也有前人填过。当你亲手调大PID_velocity.P看到电机开始啸叫,再回到源码里找“为什么输出不平滑”时,那些代码就不再是抽象符号了。
4. 第二站看 ODrive:从“能转”进阶到“工程化”
读完 SimpleFOC,你已经知道 FOC 是怎么回事了。但如果你去找工作或者做产品,你会发现 SimpleFOC 的代码距离“当一个伺服驱动器用”还差一大截。ODrive 就是补上这一大截的第二站。
4.1 为什么是 ODrive 而不是其他项目
原因有三个。
第一,ODrive 是真正的 C++ 工程,模块划分直接对标产品级控制器。你打开它的固件目录,能看到Axis、MotorControl、Controller、Sensor、Communication这些职责明确的概念。相对 VESC 的“大文件集成”,ODrive 的类结构更接近一个标准的嵌入式软件架构。
第二,ODrive 自带完善的用户态支持。你可以用它的odrivetool通过 USB 连接板子,执行校准、修改参数、读取状态。这意味着你在读源码的同时,始终可以通过实际设备的响应来验证自己的理解。
第三,也是我最看重的:ODrive 的Axis(轴)抽象。它把“一个电机 + 一个编码器 + 一圈控制逻辑”封装成一个轴对象,多个轴可以独立或协同工作。这个抽象在双轮机器人、云台、差分驱动系统里非常自然,读懂了 Axis,你对“实时控制系统如何组织”的理解会提升一大截。
4.2 从 Axis 状态机进入 ODrive 的世界
我建议你从Axis的状态机开始读,而不是从电流环开始。原因很简单:ODrive 的电流环、速度环、位置环本身和 SimpleFOC 差别不大,你已经会了;它真正区别于 SimpleFOC 的是那套“轴状态管理”。
Axis维护着一个状态枚举,大致包括:AXIS_STATE_IDLE、AXIS_STATE_STARTUP_SEQUENCE、AXIS_STATE_HOMING、AXIS_STATE_CLOSED_LOOP_CONTROL等。轴在非使能状态时,几乎不会去执行 FOC;只有请求切换到CLOSED_LOOP_CONTROL之后,控制环才会真正跑起来。你可以在用户态用一行指令切换状态:
odrv0.axis0.requested_state = AXIS_STATE_CLOSED_LOOP_CONTROL顺着Axis::run_state_machine_loop读下去,你会看到一个清晰的有限状态机:调度器根据请求状态,在空闲、校准、闭环控制之间跳转。每一跳都会前置检查,比如没有校准就不能进入闭环,编码器没有就位就不能进入控制。这套东西在 SimpleFOC 里是没有的,因为 SimpleFOC 默认你“自己管好流程”;到了 ODrive,你会发现工程化固件的核心并不只是“算得准”,而是“在错误面前不失控”。
状态机读完之后,再去看MotorControl和Controller的更新函数。你会看到级联控制的完整实现:位置环输出目标速度,速度环输出目标电流,电流环输出目标电压。这正是你以后做高性能伺服时最常接触的架构。
4.3 ODrive 源码里最值得偷学的工程细节
从 ODrive 里能偷到的东西,放在 SimpleFOC 里是看不到的。
- 编码器校准流程。ODrive 上电后的第一步通常是对电机执行校准,它不是简单地读一下零点,而是通过注入特定电压、观察电流和编码器位置变化来判断极对数、相序和零点偏移。这套校准逻辑在量产产品里是刚需。
- 参数系统与掉电保存。你通过 odrivetool 改的参数会被序列化到 FLASH,下次上电自动恢复。这背后是一套配置存取机制,很多新手第一次看到会以为“只是保存变量”,实际上它涉及数据结构定义、串行化、启动时的版本兼容处理。
- 故障保护与重试策略。ODrive 不是出故障就死机,而是进入某种错误状态后,可以通过用户指令清除错误并重新尝试。这种“可恢复性”设计,是前一个阶段读 SimpleFOC 时完全接触不到的工程思维。
读 ODrive 的时候我建议你多问一个问题:“如果这个模块温度超过 80 摄氏度,固件会做什么?”你会发现在真实项目里,温度、电压、电流、编码器异常这些分支,占据了源码体积的三分之一以上,而这部分恰恰是产品能不能稳定的关键。
5. 进阶之后再碰 VESC、Moteus 与 ST MCSDK,分工完全不同
当你按照前面的顺序把 SimpleFOC 和 ODrive 读明白了,你对电机控制开源固件已经有了自己的框架。这时候再回头看 VESC、Moteus、ST MCSDK,你能清楚地意识到:它们并不是“更难版本”的同一种东西,而是面对完全不同问题的答案。
5.1 VESC:处理真实噪声的高阶读法
VESC 最值得读的不是它的整体架构,而是它对“非理想信号”的处理方式。电机控制源码读到最后,拼的往往不是那个理想 FOC 模型,而是怎么抵抗编码器噪声、电流采样纹波、逆变器死区效应。
我建议你带着具体目标去读 VESC。比如:
- 它的电流滤波做了哪些处理,和 SimpleFOC 的单级低通滤波有什么不同。
- 它的速度估计在低速段有什么技巧。
- 它的保护逻辑在过流/过温/欠压时按什么优先级触发。
把mcpwm_foc.c当成一部“实战手册”,你看到一个不理解的点,就回到 ODrive 或 SimpleFOC 里找对应环节做横向对比。这种“三份源码对照着读”的方法,比单独啃任何一份都有效。
5.2 Moteus:看看现代 C++ 怎么做实时控制
如果你对机器人控制、四足、机械臂这类高动态场景感兴趣,Moteus 是绕不开的。它和 ODrive 面对同样的问题域,但实现风格截然不同。
Moteus 的固件用了很现代的 C++ 写法,并且建立了一套异步事件框架,很多控制逻辑不是直观的顺序调用,而是通过任务调度和回调组织起来的。初次读者可能会觉得比 VESC 还难进入,但它的代码质量、测试覆盖、单元测试组织都非常好。
我的建议是:先读它的文档和测试用例,再回去读实现。Moteus 本身就是朝着“高带宽、低延迟力矩控制”去设计的,你读到它的 CAN 通信层和内部轨迹生成模块时,会看到大量针对实时性的打磨。对想深入机器人方向的人来说,这一站比 VESC 更具启发性。
5.3 ST MCSDK:走向量产绕不开的官方 SDK
如果你最终要做的是基于 STM32 的电机控制器产品,ST 官方的 X-CUBE-MCSDK 会出现在你的职业生涯里。它的定位不是“学习 FOC”的教材,而是一套可以生成工程、带图形化调参工具的开发套件。
读 ST MCSDK 的时候切记别用“从头读到尾”的心态。它由工具自动生成大量配置代码,目录层级非常深,模块间的宏定义非常多。你真正该关注的是几个核心接口区域:速度/位置观测器接口、电流环执行入口、故障处理回调。如果你已经有了前几站打下的 FOC 认知,即使是在这样一个庞杂的 SDK 里,你也能快速定位“哪里是主算法、哪里只是胶水代码”。
6. 让代码“活”起来:示波器、逻辑分析仪与一次只改一个变量
最后一个部分,我想聊的不是选哪个项目,而是怎么看源码才能真的看懂。电机控制固件是实时系统,如果你全程只在纸上或屏幕上读代码,很多逻辑你看十遍也不明白为什么这么写。
6.1 在中断函数里下断点前,先想清楚 MOS 管
我见过不少新人第一次用调试器看 FOC 源码,兴奋地在电流环更新函数里下了断点,然后单步执行。结果电机发出尖锐的啸叫,母线电流值异常高,甚至炸驱动。
原因是:实时控制循环的执行周期非常短(可能只有 10kHz 甚至更高),你在中断里单步执行时,电机已经失去了正常换相节奏,功率管开关时序混乱,电流瞬间飙高。在控制中断里单步调试电机控制代码,是非常危险的操作。
更安全的做法是:用逻辑分析仪或示波器观察 PWM 输出和编码器信号,用串口打印关键变量,把代码作为静态对象去理解,而不是靠断点去冻结一个正在运行的系统。非要下断点,优先断在故障保护分支里,不要断在正常控制路径的核心位置。
6.2 阅读顺序建议:改参数、改逻辑、最后改硬件
我总结过一个顺序,基本不会出错。
第一步,先跑通官方示例。无论你选哪个项目,先让电机转起来,用默认参数运行。整个过程里,你的目标是建立“代码-现象”的映射:这个参数让转速变快,那个参数让电流变大。
第二步,只改参数,不改逻辑。把 PID 的 P 调大,观察振荡;把限流值改小,观察输出受限。通过修改值域,你能快速理解源码里每个变量的实际意义。这一步中“一次只改一个变量”是铁律,改两个以上参数出了问题,你根本不知道是哪个造成的。
第三步,改逻辑,比如换一种调制方式或换一个控制模式。这时候你再去增删代码,因为你已经知道哪个文件、哪个函数在做什么,改动是可控的。
第四步,换硬件平台。这一步能检验你读源码是否读出了“抽象层”。如果你能把 SimpleFOC 从 Arduino 移到另一块 MCU 上,说明你真正理解了哪些代码依赖硬件、哪些代码是纯粹的算法。
6.3 几个我从新手期带出来的实操习惯
我在读源码阶段踩过不少坑,有几个习惯如果能早点养成,会省非常多时间。
- 先画状态图再看代码。任何一个固件,先用半小时从文档/示例里提炼出它有哪些状态、状态之间怎么跳转,再对照源码验证。电机控制固件再复杂,也是在一个状态框架里打转。
- 读代码前先看 release notes。开源固件每次发版都会解释“为什么改”,这是理解项目演进方向的最便宜途径。
- 把源码里反直觉的注释抄下来。电机控制代码里经常有“看起来很奇怪但实测有效”的写法,比如死区补偿的特殊处理。这些“反直觉点”往往就是真实项目最值钱的经验。
落到行动上,我现在拿到一个陌生的电机控制开源固件,标准动作永远是:先跑 example,再找状态机,然后用串口和示波器把这个状态机的每个跳转条件验证一遍,最后才回去读核心控制环。这个方法能让你从“读得懂”,变成“敢修改,敢排查”,到那个程度,换项目就只是一个熟悉接口的过程,而不是重新学习的过程。