STM32霍尔FOC角度估算实战:从跳变沿到SVPWM的完整工程
简介本资源面向嵌入式电机控制初学者与进阶开发者提供一套基于STM32F103的永磁同步电机FOC控制程序核心亮点是采用霍尔传感器反馈转子位置并实时计算电角度解决无编码器场景下磁场定向控制的实现难题。压缩包共263个文件约1.4MB以C源码与头文件为主体48个.c、80个.h辅以95个汇编文件、Keil与IAR工程配置、hex烧录文件及少量说明文档覆盖从底层驱动到算法实现的完整工程结构。程序围绕克拉克变换、帕克变换与PI电流环展开将三相电流解耦为d轴与q轴分量配合SVPWM模块完成闭环控制便于读者理解FOC各环节的代码落地方式。目前已有1157人学习下载适合作为课程设计、电赛或电机控制入门的参考工程帮助快速搭建可编译、可烧录的PMSM控制框架并对照调试。1. 霍尔算角度做 FOC这套 STM32 工程到底能不能直接跑如果你手上有一块带霍尔传感器的无刷电机想用 STM32 跑 FOC大概率会先卡在同一个地方角度从哪来。编码器方案贵无感方案低速又飘霍尔方案看起来最接地气但真动手才发现霍尔每 60 电角度才给一次跳变中间那 60 度全靠估估不好电机就抖、就响、就发热。这套「基于 STM32 的通过霍尔计算角度的 FOC 控制 PMSM 电机程序」解决的正是这个中间段的角度估算问题——它把霍尔跳变、定时器捕获、速度估算和 FOC 电流环串成了一条完整链路不是只给你一个 SVPWM 演示。它适合两类人一类是已经能点灯、能跑定时器但没做过电机闭环的 STM32 开发者另一类是做风机、水泵、电动工具这类对成本敏感、又要求低速平稳的从业者。不适合想直接拿去做伺服级高精度位置控制的人霍尔方案的天花板就在那。下面我按「资源是什么 → 怎么用 → 坑在哪」的顺序把这份工程拆开讲清楚参数怎么设、失败看什么都落到具体位置。2. 霍尔角度估算与 FOC 链路从跳变沿到 SVPWM 的完整推导2.1 霍尔 60 度盲区为什么必须做插值三霍尔安装相差 120 机械角度电角度上每 60 度产生一次状态跳变一个电周期只有 6 个确定角度点。如果直接用「当前霍尔状态对应一个固定角度」去算转子在 60 度区间内角度是阶梯状的FOC 的 Park 变换拿到的角度误差最大能到 ±30 电角度。这个误差直接导致 Iq 和 Id 耦合表现为电机电流忽大忽小、转矩脉动明显、低速时尤其吵。常见做法是速度积分插值在霍尔跳变沿记录当前角度然后用上一次跳变到这一次跳变之间的平均速度去推算区间内每个 PWM 周期的角度。核心公式是 θ θ_last ω × Δt其中 ω 由两次跳变的时间差算出。这套工程里ω 的更新和角度累加都放在定时器中断里保证和 PWM 同频。提示插值精度取决于速度估算的平滑程度速度不做滤波角度就会跟着抖。2.2 定时器捕获霍尔跳变的具体配置霍尔信号接到 STM32 的 TIM 通道上用输入捕获模式记录跳变时刻。以 TIM3 为例三个霍尔接 CH1、CH2、CH3配置成捕获上升沿和下降沿都触发这样每个电周期能拿到 6 次时间戳。// TIM3 输入捕获初始化三通道霍尔 void Hall_TIM_Init(void) { TIM_IC_InitTypeDef ic; TIM_HandleTypeDef htim3; htim3.Instance TIM3; htim3.Init.Prescaler 71; // 72MHz/72 1MHz 计数频率 htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 0xFFFF; // 满量程靠捕获值算差值 htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(htim3); ic.ICPolarity TIM_ICPOLARITY_RISING; // 先配上升沿 ic.ICSelection TIM_ICSELECTION_DIRECTTI; ic.ICPrescaler TIM_ICPSC_DIV1; ic.ICFilter 0x0F; // 滤波防霍尔毛刺 HAL_TIM_IC_ConfigChannel(htim3, ic, TIM_CHANNEL_1); HAL_TIM_IC_ConfigChannel(htim3, ic, TIM_CHANNEL_2); HAL_TIM_IC_ConfigChannel(htim3, ic, TIM_CHANNEL_3); HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_2); HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_3); }预分频 71 把 72MHz 降到 1MHz每个计数代表 1 微秒捕获值直接就是时间戳。滤波参数 ICFilter 设 0x0F 是较重的滤波霍尔信号在电机换向时容易有尖峰不滤会误触发。Period 设满量程是为了避免计数溢出前被清零差值计算用无符号减法自然处理回绕。2.3 角度插值与速度估算的代码落点捕获中断里拿到时间戳后先算两次跳变的时间差再算电角度增量。注意霍尔状态到角度的映射表要按你的电机实际安装顺序标定不能照抄。volatile int32_t hall_dt; // 两次跳变时间差 volatile uint8_t hall_state; // 当前霍尔状态 1~6 volatile float elec_angle; // 电角度 0~2π volatile float elec_speed; // 电角速度 rad/s // 霍尔状态到基准角度的映射需按实际电机标定 const float hall_angle_table[8] { 0, 0, M_PI/3, 2*M_PI/3, M_PI, 4*M_PI/3, 5*M_PI/3, 0 }; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { static uint32_t last_cap 0; uint32_t now HAL_TIM_ReadCapturedValue(htim, htim-Channel); hall_dt (now - last_cap) 0xFFFF; // 无符号减法处理回绕 last_cap now; hall_state Read_Hall_GPIO(); // 读三路霍尔电平组合 elec_angle hall_angle_table[hall_state]; // 电角速度 60度电角度 / 时间差 if (hall_dt 0) { elec_speed (M_PI / 3.0f) / (hall_dt * 1e-6f); } }hall_dt 用无符号减法再掩码是因为定时器会回绕直接相减在回绕点会得到负数。elec_speed 单位是 rad/s后面电流环和速度环都用这个量纲。映射表里 0 和 7 是无效状态实际接线正常不会出现留 0 是防御性写法。2.4 电流环与 SVPWM 的衔接参数角度算出来之后Clarke 变换把三相电流变成 Iα、IβPark 变换用 elec_angle 转成 Id、Iq。这里有个容易忽略的点霍尔插值出来的角度有相位滞后如果电流环带宽设太高滞后会引起振荡。常见做法是电流环带宽压在 1kHz 以内速度环再低一个数量级。SVPWM 的载波频率和电流采样时机要配合。载波 20kHz 时电流采样放在 PWM 中心对齐的下溢中断里避开开关噪声。这套工程里 PWM 用 TIM1 互补输出带死区死区时间按你的 MOS 管和驱动芯片定一般 1 微秒左右起步大了会畸变小了会直通。参数典型值说明PWM 载波频率20 kHz兼顾噪声和开关损耗电流环带宽500 Hz ~ 1 kHz受霍尔角度滞后限制速度环带宽50 Hz ~ 100 Hz低于电流环一个数量级死区时间0.5 ~ 2 微秒按驱动芯片和 MOS 定霍尔滤波0x0F抑制换向尖峰3. 从零把工程跑起来时钟、外设、标定三步走3.1 时钟树和 PWM 定时器的对应关系STM32F103 常见配置是外部 8MHz 晶振PLL 倍频到 72MHz。TIM1 挂 APB2时钟 72MHzTIM3 挂 APB1也是 72MHz。PWM 载波 20kHz 意味着计数周期 3600中心对齐模式下实际中断频率是 40kHz这个要心里有数中断里别放耗时操作。// TIM1 PWM 初始化中心对齐20kHz void PWM_TIM_Init(void) { TIM_HandleTypeDef htim1; TIM_OC_InitTypeDef oc; TIM_BDTRInitTypeDef bdtr; htim1.Instance TIM1; htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; htim1.Init.Period 3600 - 1; // 72MHz / 3600 / 2 10kHz? 注意中心对齐 htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; HAL_TIM_PWM_Init(htim1); oc.OCMode TIM_OCMODE_PWM1; oc.Pulse 1800; // 50% 占空比初始 oc.OCPolarity TIM_OCPOLARITY_HIGH; oc.OCNPolarity TIM_OCNPOLARITY_HIGH; oc.OCIdleState TIM_OCIDLESTATE_RESET; oc.OCNIdleState TIM_OCNIDLESTATE_RESET; 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); bdtr.DeadTime 72; // 72/72MHz 1us 死区 bdtr.BreakState TIM_BREAK_DISABLE; bdtr.AutomaticOutput TIM_AUTOMATICOUTPUT_ENABLE; HAL_TIMEx_ConfigBreakDeadTime(htim1, bdtr); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); // CH2 CH3 同理 }Period 设 3600-1 配合中心对齐实际载波是 72MHz/(3600×2)10kHz。想要 20kHz 就把 Period 改成 1800-1。死区寄存器 DeadTime 写 72对应 1 微秒这个值要按你的驱动芯片手册调IR2103 这类常用 0.5 到 1 微秒。3.2 霍尔安装顺序标定别信默认表霍尔状态和电角度的对应关系跟电机绕组和霍尔安装位置强相关。同一套代码换一个电机映射表可能完全不对。标定方法很简单手动慢慢转电机记录每个霍尔状态跳变时转子大致位置或者用示波器同时看霍尔信号和反电动势过零点。常见做法是给电机通一个固定角度的小电流让转子吸到那个位置然后读霍尔状态记下对应关系。六个状态都标一遍填进 hall_angle_table。这一步不做电机要么不转要么反转要么抖得厉害。注意标定前先把电流限幅设小比如额定电流的 10%防止吸合时电流过大。3.3 电流采样与偏置校准FOC 要控的是 Iq 和 Id前提是相电流采得准。常见方案是下桥臂采样电阻加运放或者用霍尔电流传感器。采样时机在 PWM 下溢中断此时下桥臂导通采样电阻上有电流。偏置校准在电机不转、PWM 不输出时做读多次 ADC取平均作为零电流偏置。之后每次采样值减去偏置再换算成电流。这个偏置会随温度漂要求高的场合要定期重校。#define ADC_OFFSET_SAMPLES 256 int32_t adc_offset_a, adc_offset_b; void Current_Offset_Calib(void) { int32_t sum_a 0, sum_b 0; for (int i 0; i ADC_OFFSET_SAMPLES; i) { sum_a ADC_Read(ADC_CH_A); sum_b ADC_Read(ADC_CH_B); } adc_offset_a sum_a / ADC_OFFSET_SAMPLES; adc_offset_b sum_b / ADC_OFFSET_SAMPLES; } float Get_Phase_Current(uint8_t ch) { int32_t raw ADC_Read(ch); int32_t offset (ch ADC_CH_A) ? adc_offset_a : adc_offset_b; return (raw - offset) * CURRENT_SCALE; // CURRENT_SCALE 按运放增益和电阻算 }CURRENT_SCALE 由采样电阻、运放增益和 ADC 参考电压共同决定比如 0.01 欧电阻、10 倍增益、3.3V 参考、12 位 ADC算下来约 0.008 A/LSB。这个系数错了电流环增益怎么调都不对。4. 避坑与排查霍尔 FOC 最容易翻车的五个地方4.1 电机不转或原地抖先查霍尔映射和相序现象是给速度指令后电机嗡嗡响但不转或者来回抖。原因通常是霍尔状态到角度的映射表不对或者三相线序接错。解决方法是先不开环用手转电机看霍尔状态变化是否按 1-3-2-6-4-5 顺序循环不对就调线序或改映射表。相序错的话任意对调两根电机线再试。4.2 低速转矩脉动大角度插值没做或速度没滤波现象是低速时电机一顿一顿高速反而正常。原因是霍尔跳变之间角度没插值或者速度估算噪声大导致角度累加抖。解决方法是确认插值在 PWM 中断里执行并且对 elec_speed 做一阶低通滤波截止频率设在速度环带宽的 2 到 3 倍。4.3 电流采样值跳变采样时机和滤波没配对现象是 Id、Iq 波形毛刺大电流环输出抖。原因是采样时刻落在开关瞬间或者 ADC 滤波不够。解决方法是把采样触发点设在 PWM 下溢中断并且 ADC 采样时间设长一点比如 55.5 个周期牺牲一点速度换稳定。4.4 上电就过流保护死区或初始角度错现象是使能后立刻过流。原因是死区太小导致上下桥直通或者初始电角度不对导致给了一个反向大电流。解决方法是先把死区加到 2 微秒试确认没问题再往下调初始角度用霍尔当前状态对应的角度不要用 0。4.5 高速时角度滞后明显插值周期跟不上现象是高速时电流畸变、效率下降。原因是霍尔跳变间隔随速度缩短插值用的速度值更新不及时。解决方法是提高速度估算的更新率或者在高速段切换到基于反电动势的估算。霍尔方案本身高速精度有限这是原理决定的边界。5. 进阶技巧用霍尔信号反推极对数和验证角度线性度这套工程跑通之后我一般会做两件事来确认参数没设错。第一件是测极对数用手匀速转一圈电机数霍尔状态循环了几次循环次数就是极对数。比如转一圈霍尔状态循环了 4 次就是 4 对极。这个数填错电角度和机械角度换算全错电机表现是能转但转速指令和实际差倍数。第二件是验证角度线性度。方法是在电机匀速转动时把插值出来的 elec_angle 通过 DAC 或者串口打出来同时用示波器看某一相的反电动势。理想情况下角度应该线性增长和反电动势过零点对齐。如果角度曲线有台阶或者跳变说明插值逻辑有问题常见的是 hall_dt 在回绕点算错或者映射表某个状态填错。// 串口输出角度和速度用于上位机画图验证 void Debug_Output(void) { static uint32_t last_tick 0; if (HAL_GetTick() - last_tick 1) return; // 1kHz 输出 last_tick HAL_GetTick(); printf(%.4f,%.4f,%d\r\n, elec_angle, elec_speed, hall_state); }输出频率 1kHz 足够看趋势太高会占串口带宽。上位机用串口示波器或者自己写个 Python 脚本画图都行。重点看角度是不是单调递增速度是不是平滑。如果角度在某个霍尔状态附近总是跳回去查那个状态的映射值。还有一个实用技巧是给角度加一个偏移量校准。因为霍尔安装和绕组之间总有机械误差插值出来的角度和真实磁极位置可能差几度。做法是固定一个 Iq 指令微调角度偏移看同样 Iq 下转速最高或者电流最小那个偏移就是补偿值。这个偏移因电机而异换电机要重标。从那以后我每次调霍尔 FOC都强制先做极对数确认和角度线性度验证再动电流环参数。这两步不做后面调什么都是玄学。希望帮到你。本文还有配套的精品资源点击获取