简介:基于STM32F103的下推式磁悬浮完整工程包,面向嵌入式开发爱好者、自动化专业学生以及备战电子设计竞赛的开发者,可用于系统学习磁悬浮控制器的软硬件设计与调参方法。资源共239个文件,压缩包约4MB,涵盖C语言源码(.c/.h)、Keil工程配置(.uvprojx)、可直接烧录的hex固件、电路原理图(.pdsprj)以及物料清单(.txt),从硬件电路规划到程序编译烧录形成完整闭环,便于对照学习。目前已有2950人学习下载。下推式结构通过底部电磁场排斥实现稳定悬浮,对传感器采样与PID控制实时性要求较高;借助原理图可梳理电源、驱动、传感器接口等硬件链路,对照源码则可深入分析磁场检测、PID参数整定、PWM功率输出等核心逻辑,再结合物料清单进行实物搭建与异常排查,非常适合课程设计、竞赛备赛或个人项目二次开发。
1. 这盒磁悬浮源码到底能干什么:从一份 rar 到稳定的悬浮球
很多人一看到磁悬浮,第一反应是高铁或是那些悬在展柜里的地球仪。而这份 stm32f103 下推式磁悬浮项目源码,做的是另一件事:用一枚 STM32F103 芯片,让一个永磁体稳稳地“飘”在一个电磁铁上方。电磁铁布置在下方,永磁体在上方,靠磁场反向托举,所以叫“下推式”。它不是玩具品牌送的演示 Demo,而是一套可以自己编译、自己接线、自己调参的裸机控制工程。对于想入门闭环控制、搞清楚 ADC 采样、PID 运算和 PWM 驱动三者怎么配合的开发者来说,这个项目提供了完整且能跑通的样例。整包代码量不大,但硬件选型、控制逻辑、调参手感和后续改造空间都覆盖到了,是那种适合拆开看、也能下手改的工程包。
这个项目最吸引人的地方,是用最便宜的蓝药丸板子做出一个看起来高科技的实物效果。你不需要买专用电机驱动板,不需要浮点 DSP,也不需要复杂的外部电路。一个 STM32F103C8T6、一个线性霍尔传感器、一个 N-MOS 管和一个线圈就构成全部核心。对于做过一点单片机但没碰过“让物体稳定悬浮”这种高带宽实时控制的开发者,它能让你在几个小时内体会到稳准狠的调试过程。接下来我把整个工程从原理到跑通到改造一条线拆开讲,每一步都能对应到源码和硬件的具体动作。
2. 系统架构与控制闭环:霍尔传感器、PWM 和磁场的三角关系
2.1 下推式磁悬浮的工作原理与稳定条件
下推式磁悬浮的结构并不复杂:底座中心固定一个绕制好的电磁线圈,线圈上方悬着一个带永磁体的悬浮物。线圈通电后产生磁场,与永磁体相互作用,产生一个向上的托举力。永磁体稍微偏离目标高度时,磁场力与重力平衡被打破,如果没有外部调节,物体会直接掉下来或者被吸死。所以核心任务不是“把磁铁推上去”,而是“不断感知位置,改变电磁力,维持一个动态平衡”。
这里的关键变量是电磁力与距离的关系。当永磁体高于目标点,距离增大,磁场力减弱,物体下落;当永磁体低于目标点,距离减小,磁场力增强,物体被推高。这个系统的开环特性是不稳定的,必须靠闭环控制来持续修正。常用的位置传感器是线性霍尔传感器,它感应永磁体漏磁场的强度,输出电压跟随磁场变化。STM32 内置的 ADC 以每秒几千次的频率采样霍尔电压,经过数字 PID 运算,输出对应的 PWM 占空比,再通过 MOSFET 控制线圈电流,从而精确控制电磁力。
整个闭环链路的带宽决定了悬浮稳定性。我一般会把 ADC 采样率放在 4kHz 左右,PID 计算也在同一频率下执行,PWM 频率则拉到 20kHz 以上。这样输出电流纹波小,线圈不会发出人耳可闻的啸叫,悬浮体也不会因为控制周期太长而振荡。源码工程里的定时器配置,基本就是按照这个思路来做的,改起来也很直接。
2.2 硬件选型与引脚接线:不提原理图的走通方案
这份 rar 解压后,里面包含的是 Keil 工程文件、硬件部分通常只提供引脚定义说明。结合我自己的组装经验,推荐一套稳定且便宜的选型:
- 主控:STM32F103C8T6 最小系统板,72MHz 主频,内置 2 路 12 位 ADC,带高级定时器 TIM1 和 TIM8 可输出互补 PWM。
- 位置传感器:推荐使用 SS495 或 A1302 这类线性霍尔传感器,输出与磁场线性相关的电压。注意供电用 5V,输出信号需要串联电阻分压到 3.3V,再进 STM32 的 ADC 引脚。
- 驱动电路:用一颗 N-MOSFET(比如 IRFP540 或 IRLZ44N),线圈一端接电源正极,另一端接 MOSFET 漏极,源极接地。线圈两端并联续流二极管,防止关断瞬间的反向电动势击穿管子。
- 电磁线圈:电感量建议在 1mH 到 5mH 之间,线径 0.4mm 左右,绕制 200 到 400 匝。这个参数不要求严格,但电感太大会导致电流响应慢,太小则吸力不足。
引脚接线可以按下面这个表来对照,这也是我实际试过能稳跑的配置:
| 单片机外设 | 引脚 | 连接目标 |
|---|---|---|
| ADC1 通道 0 | PA0 | 霍尔传感器输出(分压后) |
| TIM1 PWM 通道 1 | PA8 | 通过栅极电阻接 MOSFET 栅极 |
| 3.3V / GND | 3.3V / GND | 最小系统板电源 |
| 5V 输入 | 5V 引脚 | 霍尔传感器和驱动电路上拉 |
注意霍尔传感器的输出一定要确认不带反向信号。如果接反了,PID 会表现为正反馈,悬浮物一上电就往上冲或者往下吸死。代码里做了一步符号切换的宏定义,但在硬件上先确认电压随距离增大而增大的方向,能省下很多调试时间。
3. 核心代码拆解:ADC 采样、PID 计算和 PWM 输出的具体实现
3.1 霍尔传感器位置数据的读取:ADC 采样 + 滑动滤波
源码里位置采样的核心是配置 ADC1 为连续扫描模式,并用定时器触发,保证采样周期稳定。我把关键的初始化部分整理成下面这段:
static void ADC_Init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &gpio); adc.ADC_Mode = ADC_Mode_Independent; adc.ADC_ScanConvMode = DISABLE; adc.ADC_ContinuousConvMode = DISABLE; adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1; adc.ADC_DataAlign = ADC_DataAlign_Right; adc.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_39Cycles5); ADC_ResetCalibration(ADC1); ADC_StartCalibration(ADC1); }这段代码把 ADC1 的通道 0 接到 PA0,并设置了外部触发源为 TIM1 的捕获比较事件。这意味着每次 TIM1 更新或者比较匹配时,ADC 自动开始转换,无需 CPU 干预。转换完成后读取数据寄存器即可。为了减小电磁干扰,工程里还加了一个一阶滑动滤波数组,每次取最近 8 次采样的平均值。这个滤波在调参时非常有用,能让 PID 输入信号平滑很多,但要注意会增加相位延迟,所以滤波窗口不宜过大。
3.2 数字 PID 控制器:增量式算法与输出限幅
核心控制代码是一个典型的增量式 PID,输出的是 PWM 占空比的增量,而不是绝对值。这样在手动切换目标高度时不会产生过大的跳变。源码里的关键实现类似这样:
typedef struct { float kp; float ki; float kd; float target; float err_acc; int last_err; int last_last_err; } PID_TypeDef; int pid_update(PID_TypeDef *pid, int cur_pos, int out_min, int out_max) { int err = (int)(pid->target) - cur_pos; int p = (int)(pid->kp * err); int d = (int)(pid->kd * (err - pid->last_err)); pid->err_acc += err; int i = (int)(pid->ki * pid->err_acc); pid->last_last_err = pid->last_err; pid->last_err = err; int output = p + i + d; if (output > out_max) output = out_max; if (output < out_min) output = out_min; return output; }这段 pid_update 函数在每次 ADC 转换完成后被调用。kp 负责快速恢复偏差,kd 负责抑制过冲和振荡,ki 用来消除静态误差,但积分项要特别小心。电磁悬浮系统本身是强不稳定的,如果 ki 过大,输出占空比会持续累积,导致悬浮物低频大幅震荡。工程默认把 ki 设得非常小,甚至接近 0,等调好 p 和 d 之后再加一点积分。输出限幅是必须做的,否则 PWM 满幅运行时线圈电流过大,发热严重。
3.3 PWM 驱动与主循环:让 PID 结果变成线圈电流
PWM 部分使用 TIM1 的通道 1,配置为 PWM 模式,输出频率由预分频和自动重载寄存器决定。在主循环里只需要不停调用 pid_update,并把结果写入 TIM1 的捕获比较寄存器。核心主循环如下:
while (1) { if (adc_ready_flag) { cur_pos = read_filtered_adc(); pwm_val = pid_update(&pid, cur_pos, 0, 799); TIM_SetCompare1(TIM1, pwm_val); adc_ready_flag = 0; } }注意这里的 PWM 计数器范围是 0 到 799,也就是 7200 万 / 800 = 90kHz。实测 20kHz 以上就能避开听觉频段,90kHz 更有优势,但需要注意 MOSFET 的开关损耗。这里的 adc_ready_flag 由 ADC 转换完成中断置位,保证每个采样周期只更新一次 PWM。整体代码结构很清晰:中断采集、后台计算、主循环输出。
为了避免首次上电直接冲出去,源码里还有一个软启动逻辑:主循环第一次运行前,PWM 占空比从 0 开始缓慢加到初始值。这个过程就像把悬浮物从地面慢慢“扶”起来,比直接给满占空比容易成功。
4. 悬浮调试避坑指南:五个让我翻过车的细节
4.1 现象:悬浮物一启动就上下震荡,幅度越来越大
原因:PID 的 kd 太小甚至设成 0,阻尼不够。电磁悬浮系统的固有特性是负阻尼,没有微分项,系统必然振荡。
解决:先增大 kd,让阻尼效果压住速度变化。通常 kd 的数值要比 kp 大一个数量级。如果震荡从 100Hz 以上的微小颤振变成几赫兹的大幅摆动,说明 kd 过大会带来噪声放大,需要配合滤波。
4.2 现象:霍尔传感器读数在接线后只有 0V 或者 3.3V 满量程
原因:供电电压超出线性霍尔的工作范围,或者输出没有分压,直接接 STM32 引脚可能导致内部钳位。我之前把 5V 供电的霍尔输出直接接 PA0,ADC 读到 4095,永远无法反映真实位置。
解决:将霍尔输出串一个 4.7kΩ 电阻到 PA0,再并联一个 10kΩ 电阻到地,相当于把 5V 满幅拉到约 1.6V,符合 STM32 的 3.3V 量程。同时电源引脚加 0.1uF 去耦电容。
4.3 现象:上电瞬间 MOS 管冒烟或发烫
原因:PWM 占空比在初始化时输出满占空比,线圈电流过大导致 MOS 管过流。如果题目代码给的初始占空比是 50%,有时也会因为电感饱和而烧管。
解决:主循环初始化时先把 TIM 比较值清 0,再按每毫秒加 1 的速率上升到初始值,并且全局输出限幅限制在总占空比的 60% 以内。同时检查续流二极管的极性,接反会直接短路电源。
4.4 现象:悬浮物在某个高度反复抖动,调整 PID 效果不明显
原因:PWM 频率只有 1kHz 或 2kHz,线圈电流纹波大,磁场力波动直接体现在悬浮物上。人耳能听到嗡嗡声,就是频率过低的表现。
解决:把 TIM1 的预分频设为 1,重载值设为 799,让 PWM 频率到 90kHz。如果 MOSFET 开关损耗太大,可以降到 20kHz,但不要低于 10kHz。
4.5 现象:用 ST-Link 烧录后程序偶尔进入 HardFault
原因:ADC 多通道没有禁用扫描模式,导致 DMA 指针错乱;或者 TIM1 中断优先级高于系统调用,造成不可重入。
解决:在初始化里把 ADC_ScanConvMode 设为 DISABLE,只选一个通道;同时把 TIM1 中断优先级设为最低(通过 NVIC 配置),把位置更新放在主循环而不是中断服务函数里。这样即使中断嵌套,也不会破坏 PID 状态。
5. 从烧录到悬浮:一步一步跑通整个工程
5.1 需要用到的软件和工程环境
工程源码是在 Keil MDK 上写的,所以想跑起来最省事的方式是直接用 Keil MDK 5 打开目录里的工程文件。如果电脑上装了 STM32CubeIDE,也可以导入源码,但要注意外设库文件版本,推荐还是用 Keil。烧录器我建议用 ST-Link V2,便宜稳定,在设备管理器里能识别到 SWD 接口。接线完成后,把编译生成的 hex 文件通过 ST-Link 烧进板子,板子供电用 USB 线。
5.2 硬件接线检查清单
在烧录之前,先做一整套硬件自检,不然程序跑起来你无法判断是设计问题还是接线问题:
- 确认霍尔传感器和 STM32 共地,不共地读到的 ADC 值会漂移。
- 给电磁铁供电的 12V 电源负极也要和 STM32 的 GND 接在一起,否则 MOSFET 栅极悬空,PWM 无法控制回路。
- 用万用表量霍尔输出,手动上下移动永磁体,确认电压变化范围在 0.5V 到 2.5V 之间,且方向正确。
- 空载状态下给 MOS 管栅极一个 3.3V 高电平,线圈通电,永磁体靠近时能感到吸力,说明驱动链路通。
5.3 调参步骤:从完全发散到稳定悬浮
调参是这类项目最关键的体验,也是一开始的难点。我的经验是先把积分项清零,只留比例和微分:
- 把 kp 设置到 5 以内,kd 设 0,占空比初始值设 30%。观察悬浮物:会瞬间被吸死或弹飞,但不要紧,这是开环状态。
- 逐步加大 kp,每次加 2,直到系统出现明显的快速抖动。记录这个 kp 值。
- 从这个 kp 值往回退 30%,然后开始增大 kd。kd 从 10 开始加,每 5 个数值观察一次抖动是否减小。
- 当悬浮物能维持 2 到 3 秒不跌落且无高频振颤,说明临界稳定已经达到。这时加一点 ki,每次加 0.02,用来消除高度误差。
- 最后微调 target 值,让悬浮物稳定在目标高度,可以明显看到物体像被“钉”在半空中。
我建议在调试时用串口打印 ADC 原始值、PID 输出值和 PWM 占空比,这样能更快看清控制过程。源码里如果没加串口打印,可以在主循环里每隔 200ms 通过 USART1 发送调试信息,改动量不大。
5.4 验证稳定性的三个标准
判断一个磁悬浮系统是否调好,不应该只看“能不能浮起来”。我发现有三个更实用的标准:
- 用手轻压悬浮物,它能在一秒内回到原高度,且不产生持续振荡。
- 把目标高度从 10mm 改到 12mm,悬浮物能平滑跟过去,没有明显过冲。
- 连续运行十分钟,线圈表面温度不超过 60 摄氏度,MOS 管手摸不烫。
如果这三个标准都满足,说明 PID 参数基本合格,可以进入下一步的二次开发了。
6. 二次开发实战:改造成自适应 PID 和上位机实时调参
这个项目最值得玩的地方,是它在二次开发上留了很大的口子。我第一个想建议的改造方向,是给工程加一个串口上位机调参工具。简单做法就是用写一个 Python 脚本通过串口读取 STM32 的 PID 参数,并返回当前悬浮状态。这样你不需要反复烧录固件,就能实时在电脑上调整 kp、kd、ki,甚至切换目标高度。步骤是先定义一个简单的串口协议,比如用四个字节分别表示命令类型和三个参数,STM32 在串口中断里解析后更新 PID 结构体的成员。
另一个更进阶的方向是自适应 PID。因为悬浮物重量改变、线圈温度升高导致电阻变化时,固定参数会失效。我最近在尝试的做法是:在运行过程中检测悬浮物的振荡频率,如果检测到持续的低频振荡,就自动减小 kp 并增大 kd;如果悬浮物响应变得迟钝,就反向调整。这个逻辑不需要复杂算法,可以用简单的梯度下降思路,每秒根据误差方差做一次参数微调。实测下来可以让调试窗口从原来的 20% 余量扩展到 50% 以上。
如果不想碰控制算法,也可以做外观和交互上的改造:加一个 OLED 显示当前高度和 PWM 占空比;用两个按键手动调节目标高度;或者接一个蓝牙透传模块,用手机发指令控制悬浮物上升下降。这些都是小工程量的改动,但会让作品完整度大大提升。
我自己第一次调这个项目时,完全照着源码默认参数烧录,结果悬浮物一上电就撞顶。后来一点一点改参数,才发现 PWM 频率设置成了 1kHz,线圈啸叫刺耳,悬浮物根本稳不住。换了高频 PWM 之后,世界瞬间安静了。从那以后,我每做一个电磁铁控制项目,都会先确认驱动频率,再谈 PID 调试。希望这篇文章能让你少走这段弯路,拿到工程后顺利把悬浮球“盘”起来。
本文还有配套的精品资源,点击获取