MPU6050+串级PID的STM32两轮平衡车实现与调参指南
2026/9/16 14:48:15 网站建设 项目流程

简介:这份基于STM32F407ZGT6与MPU6050的二轮自平衡车控制资料,面向电子信息、自动化等专业学生及嵌入式爱好者,完整呈现了利用速度环与位置环串级PID实现小车平衡的算法工程。资源共188个文件,压缩包约5.33MB,以C语言源码(49个.c)和头文件(59个.h)为主,辅以Keil工程配置文件(.uvprojx、.uvoptx)、编译链接产物(.o、.axf、.hex)及调试映射文件(.map、.crf等),既可直接下载运行,也便于对照学习PID调参与姿态解算流程。内容覆盖MPU6050驱动、数据读取与滤波,以及串级PID控制器的搭建与整定,适合作为课程设计、期末大作业或毕业设计的参考资料。目前已有357人学习使用,对于想要快速上手平衡车项目的读者来说,是一份结构完整的实战素材。

1. 两轮平衡车的控制难点:为什么MPU6050+串级PID是标配

基于STM32F407ZGT6和MPU6050的两轮自平衡车,直接上电你会发现它根本站不住,要么往前冲,要么往后倒。原因很简单:两轮平衡车本质是倒立摆,重心高于轮轴,天然不稳定。要实现“站住”,控制器必须以毫秒级周期读取车身倾角,并输出反向力矩。传感器方面,MPU6050集成了三轴加速度计和陀螺仪,是这类项目最常见的选型。控制算法方面,只用单环PID往往会在启动或外力扰动时失控,因为速度反馈没有参与调节。实际工程普遍采用串级PID,外环速度环输出目标倾角,内环角度环控制倾角,两个环路配合才能兼顾直立与速度稳定。这篇笔记我基于STM32F407ZGT6,加上MPU6050和电机编码器,拆解整个实现过程,适合正在做课程设计或想搞懂串级PID的嵌入式开发者。

2. MPU6050姿态解算:寄存器配置与DMP读取流程

2.1 MPU6050的硬件接口与初始化

MPU6050通过I2C接口与STM32F407ZGT6通信,SDA和SCL分别接到PB7和PB6(I2C1)。我习惯用硬件I2C,也可以模拟,但硬件I2C中断处理更省CPU。初始化分四步:打开GPIO和I2C时钟,配置引脚为开漏复用,设置I2C时钟频率,最后写MPU6050的电源管理寄存器。

void MPU_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOB, &GPIO_InitStructure); I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_ClockSpeed = 400000; // 400kHz 快速模式 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); // 写寄存器0x6B 电源管理,退出休眠 MPU_WriteReg(0x6B, 0x00); // 设置陀螺仪量程为±2000°/s,寄存器0x1B MPU_WriteReg(0x1B, 0x18); // 设置加速度计量程为±16g,寄存器0x1C MPU_WriteReg(0x1C, 0x18); }

这段代码做了两件关键事:I2C总线初始化到400kHz,然后把MPU6050从休眠模式唤醒。0x6B写入0x00是必须的,否则后续读不到数据。0x1B和0x1C分别把陀螺仪和加速度计的量程调到最大,这样在平衡车剧烈倾斜时不会溢出。量程大了分辨率会下降,但对控制足够。如果希望噪声更小,可以把量程调低,比如陀螺仪±250°/s,此时敏感度是131 LSB/(°/s),对应到后续计算换算系数不同。

2.2 加速度计与陀螺仪原始数据的读取与校验

数据读取使用I2C连续读,MPU6050的加速度计输出寄存器起始地址是0x3B,陀螺仪是0x43。一次突发读取14字节,包含acc_x, acc_y, acc_z, temp, gyro_x, gyro_y, gyro_z。读取后需要符号扩展成short。

uint8_t buf[14]; MPU_ReadRegs(0x3B, buf, 14); int16_t acc_x = (buf[0] << 8) | buf[1]; int16_t acc_y = (buf[2] << 8) | buf[3]; int16_t acc_z = (buf[4] << 8) | buf[5]; int16_t gyro_x = (buf[8] << 8) | buf[9]; int16_t gyro_y = (buf[10] << 8) | buf[11]; int16_t gyro_z = (buf[12] << 8) | buf[13];

这里要注意字节序是大端,低地址在高位。buf[0]是高字节。有的例程把高字节和低字节搞反,导致角度跳动很大。读取后判断加速度矢量的模是否接近1g,如果偏差超过一定范围,说明传感器可能被摔坏或处于饱和状态,控制逻辑应当做保护。常见做法是每帧计算sqrt(acc_x^2 + acc_y^2 + acc_z^2),正常在16384左右(±2g量程下)。如果用DMP,原始数据的读取就不需要自己写了,inv_mpu.c内部会有读取函数,但初始化本身还是要做。

2.3 使用DMP进行姿态解算与欧拉角输出

inv_mpu.c是InvenSense官方DMP驱动库,能直接从MPU6050的DMP硬件单元读出四元数,再转换成欧拉角。比纯软件解算的优势在于DMP在硬件层面完成了姿态融合,且内置了温度补偿,CPU占用低。移植时把inv_mpu.c加进工程,注意它依赖I2C_WriteReaddelay_ms等底层接口,需要自己实现。不同版本的inv_mpu.c函数命名可能有区别,比如初始化函数可能是inv_mpu_initinv_mpu_initialize,以你拿到的源码为准。

初始化DMP的流程如下:

void MPU_DMP_Init(void) { MPU_Init(); // 基础I2C和传感器配置 inv_mpu_init(); // MPU内部状态机初始化 inv_mpu_set_gyro_fsr(2000); // 和寄存器配置一致 inv_mpu_set_accel_fsr(2); // DMP要求加速度计范围固定为±2g,这里必须写2 inv_mpu_set_compass_sample_rate(10); // 无磁力计,跳过也可 inv_mpu_init_dmp(); // 加载DMP固件 inv_enable_quaternion(); // 使能四元数输出 inv_enable_dmp(); // 启动DMP }

这里有个容易忽略的坑:DMP固件只支持±2g的加速度计量程,所以之前在MPU_Init里把量程调到±16g在用DMP前必须改回±2g。代码里写inv_mpu_set_accel_fsr(2)不是没用的。我遇到过因为量程不匹配导致DMP输出四元数全是0的情况。配置完成后,DMP会以内部默认100Hz产生中断,也可以在主循环里面通过轮询fifo数据获取结果。读取四元数再转换欧拉角的代码:

long q[4]; inv_get_quaternion(q); // 获取四元数 Q float q0 = q[0] / 16384.0f; float q1 = q[1] / 16384.0f; float q2 = q[2] / 16384.0f; float q3 = q[3] / 16384.0f; float pitch = asin(-2.0f * (q1*q3 - q0*q2)) * 57.29578f; float roll = atan2(2.0f * (q2*q3 + q0*q1), q0*q0 - q1*q1 - q2*q2 + q3*q3) * 57.29578f;

四元数到欧拉角的公式有十几种,上面这个对应Z-Y-X顺序。平衡车一般只需要pitch(俯仰角)或者roll(横滚角),取决于车子前进方向在哪个轴。如果传感器安装方向不同,可能需要交换坐标轴。例如把MPU6050的正X轴对准车头,那么平衡控制使用的是pitch角;对准车侧,就用roll角。我习惯在代码里根据安装方向做一个轴映射,而不是改公式。

2.4 姿态角的滤波与数据融合边界

DMP的优势在于出厂前已经调好滤波器系数,使用中不需要额外加低通滤波。但要注意DMP的数据频率是固定的,不能随意改。如果内环控制频率是5ms(200Hz),而DMP输出100Hz,那你需要在两个DMP数据之间做插值,或者干脆把控制频率降为100Hz。另一种选择是禁用DMP,直接用互补滤波,自己写Mahony算法。我的经验是:对于课程设计级别的平衡车,用DMP足够,省心;如果偏研究,可以自己实现互补滤波来对比效果。互补滤波的公式很直白:

angle = 0.98 * (angle + gyro_y * dt) + 0.02 * acc_angle;

这里0.98和0.02的系数决定了陀螺仪和加速度计的信任权重。权重越高,响应越快但漂移大;权重低则平滑但滞后。DMP相当于这一公式的硬件最优实现。

3. 串级PID的结构:速度环与位置环的整定方法

3.1 为什么两轮平衡车必须用串级PID

单环PID控制直立时,只对角度误差作比例微分控制,输出PWM直接驱动电机。你会发现车子能短暂站住,但一旦甚至人为推一下,车身会往一个方向加速倒下去,因为控制器没有感知车速。串级PID的外环以车速为反馈,输出一个目标倾角,内环再控制实际倾角跟踪这个目标。这样物理意义清晰:速度慢了,车身前倾加速;速度快了,车身向后倾斜减速。高速时车辆不会跑飞,低速时也不会来回震荡。内环的采样频率一般建议在100Hz-200Hz,外环在20Hz-50Hz,两个环路的时间间隔差别越大,系统越稳定,但响应变慢。

3.2 内环(位置环)的PD控制模型

内环控制对象是车身倾角,角度本身是位置的积分,所以叫位置环。它只需要PD两个环节,因为一旦角度偏差为零,车身直立,不需要持续输出。P项提供回复力,D项提供阻尼。在数字实现中,微分项直接用陀螺仪角速度代替,这样噪声更小,比做差分干净得多。

float angle_ctrl(float target_angle, float current_angle, float gyro_rate) { float err = target_angle - current_angle; float output = Kp_angle * err - Kd_angle * gyro_rate; // 限幅 if (output > 400) output = 400; if (output < -400) output = -400; return output; }

注意微分项的符号:陀螺仪角速度方向与角度误差方向相反时需要取负。具体取决于你定义的倾角正方向。Kp_angle的单位是PWM/度,Kd_angle的单位是PWM/(度/秒)。一般先设Kd为0,慢慢增大Kp直到车身出现低频抖动,再增大Kd消除抖动。这里限幅400,是因为PWM定时器满幅值是1000,内环输出作为速度环的补偿量,不能占满整个PWM范围,否则电机饱和后调节失效。我见过的课程设计里限幅往往设得过大,导致车子一振荡就满速。

3.3 外环(速度环)的PI控制与防积分饱和

速度环输入是目标速度与当前速度的偏差,输出是目标倾角。平衡车的目标速度通常为零,即原地站住。速度反馈来自左右电机编码器的平均值,或者两个轮子速度的平均。速度环必须带积分项,否则因为车身为了抵消各种阻力会一直有一个残余倾角,导致车速缓慢漂移。PI控制器的公式:

float speed_ctrl(float target_speed, float current_speed) { float err = target_speed - current_speed; speed_error_sum += err * dt_speed; float output = Kp_speed * err + Ki_speed * speed_error_sum; if (output > max_angle) output = max_angle; // 目标倾角限幅 if (output < -max_angle) output = -max_angle; return output; }

速度环输出是目标倾角,限幅要很严格,通常最大5度~10度。如果限幅过大,车身倾斜太猛,容易倒。积分项要加windup保护,常见做法是当输出截断时清零或钳位误差积分。另一种用的是增量式PID:只输出倾角的增量,让目标倾角慢慢变化。增量式的好处是即使积分环节出问题,输出不会突变。平衡车这个场景,速度环相当于“缓慢移动的目标角度发生器”,增量式更适合。在后面第5章我会专门讲增量式怎么调。

3.4 串级整定顺序与经验参数

整定顺序一定是先内环后外环。因为内环是快速环,外环是慢速环。内环不先稳定,外环一闭合就会发散。先单独调角度环,用手扶住车身,给一个目标角度比如0度,观察是否能够抵抗手推动作。调好角度环后,再闭合速度环。下面是常见参数区间,实际需要根据机械重心位置作调整:

环路比例系数积分/微分系数采样周期
角度环P100-50005ms
角度环D20-8005ms
速度环P10-50020ms
速度环I0.5-5020ms

这个表格里角度环P的单位是“PWM值/度”,D的单位是“PWM值/(度/秒)”。如果使用增量式PID,参数含义不同,但整定顺序一样。一个实用的方法:先用串口把内环输出值打印出来,当车身静止时,输出应该在0附近波动;如果输出偏置很大,说明角度计算有零点误差,需要在代码中减去静止时的偏移量。

4. 基于STM32F407ZGT6的实现:从传感器数据到PWM输出

4.1 工程文件结构与关键模块

拿到完整的工程源码,先看文件结构。Template.uvguix.Administrator是Keil MDK的工程文件,双击打开即可。Template.axf是编译好的镜像,没太大用处。keilkilll.bat是清理脚本,用来删除编译中间文件。stm32f4xx_rcc.cstm32f4xx_adc.cstm32f4xx_can.c等属于STM32F4标准外设库,注意这些文件不能随意更换版本,ADC、RTC、CAN模块虽然不一定都用,但在标准库中它们有互相依赖关系。比如stm32f4xx_rcc.c是时钟配置基础,所有外设都要调用。inv_mpu.c是MPU6050的DMP驱动库,它依赖I2C和GPIO驱动。

核心调用关系是这样的:main.c初始化时钟、定时器、I2C,然后进入主循环;定时器中断每隔5ms调用一次Angle_Control,每隔20ms调用一次Speed_Controlinv_mpu.c在后台通过DMP中断或者查询方式更新姿态角。电机控制用TIM1输出两路PWM,方向用GPIO控制。如果你的工程里没有motor.c文件,可能在main.c里面直接操作寄存器了,因为ST标准外设库对高级定时器的代码比较繁琐。

4.2 控制定时器中断配置

控制周期必须精确,我用TIM6作为基本定时器,产生100Hz的中断用于速度环。TIM7产生200Hz中断用于角度环。注意两个中断的优先级要设计好,角度环优先级高于速度环,因为内环是快速保护环。

void TIM6_Config(u16 period) { TIM_TimeBaseInitTypeDef TIM_Init; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); TIM_Init.TIM_Prescaler = 8400 - 1; // 84MHz/8400 = 10kHz TIM_Init.TIM_Period = period - 1; // 10kHz/(period) 为中断频率 TIM_Init.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM6, &TIM_Init); NVIC_InitTypeDef NVIC_Init; NVIC_Init.NVIC_IRQChannel = TIM6_DAC_IRQn; NVIC_Init.NVIC_IRQChannelPreemptionPriority = 1; NVIC_Init.NVIC_IRQChannelSubPriority = 1; NVIC_Init.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_Init); TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE); TIM_Cmd(TIM6, ENABLE); }

上述代码中,预分频8400-1是因为APB1定时器时钟为84MHz,分频后得到10kHz的计数频率。如果要200Hz中断,period就设为50;100Hz就是100。这里有个新手容易踩的坑:STM32F4的标准外设库中,APB1预分频系数如果不是1,定时器时钟实际是APB1的2倍。如果你的工程改了系统时钟,必须重新确认定时器输入时钟。我建议在while(1)里翻转一个GPIO,用示波器量中断周期,确认无误再开始调PID。

4.3 角度环与速度环的串联执行逻辑

在定时器中断中,角度环和速度环一起执行。速度环输出限幅后的角度目标,作为角度环的输入。两个环之间用一个全局变量angle_target传递。

volatile float speed_output = 0; volatile float angle_measured = 0; volatile float gyro_y = 0; // TIM7中断 5ms 角度环 void TIM7_IRQHandler(void) { if (TIM_GetITStatus(TIM7, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM7, TIM_IT_Update); angle_measured = get_dmp_pitch(); gyro_y = get_dmp_gyro(); float pwm_angle = angle_ctrl(speed_output, angle_measured, gyro_y); motor_set(pwm_angle); } } // TIM6中断 20ms 速度环 void TIM6_DAC_IRQHandler(void) { if (TIM_GetITStatus(TIM6, TIM_IT_Update)) { TIM_ClearITPendingBit(TIM6, TIM_IT_Update); float speed = (motor_left_speed + motor_right_speed) / 2.0f; speed_output = speed_ctrl(0.0f, speed); // 目标速度为0 } }

两个中断都执行完之后,速度环更新speed_output,它不是目标倾角的稳态值,而是目标倾角的瞬时值。在调和周期不同的情况下,这种交替执行会导致速度环在两次更新之间保持旧值,对控制效果影响很小。需要注意speed_output的值域要约束在±5度之间,而angle_ctrl内部还有对PWM的限幅,这样双重限幅能避免异常情况。

4.4 PWM输出与电机方向控制

PWM输出使用TIM1的CCR1和CCR2通道,配置成PWM1模式,输出极性为高。电机驱动板一般为H桥,需要两个PWM或者一个PWM加一个方向引脚。这里用方向引脚加PWM方式:

void motor_set(int pwm) { if (pwm > 0) { GPIO_SetBits(GPIOE, GPIO_Pin_4); // 方向正转 GPIO_ResetBits(GPIOE, GPIO_Pin_5); TIM_SetCompare1(TIM1, pwm); } else { GPIO_ResetBits(GPIOE, GPIO_Pin_4); GPIO_SetBits(GPIOE, GPIO_Pin_5); TIM_SetCompare1(TIM1, -pwm); } }

这里pwm是带符号数,正负代表方向。注意死区保护:在切换方向的瞬间,如果上下桥臂同时导通,会烧驱动芯片。有的H桥内部有死区,有的没有,软件上加一个小延时,比如切换方向前将PWM设为0,延时1ms。我这个例程里实际上没有加延时,因为用的驱动板自带输出使能引脚控制。如果你自己搭H桥,加一句delay_ms(1)成本很低。

5. 调参与验证:增量式速度环的调试技巧

5.1 增量式速度环的工程实现

外环速度环如果使用位置式PI,在反复扶正车身的过程中容易积分饱和,表现为松开手后车子突然冲出去。增量式PID只计算目标倾角的增量,天然没有绝对误差积累,更适合平衡车。核心实现如下:

float speed_loop_incremental(float target_speed, float current_speed) { float err = target_speed - current_speed; static float last_err = 0; float delta = Kp_speed * (err - last_err) + Ki_speed * err; last_err = err; static float target_angle = 0; target_angle += delta; if (target_angle > 8.0f) target_angle = 8.0f; if (target_angle < -8.0f) target_angle = -8.0f; return target_angle; }

这里的Kp_speed是比例增量系数,Ki_speed相当于积分增量系数,数值都比位置式小一个数量级。我一般从Kp_speed=1.0fKi_speed=0.01f起步,每次增加20%,观察车身是否来回“找速度”。

5.2 用串口波形量化调参

调参不能靠肉眼感觉,至少要能看到角度、速度、PWM三个变量。我在代码里放了一个调试printf:

printf("ang:%.2f spd:%.2f pwm:%d\r\n", angle_measured, current_speed, motor_pwm);

波特率用921600,每10ms输出一次。用串口绘图工具打开后,很容易发现:角度环未收敛时波形是一根等幅正弦波;速度环参数过大时,速度波形会持续积分变成三角形波。典型的正确波形是:静止时角度在±0.5度内白噪声状波动,速度在±0.05m/s内波动,PWM在一个小范围内随机抖动。

5.3 验证平衡车性能的土办法

调好之后,把车放在瓷砖地面上,双手松开,记录它不倒下且不跑出直径0.5米圆圈的持续时间。超过20秒说明串级PID基本合格。接着用手轻推一下车壳,观察它应当在一两步内恢复直立,过程不能有两秒以上的低频摇摆。如果要测速度环的动态响应,可以把目标速度设置为0.3m/s,用手机慢动作拍摄,看车子从倾斜到匀速前进是否有明显超调。最后,把MPU6050的采样频率提高一倍,同样的参数如果车身出现抖动,说明控制频率已经接近传感器极限,这时优先考虑减小角度环的Kd而不是提高采样率。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询