STM32G431驱动三相无刷电机:六步换相与PI调速实战

STM32G431驱动三相无刷电机:六步换相与PI调速实战 简介基于STM32G431的三相无刷电机驱动与比例积分PI调速完整工程源码面向嵌入式电机控制方向的学习者与开发者用于掌握无刷电机换相逻辑、脉宽调制驱动以及闭环调速的实现方法。工程基于硬件抽象层HAL库和官方数字信号处理库搭建核心代码涵盖高分辨率定时器配置、三相逆变桥驱动、电流与速度采样、比例积分调节器以及相应控制逻辑可作为无刷电机控制项目的移植参考或二次开发基础。工程中保留了完整的构建中间文件和链接脚本便于理解嵌入式工程的组织方式。资源共1524个文件以C语言源码和头文件为主辅以汇编程序、链接脚本、编译工程配置、图形化配置工程以及编译生成的烧录文件便于对照完整工程结构进行重新编译和验证目录结构清晰从启动文件、硬件抽象层到底层驱动和上层算法均有对应代码。压缩包整体约43.48兆字节累计已有2063人下载学习适合具备一定单片机基础、希望深入电机控制底层实现与调参实践的读者。1. 为什么stm32G431驱动三相无刷电机默认先做PI调速而不是一上来就上FOC拿STM32G431驱动三相无刷电机很多人的第一反应是直接上FOC。但实际量产项目里大量方案用的是六步换相加PI调速原因很直接代码量小一个量级调试周期从几周缩到一两天而且在中低速、大扭矩的应用场景下换相逻辑清晰转速闭环同样稳定。G431这颗芯片自带170MHz主频、HRTIM、模拟比较器和高速ADC做六步换相属于资源溢出根本不需要动用FOC那种复杂架构。这篇文章就按一线工程师最常用的路线走一遍用霍尔传感器做转子位置反馈TIM1输出三路PWM驱动三相全桥PI调速闭环控制转速。从换相表怎么建、PWM怎么配、转速怎么算、PI参数怎么定到最容易踩的坑全部给出能直接抄的代码和步骤。2. 六步换相原理与G431外设选型霍尔方案为什么够用2.1 三相无刷电机的六步换相逻辑无刷直流电机BLDC的反电动势近似梯形波所以驱动逻辑不必像永磁同步电机那样追求正弦电流波形只需要在转子转到特定角度时以60°电角度为步进切换通电相电机就能持续输出转矩。这就是“六步换相”这个名字的来源。每步只有两相导通一相进电流一相出电流第三相悬空。以常见的内转子电机为例通电顺序从AB-开始依次切到AC-、BC-、BA-、CA-、CB-六个状态循环一圈转子转过360°电角度。极对数为p的电机电角度转一圈对应机械角度360°/p所以六步换相恰好覆盖一个电周期。霍尔传感器安装在定子上三个霍尔间隔120°电角度输出三路高低电平组合一共8种状态其中6种是有效位置对应转子的6个扇区。控制器读取霍尔组合后查表得到当前扇区再决定哪两相通电。这就是霍尔换相的完整闭环。2.2 霍尔信号与通电相的映射表换相表是调试的第一道坎因为霍尔安装方向、电机绕组接法不同同一张表在不同电机上表现完全不一样。以下是一张最常用的默认表以霍尔输出HA、HB、HC的顺序拼成3位二进制数0代表A相、1代表B相、2代表C相。霍尔组合H3:H2:H1上桥PWM相下桥导通相实际通电状态0b000/0b111无无无效状态停止输出0b001ACAC-0b010ABAB-0b011BCBC-0b100BABA-0b101CACA-0b110CBCB-注意这张表只是起点。如果电机通电后抖动不转或者反转优先做两件事一是把表中6个有效状态整体前移或后移一格相当于调整60°换相角二是任意交换两路霍尔输入线霍尔组合的编码顺序会整体重排。实际操作时我一般会在代码里预留一个数组存这张表然后用串口把实时霍尔值打出来配合示波器看相电流波形最多10分钟就能调对。2.3 stm32G431驱动三相无刷电机哪些外设真正干活ST特意在G431上堆了一组电机控制外设但六步换相方案真正用到的其实只有一小部分。核心是TIM1高级定时器它有3个通道可以输出PWM且每个通道带互补输出、刹车输入和死区插入。对六步换相来说三路PWM就够驱动上桥余下的互补通道可以不用把引脚当普通GPIO控制下桥。HRTIM高分辨率定时器在FOC里是主角但在六步换相里反而用不上。它的价值在于能和比较器、ADC联动产生精确的PWM中心对齐触发信号做无感FOC电流采样非常合适。如果项目下一步要升级FOCG431的HRTIM一秒切入只做PI调速TIM1完全够。G431的另一组好料是内置运放和比较器不需要外接比较电路就能做无感反电动势过零检测。但霍尔方案本身不需要过零检测比较器可以用来做过流保护。ADC的高速采样能力同样用不上全部性能每50μs采一次母线电流就足够保护逻辑用了。2.4 PWM调制方式高侧斩波与同步整流的取舍六步换相的PWM调制方式有好几种最常见的是H_PWM-L_ON上桥PWM斩波下桥常通。这种方式控制逻辑简单续流靠下桥MOSFET的体二极管缺点是关断时电流经体二极管续流二极管损耗大大电流工况下发热明显。另一种是互补PWM同步整流上下桥互补输出关断期间电流走下桥MOSFET沟道导通损耗小但代码里要处理死区、避免直通换相瞬间的逻辑也更复杂。G431的TIM1自带死区生成器理论上可以直接用但需要时刻保证同一半桥上下管不能同时导通对六步状态机的正确性要求更高。我一般选H_PWM-L_ON起步原因很实际先用最简单的驱动逻辑确认换相表、转速测量和PI调速跑通再考虑要不要加同步整流优化效率。PWM频率取20kHz以上能避开可听见的噪声G431的TIM1时钟170MHzARR值取8500就是20kHz占空比分辨率足够细腻。3. stm32G431驱动三相无刷电机的硬件接线与PWM初始化3.1 三相逆变桥与G431的引脚分配硬件上需要三相全桥逆变电路最常见的是6颗N沟道MOSFET加栅极驱动芯片比如IR2101、EG2134这类自带自举电路的高低压侧驱动。栅极驱动芯片的输入侧只需要三路上桥PWM和三路下桥电平信号正好对应G431的一组TIM1输出加三个GPIO。以下是全套接线参考按这个接法CubeMX里的引脚配置可以直接照抄。功能引脚外设/复用说明A相上桥PWMPA8TIM1_CH1接A相栅极驱动高侧输入B相上桥PWMPA9TIM1_CH2接B相栅极驱动高侧输入C相上桥PWMPA10TIM1_CH3接C相栅极驱动高侧输入A相下桥控制PB13GPIO输出接A相栅极驱动低侧输入B相下桥控制PB14GPIO输出接B相栅极驱动低侧输入C相下桥控制PB15GPIO输出接C相栅极驱动低侧输入霍尔HAPC6EXTI6外部中断输入上拉霍尔HBPC7EXTI7外部中断输入上拉霍尔HCPC8EXTI8外部中断输入上拉母线电流采样PA0ADC1_IN1经运放放大后进ADC要注意PB13、PB14、PB15虽然硬件上同时连接TIM1的互补通道但只要在CubeMX里配置成GPIO输出就不会触碰TIM1的PWM功能。霍尔引脚用外部中断而不是输入捕获是因为换相动作需要在任意一路霍尔变化时立刻响应三路霍尔共用EXTI9_5中断线在一次中断里统一读取三路电平效率更高。3.2 初始化定时器PWM和IO让电机先转起来初始化代码用HAL库写结构清楚也方便CubeMX用户直接对齐自己的工程。PWM频率按20kHz设置ARM值8500初始占空比0。static void Motor_PWM_Init(void) { GPIO_InitTypeDef gpio {0}; TIM_OC_InitTypeDef oc {0}; __HAL_RCC_TIM1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // PA8/PA9/PA10 复用为 TIM1 CH1/CH2/CH3接上桥驱动 gpio.Pin GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10; gpio.Mode GPIO_MODE_AF_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_HIGH; gpio.Alternate GPIO_AF1_TIM1; HAL_GPIO_Init(GPIOA, gpio); // PB13/14/15 做普通GPIO控制下桥 gpio.Pin GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Alternate 0; HAL_GPIO_Init(GPIOB, gpio); htim1.Instance TIM1; htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.Period 8500 - 1; // 170MHz / 8500 20kHz htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim1); oc.OCMode TIM_OCMODE_PWM1; oc.Pulse 0; oc.OCPolarity TIM_OCPOLARITY_HIGH; oc.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim1, oc, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim1, oc, TIM_CHANNEL_2); HAL_TIM_PWM_ConfigChannel(htim1, oc, TIM_CHANNEL_3); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_3); }Period取8500减一是因为计数器从0计数到8499正好8500个计数周期。PWM频率等于定时器时钟除以PeriodG431的TIM1挂在APB2上最高170MHzPrescaler为0时直接得到170MHz计数时钟。HAL_TIM_PWM_Start只启动三路主输出下桥完全由GPIO控制不涉及互补通道死区问题留给栅极驱动芯片和软件换相间隙解决。3.3 换相状态机查询霍尔并切换PWM通道换相函数是整套驱动的心脏。它根据霍尔组合查表确定当前步进应该PWM哪一相上桥、导通哪一相下桥。为了保证换相瞬间不出现上下桥直通我采用“先关下桥再恢复PWM”的序列中间留几微秒间隔。static uint8_t cur_pwm_ch 0; // 当前上桥PWM通道 0/1/2 对应A/B/C static uint8_t cur_low_ph 0; // 当前下桥导通相 0/1/2 static uint16_t cur_duty 0; // 当前PWM比较值 // 换相表行霍尔组合列[上桥PWM相, 下桥导通相] // 0A, 1B, 2C, 0xFF不动作 const int8_t step_table[8][2] { {0xFF, 0xFF}, // 0b000 { 0, 2 }, // 0b001 { 0, 1 }, // 0b010 { 1, 2 }, // 0b011 { 1, 0 }, // 0b100 { 2, 0 }, // 0b101 { 2, 1 }, // 0b110 {0xFF, 0xFF}, // 0b111 }; static const uint16_t pwm_ch_map[3] { TIM_CHANNEL_1, TIM_CHANNEL_2, TIM_CHANNEL_3 }; static const uint16_t low_pin[3] { GPIO_PIN_13, GPIO_PIN_14, GPIO_PIN_15 }; void Motor_Set_Step(uint8_t hall) { const int8_t *step step_table[hall]; HAL_GPIO_WritePin(GPIOB, low_pin[0] | low_pin[1] | low_pin[2], GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim1, pwm_ch_map[cur_pwm_ch], 0); if (step[0] 0xFF) { cur_pwm_ch 0xFF; return; } cur_pwm_ch step[0]; cur_low_ph step[1]; HAL_GPIO_WritePin(GPIOB, low_pin[cur_low_ph], GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim1, pwm_ch_map[cur_pwm_ch], cur_duty); }这段逻辑里换相第一时间把旧上桥PWM比较值清零同时关闭全部下桥避免新旧两步之间出现“旧下桥没关、新上桥已开”的瞬间。之后设置新下桥导通再恢复新上桥PWM。cur_duty是PI调速输出后写入的全局占空比这样换相时能立刻知道当前应该输出多大占空比。3.4 霍尔外部中断与转速时间戳霍尔信号变化触发EXTI9_5中断在中断服务函数里读霍尔组合、调换相函数并用DWT计数器记录两次换相的时间间隔。DWT-CYCCNT是Cortex-M4内核自带的周期计数器适合做微秒级时间测量比SysTick更轻量。volatile uint32_t hall_tick_us 0; volatile uint32_t comm_period_us 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { uint8_t hall; uint32_t now_us; if ((GPIO_Pin (GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8)) 0) return; hall (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_8) 2) | (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_7) 1) | HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_6); now_us DWT-CYCCNT / 170; comm_period_us now_us - hall_tick_us; hall_tick_us now_us; Motor_Set_Step(hall); }DWT-CYCCNT每周期加1170MHz下每微秒加170。需要在使用前使能DWT计数器开启方式是在main里执行CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk然后DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk。霍尔中断里做了两件事测量换相周期执行换相。速度计算不在中断里做除法只在需要时用comm_period_us算出转速避免中断里浮点运算拖慢响应。4. stm32G431的PI调速实现从换相周期计算转速到闭环修正4.1 转速计算公式为什么是十万除以极对数乘换相周期霍尔换相周期和转速之间是严格的几何关系。每两次换相之间转子转过60°电角度6次换相完成一个完整电周期。一个电周期对应机械角度为360°/p所以转速和换相周期的关系如下。转速RPM 60000000 / (6 * p * comm_period_us) 10000000 / (p * comm_period_us)极对数p在电机参数里通常标着比如2812电机通常是4对极或7对极。comm_period_us是霍尔中断里记录的相邻两次换相间隔。以p4、comm_period1000μs为例转速就是10000000除以4000等于2500RPM。这个公式不依赖任何传感器标定只要霍尔安装正确算出来的转速就是准的。用定时器固定1ms周期计算转速并执行PI比在换相中断里直接调PI更稳定。因为低速时换相间隔变长PI的执行频率会跟着降低比例积分参数没法固定。固定1ms采样相当于把速度环采样时间定死Kp和Ki的物理意义清晰调参也更符合直觉。#define MOTOR_POLE_PAIRS 4 volatile uint16_t motor_rpm 0; void Speed_Calc_Update(void) { uint32_t period comm_period_us; uint32_t rpm; if (period 0 || period 100000) { motor_rpm 0; return; } rpm 10000000UL / (MOTOR_POLE_PAIRS * period); motor_rpm (uint16_t)rpm; }period超过100ms说明电机已经接近停转此时转速记0更合理。如果电机高速转period很小rpm可能超过uint16_t范围但这个场景在常规电机驱动里很难出现工程上够用。4.2 PI控制器结构位置式还是增量式PI调速输出的是PWM比较值属于绝对占空比所以用位置式PI最直接。位置式PI的公式是u(k)Kpe(k)KiΣe(i)其中误差积分项需要单独累加。增量式PI输出的是每次调整量适合步进电机或输出本身就是增量的执行器无刷电机的占空比是绝对值用位置式少一次换算限幅和积分饱和处理也更直观。积分饱和是位置式PI在电机启动阶段最常见的问题。启动时转速误差大积分项快速累加等转速接近目标时积分还停留在高位导致超调甚至振荡。对抗办法是积分分离误差超过一定范围时停止积分进入稳态后再让积分器工作。下面的代码把积分分离阈值放在±200RPM区间内启动阶段积分器不会疯狂累加。#define KP_DEFAULT 0.15f #define KI_DEFAULT 0.02f #define DUTY_MIN 0.10f #define DUTY_MAX 0.95f #define INTEGRAL_LIMIT 0.30f #define SPEED_ERR_BAND 200 static float duty_ratio 0.10f; static float integral 0.0f; static uint16_t target_rpm 1500; void PI_Update(void) { float err, out; err (float)target_rpm - (float)motor_rpm; if (err -SPEED_ERR_BAND err SPEED_ERR_BAND) { integral err; if (integral INTEGRAL_LIMIT) integral INTEGRAL_LIMIT; if (integral -INTEGRAL_LIMIT) integral -INTEGRAL_LIMIT; } out KP_DEFAULT * err KI_DEFAULT * integral; if (out DUTY_MAX) out DUTY_MAX; if (out DUTY_MIN) out DUTY_MIN; duty_ratio out; cur_duty (uint16_t)(out * 8500.0f); }积分限幅0.3对应的物理意义是积分项对占空比的贡献最多30%比例项在启动阶段主导输出避免积分器单独把占空比推到极限。输出下限10%是因为PWM占空比低于10%时电机往往无法克服静摩擦占空比上不去会导致PI一直在积分实际表现为电机嗡嗡响但不转。4.3 1ms定时器中断里执行速度闭环PI放在TIM6的1ms周期中断里。TIM6是基础定时器只有中断功能适合做系统节拍。在中断里先计算最新转速再执行PI更新换相则始终由霍尔中断驱动两者互不干扰。void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) { Speed_Calc_Update(); PI_Update(); } }这种结构的好处是速度环和换相环解耦。霍尔中断频率随转速变化最高可能到几千赫兹速度环频率固定1kHz稳定可设计。PI的参数整定基于1ms采样周期Kp和Ki的量纲清晰。如果未来要调成5ms或2ms采样只需改TIM6的周期再按比例缩Ki即可。4.4 PI参数整定先P后I的实操顺序参数表给出起步值和调节方向但真正在现场调参时顺序比数值更重要。参数作用起步值调节方向Kp比例增益决定响应速度0.10~0.15过小响应慢过大会持续振荡Ki积分增益消除稳态误差0.01~0.02过大会超调过小有静差输出上限限制最大占空比0.95限制最大电流时可调低输出下限最小启动占空比0.10低于该值电机无法转动积分限幅防积分饱和0.30启动无超调时可适当增大先把Ki设为0Kp从0.05开始目标转速设到额定转速的一半观察电机是否能在1秒内接近目标并且不振荡。如果转速在目标上下反复摆动Kp减半。如果接近目标的弧线很缓、迟迟到不了Kp翻倍。找到一个“稍有超调但不持续振荡”的Kp后再把Ki从0.005开始缓慢增大观察稳态误差是否归零。这个阶段Ki过大的特征很明显转速到达目标后出现周期性浪涌声音粗细交替这时Ki立刻减半。5. 调试技巧开环验证换相顺序再闭环调PI参数5.1 第一阶段固定占空比确认换相顺序不要一上来就闭环。先把PI输出的占空比写死成30%用串口把霍尔值打印出来手动缓慢转动电机轴观察串口打印的霍尔组合是否按步进表的顺序循环。如果霍尔组合跳变顺序不是循环的说明霍尔接线或换相表次序不对交换任意两路霍尔输入再试。霍尔状态正常后再上电给定30%占空比电机应该能平滑转起来。如果抖一下停住说明换相表和霍尔组合错位把step_table的6行数据整体平移一格。5.2 第二阶段用示波器看PWM和相电压波形确认直通风险接好示波器探头到A相上桥PWM引脚和A相相电压节点空载运行电机。正常状态是PWM引脚输出20kHz方波占空比随负载变化相电压在每个换相点附近呈现梯形波形状。如果相电压在换相瞬间出现尖峰或者母线电流明显增大先检查软件换相间隙是否太短。我一般在Motor_Set_Step里加一个约2μs的延时再恢复PWM输出成本低能有效避免上下桥直通。不要小看这一步很多电机烧MOSFET都是换相时序的问题不是过流保护没做。5.3 第三阶段闭环微调PI并记录阶跃响应曲线闭环启动前把目标转速设为200RPM先用极小的Kp跑起来。重点看电机能不能稳定停在目标转速附近而不是看它多久到达。稳定后把目标转速突变到1500RPM记录转速从突变到稳定的时间。理想的阶跃响应是无超调或单次小超调稳定时间在200ms以内。如果转速在目标下方爬升缓慢增加Kp如果出现等幅振荡先减Kp再观察是否恢复。PI调好后还要验证最低转速和最高转速两个极端点。最低转速下换相周期长速度环采样不会因为换相中断太慢而跳变最高转速下输出占空比接近上限确认PI输出没有饱和过头导致突然失速。调完参数后把转速、PI输出、母线电流三路信号同时用串口以1kHz频率发出录一段空载启动到满载突加的数据。保存这组曲线它就是这台电机PI标定的最终记录也是后续批量接线差异排查的对照基准。本文还有配套的精品资源点击获取