简介:STM32F1 FOC库开发资料包面向嵌入式电机控制开发者,聚焦在STM32F103平台上实现永磁同步电机(PMSM)的磁场定向控制,适合需要快速上手FOC算法与库移植的进阶用户,也可用于电机驱动项目预研。资料内含中文版用户手册与ST官方FOC固件库v2.0,手册覆盖FOC理论基础、库结构、配置步骤、参数调节与示例应用,源码则包含电流控制、转子位置估算、SVPWM、ADC采样等核心模块,并基于已有工程文件可直接编译参考。整个压缩包共241个文件,包含88个H头文件、69个C源文件,以及IAR/Keil工程配置、链接脚本、少量库与汇编文件,覆盖从工程导入到编译调试的完整链路;压缩包仅2.39MB,轻量易获取。目前已有1114人学习下载,对希望借助官方库理解PMSM控制、调试电流环并快速搭建STM32F1电机驱动方案的开发者很有参考价值。
1. FOC库拿到手,先搞清它替你干了哪些活
一个没有FPU、主频只有72MHz的STM32F103,要在不到100微秒里完成电流采样、坐标变换、两个PI调节、一次反变换,再刷新六路互补PWM,这就是“STM32F1 FOC库”每天都在做的计算量。熟练的工程师拿到这个库,第一反应不是从头看原理,而是翻操作手册里的硬件适配表:定时器用第几路、ADC用哪个注入组、编码器接口留给了谁。这个库的真正价值在于算法已经在F1上跑通了,底层驱动和电机参数解耦了,手册把每个数组、每个宏的含义标清楚了。适合两类人:一类是刚转做无刷电机驱动、不想从零手写坐标变换的嵌入式开发者;另一类是已经做了有感方案、想低成本试水无感FOC的团队。
2. 在F1上跑FOC库:先理解三层边界,再动手改代码
2.1 库在替你做哪两层计算
FOC不是一种神秘的“新控制法”,它做的事情通俗讲就是:把三相交流电流拆成两个互不干扰的分量,一个管磁通,一个管力矩,然后分别控制。电机定子里通的是三相正弦电流,磁场方向一直在转,直接控制三相交流量非常费劲,坐标变换把它搬到转子坐标系里看,d轴电流管磁场、q轴电流管力矩,控制算法就从“追着正弦跑”变成了“控制两个直流小信号”,难度下降一个量级。
这个库在源代码里替你封装的就是这两层:第一层是Clark变换和Park变换,把采到的三相电流变成d轴、q轴直流分量;第二层是电机模型和滑模观测器,在无传感器方案里用反电动势估算转子电角度。PWM更新中断里执行的是电流环,主循环或定时中断里执行的是速度环,这个分层关系必须在看代码之前建立起来。
很多第一次接触的人会犯一个错,拿到库就扎进PI控制器实现里调参数,实则应当先确认库给你保留的对外接口是哪些。我习惯先看头文件:结构体里有哪些状态变量,哪些是只读、哪些要外部赋值。常见接口路径是motor_init、motor_start、motor_stop,外加一个速度指令参数,电流环内部自动跑,开发者改得最多的其实只是初始化表和两个PI参数。
2.2 驱动层的资源账:为什么用的是TIM1而不是TIM2
FOC需要六路PWM来驱动三相全桥,这六路基波必须是同一个时基上产生的互补信号,还要留死区时间防止上下管直通。在F1里,这个任务只能交给带刹车输入和互补输出的高级定时器,也就是TIM1或TIM8,而不是普通定时器TIM2、TIM3。TIM1在F1里是72MHz时基的六路互补PWM生成器,同时它的触发输出可以接ADC注入组,让电流采样和PWM波形严格同步。
代码里常见的驱动层配置是这样的顺序:先把TIM1配成中心对齐模式,让计数波形是一个三角形而不是锯齿波;然后在中心点附近触发ADC采样,因为此时下管导通时间中点两边电流变化最平缓,采样值最能代表一个PWM周期的平均相电流;最后使能TIM1的换相事件作为ADC外部触发。这个时序配合是FOC能在F1上跑起来的硬件前提,示例源码里给的那几个初始化函数,就是在干这个事情。
F1没有浮点运算单元,所有小数运算在库内部都用定点和查表处理。不是每个库都默认开这些优化,我之前在A同学的项目里见过一次FOC移植后跑起来发现中断时长超标,检查发现库内部某个版本用了大量double运算。这个题裁下,定点化和查表是库文件性能的隐形分水岭。
2.3 动手前先过一遍配置清单,避免改错宏
库的手册通常会在“配置向导”部分列一张表,把需要现场确认的硬件参数全部集中到几个宏定义或头文件里。常见的配置项包括:电机极对数、额定电流对应的ADC满量程值、母线电压分压系数、电流采样电阻阻值和增益、PWM频率、死区时间。这些参数不填对,代码里PI运算结果就会超出实际物理范围,表现基本上是电机一顿一顿转或者逆变器直接炸管。
我一般动手前会花半小时把这张表过一遍,重点看三处。一是极对数,它直接决定电角度与机械角度的换算倍数,错了的话有传感器方案会表现出角度抖动,无感方案基本起不来转。二是电流采样增益,ADC满量程对应多大电流决定了电流环的分辨率,通常要保证额定电流占ADC量程的六到七成,太小会有量化噪声,太大带宽再高也白搭。三是PWM频率与死区,F1跑FOC常用10kHz到20kHz的PWM频率,死区设置则要根据功率管的开关时间和驱动电路延迟来定。
下表是我常用的初始参考范围,实际值以电机和驱动板为准:
| 项目 | 典型范围 | 初始化依据 |
|---|---|---|
| PWM频率 | 10kHz ~ 20kHz | 电机电感越小取越高,兼顾开关损耗 |
| FOC电流环频率 | 与PWM频率一致 | 每次PWM更新做一次电流环 |
| ADC触发点 | 中心对齐计数的中心点附近 | 尽量避开死区换向时刻 |
| 死区时间 | 0.4us ~ 2us | 驱动器延迟+功率管关断时间留余量 |
| PI输出限幅 | 母线电压的80%左右 | 防止积分饱和后过调制 |
这套参数确认完之后,再往下走读代码时就不会一头扎进细节里。下面一章我们把最小可跑系统搭出来。
3. 把FOC库部署到工程里:最小可跑系统的三件事
3.1 添加库文件之前,先读手册三个重点页
解压库包之后,不要急着把全部文件拖进工程里,先翻操作手册里的三处内容:一是“目录结构和文件说明”,弄清楚哪些是用户文件、哪些是内部算法文件、哪些是硬件适配文件;二是“接线说明”,它会画清楚驱动板电流采样、母线电压、控制信号分别接到单片机的哪几个引脚——这是整个移植过程里最容易错的部分;三是“示例工程说明”,通常有一个标准例程,直接拿它作为起点,别自己从头建工程。
三个信息确认完,再把如下几类文件添加进你的工程:核心算法源文件(坐标变换、SMO、PID)、硬件适配文件(PWM初始化、ADC初始化、保护逻辑)、用户接口文件(速度和力矩指令入口)。内部算法文件不要改动,改动面限定在适配文件里,这个习惯能让你以后升级库包时省很多事。F1的标准外设库和HAL库在工程接入上有差异,但适配层的职责是一样的,无非是把定时器、ADC、GPIO这老三样交给库去使唤。
3.2 初始化TIM1:生成六路带死区的互补PWM
下面是一份在F1上用标准外设库初始化TIM1生成六路PWM的代码骨架,核心配置都给了注释。FOC场景里PWM必须工作在中心对齐模式,这决定了后续ADC采样时刻的分布:
static void BSP_TIM1_PWM_Init(uint16_t arr, uint16_t psc, uint16_t deadtime) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; // 打开TIM1时钟,以及输出引脚所在的GPIOA和GPIOB RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // 六个输出通道引脚配置为复用推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOB, &GPIO_InitStructure); // 中心对齐模式,计数从0加到ARR再减回0 TIM_TimeBaseStructure.TIM_Prescaler = psc; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned1; TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // 六路PWM输出配置,极性先设为高,死区另一侧由下面BDTR安排 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse = arr >> 1; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC2Init(TIM1, &TIM_OCInitStructure); TIM_OC3Init(TIM1, &TIM_OCInitStructure); TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OC2Init(TIM1, &TIM_OCInitStructure); TIM_OC3Init(TIM1, &TIM_OCInitStructure); // 刹车与死区配置 TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime = deadtime; TIM_BDTRInitStructure.TIM_Break = TIM_Break_Disable; TIM_BDTRInitStructure.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRInit(TIM1, &TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_UpdateDisableConfig(TIM1, DISABLE); }这段配置里有三个点容易踩坑。第一,F1的TIM1是高级定时器,最后必须调TIM_CtrlPWMOutputs打开主输出,否则六路PWM引脚没有波形输出,初学者经常卡在这里。第二,TIM_BDTRInitTypeDef里的死区单位不是微秒而是DTS周期的倍数,计算方式与TIM_ClockDivision直接相关,我通常在手册里用具体的Tdts参数一格格推,不在代码里硬背公式。第三,中心对齐模式下ARR重装载生效在计数顶点,所以更新中断的位置跟普通PWM不一样,后续给ADC信号时需要注意触发时刻。
3.3 初始化ADC注入组:电流采样绑定到PWM节奏上
FOC的电流采样讲究“跟PWM走”,因此ADC的触发源通常配置成TIM1的触发输出或通道事件。代码例子里用注入组是因为注入组可以抢占规则组,三个采样通道一次转换即完成三相电流快照:
static void BSP_ADC_InjectedInit(void) { ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_ADC2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_DMA1, DISABLE); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_RegSimult; // 规则组同步模式 ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); // 设置注入组外部触发源为TIM1 CC4事件,采样时刻由PWM中心点附近触发 ADC_ExternalTrigInjecConvCmd(ADC1, ENABLE); ADC_ExternalTrigInjecConvConfig(ADC1, ADC_ExternalTrigInjecConv_T1_CC4); // 注入组三个通道:对应相电流采样引脚 ADC_InjectedChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_239Cycles5); ADC_InjectedChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_239Cycles5); ADC_InjectedChannelConfig(ADC1, ADC_Channel_12, 3, ADC_SampleTime_239Cycles5); ADC_InjectedDiscModeCmd(ADC1, DISABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartInjectedConvCmd(ADC1, ENABLE); }注入组触发方式在F1上的枚举名和F4、H7不一样,直接从旧工程复制代码时容易遇到编译报错。另外,如果驱动板是三电阻采样方案,三个通道必须在同一时刻采样,ADC注入组的转换是一个通道接一个通道执行的,三个通道之间的微小延迟在大部分低速电机上可以忽略,但如果你做的是高频低电感电机,这个延迟会造成电流波形畸变,需要硬件上改用三个采样保持器同时保持。更常见的情况是使用单电阻采样,那样就只需要两个时刻的两次采样,消耗的ADC资源更少,但需要在状态机里额外管理采集中断逻辑。
3.4 启动顺序错了,电机只会嗡嗡响
驱动层配置好以后,通电测试不是直接给速度指令,而是要严格按照“零点校准、开环追赶、切闭环”的顺序走。零点校准时,控制三对桥臂输出同一个占空比,让电机轴固定在某个位置,记录此时三相电流ADC的偏置电压,这个偏置会随温度漂移,所以一般每次上电都重新校准。之后的开环追赶是给定一个旋转的开环角度,让电机跟随转起来,同时观察相电流波形有没有畸变。最后再从低速切入闭环FOC,切换瞬间如果速度误差大,多半是极对数或电角度方向配置错了。
很多开发者图省事,跳过开环追赶直接上闭环,结果电机堵转或者相电流过流。我自己的习惯是校准完先手动转一下电机,确认能感到齿槽的阻力变化,再用开环模式验证PWM和ADC时序都正确,最后才让闭环接管。
4. 示例源码里必须看懂的三个文件
4.1 单电阻电流采样:一段决定成败的状态机
F1做FOC最常见的成本方案是单电阻采样,它只用一个采样电阻,但要在两个不同的PWM时刻分别采样,才能重构出三相电流。源码里这段状态机一般在ADC中断里维护:第一个时刻采集电流值,第二个时刻采集电流值,然后通过基尔霍夫定律算出第三相。这个状态机的时序极其敏感,PWM占空比接近0或100%时,采样窗口会消失,这时必须改用重构算法或强制切换PWM策略。
我在A同学的项目里遇到过一次现象:电机空载正常,加载后波形毛刺剧增,查到最后是采样时刻落在了死区效应尚未消退的窗口里。F1的ADC采样时间设置得越长,采样电容充电越充分,但采样窗口在PWM周期里占据的死区附近时间也越多。这个矛盾没有完美解,库源码里通常会给一个min_sample_time的宏来约束最小占空比,如果你用的电机电感很小,这个宏就需要配合PWM频率一起重新评估。
4.2 电角度获取:有传感器看对齐,无传感器看SMO启动
有传感器方案(编码器或霍尔)的源码重点在于电角度对齐。机械角度和电角度的原点通常在编码器Z脉冲或霍尔换相表里定义,源码中会有一段初始化代码把电机固定在某个位置,然后把这个位置认成电角度零点。如果你的电机反转或电流环输出方向不对,第一反应不是改PID,而是检查这个零点偏移量。
无感方案的核心则是SMO或磁链观测器。它在低速时反电动势太小,基本不可用,所以库源码里通常做了开环启动和闭环切换的过渡逻辑。这部分代码里有个参数要重点关注:反电动势滤波器的截止频率或滑模增益。截止频率太高,观测角度噪声大;太低,角度滞后明显,高速时容易失步。示例源码默认值通常是为某个参考电机标定的,换成你的电机务必按反电动势系数重新核算。
4.3 两个PI环:读代码时先找限幅和积分清零条件
示例源码里的电流环和速度环通常是两个独立文件,读它们的时候优先找三样东西:比例增益和积分增益的宏、输出限幅值、积分项的清零或抗饱和条件。很多模板工程的PI写得并不规范,积分项在输出饱和后继续累加,导致电机过冲后迟迟收不回来,这种问题在源码里肉眼就能看出来。
电流环的输出限幅,在FOC里对应的是d轴和q轴电压上限,往往设置成母线电压的某个比例。速度环的输出是q轴电流指令,它的限幅直接决定了最大输出力矩。这两个限幅在库的默认值里通常写得比较保守,你可以根据实际电机和功率管能力放大,但放大前要确认硬件保护逻辑的优先级,不能让电流环先于硬件过流保护动作。
5. 避坑指南:F1上跑FOC库的5个翻车现场
5.1 上电就过流保护,电机纹丝不动
现象:程序下进去,使能输出的一瞬间,驱动板过流指示灯直接亮了,电机没有发出一丝声音。 原因:电流采样零偏没有校准。ADC在零电流时刻读到的值并不是中点,因为运放输入偏置、参考电压误差等因素,这个偏置会体现在d轴和q轴电流上,导致电流环认为有巨大的电流误差。 解决:先把示波器夹在相电流采样电阻两端,等电机静止时记录三个通道的ADC原始值;再在代码初始化流程里加入零点校准函数,把这三个值作为后续减去的偏置。记住校准必须每次上电都执行一次,因为偏置会随温度漂移。
5.2 电机能转,但电流波形像锯齿,噪声刺耳
现象:电机转起来以后,相电流波形没有平滑的正弦包络,而是有许多高频毛刺叠加,电机发出高音调啸叫。 原因:多半是采样时刻和死区混在一起了。中心对齐PWM虽然理论上给了你对称的采样窗口,但如果ADC触发时刻离死区太近,采样到的电流叠加了死区引起的续流尖峰。另一个常见原因是采样时间不够,F1的ADC采样时间配置太短,采样电容没充饱。 解决:先加大ADC采样时间到最大档试一下;若无改善,把ADC触发点向PWM中心移动,距离死区前沿留出至少1us;还不行就把PWM频率降半档,给采样窗口腾时间。这个问题的排查逻辑是从采样时刻出发,不是从滤波器出发。
5.3 开环能转,切闭环瞬间卡住
现象:开环模式里电机转动顺畅,一切换到闭环FOC,电机立即卡死或剧烈抖动。 原因:电角度方向或零点偏了。最常见的是极对数填错,或者编码器/霍尔安装方向与库默认方向相反,导致估算的角度和实际位置差了90度甚至180度。 解决:先用直流电压给电机一个固定方向,确认电机转子停稳的位置,再和电角度反馈对比。拿一个能显示角度的调试变量,手动慢速旋转电机轴,观察反馈角度是否在360度范围平滑变化、方向是否与旋转方向一致。方向反了就在代码里翻转计数方向或偏置量,这是只改一行宏的事。
5.4 代码加进原工程后,编译通过但中断跑飞
现象:程序一运行过几毫秒就跑进HardFault,或者电流环偶尔丢失一次执行。 原因:F1的RAM有限,FOC库的观测器、PI状态量、数组缓冲都占用不少内存;更隐蔽的问题是中断优先级冲突,ADC注入中断和定时器更新中断同时抢CPU,导致电流环执行时间抖动。 解决:把FOC相关的所有中断优先级调成同一个抢占优先级,按时间窗口错开;检查启动文件里堆栈大小,F1默认的堆栈经常不够用;必要时把观测器里的大型查找表放到const段而不是RAM里。
5.5 用手拧电机轴,相电流反向异常
现象:用手正转电机,示波器看到的相电流方向却是反的,或者电流值忽大忽小。 原因:电流采样引脚接错了相序,或者霍尔/编码器的机械零点换算错了。相序错误时闭环也能转,但电流环输出的角度和实际磁场方向不匹配,电机会表现出额外的力矩脉动。 解决:接线和软件定义对齐后,用手匀速慢转电机轴,在调试器里观察角度变量和反电动势观测值的相位关系。如果无法确认,干脆把三相VW两相对调,再重新校准零点,多半能排掉相序嫌疑。
6. 把FOC库调到能实战:两环调参顺序与三个验证手段
很多开发者拿到手就直接开始调速度环P值,这是最容易浪费时间打“玄学调试”的做法。正确顺序是先调电流环。把速度环断开,给一个固定的q轴电流指令,观察实际电流的跟踪误差,不断调电流环的P和I直到误差小且没有震荡。再重新接上速度环,把速度环P值从0慢慢往上加,每次只加一点点,观察速度波形是否收敛。顺序对了,即使参数不是最优,系统也能保持基本可控。
调参过程里我习惯用三个手段实时验证。第一,用内部DAC或串口把SMO观测到的电角度打印出来,配合手动旋转电机轴确认角度是否连续平滑;第二,零速锁定测试,给定q轴电流为零,用手拧电机轴,感受有没有回中力,回中力方向和角度零点对齐,说明电角度方向与相序是对的;第三,把母线电流波形接到示波器上,在速度阶跃的时刻观察包络是否有一个平滑的凸起,而不是剧烈震荡。
最后说句交过学费的话:在F1上跑这个库,最大的敌人不是算力,而是你以为算力不行。F4上Math库随手一调的代码思路不能直接套过来,F1没有FPU,我见过太多人把浮点运算堆进中断里,结果电流环周期直接翻倍。模块化把变换表、查表运算固定下来,真正留给你的业务逻辑才能跑得动。希望帮到你。
本文还有配套的精品资源,点击获取