news 2026/9/13 6:06:07

STM32平衡车串级PID控制:倒立摆姿态解算与调参详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32平衡车串级PID控制:倒立摆姿态解算与调参详解

简介:这是一份基于STM32单片机开发的两轮平衡车项目源码包,面向嵌入式爱好者、电赛备赛学生及中级单片机开发者,用于学习串级PID控制与多模式运动控制。资源共714个文件,压缩包约25.25MB,以398个C源码、183个头文件、70个汇编文件为主,同时包含uvprojx工程文件与ICF链接脚本,并附带STM32F4标准外设库,可直接通过Keil打开编译。项目核心采用串级PID算法,通过直立环、速度环、转向环协调控制保持车体平衡,并在此基础上提供蓝牙遥控、巡线、避障等完整功能实现,以及基于ADC的电池电压监测模块。整体工程按硬件驱动、控制算法、应用层任务划分目录,代码注释清晰,便于二次调参与功能扩展。已有551人学习下载,适合希望通过真实项目综合锻炼嵌入式软硬件能力的开发者。

1. 平衡两轮车是倒立摆的工程化缩影

控制目标只有一个:让车体保持直立。用单环PID,角度偏差直接驱动电机,等角度偏差大到能产生足够纠正力矩时,车体已经过冲倒向另一侧。串级PID把陀螺仪角速度放进内环,把姿态角度放进外环,内环每秒执行上千次,先把角速度压住,外环再用较慢周期修正角度回中。两道环配合等效给倒立摆加了一个可编程阻尼,解决了单环PID抗扰动弱、易振荡的通病。对从事STM32底层开发、又要弄懂控制环路的工程师,这是很好的练手项目,也是毕业设计与竞赛的高频选题。下文从硬件选型讲到姿态解算,再用C语言给出串级PID实现与调参顺序,最后落到三个排错技巧。

2. 平衡两轮车的机械结构与核心器件选型

2.1 重心位置与整车布局

倒立摆模型的恢复力矩与重心高度直接相关。重心离轮轴越高,等效摆长越长,静止平衡时同样角度偏差对应更大的重力矩,P动作必须更强,系统更容易震荡。反过来,重心压低会让静态恢复力矩变小,稳定相对容易,但转向和前进响应会变钝。用STM32做主控的平衡车,建议用三层亚克力板做车身:底层装电机和电池,中间层固定驱动芯片,顶层放STM32与MPU6050。电池是整车最重的部件,尽量贴近轮轴摆放,位置定死之后再通过前后移动配重块来微调重心,这是整车结构上最有效的一个自由度。

2.2 电机减速比、编码器与驱动接口

电机的机械时间常数决定控制环的带宽上限。常见的MG513P30减速电机额定转速300rpm,减速比1:30,配合AB相霍尔编码器,电机轴每圈输出13个脉冲,减速后轮端等效分辨率是13×30=390脉冲每圈,对直径30mm的车轮来说足够分辨毫米级位移。编码器用于两个地方:一是让速度环能把轮速锁在0附近,二是作为内环角速度之外的安全备份,判断车轮是否打滑。霍尔编码器的A、B两相必须接在支持编码器模式的定时器引脚上,接线反了速度符号会翻转,那个方向上的速度环就会变成正反馈,这是第一处容易排半天的坑。

参数JGB37-520MG513P30对控制的直接影响
减速比1:301:30内环带宽接近,调参范围可复用
空载转速300 rpm300 rpm最大PWM对应的车体角速度
编码器类型霍尔AB相霍尔AB相反馈分辨率一致
额定电压12 V12 V决定电池电压和驱动芯片选型

电机驱动选TB6612FNG而不是L298N。TB6612内部MOS管压降小,能承受20kHz以上的PWM频率,单路额定电流1.2A,峰值3.2A,覆盖1:30减速电机的堵转电流;L298N导通压降大,同样PWM下电机转速更弱,发热也更大。把TB6612的两路PWM接到STM32定时器输出通道,另一组IO控制电机方向。PWM频率选10kHz或20kHz都可以,但要避免和陀螺仪采样频率或内环控制频率接近,否则产生差频干扰。用STM32F103的TIM1输出20kHz PWM,控制更新放在TIM2中断里,两者独立,互不等待。

编码器读取直接用定时器编码器模式,代码写在中断周期内读取并清零。

// TIM3编码器模式,CH1接A相,CH2接B相 void Encoder_Init(TIM_TypeDef* TIMx) { TIM_TimeBaseInitTypeDef tb; TIM_ICInitTypeDef ic; tb.TIM_Prescaler = 0; tb.TIM_Period = 0xFFFF; TIM_TimeBaseInit(TIMx, &tb); TIM_EncoderInterfaceConfig(TIMx, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; ic.TIM_ICFilter = 0; // 编码器信号要尽量干净,不滤波 TIM_ICInit(TIMx, &ic); TIM_Cmd(TIMx, ENABLE); } // 中断周期内读取计数值并清零 int32_t Encoder_GetDelta(TIM_TypeDef* TIMx) { int32_t raw = TIMx->CNT; // 直接读寄存器,保留符号 TIMx->CNT = 0; return raw; // 正负代表正反转 }

这段代码在每个控制周期取CNT的增量后清零,得到的是本周期内的编码器脉冲数。必须用有符号变量保存CNT原始值,因为向上溢出从0xFFFF跳变到0时,符号会从正变负,这正好是编码器方向反转的真实表现。

2.3 驱动桥与电源树设计

电源拓扑建议:3S锂电池(11.1V)直接给驱动桥输入,同时经过TPS5430降压到5V给STM32板卡和编码器,再经AMS1117-3.3给MPU6050供电。电机启动瞬间电流尖峰会通过地线串进I2C,干扰姿态数据。在驱动板输入正负之间加470uF电解电容吸收低频脉动,在电机两端并联RC吸收网络(10欧电阻串联1nF陶瓷电容),能明显减少电机换向火花对MPU6050的干扰。MPU6050电源脚加10uF和0.1uF去耦电容,I2C上拉电阻用4.7k,并且传感器放在靠近机械中心的位置,远离电机整流子,否则机械振动会造成陀螺零偏漂移。

3. 姿态解算:从MPU6050原始数据到稳定角度

3.1 I2C读取与零偏校准

MPU6050的陀螺仪输出是16位有符号整数,单位是LSB/°/s,灵敏度默认配置为131 LSB/°/s。静止时陀螺仪Z轴依然有零点几度的偏移,直接积分1分钟就会偏出5度以上,角度环会把这个偏差当成真实姿态去修正,导致车身朝一个方向缓慢加速倾倒。上电后必须做零偏校准:静止状态下连续读取100次,取Pitch轴陀螺仪的均值作为零偏。

float gyro_bias = 0.0f; for (uint8_t i = 0; i < 100; i++) { MPU6050_GetAllData(&ax, &ay, &az, &gx, &gy, &gz); gyro_bias += gy; // 只校准Pitch轴(绕Y轴) delay_ms(3); // 等待传感器稳定 } gyro_bias /= 100;

这段校准代码的关键前提是车体在校准期间绝对静止。如果开机时有人碰了车,平均出来的零偏参杂运动量,后面所有控制都会带着一个固定偏差。校准时间取300ms,足够滤掉低频噪声,也不会让开机等待太久。校准完成后把零偏存成全局变量,只在复位时重新标定。

3.2 互补滤波:兼顾实时与无漂移

姿态角可以直接用加速度计反算:车身绕Y轴的Pitch角就是加速度向量在X轴和Z轴上投影的反正切。但车体运动时加速度计会混入大量线性加速度,一段猛推就让角度跳变。陀螺仪积分姿态平滑、实时,可零偏累积漂移。互补滤波把两者按下式合并:

角度 = alpha * (上一帧角度 + 陀螺仪角速度 * dt) + (1 - alpha) * 加速度计角度

物理含义是:短时间内信任陀螺仪的积分结果,长时间用重力方向把积分漂移拉回来。alpha越大,信任陀螺仪越多,输出越平滑,但响应越慢;alpha越小,信任加速度计越多,抗运动干扰越差。这本质上是一个以alpha为权重的一阶高通加低通组合,比卡尔曼滤波更省CPU,调起来也直观。

3.3 融合代码与采样周期约束

实际工程里我不会用固定的dt,因为定时器中断里还有其他任务,周期抖动会让积分量整体偏移。用HAL_GetTick或时间戳实时计算dt。

float pitch = 0.0f; // 最终姿态角,给外环 float gyro_rate = 0.0f; // 角速度,给内环 uint32_t last_update = 0; void IMU_Update(void) { MPU6050_GetAllData(&ax, &ay, &az, &gx, &gy, &gz); gyro_rate = gy - gyro_bias; // 去掉零偏 // 加速度计解算Pitch角,注意MPU6050坐标轴方向 float accel_pitch = atan2f(-ax, az) * 57.29578f; // 微分时间用时间戳计算,而不是固定常量 uint32_t now = HAL_GetTick(); float dt = (now - last_update) * 0.001f; last_update = now; // 互补滤波,alpha取0.98,在dt=5ms约等价时间常数0.245s pitch = 0.98f * (pitch + gyro_rate * dt) + 0.02f * accel_pitch; }

代码中atan2f(-ax, az)把X轴正向加速度取负后与Z轴加速度求反正切,得到绕Y轴正方向上的Pitch角,符号以电机正转方向为准,不一定照抄,接线完成后用串口看角度是否随前倾增大再决定取反。gyro_rate不滤波直接给内环,任何低通都会增加相位延迟,降低系统阻尼。alpha=0.98在5ms更新周期下时间常数约为0.245s,意思是大约四分之一秒后长期漂移被加速度计拉回,这个取值适合平衡车;如果车身剧烈晃动,要动态降低alpha,让加速度计权重更大,不过工程上少动这个值,优先把结构振动通过减震垫消除。

提示:互补滤波的输出滞后于真实角度,导致外环D项微分出来的角速度滞后更大。所以外环D用角度差分而不是陀螺仪,能避免和纯二阶差分串扰。

4. 串级PID在STM32定时器中断里的实现

4.1 控制频率分配

串级PID的性能上限由内环周期决定。我一般把内环跑到1kHz,外环200Hz:每1ms做一次角速度环负反馈,每5ms做一次角度校环。内环足够快,车体被外力推一下时角速度能在几个控制周期内回到目标,外环只负责把姿态角度对中,不需要处理高频扰动。STM32F103C8T6主频72MHz,1kHz内环加编码器读取占用CPU不到20%,剩余时间足够跑OLED刷新、串口日志和电压采集。

控制中断里用两个定时器:TIM4产生1ms中断驱动内环,TIM2每5次中断触发一次外环。不要把两个环都塞进同一个1kHz中断,否则外环频率过高,PWM变化只受内环限制,电机发热严重,且电池消耗更快。

4.2 串级PID结构与C语言实现

内外环输出的单位必须匹配。角度偏差单位是度,外环输出作为内环目标角速度,单位是°/s;内环把目标角速度和陀螺仪角速度的误差映射为PWM占空比。单独看每个环都是标准PID,但外环的输出不是直接控制量,而是一个让车体转动的期望角速度。这样的好处是:齿轮摩擦力矩、电机齿槽效应等干扰先被内环当作角速度扰动压掉,外环不需要重新学习。

typedef struct { float kp, ki, kd; float integral; float prev_err; float out_min, out_max; // 内环目标限幅或PWM限幅 } PID_t; float PID_Update(PID_t* pid, float err, float dt) { pid->integral += err * dt; // 积分限幅,防止外环长时间饱和 float i_out = pid->ki * pid->integral; if (i_out > pid->out_max) i_out = pid->out_max; if (i_out < pid->out_min) i_out = pid->out_min; float d_err = (err - pid->prev_err) / dt; pid->prev_err = err; float out = pid->kp * err + i_out + pid->kd * d_err; if (out > pid->out_max) out = pid->out_max; if (out < pid->out_min) out = pid->out_min; return out; } // 串级PID,内环周期1ms,外环周期5ms float angle_target = 0.0f; // 目标角度,可给遥控指令留接口 float target_gyro = 0.0f; PID_t pid_angle = {180, 0, 2, 0, 0, -100, 100}; PID_t pid_gyro = {300, 0, 5, 0, 0, -1000, 1000}; void Balance_Control_Loop(void) { IMU_Update(); // 更新pitch和gyro_rate // 外环,每5ms执行一次 static uint8_t cnt = 0; if (++cnt >= 5) { cnt = 0; float angle_err = angle_target - pitch; target_gyro = PID_Update(&pid_angle, angle_err, 0.005f); } // 内环,每1ms执行一次 float gyro_err = target_gyro - gyro_rate; float pwm = PID_Update(&pid_gyro, gyro_err, 0.001f); // PWM限幅和方向判断 if (pwm > 0) { __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pwm); SET_MOTOR_DIR_FORWARD(); } else { __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, -pwm); SET_MOTOR_DIR_BACKWARD(); } }

参数说明:pid_angle外环的P在100~300之间,D在1~2,I保持0;输出限制在-100~100,对应内环目标角速度最大100°/s。pid_gyro内环P在200~500,D在0~10,输出限幅是PWM全量程。限幅把输出和积分同时限制在一个范围内,这是最简单的防积分饱和写法。平衡控制不需要积分项,积分会让单一固定目标角度的系统慢慢振荡,只有在速度环或转向环里才考虑加I。

4.3 调参顺序与参数参考范围

串级PID最忌讳一上来外环加P和D然后瞎猜。正确顺序是先内环后外环。

  1. 只保留内环,target_gyro固定为0。把车拿在手里,手拨动车身,电机应该产生对抗转动的阻尼。逐步加大内环P,直到阻尼让车身快速停止晃动,但还没有开始自振。
  2. 加少量内环D,抑制高频抖动。这时手扶车身会有一种"粘稠"感,抽手时电机能主动拽住车体。
  3. 放外环,只让外环P工作,车体应当能自己站起来,但可能缓慢来回摆。依次增加外环P到回正有力,再慢慢加外环D增加阻尼。
  4. 如果车体总朝一个方向缓慢倾,说明angle_target=0与机械零点有系统偏差,去硬件上调整车体配重,不要用积分I硬扛。
参数参考范围调大后的现象调小后的现象
内环 P200~500高频颤振、电机嗡嗡作响恢复力矩不足,车身发软
内环 D0~10阻尼增强,过大会自激快速推车后仍抖不停
外环 P100~300回正有力,过大会点头能站住但晃动幅度大
外环 D1~5快速抽手能稳住抽手后余振明显

调试时PWM最大值一定要限幅在90%以下。电池满电压时,如果PWM跑到满幅,电机会过流,齿轮箱阻尼随温度变化,同一组参数在不同温度下表现完全两样。

5. 串级PID之外的三个稳定性工程细节

5.1 上电自检:先看零偏与角度是否稳定

我习惯在初始化后强停2秒,把陀螺零偏存进静态变量,同时在屏幕或串口打印当前角度和零偏值。如果初始化期间车身被碰过,后面整车会朝固定方向慢慢加速倾倒。这时该查的是静止判断逻辑,而不是调PID参数。加一个简单状态机:启动后车身必须处于低加速度波动状态2秒,才允许写入零偏和进入平衡模式。

5.2 给外环输出加斜坡限速

外环输出作为内环目标角速度,直接阶跃会带来抖动。把target_gyro的每次变化限制在一定步长内:

float gyro_target_slew(float target, float current) { float step = target - current; float limit = 0.25f; // 50°/s每5ms一个步长 if (step > limit) step = limit; if (step < -limit) step = -limit; return current + step; }

调用时机放在外环计算之后。斜坡限速不改变串级PID的动态性能,只是让PWM不会因为目标角速度突变而瞬间跳变,车身起步和停止时更柔顺,也不会触发内环微分超调。

5.3 电池电压补偿

同一套PID参数,满电状态稳如老狗,电压降到7.2V时开始抖,原因是电机转矩常数随电压下降,相同PWM对应的电机力矩变小。用ADC实时采电池电压,按满电压标称值做线性补偿:

float voltage_scale = 8.4f / battery_voltage; float pwm_comp = pwm * voltage_scale; if (pwm_comp > PWM_LIMIT) pwm_comp = PWM_LIMIT;

这样在电压下降到6.6V时PWM按比例放大,控制环的增益保持一致。只做比例补偿即可,电压在正常工作区间内线性度足够好。不想做补偿的,可以在低压报警后限制最大PWM,让电池避免工作在低压大电流区,延长循环寿命。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 6:03:54

COLMAP 安装指南:从零跑通全平台配置与 CUDA 加速

COLMAP 安装指南&#xff1a;从零跑通全平台配置与 CUDA 加速 【免费下载链接】colmap COLMAP - Structure-from-Motion and Multi-View Stereo 项目地址: https://gitcode.com/GitHub_Trending/co/colmap COLMAP&#xff08;Structure-from-Motion and Multi-View Ster…

作者头像 李华
网站建设 2026/9/13 6:03:07

InfiniSynapse Tool Market:AI协作平台的Office集成解决方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 6:01:32

国内AI知识库选型指南:四款主流工具实战对比

1. 这不是“替代”&#xff0c;而是本土知识管理范式的重新定义最近两周&#xff0c;我连续收到17位不同行业朋友的微信消息&#xff0c;开头几乎一模一样&#xff1a;“NotebookLM国内能用吗&#xff1f;有没有差不多的&#xff1f;”——语气里带着试探、期待&#xff0c;还有…

作者头像 李华
网站建设 2026/9/13 5:59:12

Midas Civil斜拉桥主梁预应力横梁计算全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 5:57:53

DeepSeek Harness本地部署全攻略:从Ollama配置到显卡驱动排错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华