做风力摆这个项目,是在一次控制类课程设计上。当时拿到题目“设计一个风力摆控制系统,摆杆末端安装风扇,通过调节风力让摆杆按指定角度摆动”,第一反应是这东西听起来不难,但真正上手才发现——机械、传感器、驱动、控制环,每一环都有坑。等我把整套系统跑通、从摆直线做到画圆之后,才觉得这条路值得走一遍。这篇博文就把我基于CubeMX和HAL库的完整实现过程拆开写清楚,从选型到CubeMX配置,从MPU6050读数到PID调参,再到现场踩过的坑,全部记录在这里。
这篇内容适合三类人:准备做电子类竞赛控制题的学生、想用HAL库快速搭一个闭环控制系统的人、以及已经能让摆杆动了但一直调不稳的折腾党。
1. 风力摆到底是一个什么系统
1.1 先弄明白风力摆的动力学本质
风力摆的结构并不复杂:一个可以自由摆动的摆杆,末端装一个风扇和一个姿态传感器。风扇产生的推力让摆杆偏离垂直位置,重力提供回复力,姿态传感器测出当前摆角。控制器要做的事情就是根据“当前角度”和“目标角度”的差值,算出风扇该用多大推力、朝哪个方向吹。
从控制理论角度看,这是一个典型的二阶系统。摆杆在平衡位置附近小角度摆动时,运动方程可以近似为:
[ J \cdot \ddot{\theta} = F_{fan} \cdot L - m \cdot g \cdot L \cdot \sin(\theta) ]
其中 (J) 是转动惯量,(L) 是摆杆有效长度,(F_{fan}) 是风扇推力沿摆动方向的分量。风扇推力与PWM占空比的关系并不是严格线性,但在工作范围内可以近似看作正比。重力项在小角度下近似为 ( -mgl \cdot \theta ),所以系统天然带有回复力,本质上是一个“带阻尼的振荡环节”。
这个理解非常重要,因为它决定了控制策略的方向:系统是不稳定的吗?不是。系统是稳定的吗?也不是完全稳定——在没有控制时,摆杆会在重力作用下回到垂直位置并来回衰减摆动几次,最终停在垂直位置。问题是这个衰减过程很慢,而且摩擦不确定。控制器的任务就是“主动阻尼”和“目标跟踪”二合一。
1.2 关键部件怎么选,为什么这么选
主控芯片我选的是STM32F103C8T6。不需要更高级的芯片,一个72MHz的Cortex-M3内核足够跑完姿态解算和双轴PID,价格还便宜。CubeMX对F103的支持非常成熟,选它在工程配置阶段能省下不少时间。
姿态传感器选了MPU6050。理由很直接:六轴MEMS传感器,便宜、资料多、精度对风力摆够用。风力摆的摆动角速度不会特别夸张,陀螺仪量程设到±500dps就够;加速度计量程±2g即可。量程选大了反而会让有效分辨率变差,这是一个很多人忽略的细节。
风扇和执行部分,我对比过两种方案:
- 空心杯电机加小螺旋桨:重量轻、响应快、推力小,适合小幅摆动和精细控制。
- 涵道风扇:推力大、惯性大、响应慢,适合需要克服大阻力的场合,但对PID的抗扰要求更高。
最终我选了空心杯电机加直径约7cm的螺旋桨,因为课程设计场景里摆杆角度范围不大,推力不追求极限,反而更看重控制的平滑性。电机驱动用了MOS管加续流二极管的方案,PWM直接控制电机电枢电压。
电源设计上有一点必须注意:电机和传感器要分开供电。电机启动和PWM换相时会产生明显的电源纹波,如果MPU6050和主控共用同一路电源,加速度计数据会出现周期性毛刺,直接影响控制稳定性。我用了一块5V转3.3V的LDO给主控和传感器供电,电机单独走12V,两路电源只在总输入端共地。
1.3 系统架构和控制链路
整体链路是这样的:
传感器(MPU6050)→ I2C → MCU(Cortex-M3)→ 姿态解算(互补滤波)→ 双轴PID控制器 → PWM输出 → MOS驱动 → 直流电机 → 风扇推力 → 摆杆运动 → 传感器 ...
这套链路里面,最关键的参数其实是“控制周期”。角度解算和PID控制都放在定时器中断里执行,周期设成2ms,也就是500Hz控制频率。风扇的机械响应时间大概在几十毫秒量级,500Hz的控制频率已经足够,再高反而可能因为执行器响应跟不上而产生无效输出。
2. CubeMX工程搭建与HAL库配置
2.1 时钟树和调试接口怎么配
CubeMX配置的第一步是时钟。F103用8MHz外部晶振,PLL倍频到72MHz。这里建议在CubeMX的Clock Configuration页面里自动分配,软件会自动帮你算好各条总线时钟。APB1和APB2的定时器时钟要重点看一下,因为后面PWM和定时器中断的时基都依赖它们。
调试口我选了SWD四线模式,占用PA13和PA14。这不算什么技术决策,但如果图省事选成JTAG,会白占掉更多引脚,全功能模式下复用引脚很麻烦。
2.2 外设配置明细
- I2C1:专用模式,Fast Mode 400kHz,用于读取MPU6050。引脚用PB6/PB7默认映射就行。
- 两个通用定时器做PWM输出:一个管X轴电机,一个管Y轴电机。F103里TIM2是32位定时器,用来产生PWM时反而有点浪费,我用的是TIM3和TIM4这样的16位定时器,PWM频率设10kHz。
- USART1:115200bps,调试打印用。主要输出现场角度、目标角度、PID输出量,方便串口上位机绘图观察。
- 一个基础定时器(TIM6)做控制周期中断:2ms进一次中断,在中断里跑完“读传感器 → 解算 → PID → 输出PWM”整个控制链。
PWM频率选10kHz是经过权衡的。空心杯电机的电气时间常数很小,PWM频率太低会听到明显的啸叫,而且电枢电流纹波大;太高又会让MOS管的开关损耗变大。10kHz在这里是一个平衡点,实测下来噪声可以接受,电流也挺平稳。
CubeMX生成工程之后,不要急着写代码。先把生成的文件结构过一遍,确认HAL层的初始化函数已经在main函数里按顺序调用。HAL库的初始化顺序是:HAL_Init → 时钟配置 → GPIO → 各外设(I2C、TIM、USART等)。这个顺序是固定的,靠CubeMX生成不会乱,但如果你手动加外设,务必注意先初始化时钟再初始化外设。
2.3 从模板代码到可用工程
生成后需要动手的地方有三个:
第一,在main.c的USER CODE区域里加MPU6050的初始化。不要放在main函数外面,CubeMX的代码生成会把USER CODE区域原样保留,这才是安全位置。
第二,在定时器更新中断回调里加控制逻辑。HAL库的中断服务函数HAL_TIM_PeriodElapsedCallback是弱定义,在自己的文件中重写即可,不需要直接动中断向量表。
第三,把MPU6050驱动拆成独立的.c/.h文件。我是把传感器驱动、姿态解算、PID控制各拆成一个模块,主程序只负责把它们串起来。这样做的好处是:后面想换传感器、换控制算法,改动范围都是局部的,不会牵一发动全身。
3. 传感器数据链:从原始寄存器到可用角度
3.1 MPU6050初始化,别跳过这些细节
MPU6050的初始化顺序有讲究,我实测过跳步的后果,这里按正确顺序说:
先上电,等待100ms以上,让传感器内部稳定。然后向PWR_MGMT_1寄存器写入0x00,让芯片退出睡眠模式,并选择内部时钟源(或者外部PLL时钟源,都可以)。接着配置采样率分频寄存器SMPLRT_DIV,让输出速率落在1kHz左右。
然后是数字低通滤波器(DLPF)的配置,这一步直接影响数据质量。DLPF的截止频率写进CONFIG寄存器的低三位,我选的0x03,对应约44Hz的截止频率。为什么选这个?因为风力摆的摆动频率也就是1到几赫兹,44Hz的截止频率能有效滤掉机械振动和电机干扰,同时又不影响真实信号。
最后配置加速度计和陀螺仪的量程。加速度计量程寄存器ACCEL_CONFIG写0x00(±2g),陀螺仪量程寄存器GYRO_CONFIG写0x08(±500dps)。量程越小,同样的ADC位数下分辨率越高。
用HAL库读数据的核心就是I2C寄存器读取,比较标准的写法是:
uint8_t reg = 0x3B; uint8_t data[14]; HAL_I2C_Mem_Read(&hi2c1, 0xD0, reg, 1, data, 14, 100);注意MPU6050的7位地址是0x68,对应8位写地址0xD0。这个地方最容易出错,有人在这里卡了一整天,读回来的数据全是0xFF。
读回来的14个字节里,前6个是加速度计XYZ,中间2个是温度,后6个是陀螺仪XYZ。每个轴的数据是两个字节的高低位组合,要拼成有符号16位整数:
int16_t ax = (int16_t)((data[0] << 8) | data[1]);3.2 倾角解算和互补滤波,为什么不用卡尔曼
有了加速度计和陀螺仪原始数据,下一步是把它们变成“摆杆角度”。这里我经历过一个探索过程,最终用的是互补滤波,这里也对比一下其他方案。
加速度计可以直接算倾角,静态时很准,但动态时会被运动加速度污染。陀螺仪积分出的角度动态响应好,但会漂移。两者正好互补。
常用的小角度倾角解算公式(针对一个轴):
[ \theta_{acc} = \arctan2\left(a_y, a_z\right) ]
陀螺仪的积分则是:
[ \theta_{gyro} = \theta_{prev} + g_y \cdot dt ]
互补滤波的核心思想是:用加速度计修正长期漂移,用陀螺仪提供瞬时动态。公式就一行:
angle = 0.98f * (angle + gyro_y * dt) + 0.02f * angle_from_acc;这个0.98和0.02的比例是互补滤波的精髓。系数0.98意味着“更信任陀螺仪的短期变化”,0.02意味着“用加速度计做慢速修正”。在风力摆这种场景下,摆杆运动频率低、加速度计动态误差不太大,这个比例是合理的。
有人会问为什么不用卡尔曼滤波。我试过,卡尔曼滤波在MPU6050上也能跑,但两个问题很实际:一是Q矩阵和R矩阵的参数调节非常依赖经验,调不好比互补滤波还差;二是F103上跑卡尔曼滤波每个控制周期要花不少时间,留给PID的计算预算被压缩。互补滤波在这个项目里已经能稳定输出±0.1度级别的角度,没有必要上卡尔曼。
3.3 零点标定,这个步骤很多人会漏掉
MPU6050出厂有零偏,安装到摆杆上之后,机械零位和传感器零位也不可能完全重合。所以上电后要做一次零点标定:让摆杆静止垂下,连续采样200到500组数据,取平均作为零点偏移。
标定完成后,把这个偏移量存到全局变量里,在每次姿态解算后减去。这一步看似简单,实际影响非常大。零偏没消除之前,PID输出会一直带着一个固定的偏置,摆杆看起来“站不直”,总往一个方向歪。
标定的代码逻辑大致是:
for (int i = 0; i < 300; i++) { read_mpu6050(); acc_pitch_sum += acc_pitch; gyro_y_sum += gyro_y; HAL_Delay(1); } acc_pitch_offset = acc_pitch_sum / 300; gyro_y_offset = gyro_y_sum / 300;之后每次读取,计算角度前先把这个偏移减掉。
4. 控制算法落地:PID怎么调才稳
4.1 从系统模型到控制器设计
回到之前的运动方程。风力摆系统的关键特点有两个:一是系统本身阻尼很小,摆杆一旦激励起来会持续摆动很久;二是执行器(风扇)的响应有明显滞后。
针对这两个特点,纯比例控制是不够的。纯P控制会让摆杆围绕目标位置做等幅振荡,因为比例项只与当前位置误差有关,仿佛一个没有阻尼的弹簧。要抑制振荡,必须加入微分项——这就是PD控制在这个系统里的必要性。
这里有一个很多人理解偏了的地方:微分项不是“刺客”,它是系统里人为引入的阻尼。它的输出正比于误差的变化率,当摆杆快速冲向目标点时,微分项会给出反向的“刹车”力,防止冲过头。
完整的位置式PID公式是:
[ u(t) = K_p \cdot e(t) + K_i \cdot \int e(t)dt + K_d \cdot \frac{de(t)}{dt} ]
在数字控制器里,微分项用差分近似:
[ \frac{de(t)}{dt} \approx \frac{e(k) - e(k-1)}{T} ]
4.2 控制器的程序实现
我的控制代码写在TIM6的更新中断回调里,2ms一次。整体逻辑是:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { read_angle_x(); // 读取X轴解算角度 read_angle_y(); pid_x.target = setpoint_x; pid_x.actual = angle_x; pid_x.err = pid_x.target - pid_x.actual; pid_x.integral += pid_x.err * dt; // 积分限幅保护 if (pid_x.integral > INTEGRAL_LIMIT) pid_x.integral = INTEGRAL_LIMIT; if (pid_x.integral < -INTEGRAL_LIMIT) pid_x.integral = -INTEGRAL_LIMIT; pid_x.derivative = pid_x.err - pid_x.last_err; pid_x.last_err = pid_x.err; pid_x.output = Kp*pid_x.err + Ki*pid_x.integral + Kd*pid_x.derivative; // 输出限幅到[-100,100] constrain(&pid_x.output, -100, 100); // 根据PID输出控制电机方向与占空比 drive_motor_x(pid_x.output); drive_motor_y(pid_y.output); } }两个轴各跑一套PID,互不干扰。X轴控制摆杆在某个平面内的偏角,Y轴控制另一个正交平面的偏角。到这一步,系统的全部关键代码就串起来了。
4.3 风力正反转,怎么让风扇朝两个方向吹
这是风力摆设计里一个很实际的问题。摆杆偏离中心后,要让它回来,风扇必须朝“反方向”吹——但风扇转动方向是靠电机正反转决定的。
我用的是两个GPIO控制一个H桥,或者两颗MOS管分别控制正转和反转。PID输出的正负表示方向:正值让风扇正转,负值让风扇反转。PWM占空比由PID输出的绝对值决定。
具体映射逻辑:
- PID输出在[-5, 5]区间:设置死区,PWM置0,什么都不做。
- PID输出大于5:正转,PWM设置为min(output, 100)。
- PID输出小于-5:反转,PWM设置为min(-output, 100)。
死区是必须有的。空心杯电机加螺旋桨,PWM占空比太小电机根本转不起来,只会发出嗡嗡声而且发热。我在现场测过,占空比低于8%左右螺旋桨基本没有推力,低于3%电机原地抖动。这个死区值不是固定的,不同电机、不同供电电压差异很大,要自己现场标定。
驱动电路上还有一个细节:电机正反转切换时,MOS管一定要先关断再换向,不能直接从一个方向切换到另一个方向,否则会产生很大的冲击电流。软件上实现就是先在GPIO层全部置低,延时几毫秒,再开新的方向。
PID输出到PWM的映射还需要考虑风扇的非线性。风扇推力和占空比在小范围内近似线性,但大占空比时会饱和。常规做法是先按近似线性映射,如果发现大角度响应不够,再在PID输出后加一个针对大占空比的补偿曲线。
5. 现场调试实录:现象、原因与解决
5.1 MPU6050读不到数据,常见原因有哪些
这个问题的排查路径基本固定。先查硬件连接:SDA和SCL有没有接反,VCC是不是3.3V而不是5V,GND有没有共地。再用I2C扫描程序跑一遍,看总线上能不能发现0x68这个地址。如果扫描不到,很可能就是I2C引脚配置不对,或者芯片供电不够。
还有一种情况让人抓狂:读数从寄存器里读出来但全是同一个值,或者偶发跳变。这是因为I2C速率太高或者线上干扰。把I2C速率从400kHz降到100kHz能解决大部分这类问题。
5.2 摆杆一直低幅振荡,PID参数怎么调
遇到振荡是最常见的情况,我的调参顺序是:
先只保留P项,I和D都归零。从很小的Kp开始慢慢加,直到摆杆能有一个明显的“往目标方向拉”的趋势。这时候系统大概率会振荡,没关系。然后加D,从小往大调,观察振荡幅度是否快速收敛。
简单来说:P决定“拉回力量”,D决定“刹车强度”。有个好用的比例——如果P已经加到系统在强烈振荡,D加到能够把振荡收敛到允许阻尼的程度。如果P太小,加D也没有用。
我最终调出来的X轴参数大致是Kp=120,Ki=0.1,Kd=4.5。这个数值完全依赖你的机械结构,直接抄没有意义,但调参顺序是通用的:先P后D再I,I的系数一定要从很小开始加,否则积分饱和会让系统一冲一冲的。
积分项在风力摆中不是必须的。由于重力提供了天然的回复力,稳态误差并不会像“电机位置伺服”那样明显。如果目标就是让摆杆停在某个角度,P加D已经能做到相当精确。积分项只在做精确角度保持时有意义。
5.3 从摆直线到画出圆形轨迹
题目做到后来都会加任务:让摆杆末端画圆。其实本质还是双轴PID,只是目标值不再是一个固定角度,而是一组随时间变化的参考值。
画圆的核心是给X轴和Y轴各一个相位差90度的正弦参考轨迹:
setpoint_x = radius * sin(2 * PI * freq * t); setpoint_y = radius * cos(2 * PI * freq * t);两个轴各自跟踪自己的正弦目标。关键是两个轴的增益要匹配,否则画出来是椭圆。匹配的方法是分别让系统在X轴和Y轴摆相同角度,观察PID输出是否相同,如果不相同,调整各轴的Kp让它对齐。
实际操作中画不圆还有一个隐蔽原因:机械零位没标好。如果摆杆的传感器安装得偏了,哪怕两个轴控制效果都很好,合成轨迹也会是歪的椭圆。先把每个轴的静态归零做好,再谈画圆。
5.4 高频抖动、电机鸣叫、传感器数据毛刺
还有一个出现概率很高的问题:摆杆静止时角度数据就有明显的“毛刺”跳动。排查后发现是电机换相产生的电磁干扰耦合到了I2C线上。解决办法有三个方向:
一是给MPU6050供电加一个100nF和10uF的旁路电容,尽量靠近VCC引脚。
二是把I2C数据线和电机线物理分开,不要在排线里并排贴着走,电机线尽量用短粗线。
三是降低I2C速率,从400kHz降到100kHz。代价是读取时间变长,但2ms控制周期里依然绰绰有余。
这三种措施配合起来,传感器数据可以做到静止时角度漂移小于0.1度。数据是干净的,后面所有调试才有意义。
6. 收个尾:关于这个项目的一点个人体会
做完这个风力摆,我最深的感受是:系统能不能稳,机械和传感器的底子决定了上限,PID只是把上限发挥出来的手段。很多同学一开始纠结于PID参数怎么调,结果发现真正让系统不稳定的是风扇装在摆杆上的螺丝松了、是MPU6050没有标零、是电源纹波把传感器数据搅乱了。
用CubeMX加HAL库确实让工程搭建省了很多事,不需要再手撸寄存器级别的初始化,能把精力集中在控制逻辑本身。但HAL库也有它的脾气——中断回调、用户代码区的放置位置、外设初始化顺序,这些规则不搞清楚,生成再好的模板也会踩坑。
这个系统的可玩性非常高。如果后续想继续扩展,可以尝试加一个无线通信模块,让摆杆状态实时传到上位机上绘图;也可以把固定PID换成模糊自适应PID,观察摆杆在不同摆角下怎么自动调整参数;再进一步,用两个摆杆做协同运动,就又是一个新的题目了。
最后想分享一个小技巧:调试过程中给自己做一个简单的串口上位机显示,把目标角度、当前角度、PID输出三条波形放到一起看。参数合不合适不用猜,一眼就能看出来。我当时用串口把数据抛到电脑上用串口绘图工具画的,调整Kp和Kd的过程就像在给系统“配眼镜”,调到波形平滑贴合目标曲线的那一刻,整个系统忽然就安静了。这是做控制最上瘾的瞬间。