简介:这是一份基于STM32F103RB的无刷直流电机(BLDC)控制工程实例,面向嵌入式初学者和电机控制开发者,可帮助理解三相逆变器驱动、PWM调速、反电动势或霍尔位置检测以及PID闭环控制等关键环节。包内共402个文件,约9.12MB,以C语言源码、头文件、STM32工程配置(如uvproj、sct、ini)为主,同时包含hex、axf、map等编译输出文件,便于直接烧录与对照分析;少量txt与htm文档可用于查阅说明。整个工程目录完整,从电机驱动到底层外设配置均有涉及,非常适合通过实际代码掌握STM32定时器、ADC、DMA等资源的综合运用。已有214人学习,是一份适合自学与项目参考的BLDC控制实例。
1. 为什么 BLDC 无刷直流控制总要落到 STM32 定时器上
如果只给一个直流电机通电,它只会疯狂加速直到机械极限。BLDC 与有刷直流电机最大的区别不在于"没有电刷"这个机械特征,而在于换相必须由控制器主动完成——转子转到什么位置、哪一相通电、通电多少时间,都要在微秒级内同步完成。这个同步点恰好落在 STM32 这类 MCU 的优势区:高级定时器输出互补 PWM 并插入死区,配合 ADC 和比较器做反电动势检测,一套外设就撑起六步换相控制。很多人最终卡住的不是电机本身,而是"代码写完了,电机不转"。下面按六步换相时序、TIM1 六路 PWM 生成、霍尔换相中断、PID 调参这个顺序推进,适合能烧录程序但没系统性跑通过 BLDC 的开发者。
2. 六步换相原理需要先于代码存在:从 Hall 信号推导导通相
对于方波驱动的 BLDC,控制器的工作本质是把三相桥中的功率管按"两两导通"模式轮流打开。难点在于导通顺序必须与转子位置严格同步,而这个同步信号来自于霍尔元件或反电动势过零检测。不了解这一点就直接抄换相表,电机要么不转,要么反转后过流保护。
2.1 梯形波反电动势与两两导通的时间窗口
BLDC 的永磁转子旋转时,每相绕组中会感应出梯形波反电动势。梯形波的平顶区域意味着该相电势稳定,适合在对应时间窗口内对该相通电做功。六步换相(Six-Step Commutation)正是利用这个特征:任意时刻只让两相导通,第三相悬空,用于检测反电动势或处于非导通状态。通电相的选择必须领先转子磁场位置 90 度电角度,才能获得最大转矩输出。如果偏置角度错位,电流做的功一部分变成热量,表现为电机温升快、效率低、噪音大。
2.2 六步换相序列表:拿到 Hall 值后如何映射占空比
使用 3 个霍尔传感器时,转子每转过 60 度电角度,三路 Hall 信号中就会有一路发生变化,组合出 6 个有效状态。以 120 度电气安装角的霍尔排列为例,霍尔值从 101 变为 100,对应换相表需要将导通相从 A+B- 切换为 A+C-。
以下是标准的逻辑换相表——这里的核心是 Hall 值与功率管开关的对应关系:
| 霍尔值 (H1 H2 H3) | 电角度范围 | 上桥导通 | 下桥导通 | 电流路径 |
|---|---|---|---|---|
| 010 | 0°~60° | Q1 (A+) | Q4 (B-) | A → B |
| 011 | 60°~120° | Q1 (A+) | Q6 (C-) | A → C |
| 001 | 120°~180° | Q3 (B+) | Q6 (C-) | B → C |
| 101 | 180°~240° | Q3 (B+) | Q2 (A-) | B → A |
| 100 | 240°~300° | Q5 (C+) | Q2 (A-) | C → A |
| 110 | 300°~360° | Q5 (C+) | Q4 (B-) | C → B |
如果电机需要反转,通常把这张表倒序使用,或者把 PWM 占空比放在上桥、下桥做同步调制。表中的 Hall 值与导通相映射,取决于霍尔传感器的安装角度和三相绕组线序。逻辑上正确不代表物理上能转,首次上电前应先用示波器测量三路 Hall 信号与反电动势的相对相位关系,再决定是否需要对换相表做整体偏移。
提示:这张表是工程基线。同一颗电机在不同驱动板上,因为采样电阻位置、母线电压检测点不同,换相时刻会产生微小的电角度偏移,表现就是相电流畸变。
2.3 Hall 安装角度的两种排布:120° 与 60° 的区别
120° 安装是指三个霍尔传感器物理上相差 120 度,输出信号按顺序跳变,换相表对应上面 6 个状态。60° 安装的霍尔器件彼此间隔 60 度,信号跳变更多,但 6 个有效状态里会出现连续两次"同一相切换"的边界情况。市面上绝大多数 BLDC 驱动方案都按 120° 电气安装,配合 TIM 的编码器模式读取 Hall 信号。对于 60° 安装,换相表的地址映射要重新推导,不能直接套用 120° 的表格。
常见做法是把 Hall 信号接入 STM32 的输入捕获通道,利用 TIM 编码器模式直接解码出转子位置。也可以把三路 Hall 接在普通 GPIO 上,用 Hall 值查表——这是后文将要采用的实现方式,因为它不依赖特定定时器的编码器资源,逻辑也直观。Hall 信号本身是数字电平,输出形式分为推挽和开漏两种。开漏输出的 Hall 元件需要外部上拉电阻,通常 4.7kΩ 到 10kΩ;推挽输出可直接接 MCU 引脚,但要注意 3.3V 电平容忍度。电机线束中的 UVW 功率线与 Hall 信号线捆扎在一起时会引入共模噪声,建议在 PCB 上给三路 Hall 信号加上 100Ω 串联电阻和 10nF 对地电容,形成低通滤波,边沿抖动严重的场合还能在 GPIO 配置里启用施密特触发器。
3. 用 STM32 高级定时器生成 6 路 PWM:TIM1 配置与死区参数
从上一章的换相表可以看出,三相桥 6 个功率管的开关组合由 Hall 值唯一确定。接下来要解决的是:如何让 STM32 在微秒级内完成 PWM 占空比切换、死区插入和输出使能。写成裸机代码时,这三件事必须在一个中断周期内全部完成,任何一个环节延时不均都会造成换相噪音。
3.1 三相全桥驱动电路的关键参数
不要把 MOSFET 直接接到 MCU 引脚。虽然部分集成驱动芯片内部已经集成了逻辑翻译和电荷泵,但仍有相当多项目使用分立元件方案:MCU 输出 3.3V PWM → 栅极驱动器(如 IR2101、EG2130)→ 高压半桥。分立方案成本低、功率级灵活,但外围参数必须与外设配置对齐。
挑选栅极驱动器时,三个参数与 MCU 外设配置直接相关:
- 死区时间:驱动器本身不会产生死区,必须在 MCU 定时器时基里配置。死区太短会造成上下桥直通,典型值取 500ns~1μs。
- 输入逻辑电平:部分驱动器为 3.3V 兼容,另一些需要 5V 电平,这决定了是否要在 TIM 输出通道上做电平转换。
- 高端供电方式:自举电容靠下桥导通时充电。若 PWM 占空比长时间接近 100%,下桥几乎没有导通窗口,自举电容电压跌落,高边 MOS 进入线性区,发热剧增。
如果电机额定 48V 或更高,还要在低压控制侧和高压母线之间保证地电位的一致性,否则 MCU 复位是家常便饭。栅极驱动芯片的 VBS 欠压锁定值通常在 8V 左右,母线电压低于 12V 时高边驱动可能无法建立,这也是低压 BLDC 板有时电机抖而不转的原因之一。
3.2 TIM1 六路 PWM:互补输出与中心对齐模式
TIM1 和 TIM8 是 STM32 里少有的带互补输出通道的高级定时器。TIM1_CH1/CH1N、CH2/CH2N、CH3/CH3N 可以直接产生三相半桥所需的六路 PWM,且能在硬件层面插入死区,无需软件干预。下面以标准外设库为例:
void Bldc_Tim1_PwmInit(uint16_t arr, uint16_t psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 使能时钟:GPIOA用于CH1-CH3,GPIOB用于CH1N-CH3N RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // PA8: TIM1_CH1, PA9: TIM1_CH2, PA10: TIM1_CH3 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // PB13: CH1N, PB14: CH2N, PB15: CH3N GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOB, &GPIO_InitStructure); // 中心对齐模式下 PWM频率 = 72MHz / (psc+1) / (arr+1) / 2 TIM_TimeBaseStructure.TIM_Period = arr; // arr=2249 时 = 16kHz TIM_TimeBaseStructure.TIM_Prescaler = psc; // psc=0 不分频 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned1; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // 三路PWM同时输出,极性为高有效 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC2Init(TIM1, &TIM_OCInitStructure); TIM_OC3Init(TIM1, &TIM_OCInitStructure); // 死区寄存器值 DTG:Tdts = 1/72MHz ≈ 13.9ns,DTG=72 → 死区≈1us TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime = 72; TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRInit(GPIOA, &TIM_BDTRInitStructure); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); // MOE主输出使能,少这行则无输出 }这段代码做完三件事:配置 PA 和 PB 引脚为复用推挽;把 TIM1 设置为中心对齐模式下的 PWM1;通过 BDTR 寄存器插入死区并打开主输出使能。参数上有几个容易出问题的位置。第一,中心对齐模式里更新事件同时在上溢和下溢产生,换相时刻要以此为同步基准,否则 CCR 更新可能被覆盖。第二,TIM_CtrlPWMOutputs必须在TIM_Cmd之后调用,否则 MOE 位被清零,PWM 引脚全部处于无效电平。第三,死区时间如果配置过大,半桥有效导通时间变短,电机在高转速下出现"加不上速"的现象,这不是 PID 问题而是死区吃掉有效占空比。
3.3 Hall GPIO 输入与触发源选择
Hall 信号是边沿敏感的,换相中断必须在 Hall 跳变的瞬间进入。把三路 Hall 接到 PC0/PC1/PC2,同时配置为 EXTI 外部中断源。为避免边沿抖动时重复触发中断,GPIO 应开启上拉,硬件上加 RC 滤波。
void Bldc_Hall_GpioInit(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE); GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource0); GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource1); GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource2); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOC, &GPIO_InitStructure); EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1 | EXTI_Line2; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // EXTI1_IRQn 和 EXTI2_IRQn 的NVIC配置同理 }EXTI_Trigger_Rising_Falling是双沿触发,不论 Hall 从 0 变 1 还是从 1 变 0 都会进中断。中断服务函数里必须在读取 GPIO 电平后重新组合出完整的 Hall 码,不能只依据触发的那一路。每次退出中断前清挂起位,否则同一边沿会反复触发,换相顺序错乱后电机直接抖动停滞。
4. 换相中断与速度闭环:从电角度同步到 PID 调速
定时器输出 PWM 只是手段,真正的 BLDC 控制中枢在 Hall 中断里。每次霍尔状态变化,要在几微秒内完成"读取新 Hall 码 → 查找换相表 → 更新 CCR 寄存器"的全流程,同时把上一次换相的时间间隔记录到速度测量单元。
4.1 Hall 换相中断里的最小动作集
#define HALL_A() GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_0) #define HALL_B() GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_1) #define HALL_C() GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_2) static volatile uint8_t bldc_comm_step[8]; static volatile uint16_t bldc_duty; void Bldc_SetPwm(uint16_t ch1, uint16_t ch2, uint16_t ch3) { TIM_SetCompare1(TIM1, ch1); TIM_SetCompare2(TIM1, ch2); TIM_SetCompare3(TIM1, ch3); } void Bldc_HallIsr(void) { uint8_t hall_code, step; uint32_t now_tick; static uint32_t last_tick; hall_code = ((uint8_t)HALL_C() << 2) | ((uint8_t)HALL_B() << 1) | (uint8_t)HALL_A(); step = bldc_comm_step[hall_code & 0x07]; switch (step) { case 0: Bldc_SetPwm(bldc_duty, 0, 0); break; case 1: Bldc_SetPwm(bldc_duty, 0, 0); break; case 2: Bldc_SetPwm(0, bldc_duty, 0); break; case 3: Bldc_SetPwm(0, bldc_duty, 0); break; case 4: Bldc_SetPwm(0, 0, bldc_duty); break; case 5: Bldc_SetPwm(0, 0, bldc_duty); break; default: TIM_CtrlPWMOutputs(TIM1, DISABLE); break; } now_tick = TIM_GetCounter(TIM2); speed_period_ticks = now_tick - last_tick; last_tick = now_tick; EXTI_ClearITPendingBit(EXTI_Line0 | EXTI_Line1 | EXTILine2); }上面 switch 的含义是:根据 step 把bldc_duty写入当前导通相所在通道的比较寄存器,并把悬空相的比较值清零。实际工程里更常见的写法是把六组 PWM 配置预先存成uint16_t pwm_table[6][3]数组,中断里直接做三次赋值,避免 switch 块随引脚重映射而散落。数组方式也方便日后把"某相上桥常通、下桥斩波"的调制方式做成实时切换。
4.2 占空比在这里如何生效
设定TIM_OCMode_PWM1且极性为高时,CCR 值越大,有效电平宽度越宽。在中心对齐模式下,CCR 等于 ARR 时为 100% 占空比,CCR 等于 0 时为 0%。BLDC 两两导通模式里,上下桥由硬件按互补关系工作,CCR 数值本身只决定上桥脉冲宽度,下桥自动反相并插入死区,不需要软件干涉。调速阶段改变bldc_duty即改变所有导通相的比较值。如果只调节下桥导通窗口,需要把对应通道的TIM_OCPolarity改为低,用相反方向改变 CCR——这是"上桥常通、下桥斩波"的调制方式,EMI 表现好,但母线利用率略低。
4.3 用增量式 PID 把目标速度锁在 Hall 换相频率上
BLDC 速度环的反馈可以直接测量 Hall 换相周期。设定目标转速后,把换相间隔换算为速度值,用增量式 PID 输出占空比。电机电气频率与转速的关系:电气频率 = 转速 × 极对数 / 60,而每电气周期发生 6 次换相,所以换相频率 = 电气频率 × 6。
typedef struct { float kp, ki, kd; float err_prev1, err_prev2; float out_prev; float out_max, out_min; } PidInc_t; uint16_t Bldc_PidUpdate(PidInc_t *pid, float target, float feedback) { float err = target - feedback; float delta = pid->kp * (err - pid->err_prev1) + pid->ki * err + pid->kd * (err - 2.0f * pid->err_prev1 + pid->err_prev2); float out = pid->out_prev + delta; if (out > pid->out_max) out = pid->out_max; if (out < pid->out_min) out = pid->out_min; pid->err_prev2 = pid->err_prev1; pid->err_prev1 = err; pid->out_prev = out; return (uint16_t)out; }delta是本次增量,ki项直接作用在误差上而不是积分累积上,这是增量式 PID 的标准形式。out_cur被限幅到arr / 2以内,防止满载时占空比接近 100% 导致自举电容无法充电。调用时机放在速度测量完成后、下一次 Hall 中断到来之前,在后台循环里以定时器节拍执行即可。
4.4 启动策略:开环强拖再切闭环
BLDC 不能像有刷电机那样一上电直接给额定电压。静止时霍尔输出只能反映转子停在哪个扇区,无法知道确切角度,直接按换相表通电可能产生零转矩盲区。常见做法有两种:
- 定位法:任意一相通电 1 到 2 秒,利用定子磁拉力把转子钳到已知位置,再按换相表启动。适用于负载转动惯量不大的场合。
- 强拖法:以低占空比、缓慢递增的换相频率驱动电机,转速上升后才切换到 Hall 闭环。强拖法启动平稳,但需要记录定位过程中的换相次数,否则转子上电瞬间的加速度不可控。
对于风机、泵这类空载启动需求,通常用定位法,150ms 定位加 300ms 斜坡加速到目标转速的一半,然后切到 PID 闭环。惯量较大的负载,定位法的停滞感明显,可以改为先检测零速状态,再快速扫频强拖。
5. Hall 相位偏置角度的校准技巧与验证
上一章的控制环正常工作之后,大概率会碰到两类现象:电机能转但电流大、噪音尖;或者某些转速段电机振动剧烈,像堵转却未停。这两种症状的根源指向同一个问题——Hall 信号与反电动势的相位偏置没有校准到位。
5.1 什么情况下必须重新校准偏置角度
霍尔传感器安装在电机内部,机械上存在个体公差。直接套用电机的出厂换相表上电,相电流波形必然存在相位偏移。判断依据是看母线电流:给定固定占空比,用钳形表或采样电阻观测相电流,如果波形高电平宽度与低电平不对称,或换相时刻附近出现明显电流尖峰,就是相序或偏置角度需要调整。
校准思路是重写换相表的偏移值,把"Hall 值变化物理上对应的电角度"与实际导通时刻对齐。若 Hall 安装超前中性点 10 度电角度,换相表的索引应整体后移对应角度,即按 60 度一个扇区计算约偏移 0.167 个扇区,工程上直接在 Hall 中断里对 hall 码做位循环移位:
#define HALL_PHASE_ADJUST 1 // 单位: 扇区,正值代表提前换相 uint8_t hall_code = ((uint8_t)HALL_C() << 2) | ((uint8_t)HALL_B() << 1) | (uint8_t)HALL_A(); #if HALL_PHASE_ADJUST > 0 hall_code = ((hall_code >> HALL_PHASE_ADJUST) | (hall_code << (3 - HALL_PHASE_ADJUST))) & 0x07; #endifHALL_PHASE_ADJUST调整的是扇区级粗校准,适用于霍尔安装公差在正负半个扇区内的场景。更细的亚扇区偏差,通过在 Hall 边沿后延迟若干微秒再切换 CCR 补偿,单位延迟对应的电角度 = 360 × 延迟时间 × 电气频率。
5.2 用反电动势过零做一次软件自校准
不需要额外硬件,利用 BLDC 悬空相的反电动势过零检测即可完成精确校准。算法原理:电机由外部带着匀速旋转时,每相反电动势过零点相对 Hall 跳变的位置应当是固定的电角度。把过零信号捕获后与 Hall 事件的时间差换算成角度补偿值,存到 Flash 参数区。需要注意反电动势过零检测只能在中高速区使用,低速时反电动势幅值太小,过零比较器容易误触发,校准时的转速应取额定转速的 30% 以上。
5.3 换相表调乱后的典型排查顺序
排查顺序固定为以下三步:先确认三个 Hall 对地波形为标准的 120 度互差方波;再用手缓慢旋转转子,逐个记录 9 种 Hall 组合中实际出现的 6 种有效码;最后用示波器对比"换相时刻"与"某相反电动势从正到负的过零点"的相位差,差多少度就反推偏移量。如果下载了别人的工程代码但没有改 Hall 引脚映射,还有一种低级错误:GPIO 的上拉配置不对,导致 Hall 悬空值不稳定,查表有时命中有时偏一个扇区,症状看起来像偏置乱跳。这种情况排除相位校准之前,先把三路 Hall 的上拉全部打开,并确认 Hall 到 MCU 的排线没有引入串扰。上面给出的六步换相表、TIM1 互补 PWM 初始化和增量式 PID 都是可复现的工程基线,你把 Hall 偏置校完,再回看整个启动过程,BLDC 控制的难点就只剩换相时序和电流限制两件事了。
本文还有配套的精品资源,点击获取