简介:基于STM32的磁悬浮控制项目开发包,面向毕业设计、期末大作业和课程实训场景,帮助开发者解决从硬件搭建到软件调试的完整流程问题。压缩包共存八个文件,以Keil工程文件为主,附带电路原理图、三维结构模型、说明文档和配置数据,整体大小约两兆多字节,轻便齐全。现有近一百五十人学习下载,适合刚接触嵌入式开发或需要短期完成课程设计的学生参考。内容包含磁悬浮控制程序源码、硬件设计电路图、可用打印的结构件模型和相关说明,支持直接制作实物并调试。包内文件角色清晰:Keil工程用于编译下载,电路原理图便于分析接线,STL模型可导入3D打印,说明文档帮助快速掌握系统参数;同时也可作为学习PWM驱动、传感器采集和PID算法的实战范例,对完成毕设和技能提升有切实帮助。
1. 这个压缩包里装着一套完整STM32磁悬浮系统
拿到一个名字很长的zip,里面通常是四个东西:磁悬浮控制程序源码、Keil工程文件、电路原理图和3D结构件。它解决的不是“让磁铁吸住铁块”,而是“让一块永磁体悬停在线圈下方某个指定位置”。开环通电时,浮子只会被吸上或掉下去,真正让它悬在空气里的是STM32以微秒级周期读取位置、计算偏差、再调整线圈电流。你可以照着电路图打样板,用FDM打印机输出外壳,再在Keil uVision5里打开工程直接烧录。作为毕业设计或嵌入式控制练习,这套东西比倒立摆更直观:对象是强非线性系统,对传感器噪声和机械装配误差都极其敏感,把这一套调通,等于同时摸清了PID、H桥驱动和运放信号调理。
2. 磁悬浮控制的原理与PID参数边界
2.1 为什么开环磁悬浮一定会失败
永磁体受到的电磁力与距离平方近似成反比,线圈电流变化又会改变场强。浮子在平衡点附近时,只要受到一点点推力,等效刚度是负数:它离线圈远一点,吸力骤减,于是继续远离;离线圈近一点,吸力骤增,于是直接吸上去。这就是典型的开环不收敛系统,所以控制程序里必须有实时位置反馈。
磁悬浮控制程序源码里最常见的方案是:位置传感器输出电压 -> STM32 ADC采样 -> PID算出PWM占空比 -> H桥或半桥驱动电磁铁。控制周期不需要极端的微秒级,常见的做法是1kHz到10kHz。PWM频率反而要更高,20kHz左右可以把电流纹波和可听噪声都压下来,也给ADC留出避开开关边沿的采样窗口。
2.2 位置PID模板与抗积分饱和
Keil工程里可以放一个独立的位置PID实现,我用一个适合HAL库的简化模板说明。
/* pid.c - 磁悬浮位置PID模板 */ typedef struct { float Kp, Ki, Kd; float ref; /* 目标悬浮位置,用ADC码值表示 */ float integral; float prev_err; float out_max; /* 输出占空比上限,例如 9000 */ float i_max; /* 积分限幅 */ } MagPID; uint32_t MagPID_Update(MagPID *p, uint32_t adc_now) { float err = p->ref - (float)adc_now; p->integral += err; /* 抗积分饱和:积分先夹紧,避免深积分导致回冲 */ if (p->integral > p->i_max) p->integral = p->i_max; if (p->integral < -p->i_max) p->integral = -p->i_max; float d_term = (err - p->prev_err); float u = p->Kp * err + p->Ki * p->integral + p->Kd * d_term; p->prev_err = err; if (u > p->out_max) u = p->out_max; if (u < 0) u = 0; /* 单向电磁铁只输出正占空比 */ return (uint32_t)u; }参数说明:这套代码针对的是单个电磁铁,所以输出下限直接截到0。如果你的结构件里用了上拉吸和下拉吸两个线圈,就要允许负输出,再用两路PWM或H桥换向实现正负电流。ref用的是ADC原始码值而不是毫米,因为霍尔传感器输出在近距离范围内不是严格线性,控制程序里直接用原始码可以省略一次非线性换算。
下面是起始调试用的参数表:
| 目标悬浮距离 | Kp | Ki | Kd | 控制周期 |
|---|---|---|---|---|
| 2~4mm 近距 | 800~1200 | 20~50 | 100~200 | 500us |
| 5~8mm 远距 | 1500~2500 | 30~80 | 200~400 | 200us |
这些值只是起点,真实工程里要按线圈匝数、电源电压、传感器增益重新标定。一般从低Kp开始,逐步增加直到出现高频抖动,再退回60%;然后加Kd抑制高频振荡;最后用小Kp修正长时间漂移。如果加上Kp后发现浮子缓慢振荡,大部分时候不是Kp过大,而是积分项在平衡点附近反复累积,需要调低Ki。
2.3 ADC采样与PWM触发的联动
在STM32控制程序源码中,最容易出错的是采样时刻。如果ADC在PWM高边开关瞬间采集,线圈电流突变会在传感器地线上产生尖峰,位置信号会出现大量毛刺。可靠做法是用定时器TRGO触发ADC注入组采样,PWM周期中断里读结果,再用DMA循环搬运。这样能保证每次控制周期读到的都是确定的电流状态下的位置。
Keil工程里如果只开一个普通ADC中断,不断转换且不关联PWM,那么示波器上看到的ADC码值会和实际位置差出几十个码值,导致PID输出忽大忽小。所以初始化时建议写一个四元素循环缓冲,连续取最近4次ADC结果求平均,替代单点采样。
3. 在Keil里跑通STM32磁悬浮工程
3.1 解压后如何组织Keil工程
把zip解压后,先不要急着双击工程文件。先确认你的Keil MDK版本,磁悬浮工程通常基于MDK 5,需要从Keil官网下载并安装正确的芯片包。打开工程时如果提示缺少设备,打开Pack Installer安装STMicroelectronics对应的DFP包。
Keil工程文件内部至少包含启动文件、HAL或标准外设库、硬件初始化代码和应用控制代码。芯片型号不同,启动文件也分hd、md、ld,如果你手里的板子不是工程默认型号,在魔术棒Device里切换后要检查启动文件是否能匹配。比如STM32F103C8T6是64KB Flash,工程里如果包含Debug和Release两组配置,切换后注意Flash Download算法的容量设置。
3.2 配置ADC、PWM、串口的引脚分配
磁悬浮系统的电路原理图决定引脚,我这里给出一份常见分配表,你可以直接对照Keil工程里的GPIO初始化:
| 功能 | 外设 | 引脚 |
|---|---|---|
| 位置传感器电压采集 | ADC1_IN1 | PA1 |
| 电磁铁PWM输出 | TIM1_CH1 | PA8 |
| 串口调试 | USART2_TX/RX | PA2/PA3 |
| 蜂鸣器报警 | GPIO推挽输出 | PB12 |
| 方向控制 | GPIO输出 | PB13/PB14 |
下面是用HAL库初始化PWM通道的片段,这套代码在Keil里能直接编译运行:
/* pwm_init.c - 初始化TIM1_CH1输出20kHz PWM */ GPIO_InitTypeDef g = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_TIM1_CLK_ENABLE(); g.Pin = GPIO_PIN_8; g.Mode = GPIO_MODE_AF_PP; g.Alternate = GPIO_AF1_TIM1; HAL_GPIO_Init(GPIOA, &g); htim1.Init.Prescaler = 4; /* 72MHz / (4+1) = 14.4MHz */ htim1.Init.Period = 720; /* 14.4MHz / 720 = 20kHz */ htim1.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(&htim1); TIM_OC_InitTypeDef oc = {0}; oc.Pulse = 0; HAL_TIM_PWM_ConfigChannel(&htim1, &oc, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);代码逻辑说明:Prescaler和Period共同决定PWM频率,这里得到20kHz。Pulse对应占空比累加值,PID每次更新后调用__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, duty),就能立刻改变线圈平均电流。如果使用的是STM32G4/F4系列,引脚复用映射可能不同,需要查数据手册确认GPIO_AF*_TIMx的定义。
3.3 Keil报错和下载失败的排查
烧录时最常看到的是Error: No STM32 Target Found!。这意味着ST-Link和板子还没握手成功。依次检查:Debug下拉框是否选了ST-Link;Settings里是否能识别到IDCODE和电压;板子是否上电;如果是SWD接口,确认SWDIO和SWCLK没有反接。更低级的问题是Keil MDK和C51共存导致配置串了,一个工程同时被C51工具链打开后,Device选项会变成空白。
另一个常见现象是从网上下载的工程打开后,在Build时满屏宏定义错误。多半是芯片Pack版本不一致,比如工程是基于STM32F4xx_DFP 2.13.0建立的,而你装的是F1的包。Pack Installer里会提示硬件错误,关闭工程后重新安装对应包即可,不要在Keil里的半打开状态直接删除包。
4. 电路原理图解析:运放减法、H桥驱动和电源
4.1 用运放减法电路还原位置信号
磁悬浮的霍尔传感器常常贴着电磁铁安装,线圈电流产生的强磁场会叠加在传感器输出上。如果直接用单端输出接ADC,你会发现PWM占空比一变,ADC值就跟着跳动。电路原理图里加一级运放减法电路,就是要把霍尔传感器的差分信号转成单端信号,同时把线圈产生的共模干扰抵消掉。
运放减法电路原理图的核心是四个等值电阻。我用下面这张表说明闭环增益和电阻精度的关系:
| 电阻精度 | 理论上共模抑制比 | 实际工厂测试结果 |
|---|---|---|
| 5% | 约26dB | 干扰会明显出现在输出端 |
| 1% | 约40dB | 勉强可用 |
| 0.1% | 约60dB | 推荐,ADC采到的是纯位置 |
接线拓扑是把霍尔S+接到运放同相端,S-接到反相端,运放输出再串100欧电阻和10nF电容,形成一阶低通到ADC引脚。如果PCB空间不够,也可以把低通放在运放反馈回路里,但计算麻烦一些,建议还是两级分开。
这里的ADC输入范围必须匹配运放输出。3.3V单片机的ADC参考电压是3.3V,运放供电如果是5V,输出饱和就可能在3.7V左右,把ADC打满。所以运放做2倍增益后,要么把输出钳位到3.0V,要么改用3.3V供电的轨到轨运放。Keil工程里读取并滤波的代码可以这样写:
/* filter.c - 对运放输出做软件均值 */ uint32_t sensor_value; uint16_t sum = 0; for (int i = 0; i < 8; i++) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 100); sum += HAL_ADC_GetValue(&hadc1); } sensor_value = sum >> 3;逻辑说明:取8次ADC结果的平均值,可以有效衰减模拟滤波器没有完全压掉的噪声。注意PollForConversion这个方式效率不高,只适合低速调试;控制程序跑起来后要改成DMA或定时器触发方式,不能让这8次阻塞发生在PID中断里。
4.2 H桥驱动电路与方向控制
有些入门设计只用一颗MOS管下拉驱动电磁铁,这种电路能吸住磁铁,但断电后磁铁会有剩磁,浮子被残磁吸住无法顺利释放。Keil工程对应的电路原理图里常见的是H桥或半桥驱动,通过两个方向引脚让线圈电流可以反向,实现主动消磁。
H桥驱动电路原理图一般由两个半桥组成,高边用PMOS或带自举的NMOS驱动芯片,低边用普通NMOS。磁悬浮线圈电流方向只有正、反、续流三种状态,我把控制表整理如下:
| DIR1 | DIR2 | 线圈状态 | 效果 |
|---|---|---|---|
| 1 | 0 | 正向通电 | 电磁铁吸起浮子 |
| 0 | 1 | 反向通电 | 主动退磁,浮子下落受控 |
| 0 | 0 | 下管续流 | 电流缓慢衰减 |
| 1 | 1 | 禁止 | 直通短路 |
驱动芯片的使能引脚如果单独拉出来,就先关PWM再切换方向,避免在电流未衰减时突换方向产生浪涌。H桥两个半桥的死区时间参数也很重要,死区太短会上下管直通烧驱动,太短具体多短取决于MOS管栅极电荷。使用IR2104一类驱动芯片时,死区由芯片内部延时决定,比较安全;如果用三极管搭离散H桥,必须在代码里加入两个方向引脚翻转之间的延时。
4.3 5V转3.3V和电源纹波抑制
磁悬浮对电源纹波的要求比普通单片机项目高:线圈大电流会拉扯母线电压,导致单片机、运放、霍尔传感器供电同步跌落,控制程序会误以为位置突变。电路原理图里通常在电源入口放一只470uF电解电容和几颗100nF陶瓷电容。5V转3.3V部分用AMS1117或更低压差的LDO,输出端至少10uF钽电容。
蜂鸣器电路原理图则简单很多:PB12输出高电平,经过1k电阻接到NPN三极管基极,三极管集电极接蜂鸣器负极,发射极接地。单片机引脚直接驱动蜂鸣器电流不够,加了三级管后声音也更响。浮子失控时控制程序拉高PB12,蜂鸣器报警提示维护人员先断电。
5. 3D结构件如何配合传感器与控制板
5.1 打印磁悬浮机架的核心尺寸
3D结构件往往比电路更影响控制效果。线圈筒、传感器支架、浮子导向套、外壳底座这四个零件必须有稳定夹持关系。线圈筒如果打印得比线轴大太多,线圈发热后会松动,位置传感器和线圈间隙发生变化,相当于每启动一次悬浮的机械零点都在变。
FDM打印时使用PETG比PLA更耐热,线轴靠近线圈的地方局部温度可能超过50度。下表是我自己打印几版后觉得可用的尺寸:
| 特征 | 推荐数值 | 公差 |
|---|---|---|
| 线圈筒内径 | 18.2mm | +0.2mm |
| 传感器安装面到线圈距离 | 5mm | +0.5mm |
| 浮子导向孔直径 | 8.2mm | +0.2mm |
| 外壳壁厚 | 2mm | 无 |
| 底脚螺丝孔 | M3 | 过孔3.2mm |
浮子导向孔不需要完全贴住浮子,留0.2~0.3mm让空气轴承效应发挥作用,但间隙太大浮子会左右摆动,PID的微分项会持续放大噪声。
5.2 安装顺序和零点校准
组装顺序应当是先固定线圈,再装霍尔传感器,最后装浮子。浮子下部加一根透明限位柱,让失控时磁铁砸不到桌面上。机械装配完成后,先把PID的占空比输出设为0,读取一次传感器ADC码值作为零点。
控制程序初始化时建议做一次平均采样,代码大致如下:
/* zero_calib.c - 上电时等待浮子贴在底部,读取零点 */ uint32_t zero = 0; for (int i = 0; i < 8; i++) { zero += ReadADC1(); HAL_Delay(10); } zero >>= 3; SetRef(zero + 80);参数说明:平均8次是为了滤除电源低频波动。80这个偏移量不是固定值,它表示浮子悬停位置相对起始位置在ADC码值上的变化量。如果运放增益是2倍,霍尔传感器灵敏度为2.5mV/Gs,悬浮距离5mm时传感器输出变化约200mV,对应ADC码值大约200左右,你可以据此调整偏移。
5.3 机械谐振对控制环路的限制
3D打印件刚度不足,线圈支架和传感器支架之间一旦出现低频机械谐振,PID参数怎么调都压不住。这时候悬浮物会以20~80Hz的频率来回抖。先判断是机械问题还是电学问题:用手按压壳体,如果抖动频率变化,说明机械刚性不足;如果不变,则是控制器参数问题。机械谐振限制的是控制带宽,所以设计时优先把传感器支架做成三角形加强筋,或者把传感器支架和线圈底座做成一体件,而不是分两个零件用螺丝连接。
6. 进阶技巧:用串口数据曲线验证悬浮动态
6.1 通过UART定时上报控制量
肉眼只能看到浮子有没有悬住,却看不到PID输出是否在极限位置反复饱和。把ref、adc_now和duty通过串口发出来,可以在一张图上分析出问题。
在控制循环里用一个10ms计数器,每次到时间就通过USART2发送一行数据:
/* debug_report.c - 只发关键量,避免阻塞控制中断 */ if (++dbg_cnt >= 10) { dbg_cnt = 0; printf("%lu,%lu,%lu\n", (unsigned long)pid.ref, (unsigned long)adc_now, (unsigned long)last_duty); }注意printf重定向到串口后带浮点格式会大幅增加耗时,尽量不要用%f,全部按整数发送。如果必须看浮点,把PID常量和输出放大100倍再打印。
6.2 用Python脚本接收并绘制悬浮数据
虚拟串口抓到的数据可以直接用PySerial在电脑上画阶跃响应。下面脚本读取三列整数并绘制:
import serial, matplotlib.pyplot as plt ser = serial.Serial('COM5', 115200, timeout=1) refs, adcs, duties = [], [], [] for _ in range(300): line = ser.readline().decode().strip() if len(line.split(',')) == 3: r, a, d = map(int, line.split(',')) refs.append(r); adcs.append(a); duties.append(d) plt.plot(adcs, label='position') plt.plot(refs, label='ref') plt.legend() plt.show()通过曲线能直观看到从启动到悬浮的过渡过程:如果duty长时间在最大值和0之间来回跳,说明PID增益太大;如果adcs缓慢上升且duty保持在中间值,说明重力补偿不够。用这个办法最多花十分钟,就能把PID从“不稳定”调到“悬停且没有持续抖动”的状态。串口波特率设成115200时,数据线不要超过20cm,否则干扰会让误码率升高,进而导致绘图曲线毛刺比实际还大。
本文还有配套的精品资源,点击获取