从零构建:基于STM32与霍尔传感器的SVPWM-FOC实战解析

📅 发布时间:2026/7/7 2:24:32 👁️ 浏览次数:
从零构建:基于STM32与霍尔传感器的SVPWM-FOC实战解析
1. 项目缘起与硬件准备为什么从零开始大家好我是老张一个在电机控制领域摸爬滚打了十来年的工程师。今天想和大家聊聊一个非常经典且实用的项目用一块STM32微控制器加上几个霍尔传感器亲手搭建一个完整的FOC磁场定向控制系统。你可能会问现在ST不是有现成的电机库和Workbench工具吗一键生成代码多方便没错我一开始也是这么想的。但实际用下来特别是用ST的Workbench配置霍尔传感器HALL的无刷直流电机BLDC控制时电机转起来那个抖动啊简直让人怀疑人生。ST的官方代码写得确实漂亮HAL库和LL库混用效率很高但问题就在于它封装得太好了很多中断回调直接重写底层逻辑像黑盒子。当你想优化性能或者电机运行出现异常时根本不知道从哪里下手去改。所以我决定抛开现成的库从最底层开始自己敲一遍代码。这个过程就像学武术光看套路不行得从扎马步、练基本功开始才能真正理解每一招每一式的发力点。这个项目的目的就是带你一起从硬件选型、软件配置到核心算法实现和调试完完整整地走一遍。当你自己把电机稳稳当当地驱动到上万转时那种成就感是无与伦比的。我们这次实战的平台是STM32G474RET6核心板搭配ST官方的X-NUCLEO-IHM07M1三相电机驱动板电机是一对极的BLDC。你还需要准备一个12V的直流电源一个逻辑分析仪或者示波器用于观察PWM和电流波形以及一个红外测速仪初期验证转速用。别被这些工具吓到它们都是我们调试过程中的“眼睛”能帮我们看清电机内部到底发生了什么。2. 软件环境搭建与核心外设配置工欲善其事必先利其器。软件方面我们使用经典的Keil 5 MDK作为开发环境用STM32CubeMX进行图形化引脚和时钟初始化配置。这里有个非常重要的前提务必拿到电机控制板和驱动板的原理图。尤其是驱动板你需要清楚它的MOS管驱动逻辑、电流采样电路设计以及保护机制否则调试起来会一头雾水。我们的核心是产生驱动电机的PWM波。这里会用到STM32G474的高级定时器TIM1。STM32G474的主频高达170MHzTIM1的时钟源也同步到这个频率。我们需要配置TIM1产生中心对齐的PWM模式也就是常说的“中央对齐”或“边沿对齐模式1”。这种模式能有效降低电机运行时的噪声。我通常会将预分频器Prescaler设置为5这样定时器的计数时钟就是170M / (51) ≈ 28.33MHz。然后设置自动重装载值ARR为1000。那么最终的PWM频率计算公式是Fpwm 定时器时钟 / ((ARR 1) * 2)。代入我们的参数28.33MHz / (1000 * 2) ≈ 14.16kHz。这是一个比较常用的开关频率在开关损耗和电流纹波之间取得了一个不错的平衡。配置TIM1输出四路PWM其中通道1、2、3对应PA8 PA9 PA10连接到驱动板的三个上桥臂或下桥臂具体看驱动板逻辑控制端。这里要注意X-NUCLEO-IHM07M1这块板子内部有栅极驱动芯片它自己会处理死区时间所以我们不需要在STM32中配置互补输出和死区直接输出普通的PWM信号即可。第四路PWM通道4我们并不打算真的输出到电机而是把它作为一个精准的“定时触发器”。我们将TIM1的“主模式”触发输出TRGO源设置为“OC4REF”意思是当TIM1的通道4比较匹配时会产生一个内部触发信号。这个信号我们将用来触发ADC采样确保我们总是在每个PWM周期的固定时刻去采样电流避开MOS管开关的噪声干扰这是实现高精度FOC的关键一步。生成代码后别忘了在main函数中手动启动PWM输出HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_3); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_4); // 这一路不接电机只用于内部触发同时我们还需要开启一个UART串口这是连接上位机VOFA的桥梁。VOFA是一款非常强大的国产可视化调试工具可以实时绘制波形对于PID调参和观察系统状态至关重要。我们需要定义一个简单的数据发送协议比如每次发送三个浮点数电流、速度、位置。在代码里我们可以这样组织数据并发送// 定义发送缓冲区末尾的0x00, 0x00, 0x80, 0x7f是VOFA的“帧尾”用于数据对齐 float tx_buffer[4] {0}; tx_buffer[0] Motor.Current_d; // D轴电流 tx_buffer[1] Motor.Current_q; // Q轴电流 tx_buffer[2] Motor.Speed; // 转速 // 使用DMA发送不阻塞CPU HAL_UART_Transmit_DMA(huart2, (uint8_t*)tx_buffer, sizeof(tx_buffer));2.1 开环SVPWM让电机先转起来在进入复杂的闭环之前我们先让电机在开环状态下转起来这能验证我们的硬件连接和PWM生成是否正确。开环运行的原理很简单我们假装已经知道了转子的位置用一个不断自增的角度变量theta模拟然后给定一个固定的Q轴电压VqD轴电压Vd设为0经过Park逆变换和SVPWM模块最终生成三相占空比驱动电机。SVPWM空间矢量脉宽调制是FOC的核心它的目标是用三个半桥的8种开关状态去合成一个任意方向和幅度的电压空间矢量。听起来抽象你可以把它想象成三个人划船通过协调每个人划桨的力度和时间让船朝着任意想要的方向前进。代码实现上我们需要先进行Clarke变换将三相电流Ia, Ib, Ic转换为两相静止坐标系下的Iα, Iβ但开环时我们直接给定电压。SVPWM算法包括扇区判断、基本矢量作用时间计算、以及最终三相占空比合成。下面是我常用的一个SVPWM函数实现它直接操作Uα和Uβ电压矢量在α-β轴的分量并计算出三相占空比Ta, Tb, Tc。代码里我用了定点数运算来提升在MCU上的执行效率但为了清晰这里用浮点数逻辑展示typedef struct { float a; float b; float c; } Dabc_t; typedef struct { float alpha; float beta; } Ualpha_beta_t; void Svpwm_Gen(Ualpha_beta_t Uout, Dabc_t* DutyCycle) { float U1 Uout.beta; float U2 (SQRT3 * Uout.alpha - Uout.beta) / 2.0f; float U3 (-SQRT3 * Uout.alpha - Uout.beta) / 2.0f; // 扇区判断 uint8_t sector 0; if (U1 0) sector | 0x01; if (U2 0) sector | 0x02; if (U3 0) sector | 0x04; float T1, T2; switch(sector) { case 1: // 扇区 I T1 U2; T2 U1; break; case 2: // 扇区 II T1 -U3; T2 -U2; break; // ... 其他扇区类似 default: break; } // 电压矢量幅值限制圆形限制 if((T1 T2) PWM_PERIOD) { float scale PWM_PERIOD / (T1 T2); T1 * scale; T2 * scale; } float T0 (PWM_PERIOD - T1 - T2) / 2.0f; // 计算各相占空比 switch(sector) { case 1: DutyCycle-a T0; DutyCycle-b T0 T1; DutyCycle-c T0 T1 T2; break; // ... 其他扇区 } // 最后将占空比归一化并写入定时器比较寄存器 }在main函数的循环里我们让theta每次增加一个小量比如0.001弧度然后调用Svpwm_Gen和更新占空比的函数。如果硬件连接正确你应该能看到电机开始缓慢、平稳地旋转。用示波器测量电机三相线对地的电压应该能看到中心对齐的、正弦波调制后的PWM波形。这个阶段电机可能有噪音或轻微抖动这是正常的因为开环没有反馈来纠正误差。3. 电流采样与闭环系统的“感官”与“反射弧”开环转起来只是第一步就像蒙着眼睛走路迟早会撞墙。要让电机听话我们必须引入反馈形成闭环。FOC的第一个内环也是最重要的环就是电流环。而实现电流环的前提是能准确、实时地测量电机的相电流。3.1 硬件采样原理与配置我们用的X-NUCLEO-IHM07M1驱动板采用经典的“三电阻采样”方案。在三相逆变桥的下桥臂每个相都串联了一个小阻值的采样电阻通常是0.33欧姆或0.5欧姆。当该相的下桥臂MOS管导通时相电流会流经这个电阻产生一个微小的电压降。这个电压经过运放放大板子上通常是1.53倍后送到STM32的ADC输入引脚。这里有个关键点采样时刻必须严格对应下桥臂导通的时候否则采样到的电压没有意义甚至是错误的。这就是为什么之前我们要用TIM1的通道4来触发ADC我们可以通过调整通道4的比较值将采样点精准地设置在PWM周期中间、下桥臂稳定导通的时刻避开MOS管开关瞬间的噪声。在CubeMX里配置ADC1启用两个通道例如PA0和PC1对应驱动板输出的两相电流信号。关键步骤是选择“外部触发”模式触发源选择“Timer 1 Trigger Out event”。这样ADC就会乖乖地等待我们PWM定时器发出的采样指令。同时为了不占用CPU我们启用DMA直接存储器访问来搬运ADC的转换结果。DMA配置为循环模式数据宽度为半字16位对应12位ADC结果这样每次转换完成数据会自动存到我们指定的数组里并产生一个中断通知我们处理。配置完成后在代码中启动ADC和DMAuint16_t adc_buf[2]; // 存储两相电流的ADC值 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buf, 2);然后设置TIM1通道4的比较值比如设为500假设ARR是1000这样采样点就在PWM周期的正中间。__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_4, 500);3.2 电流计算与软件处理在DMA传输完成的中断回调函数HAL_ADC_ConvCpltCallback中我们将原始的ADC值转换为实际的电流值。这个过程需要校准和计算零点校准电机不通电时采样到的电压不是0V因为运放有偏移。我们需要记录这个“零点”ADC值比如是1910。电压转换电压 (ADC采样值 - 零点值) * 3.3V / 4096。3.3V是ADC参考电压4096是12位ADC的量程2^12。电流计算电流 电压 / (采样电阻阻值 * 运放放大倍数)。代入我们的参数电流 电压 / (0.33 * 1.528)。void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc-Instance ADC1) { // 计算A相和B相电流 Motor.Ia (float)(adc_buf[0] - 1910) * 3.3f / 4096.0f / (0.33f * 1.528f); Motor.Ib (float)(adc_buf[1] - 1910) * 3.3f / 4096.0f / (0.33f * 1.528f); // 根据基尔霍夫电流定律三相电流之和为零计算C相电流 Motor.Ic -Motor.Ia - Motor.Ib; // 可以在这里翻转一个测试用的LED用逻辑分析仪看翻转间隔是否等于PWM周期验证采样时机 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }现在通过VOFA你应该能看到三条相位相差120度的正弦波电流波形。如果波形毛刺很大说明采样时刻可能还在开关噪声附近需要微调TIM1通道4的比较值找到电流最平稳的那个点。3.3 电流环内环的实现有了准确的电流反馈我们就可以构建电流环了。电流环是FOC系统的“反射弧”它要求响应速度极快因此我们把它放在PWM周期中断也就是TIM1的溢出中断里执行每个PWM周期约70us执行一次。其核心流程就是经典的FOC链Clarke变换 - Park变换 - PI调节 - Park逆变换 - SVPWM。// 在TIM1更新中断中调用 void Current_Loop_Handler(void) { Ialpha_beta_t Iab; // 静止两相电流 Idq_t Idq; // 旋转两相电流D-Q轴 Idq_t Vdq_ref; // D-Q轴电压参考值 Ualpha_beta_t Vab_ref; // 静止两相电压参考值 // 1. Clarke变换将测得的三相电流Ia, Ib, Ic转换为Iα, Iβ Iab.alpha Motor.Ia; Iab.beta (Motor.Ia 2.0f * Motor.Ib) * ONE_BY_SQRT3; // 常用的一种变换形式 // 2. Park变换将静止坐标系的Iα, Iβ转换到随转子旋转的D-Q坐标系 // 这里需要转子的实时角度theta开环时我们用自增的角度模拟闭环后由位置传感器提供 float sin_theta sinf(Motor.theta); float cos_theta cosf(Motor.theta); Idq.d Iab.alpha * cos_theta Iab.beta * sin_theta; Idq.q -Iab.alpha * sin_theta Iab.beta * cos_theta; // 3. PI控制器计算D轴和Q轴的电压指令 // 通常我们让Id_ref 0磁链电流为零 Iq_ref由外环速度环给出 Vdq_ref.d PID_CurrentD(0 - Idq.d); // D轴电流PI控制 Vdq_ref.q PID_CurrentQ(Motor.Iq_ref - Idq.q); // Q轴电流PI控制 // 4. Park逆变换将旋转坐标系的电压指令Vd, Vq转换回静止坐标系 Vab_ref.alpha Vdq_ref.d * cos_theta - Vdq_ref.q * sin_theta; Vab_ref.beta Vdq_ref.d * sin_theta Vdq_ref.q * cos_theta; // 5. SVPWM将Vα, Vβ转换为三相占空比并更新PWM寄存器 Svpwm_Gen(Vab_ref, Motor.Duty); Update_PWM_Duty(Motor.Duty); }电流环的PI参数整定是关键。我的经验是先给一个很小的积分项Ki然后慢慢增大比例项Kp直到电流指令能快速跟踪但又没有明显的超调和振荡。可以用VOFA给Iq_ref一个阶跃信号比如从0到0.5A观察实际Iq的响应曲线来调整。4. 霍尔传感器低成本下的位置与速度感知对于很多成本敏感的应用光电编码器或旋转变压器太贵而霍尔传感器是绝佳的选择。三个霍尔元件以120度电角度间隔安装在电机定子上它们输出的是数字方波信号每60度电角度变化一次状态总共6个状态。我们需要做的就是捕获这三个引脚的电平变化从而推断出转子的粗略位置分辨率为60度和速度。4.1 硬件连接与定时器配置将三个霍尔传感器输出引脚连接到STM32的定时器如TIM2的通道1、2、3CH1, CH2, CH3。在CubeMX中将TIM2的工作模式设置为“Hall Sensor Mode”。在这个模式下三个通道会被内部连接在一起任何一个通道的电平变化都会触发一次捕获事件。我们需要开启定时器的输入捕获中断同时为了测量两个霍尔信号之间的时间用于计算速度我们还需要开启定时器的溢出中断。配置时有一个细节需要注意在“Hall Sensor”参数设置里有一个“IC Selection”。如果选择“直接”那么上升沿和下降沿都会触发中断如果选择“反相”则只有上升沿触发。这需要根据你的霍尔传感器输出极性来定。另一个关键点是设置“更新事件源”建议选择“仅计数器上溢/下溢”这样可以避免每次霍尔变化都产生一个不必要的溢出中断减少CPU负担。初始化代码大致如下HAL_TIMEx_HallSensor_Start_IT(htim2); // 启动霍尔传感器模式并开启中断 __HAL_TIM_ENABLE_IT(htim2, TIM_IT_UPDATE); // 使能更新中断 __HAL_TIM_URS_ENABLE(htim2); // 设置只有计数器溢出/下溢才产生更新中断4.2 位置与速度计算在霍尔中断回调函数HAL_TIM_IC_CaptureCallback中我们需要做以下几件事读取当前霍尔状态通过读取TIM2的通道1、2、3的GPIO电平或者直接读取定时器捕获/比较寄存器CCRx的映射值得到一个0-7之间的数字实际有效是1-6。判断旋转方向比较本次霍尔状态和上一次的状态。霍尔状态在一个电周期内按5-1-3-2-6-4的顺序变化为正转反之则为反转。更新电角度每次霍尔中断意味着转子转动了60度电角度。我们可以根据方向在当前角度上增加或减少60度。但这样得到的位置是离散的6个点。捕获计数值计算速度在霍尔中断发生时定时器的计数器CNT值会被自动捕获到CCR1寄存器中。这个值代表了从上一次霍尔变化到这一次变化所经过的定时器时钟周期数。由于我们知道两次变化之间转子转过了60度电角度所以瞬时电角速度可以计算为ω (60度) / (捕获值 * 定时器时钟周期)。将这个速度单位转换为弧度/秒或转/分RPM。重置计数器在中断里通常需要手动将计数器清零以便测量下一个间隔。uint8_t last_hall_state 0; float electrical_angle 0.0f; // 电角度单位弧度 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { uint8_t hall_state (HAL_GPIO_ReadPin(HALL_U_GPIO_Port, HALL_U_Pin) 0) | (HAL_GPIO_ReadPin(HALL_V_GPIO_Port, HALL_V_Pin) 1) | (HAL_GPIO_ReadPin(HALL_W_GPIO_Port, HALL_W_Pin) 2); // 判断方向并更新离散位置 int8_t direction Get_Hall_Direction(last_hall_state, hall_state); electrical_angle direction * (PI / 3.0f); // 增加或减少60度PI/3弧度 // 计算速度捕获值CNT代表了60度电角度所用的时间 uint32_t capture_value __HAL_TIM_GET_COMPARE(htim2, TIM_CHANNEL_1); float delta_time (float)capture_value / (float)SystemCoreClock; // 单位秒 float speed_electrical_rad_per_sec (PI / 3.0f) / delta_time; // 电角速度弧度/秒 // 转换为机械转速RPM需要除以电机极对数Pole_Pairs Motor.speed_rpm (speed_electrical_rad_per_sec * 60.0f) / (2.0f * PI * Motor.Pole_Pairs); last_hall_state hall_state; __HAL_TIM_SET_COUNTER(htim2, 0); // 清零计数器准备下一次测量 } }这样我们就得到了两个关键信息一个分辨率较低但绝对准确的位置信号每60度一个点和一个实时更新的速度信号。4.3 速度环外环与位置插值电流环稳定后我们就可以在上面搭建速度环了。速度环的执行频率可以比电流环低比如1kHz。在速度环的中断可以用另一个定时器实现里我们读取由霍尔计算得到的Motor.speed_rpm与目标速度进行比较通过一个PI控制器输出就是电流环的Q轴电流指令Iq_ref。但是对于FOC的电流环来说我们需要的是连续、高分辨率的位置信号theta而霍尔只提供了6个离散点。怎么办答案是位置插值。在两个霍尔信号之间我们利用计算得到的速度信息对位置进行积分。假设速度环在1ms中断一次我们在这1ms内认为速度是恒定的那么电角度就可以这样更新// 在1kHz速度环中断中执行 void Speed_Loop_Handler(void) { // 1. 速度PI控制输出Q轴电流指令 Motor.Iq_ref PID_Speed(Target_Speed - Motor.speed_rpm); // 2. 位置插值离散的霍尔角度 速度积分 // 获取最新的离散霍尔电角度由霍尔中断更新单位弧度 float hall_electrical_angle Motor.hall_angle; // 计算自上次速度环中断以来转子转过的电角度增量 // speed_rpm 是机械转速需要转换为电角速度乘以极对数再乘以时间间隔0.001秒 float delta_angle (Motor.speed_rpm * 2.0f * PI * Motor.Pole_Pairs / 60.0f) * 0.001f; // 更新用于FOC的连续角度 Motor.theta hall_electrical_angle delta_angle; // 3. 将角度归一化到0~2π之间 if (Motor.theta 2.0f * PI) Motor.theta - 2.0f * PI; if (Motor.theta 0) Motor.theta 2.0f * PI; }通过这种“离散位置速度积分”的方法我们得到了一个连续变化的theta虽然长期精度依赖于速度测量的准确性但在几个毫秒的短时间尺度内其精度足以满足FOC电流环对位置信息的需求。至此一个完整的、基于霍尔传感器的FOC控制系统就搭建完成了。剩下的就是耐心地调整三个PID环电流D轴、电流Q轴、速度环的参数。先调内环电流环确保电流响应快速无振荡再调外环速度环让电机能平稳地加速、减速并稳定在目标转速。调试过程中VOFA是你的最佳伙伴多观察波形理解每个参数变化对系统的影响。当你能让电机从静止安静地加速到上万转并且转速波动很小时恭喜你你已经掌握了基于STM32和霍尔传感器的FOC实战精髓。这个过程会遇到很多坑比如采样噪声、开关死区影响、参数整定困难但每一个坑填平后你对电机控制的理解都会更深一层。