基于STM32的磁悬浮项目:原理、硬件、PID调参实战

基于STM32的磁悬浮项目:原理、硬件、PID调参实战 简介磁悬浮stm32.rar是一份基于STM32F103的下推式磁悬浮完整工程文件面向嵌入式学习者和电子设计爱好者帮助理解磁悬浮控制系统的软硬件实现。压缩包共239个文件容量约4MB既包含C语言源码.c/.h、编译中间文件.o/.axf与可烧录的hex文件也提供电路原理图工程.pdsprj和物料清单.txt从代码到硬件设计均有覆盖。工程针对下推式磁悬浮的核心控制流程涉及传感器数据读取、PID调节、功率驱动等关键环节适合结合原理图逐模块分析再通过修改代码观察悬浮稳定性变化。目前已有2947人学习下载对有单片机基础、想动手实践磁悬浮项目的读者很有参考价值。 第一次看到直径十二毫米的钕铁硼磁体稳稳悬在电磁铁下方三四毫米的位置现场的人都会愣一下它既不落下也不被吸走就那么“钉”在空中。磁悬浮stm32这个项目玩法就是自己搭一套同样的系统——用STM32做控制核心实时读位置、实时调电流让一个本质不稳定的物理系统进入动态平衡。项目的难点不在焊接也不在代码量而在真正理解“闭环”这两个字。本文按照我自己实际走过的路线把这套系统的原理、硬件选型、PID调参、STM32工程实现和常见故障完整梳理一遍。如果你想做一个桌面悬浮摆件、毕业设计或者只是找个小项目练手PID和STM32定时器/ADC配合这篇文章可以直接作为复现底稿。1. 先搞清楚磁悬浮到底是怎么“悬”起来的1.1 上吸式结构最经典也最好调试的方案磁悬浮的方案不少最直观的是“上吸式”一枚电磁铁固定在支架顶部永磁体悬浮在电磁铁正下方。电磁铁通电后产生的磁场把永磁体往上吸重力把永磁体往下拉两个力相等的位置就是稳定工作点。问题是这个工作点极其脆弱。永磁体位置稍微往下偏离一点吸力就会变小而重力不变于是它继续下落越远吸力越小最终掉下来稍微往上偏离一点吸力变大于是加速撞向电磁铁直接吸死。也就是说磁悬浮本质上是“负反馈缺席的不稳定系统”——它不会自己稳定下来必须有人时刻纠正偏差要求系统以很高的频率反复执行“检测位置、计算偏差、调整电流”这三个动作。电路上这三步对应得很直白霍尔传感器把磁场变化转换成电压STM32的ADC把电压读进来PID算法算出当前应该给出的占空比最后通过MOS管控制电磁铁电流。整个过程每1毫秒循环一次物体会觉得每个瞬间都有一股力气在正正好好托着它。这里顺带提一下“下推式”。网上很多悬浮地球仪用的是斥力悬浮电磁铁在底部用一个向上的斥力把磁体托起来控制思路和上吸式完全一样只是力的方向反了。第一次自己做我更推荐上吸式因为传感器安装、线圈固定和调试都直观得多悬浮体的摆动也相对好控制。1.2 为什么这个系统离不开STM32这类微控制器理论上用分立元件搭模拟PID也可以但实际体验会非常痛苦。见过很多人用运放搭控制器调了一个月还在振荡。STM32这类微控制器的意义在于它把整个控制回路做成了可实时修改参数的软件调参只需要改两个变量不需要反复换电阻电容。具体到F103这代芯片三个资源刚好覆盖需求一是12位ADC分辨率足够分辨毫米级的位移变化二是硬件定时器能非常精确地产生固定频率的中断和PWM不需要靠delay抖动循环三是USART可以把目标值、当前位置、PWM输出实时发给PC直接在波形的引导下调PID。我见过有人用Arduino Nano也做成功过8位AVR同样能跑。但STM32F103C8T6的72MHz主频和浮点运算能力让控制代码写起来非常从容而且教程、例程、CubeMX配置随手就是一大把几乎不需要为开发环境费心。这块板子的成本现在也就是一杯咖啡的钱作为起步平台没有任何理由不选它。2. 硬件清单与自制细节照着买就不会踩大坑2.1 传感器选型线性霍尔和安装位置位置检测是整套系统里最容易被低估的一环。位置读不准PID再好也白搭。我用的是线性霍尔传感器SS49E它在3.3V供电下直接输出模拟电压接到STM32的ADC引脚就行不需要分压。如果你手里是SS495A或者A1302这两款需要5V供电输出范围会超过3.3V必须用两个电阻分压之后才能进ADC。霍尔片安装的位置直接决定标定是不是好做。我的做法是在电磁铁的铁芯中心打一个通孔把霍尔片塞进孔里让它正对铁芯下端面这样永磁体靠近端面时磁场变化能稳定反映在输出上。如果没有条件打孔把霍尔片贴在铁芯侧面、靠近端面的位置也可以但灵敏度会差一些ADC变化量变小控制分辨率变差。型号供电输出特性接STM32方式SS49E2.7~6.5V2.5V中心约1.4mV/G3.3V供电直接接ADCSS495A4.5~10.5V2.5V中心约3.125mV/G5V供电分压后接ADCA13024.5~6V2.5V中心约2.5mV/G5V供电分压后接ADC别小看标定这一步。传感器输出和距离并不是严格线性的所以在调PID之前建议先做一次粗标定用不同厚度的垫片把永磁体垫在几个固定高度上用串口读对应ADC值记下一张“高度-ADC”表。这张表后面有两个用途一是帮你确定PID程序里的目标值二是帮你判断PID响应的方向对不对。2.2 功率级一个MOS管就够续流二极管绝不能省电磁铁是一个大电感工作电流一般在0.31A之间不需要H桥一个N沟道MOS管做低边开关就够。型号我推荐IRLZ44N它是逻辑电平MOS3.3V的栅极电压就能完全导通。如果你手里只有IRF540N建议加一级三极管驱动或者降低栅极信号电平否则管子工作在放大区会严重发热甚至烧掉。线圈两端必须并联一颗快恢复续流二极管方向是阴极接电源正极、阳极接MOS管漏极。这个二极管的作用是PWM关断瞬间线圈里的电流没有地方去会产生一个反向高压尖峰如果没有二极管泄放这个尖峰几十伏起步直接击穿MOS管。这一条被很多人当成“电感的常规操作”略过去但在磁悬浮这种频繁开关的场合它是整块板子不冒烟的关键。供电建议用12V、输出电流2A以上的适配器线圈单独占一个回路STM32板子用同一个电源降压稳压供电。功率地和控制地在底板汇合处单点接地这样大电流变化不会干扰ADC的参考地位置读数会干净很多。2.3 机械与电磁铁绕线匝数和悬浮体重量线圈自己做一点也不难找个塑料或尼龙骨架罐形磁芯最好没有就用长约25mm、直径10mm左右的铁氧体磁棒上面绕漆包线。线径0.350.5mm匝数200300匝绕完量一下直流电阻控制在25Ω之间比较合适。低于1Ω的线圈对MOS管和电源冲击太大高于8Ω又吸力不足。悬浮体我用的是一块N35直径1012mm的钕铁硼圆片厚3mm用热缩管包好后连同轻质外壳总重量控制在5g左右这个重量匹配上吸式结构很合适。吸力不足时优先检查三个方向电流方向对不对、匝数够不够、悬浮体有没有太重。上电的时候可以先把线圈两端引线对调一次再试这是最快的方向排查法。正常情况下通电后永磁体应该被稳稳吸向铁芯如果是推开或者没反应几乎可以断定是电路或者绕向的问题把引线对调即可不需要改程序。3. PID控制让悬浮体“听话”的算法以及完整调参流程3.1 先理解为什么默认就会振荡很多第一次做磁悬浮的人上来就抄一个PID公式发现永远在振荡然后开始怀疑电容、怀疑电感实际上振荡是必然的。电磁吸力随距离变化是非线性的距离越近吸力增长越快。把电磁力简化成 F ≈ k·I²/d² 来看d 减小一点F 会增加很多这相当于让物体具有“负刚度”——偏离平衡点之后力不是把你推回去而是把你推得更远。这就像一个装了“反弹簧”的摆普通弹簧在偏离时产生恢复力而磁悬浮在偏离时产生发散力。控制器的任务就是每秒一千次地给系统补充一个等效弹簧和等效阻尼让发散的系统重新具有回复能力。PID里的P对应弹簧刚度力不够就加大D对应阻尼振荡就靠它压I用来处理长期漂移。从这个角度看调参就不玄学了先有一个足够大的P把物体“拉回”正确位置再用D把拉回过程中积累的速度耗散掉最后用I把因为重力或温漂引起的小偏差修掉。3.2 调参顺序P→D→I每一步都观察现象我在调的时候顺序是严格的P先D后I最后。开始之前做一个工程约定把ADC读数映射到0100目标值也设在这个量级这样误差和PWM占空比是同一个尺度Kp、Kd、Ki的数值才有直觉意义。把Ki和Kd都设成0Kp从小到大慢慢加。Kp很小时物体从下方掉下去毫无反应Kp加大一点能看到物体被吸起来坚持半秒钟然后开始嗡嗡抖动最后被吸死。这个“抖动然后吸死”的临界点说明Kp已经够强了。引入Kd。微分项相当于“看见变化提前刹车”会直接抑制高频振荡。把Kd从小往大加直到抖动幅度明显变小。注意Kd加得过多时物体不是振荡而是整体发麻一样地细碎颤抖因为微分项放大了ADC噪声。最后加Ki。积分项解决的是悬浮高度慢慢往下掉的问题。正常情况下比例和微分已经能让物体悬浮住但观察几十秒会发现高度在以很慢的速度下移这是重力静态偏差导致的稳态误差以及MOS管发热后参数漂移导致的。加一个不大的Ki能把这个漂移吃掉。以1ms控制周期、PWM占空比099为例我最终停在Kp≈3、Kd≈5、Ki≈0.1附近。参数因传感器安装和线圈结构不同会有差异但数量级可以参考。3.3 PID输出必须有上限启动阶段单独处理PID算出来的输出不能直接无脑写到PWM寄存器。磁悬浮的PWM只控制电流大小方向是固定的所以占空比范围是0到ARR。输出一旦超过上限会引发积分项的“积分饱和”积分项一直累计等物体到达目标位置后PWM仍然长期停在最大值系统就吸死了。所以每次算完PID之后必须对输出做限幅同时对积分项也做限幅这是很多教程里不提但极重要的一步。另一个容易被忽略的是上电启动。刚上电时永磁体放在支架上离电磁铁还有几厘米如果此时直接闭环控制PID会觉得偏差极大并满输出结果是把磁体猛吸上来然后又因为过冲吸死。我加了一个简单的启动逻辑如果当前位置落在控制范围之外就先用一个固定占空比比如50%开环吸拉一旦ADC读数进入控制范围内立刻切换成PID闭环。实测这个策略能让每次上电都平稳起浮。3.4 用串口把过程数据拉出来看调参不靠猜靠看波形。我启用了一个USART以大约5ms间隔输出三个数据目标值、当前位置ADC值、PWM输出。在PC上用串口助手或者VOFA打开能直接画出三条曲线效果非常直观。重定向printf到USART1的代码很常规int fputc(int ch, FILE *f) { while (HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF) ! HAL_OK); return ch; }有一个坑千万别在控制中断里调用printf。串口发送一个字节的时间在115200波特率下大约是87微秒看起来很快但printf内部可能出现阻塞一旦在1ms中断里调用很容易导致控制周期抖动甚至卡死。正确做法是在定时器中断里只置一个标志位主循环看到标志位后再打印数据。4. STM32工程实现定时器、ADC和中断的三方配合工程基于STM32CubeMX生成HAL库版本随意芯片选STM32F103C8T6。时钟按72MHz配SYS的Debug必须设为Serial Wire不要用默认的No Debug否则程序烧进去一次后调试口就废了外设开启USART1115200、TIM3的PWM输出PA7、TIM2的1ms中断、ADC1单通道DMA循环PA0接霍尔输出。这几个外设配置好剩下就是写回调逻辑。4.1 控制周期凭什么定1ms怎么用定时器保证磁悬浮的固有振荡频率和机械结构有关用1ms的控制周期相当于控制带宽远高于机械振荡频率控制器有足够余量去抑制发散。控制周期拉长到10ms可以明显感觉到吸力动作像“慢半拍”悬浮体开始大范围来回摆。按我自己的体验控制周期超过5ms一般的机械结构就压不住了。要精确保证1ms不能用delay必须用硬件定时器。在CubeMX里用TIM2产生1ms更新中断系统时钟72MHz预分频器PSC设为71计数时钟变1MHz自动重装载值ARR设为999刚好1ms进入一次更新事件在更新中断回调里执行一次控制计算。计数器本身的抖动是纳秒级比软件循环稳定得多。void MX_TIM2_Init(void) { htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; htim2.Init.Period 1000 - 1; htim2.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(htim2); HAL_TIM_Base_Start_IT(htim2); }4.2 PWM通道选择与占空比方向PWM我用TIM3的第2通道放在PA7引脚。配置成PWM模式1预分频设71、ARR设99得到10kHz的PWM。选10kHz有讲究低于2kHz会有明显的音频噪声线圈会“吱吱”响高于30kHz开关损耗变大MOS管容易发热。10kHz是噪音和损耗之间的平衡点。占空比写入099的整数。由于磁悬浮只需要单向电流ARR本身代表100%输出所以代码里对PID输出做了099的限幅。void MX_TIM3_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 72 - 1; htim3.Init.Period 100 - 1; htim3.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(htim3); sConfigOC.Pulse 0; sConfigOC.OCMode TIM_OCMODE_PWM1; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); }4.3 ADC连续采样和中断里的“最新值”问题ADC采用单通道连续转换外加DMA循环模式目的是不让CPU反复等待转换完成。ADC每次转换完成DMA自动把结果写进一个全局变量控制中断在需要时直接读这个变量不会阻塞。霍尔输出会有轻微的噪声直接拿瞬时值给PID会让D项疯狂抖动。我在定时器中断里做了一阶低通滤波滤波系数对应时间常数大约3ms既能滤掉高频噪声又不会滞后太严重。完整控制回调如下volatile uint16_t adc_raw; float adc_filtered 0; float integral 0; float last_error 0; float target 50.0f; // 归一化后的目标位置0~100 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { adc_filtered adc_filtered * 0.7f adc_raw * 0.3f; float pos adc_filtered / 4095.0f * 100.0f; float error target - pos; // 注意霍尔越近ADC越大符号按此安装方向设计 integral error * 0.001f; if (integral 10.0f) integral 10.0f; if (integral -10.0f) integral -10.0f; float derivative error - last_error; last_error error; float output Kp * error Ki * integral Kd * derivative; if (output 99.0f) output 99.0f; if (output 0.0f) output 0.0f; __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, (uint32_t)output); } }整个控制循环在1ms内完成实际上STM32F103跑完这段计算只需要十几微秒CPU还留了很大余量给串口打印、显示等任务。5. 实测中翻车最多的五种情况附完整排查链路5.1 ST-LINK连不上目标板怎么救调试器报出“error: no stm32 target found”是最常见的开局我自己也遇到过三四次。先别急着怀疑芯片按顺序排查SWDIO和SWCLK两根线是否接到正确引脚目标板的3.3V供电是否正常复位脚有没有被外部下拉最后再考虑调试器本身。有一个场景特别容易遇到之前某次实验代码里把SWD引脚复用成了普通GPIO程序跑起来后调试口就失效了之后再也连不上。解决方法是按住目标板的复位键不放点击IDE里的连接连接开始的一瞬间松开复位很多情况下能成功如果这招也不行把BOOT0引脚拉高让芯片从系统存储器启动用串口烧录一个全片擦除命令把Flash清空后再恢复BOOT0芯片就回来了。如果调试器支持connect under reset直接在ST-LINK Utility里勾选这个选项再连接成功率会高很多。5.2 悬浮体高频发抖或者直接吸死高频发抖的本质是控制环里出现了高频扰动来源通常是两种D项太大放大了ADC噪声或者PWM频率正好落在机械共振点上。对治方法是把Kd降下来同时确认ADC滤波有没有生效。如果压低Kd后依然抖就把PWM频率从10kHz往上调几档避开共振。吸死是另一种情况——输出饱和单片机把占空比干到满值电磁铁全力吸住磁体。最常见原因是D项不够比例项把物体拉近后没有阻尼刹车直接飞过头于是越吸越紧。对策是不要急着加Kp先确认Kd是否已经压住了过冲再用Kp提升响应速度。启动时吸死优先检查第3.3节说的启动逻辑是否正常。5.3 线圈发烫和电源噪声导致的位置乱跳线圈发烫的本质是无效功率过大。正常悬浮半小时线圈微温如果烫到不敢碰说明平均电流太高要么悬浮体重了要么占空比基线太高。对策是减少线圈匝数降低电阻或者降低电源电压让平均电流降下来发热自然缓解。电源噪声在示波器上看特别明显MOS管开关瞬间功率回路里的电流突变会通过地线干扰ADC参考导致读数乱跳表现为悬浮体轻微抖动。处理办法是功率地和控制地单点连接霍尔传感器单独走模拟地最后汇聚到电源地。如果板子已经画死不好改直接在STM32的VREF和地之间多焊几个100nF和10uF电容通常能改善不少。5.4 悬浮高度整体漂移如果悬浮稳定但高度几十秒内缓慢往下移多半是MOS管和线圈温度升高后参数漂移或者霍尔传感器温漂。我给积分项留了一个不太大的Ki目的就是吃掉这种慢变化。需要注意的是Ki不能给大否则系统会出现周期很长的“呼吸感”——悬浮体慢慢下降又慢慢回升来回荡那是积分环节引起的极限环把Ki降下来就没了。5.5 用USB充电头供电引发的疑难杂症用手机充电头带12V负载容易出现莫名其妙的复位和抖动原因是很多充电头电流反馈很慢负载突变时电压跌落明显。如果手头没有稳压电源至少选一个额定电流是工作电流两倍以上的适配器并在电源输入端加一个470uF电解电容给瞬态电流一个蓄水池。加了电容之后我自己遇到的乱跳问题消失了大半。现象根本原因快速对策高频发抖D过大或PWM频点共振降Kd、调整PWM频率直接吸死阻尼不足或启动逻辑失效加Kd、检查限幅和启动逻辑线圈烫手平均电流过大减匝数降电阻或降低电压位置乱跳功率地干扰ADC单点接地、加滤波电容高度缓慢漂移温漂和静态误差加合适的Ki这个项目做到最后悬浮稳定性基本等于三件事的乘积ADC读数干不干净、控制周期准不准、PID参数贴不贴系统。三件事都对了整个系统就像被按了暂停键一样安静地悬着。如果还有余力把OLED屏幕接上实时显示PWM和位置或者加一个蓝牙模块远程调整悬浮高度玩法可以继续往深了做。本文还有配套的精品资源点击获取