☰
STM32无刷直流电机六步换相控制器实现:从霍尔捕获到PID闭环
2026/10/17 10:37:19 网站建设 项目流程

简介:基于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 * (999+1)) = 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 或 IGBT:2us 到 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 种换相表排列挨个试过去,脚本自动输出每组排列下霍尔状态和换相状态的匹配度,哪个平滑哪个有冲突一目了然。做电机控制,调试工具花点心思,比盲目改参数靠谱得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询