
简介基于STM32的无刷直流电机控制器程序是一份面向电机控制与嵌入式开发场景的完整工程源码适合需要深入理解 BLDC 换相、PWM 调速及保护机制的工程师、学生和相关竞赛选手参考。工程共 108 个文件以 57 个 H 头文件和 35 个 C 源文件为主覆盖定时器、ADC、USART、Flash、CAN 等 STM32 标准外设驱动同时包含启动汇编、链接映射文件和 Keil 工程配置整体压缩包仅 256KB结构清晰便于逐模块阅读与二次开发。源码围绕三相逆变器控制、反电动势无传感器六步换相、PWM 占空比调速、过流与过热保护等核心问题展开并展示了中断实时监控、异常处理以及多任务调度思路对反电动势采样时序、换相点判断和 PWM 更新时刻等细节也有完整呈现。代码注释清晰外设驱动与电机控制逻辑分层明确配合 ST-Link/J-Link 在线调试可快速部署到实际电路板验证。当前已有 1746 人学习或下载适合希望从源码层面掌握 BLDC 控制流程并在自有硬件上完成移植、修改与功能扩展的嵌入式开发者。1. 先判断你该写哪种控制器六步换相与FOC的差别不是玄学1.1 说在前面这类程序到底在干什么做 STM32 的工程师迟早会被一个三相无刷直流电机递到你面前。它的控制器程序和普通直流电机完全不同你不能只给它通电你得知道转子现在在哪然后按顺序点燃三相绕组把它转起来。这个按顺序点燃的过程就叫换相整个控制器程序的核心其实就是围绕这件事做文章。我见过太多人一上来就搜FOC觉得不上 FOC 就不专业。但实际项目中很多风机、水泵、电动工具、车模用六步换相就非常成熟程序量比 FOC 少一个量级调试周期也短得多。这篇文章就以 STM32 为主线把霍尔方波六步换相从输入捕获、PWM 生成到 PID 闭环控制的完整链路讲清楚最后再聊几个我实测里踩过的坑。打算自己写一版无刷直流电机控制器程序的朋友可以直接照着这条路线走。1.2 六步换相控制的本质是六个状态的轮换无刷直流电机没有换向器三相绕组需要外部逆变桥按顺序通电才能让磁场始终拉着转子跑。霍尔方案会在电机里装三个霍尔传感器转子每转 60° 电角度霍尔输出组合就变化一次一个电周期内一共会产生六种有效组合所以叫六步换相。每次换相时逆变桥只导通两个管子一个高侧 MOS 做 PWM 斩波一个低侧 MOS 长通第三相悬空。这样做的原因很实际少一个开关管动作就少一半开关损耗低侧也方便放采样电阻做电流检测。换相顺序一旦错乱轻则抖动重则堵转甚至烧管子所以程序里那张换相表是整个工程的关键资产。1.3 FOC适合什么场景程序量差在哪FOC磁场定向控制是不一样的技术路线。它把三相电流通过 Clarke 和 Park 变换映射到 d-q 坐标系让定子电流的力矩分量和磁场分量解耦从而做到正弦电流驱动、全转速范围平滑运行。低噪声、高动态响应、低速大扭矩这些优点都是实打实的。但代价也明显需要至少两路相电流采样需要位置传感器或者高精度观测器需要 SVPWM 调制还需要多环 PI 调节器。简单对比一下维度六步换相霍尔FOC传感器3 个霍尔即可相电流采样 转子位置换相方式60° 电角度跳变连续矢量旋转转矩脉动较大有换相噪声小运行安静程序复杂度可控一个状态机搞定需要数学库和大量浮点运算适合场景风机、水泵、电动工具、车模机器人关节、无人机、伺服类1.4 这篇文章的路线选择我这边项目是 200W 以内的航模电机带动小负载噪声要求不算苛刻因此霍尔六步换相加双闭环是性价比最高的路线。程序结构清晰出了问题也好定位。后面如果要做 FOC这套外设配置里 ADC 采样、定时器触发、PID 框架都能复用大半不存在白做这回事。2. 霍尔信号处理从输入捕获到换相状态机的工程实现2.1 三个霍尔如何变成六个扇区霍尔传感器按 120° 电角度安装转一圈下来三路输出会经历 6 个边界状态比如 001、011、010、110、100、101。每个状态对应转子当前位置所在的一个 60° 扇区。程序要做的事情很朴素时刻知道当前是哪个扇区然后从换相表里查出应该打开哪两个 MOS。这里有个重要细节霍尔状态和三相绕组的相位关系取决于电机内部绕线方式以及霍尔安装位置。同样一张换相表换一个电机型号可能就完全反了。所以程序里换相表不能写死成标准表就完事最好做成一个可配置表格上电先根据实测结果校正。2.2 定时器输入捕获与中断配置三路霍尔信号我习惯接在同一个定时器的三个输入通道上比如 TIM2 的 CH1/CH2/CH3配置成输入捕获双边沿触发。这样三个霍尔只要有任何一个跳变定时器都会产生捕获中断在中断里统一读回三个引脚的实时电平再判断换相。// 以 STM32 HAL 为例TIM2 三个通道配置为输入捕获 TIM_IC_InitTypeDef sIC {0}; sIC.ICPolarity TIM_ICPOLARITY_BOTHEDGE; // 双边沿都捕获 sIC.ICSelection TIM_ICSELECTION_DIRECTTI; sIC.ICPrescaler TIM_ICPSC_DIV1; sIC.ICFilter 0x0F; // 数字滤波滤掉霍尔信号毛刺 sIC.Channel TIM_CHANNEL_1; HAL_TIM_IC_ConfigChannel(htim2, sIC, TIM_CHANNEL_1); sIC.Channel TIM_CHANNEL_2; HAL_TIM_IC_ConfigChannel(htim2, sIC, TIM_CHANNEL_2); sIC.Channel TIM_CHANNEL_3; HAL_TIM_IC_ConfigChannel(htim2, sIC, TIM_CHANNEL_3); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_2); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_3);ICFilter 这个参数容易被忽略但对工业现场特别重要。霍尔信号线稍微长一点电机运行时的高压开关噪声很容易耦合进来造成霍尔边沿毛刺轻则换相提前重则直接在中断里读到非法组合。数字滤波会引入几十纳秒到几百纳秒的延迟对低速应用完全无感但能挡掉大量假边沿。2.3 换相表与状态机换相中断里不能做太重的运算我的做法是只查表、改寄存器、清标志所有滤波、测速、保护逻辑放到低速任务里。核心换相表可以这样组织// 扇区号到三相 PWM 输出的映射 // pwm[0..2] 分别对应 TIM1_CH1/CH2/CH3 // ARR 1000 // 值 1000 表示该相高侧常通CCR ARR // 值 0 表示该相低侧常通CCR 0 // 中间值 表示该相高侧按占空比 PWM 斩波 static const uint16_t comm_table[6][3] { /* 扇区 0 */ {0, 1000, 0}, // BL 长通AH PWM /* 扇区 1 */ {0, 0, 1000}, // CL 长通AH PWM /* 扇区 2 */ {1000, 0, 0}, // AL 长通BH PWM /* 扇区 3 */ {0, 1000, 0}, // BL 长通CH PWM /* 扇区 4 */ {0, 0, 1000}, // CL 长通BH PWM /* 扇区 5 */ {1000, 0, 0}, // AL 长通CH PWM };注意这张表只是演示用实际必须根据电机霍尔顺序重新排列。换相中断里拿到hall_state映射成扇区号sector然后把comm_table[sector]的三个值写入 TIM1 的 CCR1、CCR2、CCR3 即可。写寄存器这一步一定是先关中断、再更新避免运行中途出现上下管同时导通的组合。2.4 霍尔异常组合与滤波霍尔三路信号理论上有 8 种组合但 000 和 111 在正常旋转过程中不会出现。一旦读到这两个值通常意味着线松了、霍尔损坏或者毛刺干扰。我的做法是在换相中断里加一个检测分支非法组合直接关闭 PWM 输出并置故障标志而不是继续执行换相。这个保护比任何软件延时都稳妥。另一个实用技巧是边沿确认。有些低速电机在启动瞬间霍尔信号建立不完全会出现几个连续的假边沿。可以在中断里读回三路电平后和上一状态做比对只有状态合法且不是原地重复时才执行换相。但如果对换相实时性要求高这个确认逻辑要尽量精简否则高速时容易丢步。3. PWM 生成互补输出、死区时间与安全保护3.1 高级定时器 TIM1/TIM8 的互补 PWM 配置BLDC 三相逆变桥需要六路 PWM 驱动上下管互补是最稳妥的做法防止程序跑飞时上下管同时导通。STM32F103 的 TIM1、TIM8 是高级定时器天生带三相互补输出、死区插入和刹车输入这个组合就是为电机控制准备的。以 TIM1 为例配置成中心对齐模式这样 PWM 周期中间点天然适合做电流采样。三路通道全部用 PWM1 模式使能互补输出挂上刹车功能// 中心对齐模式ARR 999预分频为 0 // PWM 频率 72MHz / (2 * (9991)) 36kHz htim1.Init.Period 999; htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; HAL_TIM_PWM_Init(htim1); TIM_OC_InitTypeDef sOC {0}; sOC.OCMode TIM_OCMODE_PWM1; sOC.Pulse 0; sOC.OCPolarity TIM_OCPOLARITY_HIGH; sOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sOC.OCIdleState TIM_OCIDLESTATE_RESET; sOC.OCNIdleState TIM_OCNIDLESTATE_RESET; HAL_TIM_PWM_ConfigChannel(htim1, sOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim1, sOC, TIM_CHANNEL_2); HAL_TIM_PWM_ConfigChannel(htim1, sOC, TIM_CHANNEL_3); TIM_BreakDeadTimeConfigTypeDef sBD {0}; sBD.OffStateRunMode TIM_OSSR_ENABLE; sBD.OffStateIDLEMode TIM_OSSI_ENABLE; sBD.LockLevel TIM_LOCKLEVEL_1; sBD.DeadTime 36; // 72MHz 时钟下约 500ns sBD.BreakState TIM_BREAK_ENABLE; sBD.BreakPolarity TIM_BREAKPOLARITY_LOW; sBD.BreakFilter 0; HAL_TIMEx_BreakDeadTimeConfig(htim1, sBD);配置完成后运行中只需要修改三个 CCR 值和通道的使能状态六路 PWM 就自动按互补规则输出。需要特别提醒的是TIM1 和 TIM8 属于 APB2 总线时钟使能和重映射和普通定时器不一样新手容易在 CubeMX 配置里卡半天。3.2 死区时间的工程计算死区时间直接关系逆变桥会不会直通。上下管同时导通意味着电源短路MOS 会在几十微秒内过热烧毁。死区的工作原理是关断一个管子之后等一小段时间再让另一个管子导通这个时间就是死区。死区取值要考虑几个因素栅极驱动器的传输延迟、MOS 管的关断延迟、Miller 平台的影响。工程经验值大概是低压小功率 MOS电机电流 10A 以内500ns 到 1us大功率 MOS 或 IGBT2us 到 5us死区太小上管还没完全关断下管就开了危险死区太大电流波形失真换相噪声变大效率下降。正确的做法不是拍脑袋而是用示波器同时抓同一个半桥的上下管栅极波形看关断沿和开通沿之间是否留足了时间余量。我自己的板子先用 500ns 起步实测没问题再逐步减小最终稳定在 300ns 左右。3.3 启动时序先导通哪两相程序上电后不能直接按霍尔状态跑闭环因为电机还在静止状态霍尔信号对应的只是初始位置一旦直接给某相强制电压转子可能纹丝不动或者往反方向扭一下触发堵转。我的启动分三步先预定位随便选一组换相状态用较小占空比导通 200ms 左右让转子被磁场拉到一个已知位置。然后强制换相按固定时间间隔换相占空比从 5% 逐步往上加电机开始转动霍尔信号逐渐能给出真实位置。最后当转速超过阈值且霍尔状态连续稳定出现才切入正常霍尔换相并交给 PID 闭环。这个强制换相的加速曲线要跟着负载惯量调。惯量大的负载加速斜率太陡会失步太缓又迟迟进入不了闭环。没有捷径只能多试几组参数。3.4 刹车输入与过流保护联动TIM1 的 BKIN 刹车输入是硬件层面的保护一旦触发PWM 输出立刻进入安全状态所有输出无效或封锁不需要 CPU 干预。这是控制器程序的最后一道保险。我习惯把硬件过流比较器的输出接到 BKIN 引脚同时软件里也保留过流标志。硬件保护负责兜底软件负责诊断当刹车中断触发时程序记录故障码、停止换相、通过串口上报方便定位问题。有些设计会把欠压、过温也或联到 BKIN 上逻辑一样只要保证触发信号是电平有效的持续信号而不是短暂脉冲否则容易误触发。4. 双闭环控制电流环响应速度与速度环整定方法4.1 电流采样同步触发 ADC 的重要性程序要做闭环第一步就是拿到可靠的速度和电流反馈。电流采样我推荐低边采样电阻方案在每个低侧 MOS 的源极和地之间串一个毫欧级采样电阻电流流过时产生压降再经过差分放大器和 ADC 读取。这个方案成本低电路简单缺点是不能直接测出高侧导通期间那一相的电流但做六步换相的电流环够用了。采样时刻非常关键。PWM 开关瞬间采样电阻上的电压会有剧烈的振铃这时启动 ADC 读到的数据基本是废的。正确做法是把 ADC 触发源配置成 TIM1 的更新事件或中心对齐模式的溢出点让采样时刻准确落在 PWM 周期的中间。此时低侧 MOS 已经稳定导通电流基本是平缓的读回来的值才有意义。我用注入组 ADC 配合 DMA一个 PWM 周期内完成三路电流采样加转换几乎不抢 CPU。4.2 转速计算M 法和 T 法霍尔信号除了用来换相本身也是测速传感器。每换相一次转子转过 60° 电角度电机完成一圈机械旋转需要 6 × 极对数 次换相。测速有两种思路M 法适合中高速固定时间窗口比如 10ms内统计换相次数n转速 n / (0.01 × 6 × 极对数) × 60 rpm。窗口越短更新越快但低速时换相次数太少分辨率急剧下降。T 法适合低速测量相邻两次霍尔边沿的时间间隔间隔越长转速越低低速时分辨率反而高。我的程序里做了一个切换逻辑3000rpm 以下用 T 法以上用 M 法中间带一个滞环避免两种算法在临界点反复横跳。4.3 PID 结构与中断优先级安排控制结构上我外环是速度环输出的是电流目标值内环是电流环输出的是 PWM 占空比。外环周期 1ms 到 5ms 都可以内环要快一般放在 PWM 中断里或者 100us 周期的定时器中断里。内环跟不上速度环的指令就白瞎。这里给一个增量式 PID 的实现参考typedef struct { float Kp, Ki, Kd; float integral; float last_error; float out_limit; float integral_limit; } PID_t; float PID_Update(PID_t *pid, float target, float current) { float err target - current; float out; pid-integral err; if (pid-integral pid-integral_limit) pid-integral pid-integral_limit; if (pid-integral -pid-integral_limit) pid-integral -pid-integral_limit; out pid-Kp * err pid-Ki * pid-integral pid-Kd * (err - pid-last_error); pid-last_error err; if (out pid-out_limit) out pid-out_limit; if (out -pid-out_limit) out -pid-out_limit; return out; }中断优先级我的排序是换相捕获中断 电流环定时器中断 速度环定时器中断 串口通信。换相是最怕延迟的一旦延迟电机就可能在该换相的时刻还在给老一相通电表现在运行上就是噪声大、电流尖峰。串口这种低优先级任务哪怕偶尔丢一帧日志都无所谓绝不能让通信打断换相。4.4 从零开始整定双环参数整定顺序一定是先内环后外环。先把速度环断开给电流环一个固定目标电流从小到大加 Kp观察 PWM 占空比是否稳定跟随有没有持续振荡。电流环 Kp 够快之后再加一点点 Ki 消除稳态误差。电流环稳定了再接速度环。速度环 Kp 从 0 慢慢加电机在阶跃目标下出现轻微啸叫但能收敛就说明 Kp 差不多到了。此时加 Ki 消除稳态误差Kd 一般只用来抑制超调加多了会把噪声放大得非常厉害。我的实用建议所有参数都用浮点运算调试时通过串口实时打印目标值和反馈值画成曲线看。一次改一个参数改完记录效果再动下一个。指望一次把所有参数调到位十个项目九个翻车。5. 实测中的三个大坑误换相、电流尖峰与启动抖动5.1 坑一霍尔换相表明明对电机却抖动第一次上电电机死活转不顺来回抖动或者转到某个位置就卡住。我的第一反应就是换相表错了但换了 6 种排列之后发现不是这个问题。后来用示波器同时抓霍尔波形和相电流波形发现每次霍尔状态切换后电流波形要过好几微秒才开始换相。根因是换相中断里做了太多事情滤波、测速、状态打印全塞在里面执行时间达到十几微秒。电机转速到 20000rpm 时60° 电角度只有几十微秒这个延迟已经占掉很大比例换相自然乱掉。排查链路是这样的先在换相中断入口翻转一个 GPIO用示波器对比霍尔边沿和 GPIO 翻转的延迟发现延迟过大再把中断里的重逻辑全部移出只留查表和更新 CCR问题立刻消失。5.2 坑二MOS 管发热和驱动损坏有一次程序跑起来电机能转但十分钟内连续烧了两片驱动。查电路没有短路MOS 也没选错最后用示波器看栅极波形才明白死区时间留得太小同一桥臂上下管在开关切换时出现了微秒级的直通。管子高速轮流导通相当于每周期都在短路发热自然降不下来。另一个案例是母线电压尖峰导致的过压击穿。电机急停或者快速减速时反电动势回馈到母线如果母线电容离逆变桥太远尖峰能叠加到远超母线电压。这个问题的排查链路和散热完全不一样要么在桥臂两端并联 TVS 管要么在布线时把母线电容紧贴逆变桥还要检查有没有续流二极管位置放错。5.3 坑三启动时反转与堵转重启启动反转是最迷惑的一个现象正转启动转子先往后顿一下然后才正转。原因很简单预定位阶段用的强制电压方向如果正好和转子当前位置相反转子会先被吸到反方向的位置再被后续换相拉回来看起来就像反转。解决办法不是改换相表而是把预定位导通时间拉长、占空比减小让转子有充分时间稳定到预定位置。另外要确认换相表的第一组状态和预定位用的是同一个方向否则强制阶段和闭环阶段会冲突。堵转重启的情况我一般会做一次几百毫秒的退磁等待让绕组电流完全归零再进入预定位否则定位电流和剩余电流叠加可能把转子推到错误位置。5.4 调试效率工具逻辑分析仪加脚本比对最后分享一个我一直在用的调试技巧。STM32 的调试器加上 Keil 的逻辑分析仪窗口可以把三个霍尔引脚和换相状态变量一起抓出来导出 CSV 文件。回到电脑上写个小脚本把每个霍尔状态变化时刻和换相函数实际执行的时刻做对齐一眼就能看出是换相表顺序问题还是中断延迟问题。这个方法比单纯看示波器省事得多尤其在做霍尔相序校正的时候6 种换相表排列挨个试过去脚本自动输出每组排列下霍尔状态和换相状态的匹配度哪个平滑哪个有冲突一目了然。做电机控制调试工具花点心思比盲目改参数靠谱得多。本文还有配套的精品资源点击获取