ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从Arduino到STM32:DengFOC无刷电机位置控制移植实战

从Arduino到STM32:DengFOC无刷电机位置控制移植实战 简介面向STM32F4系列MCU的DengFOC闭环位置控制移植工程适合正在学习无感FOC电机控制、需要将DengFOC算法落地到HAL库开发环境的开发者。工程由STM32CubeMX生成包含完整Keil MDK项目可直接编译烧录验证。资源共183个文件以c源码和h头文件为主覆盖HAL驱动库、DengFOC算法核心、闭环位置控制逻辑另含uvprojx/uvoptx配置、hex/axf固件、map映射、ioc图形化配置等辅助文件整体仅1.4MB。已有578人学习下载。读者可获得一套可直接借鉴的FOC控制移植范例从外设初始化到位置环均有迹可循适合学习STM32定时器、ADC采样与DengFOC算法的融合方式也可作为开发无刷电机驱动器的起点。1. DengFOC移植先搞清楚要搬的是什么最近在搞无刷电机FOC我把邓涵洋的DengFOC项目从Arduino环境整个搬到了STM32HAL库上最终跑通了闭环位置控制。先说结论DengFOC不是SimpleFOC那种庞大框架它是一个极简的、能在Arduino上直接跑的FOC实现代码量不大但麻雀虽小五脏俱全——SVPWM、反Park变换、电流采样、电角度估算全都有。移植到STM32后控制周期和实时性反而比Arduino更好。这篇文章适合两类人一是已经在玩DengFOC或SimpleFOC、想从Arduino换成STM32做更正经项目的二是手里有STM32开发板和无刷电机想抄一套能跑的FOC位置控制代码但不知道从哪里下手的。我会把DengFOC各文件的作用、Arduino API向HAL库的映射关系、编码器读取与校准、三环参数整定这些过程都拆开讲最后附上我踩过的坑。1.1 DengFOC的核心价值一套“能跑”的FOC算法DengFOC最让我欣赏的一点是它把FOC中最容易被劝退的那部分——SVPWM算法的查表与扇区判断、电流环的PI运算、电角度的积分更新——压缩到了一个非常小的代码量里。在Arduino上打开串口监视器就能看到角度、电流实时值输入目标角度电机就自己转过去这种“开箱即转”的体验对一个刚接触FOC的人来说太重要了。它的代码结构大致这样DengFOC.h对外暴露的大多数函数接口比如DFOC_Vbus、DFOC_Sensor_Align()、DFOC_LoopFOC()、DFOC_Move()。DengFOC.cpp核心实现包括_readSensor()编码器角度读取、_setPhaseVoltage()相电压计算与SVPWM输出、_currentLoop()电流环PI等。底层依赖Arduino的analogRead、analogWrite、micros、delayMicroseconds。所以移植的本质很清晰算法层的DengFOC.cpp几乎可以一个字节不改地搬过来它只依赖于底层接口和数学库真正需要重写的是“Arduino API”对应的那层硬件驱动。提示DengFOC默认是给带ABZ或者IIC磁编码器、带电流采样放大电路的驱动板用的。你不需要完全照搬他的电机驱动板设计但电角度、相电流这两个数据必须有来源。1.2 为什么我用STM32HAL而不是寄存器或标准库其实做FOC移植用寄存器版本理论上执行效率最高标准库也有大量现成例程。但我的选择是HAL库原因很现实STM32CubeMX能图形化配置定时器、ADC、DMA和中断尤其是做FOC要同时用多路PWM互补输出、ADC注入采样、编码器接口手写寄存器初始化代码真的容易写错。HAL库虽然函数调用有额外开销但在72MHz主频下跑100us周期的电流环完全够用。另外HAL库的资料多。MT6701磁编码器的读取、模拟IIC的实现、DMA多次采样这些在HAL库下都有大量现成参考踩坑时能搜到解决方案的概率高得多。比如我后来遇到“串口中断只收一次”的问题就是HAL的HAL_UART_RxCpltCallback里没重新调用接收函数导致这种坑在搜索平台上随便一搜就有答案。当然HAL库也有缺点中断回调机制藏得深HAL_UART_Receive_IT这种“一次接收一次回调”的机制在连续数据流场景下配置很容易出问题。我的经验是该绕开的地方绕开比如电流采样触发直接配置定时器TRGO输出而不是依赖HAL的中断回调函数。1.3 移植的整体拆解哪些代码能复用哪些必须重写我把整个移植过程拆成了下面这张表先看清每一块属于“搬运”还是“重建”再动手就很有条理原Arduino API / 模块功能移植到STM32HAL的替代方案analogRead()相电流采样ADC1 DMA多通道采样定时器TRGO触发analogWrite()PWM输出电压高级定时器TIM1/TIM8PWM1模式互补输出micros()微秒时间基准DWT-CYCCNT或TIM2微秒计数millis()毫秒时间基准HAL_GetTick()即可delayMicroseconds()模拟IIC延时简单空循环或者用DWT做精确延时Wire.h的IIC库MT6701角度读取自己写GPIO模拟IIC代码很短DengFOC.cpp算法层FOC核心原封不动搬过去这里顺便说一句DengFOC.cpp在Arduino上默认是用浮点运算的STM32F103C8T6虽然没硬件浮点单元但100us周期内做几次浮点乘加运算没有问题。我实测在72MHz下跑完DFOC_LoopFOC()加上电流环PI大约耗时15~20us满足100us控制周期的要求。2. 闭环位置控制的核心三个环的级联设计很多第一次接触FOC的朋友容易把“闭环位置控制”理解成“给个角度目标值电机转过去就行”但实际代码里是三套控制环串在一起的。位置环在最外面输出是目标速度速度环在中间输出是目标电流电流环在最里面输出是PWM占空比。你可以类比成公司三层管理位置环是老板定KPI目标角度速度环是经理拆解成目标车速电流环是员工直接干活驱动电机输出力矩。2.1 电流环、速度环、位置环各自干什么电流环控制电机三相绕组的电流追踪Iq_ref和Id_ref。通常在100us左右执行一次是三个环里最“紧”的一个。速度环根据目标转速和当前转速的误差计算出需要输出的Iq_ref。一般在1ms执行一次。位置环根据目标角度和当前角度的误差输出目标转速。位置环的输出不是直接给占空比而是给“目标速度”正因为隔了速度环电机才能带载的时候自动加大力矩而不是傻乎乎地硬顶。我的项目里直接用DengFOC的DFOC_Move()作为底层执行入口它内部是“位置环-速度环-电流环”逐级计算。位置环部分没有PID库核心就是一句比例控制// 简化后的位置环核心逻辑 float position_error target_angle - current_angle; float target_speed position_error * Kp_pos;但实测下来“只有P”是不够的。目标位置和当前位置相差180度时位置误差很大好的做法是配合速度前馈给出的目标速度由“比例项前馈项”共同构成。比如目标角度匀速扫描时如果不加前馈位置环输出会滞后表现为“永远追不上”。2.2 为什么我选择“位置环P 速度环PI”的结构我在项目里采用的组合是位置环只做纯比例控制速度环做PI控制电流环做PI控制。原因是位置环如果加积分项I很容易造成积分饱和——电机被外力顶住时位置误差持续存在积分项越积越大等外力消失后电机会猛地冲过目标位置。更合理的做法是让速度环去消化静态误差因为速度环天然就需要积分项来保持恒定转速。实际试过几种组合后我的体会是位置环Kp取多少跟速度环的带宽直接相关。速度环调得越“硬”位置环Kp可以越大定位速度越快但一旦超过临界值就会震荡。我最后的位置环Kp是5.0速度环Kp是0.05Ki是0.01电流环Kp是0.3Ki是0.05。这个参数不是定理但可以作为起点。2.3 控制周期的选择与定时器安排DengFOC在Arduino上是在主循环里顺序执行控制周期完全看循环跑了多久这其实不太严谨。移植到STM32后我做了改进用定时器产生固定时基主循环里按时间标志决定“这次要不要跑位置环/速度环/电流环”。我用的时间分配控制环周期说明电流环含SVPWM输出100us必须在固定周期执行否则电机响应会变差速度环1ms每10个电流环周期执行一次位置环1ms跟速度环同一个时间基准先算位置环再算速度环具体实现是用TIM3产生100us中断在中断里只跑电流环主循环中检查HAL_GetTick()的变化若过了1ms就执行位置环和速度环。这么分层的意义在于电流环实时性最高不能被打断位置环和速度环实时性相对低但对运算时间更宽容。3. 移植实操编码器、定时器、ADC一个都不能少3.1 硬件接线与选型清单我手头的硬件是STM32F103C8T6核心板、带MT6701磁编码器的无刷电机这种电机云台市场上很多、EG2133驱动板自带三相电流采样放大和比较器。如果没有同款驱动板只要你的驱动板能输出互补PWM并反馈相电流就可以参考接线。STM32引脚外设功能连接对象PA8 / PA9 / PA10TIM1_CH1 / CH2 / CH3驱动板三路PWM输入PB13 / PB14 / PB15TIM1_CH1N / CH2N / CH3N驱动板三路互补PWMPA0 / PA1ADC1_IN0 / IN1驱动板两路电流采样输出PB6 / PB7模拟IIC的SCL / SDAMT6701传感器PA2 / PA3USART2_TX / RX串口调试提示驱动板的PWM输入接口可能是单端而非互补的看你手上的板子。如果只需要单端PWMTIM1的CHxN可以不接。我用EG2133时就是只用了三路PWM另外三路互补输出配置了但悬空不用。3.2 读取MT6701磁编码器模拟IIC读取与滤波校准MT6701是14bit磁编码器IIC接口读取角度非常方便。DengFOC在Arduino上用的也是IIC读取但STM32的硬件IIC确实有兼容性问题我承认HAL库的硬件IIC已经好很多了但模拟IIC的可靠性更容易控制所以我干脆用GPIO模拟IIC。MT6701的角度寄存器是16bit读取流程是主机发送设备地址写然后发寄存器地址0x00再发起重复起始信号切换为读模式连续读取两个字节。角度值是14bit所以要把两字节拼起来后右移2位// 模拟IIC读取MT6701角度返回0~1638314bit uint16_t MT6701_ReadAngle(void) { uint8_t buf[2]; IIC_Start(); IIC_SendByte(0x06 1 | 0x00); // 设备地址写 IIC_SendByte(0x00); // 寄存器地址 IIC_Stop(); IIC_Start(); IIC_SendByte(0x06 1 | 0x01); // 设备地址读 buf[0] IIC_ReadByte(1); // ACK buf[1] IIC_ReadByte(0); // NACK IIC_Stop(); return (buf[0] 8 | buf[1]) 2; }这段代码里有两个细节容易出错一是设备地址MT6701的7位IIC地址是0x06左移一位后是0x0C写、0x0D读二是模拟IIC的时序延时要匹配传感器要求我用的是5us级别的延时实测稳定。接完线后先把角度值直接打印出来转动电机轴看是不是连续递增/递减如果跳变就说明SCL或SDA接反了。滤波和校准方面我做了一个简单的递推平均滤波每读10次角度值取平均作为当前角度。注意取平均前要对角度做“跨零处理”不然角度从359.9度跳到0.1度时平均值会算成180度左右。处理方法很简单// 角度跨零修正后再滤波 float diff raw_angle - last_angle; if (diff 8192) diff - 16384; // 半圈以上视为跨零 if (diff -8192) diff 16384; filtered_angle diff * 0.2f; last_angle filtered_angle;这里的16384是14bit编码器的分辨率8192就是半圈。滤波系数0.2是我试出来的太小会让角度响应变慢位置环会顿挫太大会引入噪声。对于F103这种主频不高的MCU用这种一阶低通滤波比滑动平均更省内存。3.3 HAL库定时器PWM与ADC电流采样配置我用的是TIM1作为PWM发生器配置成中心对齐模式——这是FOC电流采样的关键。中心对齐模式下计数器计到峰值时PWM占空比更新此时三相桥臂上下管都处于“中间状态”采样得到的电流最接近相电流平均值。HAL库配置时注意三件事TIM1的时钟来自APB272MHz预分频设为0自动重载值设为电机PWM频率对应的数值比如20kHz对应3600。开启TIM1的更新中断或者直接利用TRGO输出触发ADC采样。我推荐后者把TIM1的TRGO配置成更新事件ADC配置成外部触发注入采样这样电流采样的时机由硬件自动保证不会因为软件中断延迟而抖动。ADC要配置成扫描模式非连续模式同时采样两个电流通道用DMA把结果搬进内存。两通道的采样顺序要和电流采样电阻的物理连接对应否则会出现在低速时电流方向判断错误的问题。我调试时遇到过一个非常典型的坑PWM配置改了好几次但电流采样值始终是某个固定值。原因是HAL_TIM_PWM_Start_IT和HAL_TIMEx_PWMN_Start_IT两个函数必须分别调用不能只启动主输出而忘了互补输出通道。因为CHxN悬空或者未初始化时PWM输出并不影响ADC触发但驱动板实际没有收到有效PWM信号所以电流为零。3.4 DengFOC算法文件在HAL工程中的集成把DengFOC移植到STM32工程时我的做法是在Core/Src下新建一个dengfoc_port.c/h里面放三个模块port_pwm_set()把DFOC_setPwm需要的“三相占空比”写入TIM1的CCR寄存器。port_adc_read()从DMA缓冲区里取电流值。port_sensor_read()调用MT6701_ReadAngle()并返回0~2π之间的机械角度。DengFOC的DFOC_LoopFOC()内部需要知道当前机械角度_electricalAngle而这个角度计算公式依赖SensorDirection传感器方向和极对数。这两个量在DFOC_Sensor_Align()里自动校准在校准时需要电机能够自由旋转。如果电机卡住或没供电校准结果就会错后面位置控制必然飞。把这三个接口文件写好后主循环里的调用非常简洁while (1) { // 100us中断里已经跑完电流环和SVPWM // 这里处理1ms位置环和速度环 if (HAL_GetTick() - last_1ms 1) { last_1ms HAL_GetTick(); DFOC_Move(target_angle_deg); } // 串口打印调试信息 }4. 位置环调试实录从“会转”到“到位”4.1 调试顺序不能乱开环对齐、电流闭环、速度闭环、位置闭环移植完成后不要急着调位置环我按照这个顺序一步步验证每次只改动一个变量第一步开环测试给定一个固定的Iq和电角度让电机转起来确认PWM输出相序、电流采样方向没接反。第二步电流闭环目标就是让电机轴在电流闭环下能锁定住位置不抖动。第三步速度闭环给定目标速度让电机匀速旋转两个方向都要测确认方向和代码的正负号一致。第四步位置闭环给定目标位置看电机的阶跃响应。调试过程中的每一层都用串口打印出来尤其推荐把电流值、角度值、目标速度、目标位置打包成CSV格式存到电脑里看波形。我是在UART中断里加了一个简单的环形缓冲区用1ms周期把数据发到串口调试助手再在电脑里用“虚拟示波器”方式看曲线。4.2 位置环整定的两个关键参数Kp、速度前馈位置闭环的表现直接取决于“位置环Kp”和“速度环动态响应”的配合。位置环只做P控制刚开始我随便给了个Kp1.0现象是电机能够缓慢转到目标位置但最后有一段明显的“爬”快到位时速度趋近于零。然后我把Kp逐步加大到5.0定位速度明显加快但出现了约1.5°的超调来回震荡了大约200ms才停下。继续加到10.0系统开始发出明显的振荡声位置在目标位置附近来回飘。最后我总结出这样一个调参口诀速度环越稳位置环Kp才能越大Kp加到“刚好出现振荡”再退回来一半就是比较理想的值。另外把目标位置变化幅度太大时我会在应用层加一个梯形速度规划而不是直接让位置环去追一个大阶跃——这样电机不会瞬间给满力矩保护机械结构也保护驱动板。速度前馈的代码非常简单// 位置环前馈 float pos_err target_angle - current_angle; float vel_target pos_err * Kp_pos target_velocity_feedforward;如果不需要目标位置移动前馈就是0这一项不影响静态定位精度。4.3 实测记录不同Kp阶跃响应对比我用串口记录了目标位置90度、速度环参数固定的情况下不同位置环Kp的表现汇总如下位置环Kp到达90度时间超调量稳定时间现象备注1.01.2s无0.2s速度过慢接近目标时有爬行3.00.5s约1°0.1s定位流畅轻微过冲5.00.3s约3°0.2s有震荡但肉眼观察不严重8.00.25s约8°0.6s明显震荡带响声这个结果也能看出并不是“响应越快越好”因为超调和稳定时间都在恶化。最后我用了Kp3.5同时把速度环的Ki调高了一点让速度环响应更快这样位置环Kp即使不大整体定位速度也上来了。这个“让内环更快而不是一味拉高外环增益”的思路是调FOC三环最重要的心法。5. 常见问题与排查技巧实录5.1 芯片连接失败“no stm32 target found”F103C8T6这种没有独立SWDIO引脚保护的板子容易因为引脚复用或者程序跑飞导致ST-Link连不上。我这里踩过一次配置完TIM1后不小心把PA13/PA14复用成了普通IO输出然后一烧录第二次就无法下载了。解决办法是按住复位键点击烧录时松开让芯片在复位期间响应连接如果还不行就把BOOT0拉高进入串口ISP模式用串口工具擦除整个Flash然后再拉回BOOT0重新烧录。这招对“SWD锁死”基本是万能解药。5.2 编码器角度跳动位置环跟着乱抖角度跳动的原因大多是模拟IIC时序不稳或者电源纹波干扰。我的排查顺序是先看波形把角度值打印出来用手慢慢转轴看串口输出是不是连续变化。如果偶尔跳变一个很大的值我优先检查IIC总线是不是太长SCL/SDA有没有接上拉电阻。MT6701模块一般带4.7k上拉如果你的模块没带一定要外接。另一个原因是磁铁没安装在编码器芯片正中心导致角度在某个区间非线性误差大重新点胶固定一下就好。电源方面把电机供电和逻辑供电分开模拟IIC的两根线远离电机三相线可以大幅减少干扰。5.3 电机啸叫、震动电机在闭环控制时发出高频啸叫一般有两种情况PWM频率低于16kHz人耳能听到把PWM频率提高到20kHz以上就能解决另一种是电流环增益太高导致电流震荡表现为低频“嗡嗡”声把电流环Kp/Ki同时降低30%再重新试。5.4 霍尔位置反馈的电机位置来回找零DengFOC默认支持ABZ编码器和磁编码器。如果使用霍尔传感器作反馈位置环表现会差很多因为霍尔分辨率只有600步左右电角度估算在换相点附近有跳变。我的建议是位置环至少用12bit以上的磁编码器或光电编码器MT6701的14bit已经足够不需要额外的“zero offset”代码。5.5 HAL库串口中断只收一次这个问题我遇到过不止一次现象是串口调试工具第一次发送数据单片机能够收到并回显之后就再也不进接收中断了。原因是HAL_UART_Receive_IT每次只能设置一次接收目标接收完一个字节后必须重新调用一次。正确做法是在HAL_UART_RxCpltCallback里再次调用HAL_UART_Receive_IT。void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { rx_data received_byte; // 重新开启下一次接收 HAL_UART_Receive_IT(huart2, received_byte, 1); } }这个坑跟FOC本身关系不大但在调试FOC时经常要靠串口看数据串口被“关”了会非常影响心情所以列出来提醒大家。5.6 启动瞬间驱动板发烫如果一上电驱动板就发热先怀疑PWM初始化阶段引脚状态不对。比如TIM1初始化前PA8/PA9/PA10默认可能是高阻态或者被拉高此时驱动板的栅极驱动芯片会误以为输入了占空比信号导致上下桥直通。解决办法是在主循环FOC输出前先把占空比寄存器全部清零再启动PWM输出PB13/14/15这类互补输出引脚如果没用到也要配置为GPIO推挽输出并输出低电平。最后说点实在的移植DengFOC到STM32HAL这件事技术上并没有什么“神秘魔法”核心就是把DengFOC算法层的“输入输出”和STM32硬件外设正确对接起来剩下的时间基本都花在调试和整定参数上。我个人的切身体会是FOC不是一个“接上就转”的东西每一步都要验证传感器数据是否准确、电流采样时机是否合理、每个环是否足够稳定再往上叠加更高层级的控制。如果你在调试过程中遇到某个环节现象很怪别急着改参数先用串口把角度、电流、目标值全打出来看一遍80%的问题都会一目了然。最后再分享一个小技巧调试位置环时给目标位置加一个缓慢变化的“斜坡”而不是每次直接跳90度。这样就算参数没调好电机也不会失控乱打排查问题时安全得多。这个习惯帮我避免了好几次电机撞限位的麻烦。本文还有配套的精品资源点击获取
返回列表