1. 项目概述
两轮自平衡小车是一个经典的嵌入式控制项目,它完美融合了传感器技术、电机控制和PID算法。作为一名嵌入式开发者,我花了三个月时间从零开始打造这台基于STM32F103C8T6的小车,期间踩过无数坑,也积累了不少实战经验。现在就把这个项目的完整实现过程分享给大家,特别是硬件选型、PID调参这些关键环节的实操细节。
平衡车的核心原理其实很简单:通过MPU6050传感器检测车身倾斜角度,用PID算法计算出电机控制量,驱动两个直流电机转动来维持平衡。但真正做起来,从硬件组装到软件调试处处都是坑。比如我第一次试车时,小车不仅没站稳,反而加速倒地——后来发现是电机极性接反导致了正反馈。这种实战中的经验教训,我都会在文中详细说明。
2. 硬件系统设计
2.1 核心控制器选型
作为项目的"大脑",主控芯片需要满足几个关键需求:
- 足够的GPIO接口(连接传感器、电机驱动等外设)
- 硬件PWM输出(用于电机调速)
- 编码器接口(测量电机转速)
- I2C通信(连接MPU6050传感器)
STM32F103C8T6(俗称"蓝莓派")完美契合这些需求:
- 72MHz主频,性能足够运行复杂的PID算法
- 3个定时器(TIM1高级定时器用于PWM,TIM2/TIM4用于编码器)
- 1个I2C接口(连接MPU6050)
- 价格仅10元左右,性价比极高
我选择的是现成的最小系统板,自带:
- 5V/3.3V稳压电路
- SWD下载接口
- 复位电路
- 所有GPIO引脚引出
这样省去了自己设计电源和下载电路的麻烦,特别适合新手。实际使用中发现,这种核心板的AMS1117稳压芯片最大输出电流只有800mA,当同时驱动多个外设时可能出现电压不稳。解决方法是在电源入口处并联一个大电容(我用了470μF)。
2.2 姿态传感器模块
MPU6050是平衡车的"内耳",负责感知车身倾斜状态。选型时我对比了几种方案:
| 传感器型号 | 优点 | 缺点 | 适用性 |
|---|---|---|---|
| MPU6050 | 6轴(3轴加速度+3轴陀螺仪),集成DMP,价格15元 | I2C通信速率受限 | 首选 |
| BMI160 | 低功耗,高精度 | 价格高(约50元),需额外磁力计 | 高端项目 |
| ICM20602 | 高刷新率(最高8kHz) | 需要SPI接口,电路复杂 | 专业应用 |
最终选择MPU6050模块,注意要选带电平转换芯片的版本(支持3.3V/5V双电压)。模块上有几个关键引脚:
- VCC:接3.3V(更稳定)
- SCL/SDA:I2C通信线,接PB6/PB7
- INT:中断输出,接PB5
- AD0:地址选择,接地固定为0x68
安装时要特别注意:
- 模块必须固定在车体中心位置
- 保持水平安装(可用热熔胶固定后微调)
- 远离电机等干扰源
我最初把MPU6050装在电机旁边,结果数据漂移严重。后来用双面胶+热熔胶固定在中轴线上,数据立即稳定了。
2.3 电机与驱动系统
电机选型需要考虑扭矩和转速的平衡:
- 扭矩太小:带不动车身
- 转速太高:控制响应跟不上
经过实测,MG513P-12V直流减速电机表现最佳:
- 额定电压:12V
- 空载转速:200RPM
- 减速比:1:30
- 扭矩:5kg·cm
配套的L298N驱动板要注意选择双通道版本,关键参数:
- 驱动电压:5-35V
- 逻辑电压:5V
- 单通道峰值电流:2A
- 内置5V稳压输出
接线时有几个易错点:
- 电机线序:OUT1/OUT2接左电机,OUT3/OUT4接右电机
- 使能信号:ENA/ENB必须接PWM引脚
- 逻辑供电:5V输出可以给STM32供电,但要注意总电流
重要提示:L298N工作时发热严重,必须加装散热片!我曾因忘记装散热片导致驱动芯片烧毁。
2.4 编码器选型
增量式光电编码器是速度闭环的关键,选型要点:
- 分辨率:每转脉冲数(PPR)
- 输出类型:正交AB相
- 安装方式:轴套式或法兰式
我选用的是E6B2-CWZ6C型编码器,主要参数:
- 分辨率:60PPR
- 工作电压:5-24V
- 输出方式:NPN开路集电极
编码器与STM32的连接方式:
c复制// 左轮编码器
PA0 -- A相
PA1 -- B相
// 右轮编码器
PB6 -- A相
PB7 -- B相
实际调试中发现,编码器信号线过长会导致计数错误。解决方法:
- 信号线长度控制在15cm以内
- 在GPIO口加10K上拉电阻
- 在代码中配置输入滤波
2.5 电源系统设计
电源是很多新手容易忽视的部分。平衡车需要:
- 12V给电机驱动
- 5V给编码器
- 3.3V给STM32和MPU6050
我的电源方案:
- 2节18650锂电池串联(7.4V)
- 使用LM2596降压模块得到5V
- AMS1117-3.3得到3.3V
实测电流需求:
- 空载:约200mA
- 平衡状态:约500mA
- 加速/转向:峰值1.2A
因此电池容量建议选择2000mAh以上,我用的两节3400mAh电池可连续工作4小时。
3. 软件系统实现
3.1 系统架构设计
软件采用分层架构:
code复制应用层(main.c)
│
▼
控制层(control.c)←→蓝牙调试
│
▼
驱动层(motor.c/pwm.c/mpu6050.c/encoder.c)
关键设计思想:
- 硬件驱动层封装底层操作
- 控制层实现核心算法
- 应用层处理用户交互
3.2 MPU6050驱动实现
MPU6050初始化流程:
c复制void MPU_Init(void)
{
I2C_Init(); // 初始化I2C接口
MPU_Write_Byte(PWR_MGMT_1, 0x80); // 复位设备
delay_ms(100);
MPU_Write_Byte(PWR_MGMT_1, 0x00); // 唤醒
MPU_Set_Gyro_Fsr(3); // 陀螺仪量程±2000dps
MPU_Set_Accel_Fsr(0); // 加速度计量程±2g
MPU_Set_Rate(100); // 采样率100Hz
MPU_Write_Byte(INT_EN_REG, 0x00); // 关闭中断
MPU_Write_Byte(USER_CTRL_REG, 0x00);
MPU_Write_Byte(FIFO_EN_REG, 0x00); // 关闭FIFO
MPU_Write_Byte(INTBP_CFG_REG, 0x80); // INT低电平有效
}
使用DMP库获取姿态角的要点:
- 先加载DMP固件
- 设置传感器融合参数
- 启用DMP功能
c复制u8 mpu_dmp_init(void)
{
if(mpu_init()==0)
{
dmp_load_motion_driver_firmware(); // 加载固件
dmp_set_orientation(gyro_orientation); // 设置方向
dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT | // 启用6轴低功耗四元数
DMP_FEATURE_SEND_RAW_ACCEL |
DMP_FEATURE_GYRO_CAL);
dmp_set_fifo_rate(100); // 设置FIFO速率
mpu_set_dmp_state(1); // 启用DMP
}
return 0;
}
3.3 电机驱动实现
PWM初始化关键参数:
c复制void PWM_Init_TIM1(u16 Psc, u16 Per)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 时基设置:72MHz/(7199+1) = 10kHz
TIM_TimeBaseStructure.TIM_Period = 7199;
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
// PWM模式设置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比0
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC3Init(TIM1, &TIM_OCInitStructure);
TIM_OC4Init(TIM1, &TIM_OCInitStructure);
TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_OC4PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须启用
TIM_Cmd(TIM1, ENABLE);
}
电机控制函数实现正反转控制:
c复制void Load(int moto1, int moto2)
{
int pwm1 = moto1 * PWM_SCALE;
int pwm2 = moto2 * PWM_SCALE;
// 电机A控制
if(moto1 > 0) { // 正转
TIM_SetCompare3(TIM1, 0);
TIM_SetCompare4(TIM1, -pwm1);
} else if(moto1 < 0) { // 反转
TIM_SetCompare3(TIM1, pwm1);
TIM_SetCompare4(TIM1, 0);
} else { // 停止
TIM_SetCompare3(TIM1, 0);
TIM_SetCompare4(TIM1, 0);
}
// 电机B控制(类似逻辑)
...
}
3.4 编码器测速实现
编码器接口配置:
c复制void Encoder_TIM2_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// GPIO初始化
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 定时器基础设置
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_Period = 65535;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 编码器接口模式
TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12,
TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
// 输入捕获滤波
TIM_ICInitStructure.TIM_ICFilter = 10;
TIM_ICInit(TIM2, &TIM_ICInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
速度计算函数:
c复制int Read_Speed(int TIMx)
{
int value;
switch(TIMx) {
case 2: // 左轮
value = (short)TIM_GetCounter(TIM2);
TIM_SetCounter(TIM2, 0);
break;
case 4: // 右轮
value = (short)TIM_GetCounter(TIM4);
TIM_SetCounter(TIM4, 0);
break;
}
return -value * ENCODER_SCALE_FACTOR; // 缩放和方向调整
}
4. 控制算法实现
4.1 三环PID控制架构
平衡车采用三级PID控制:
- 直立环(内环):保持车身垂直
- 速度环(中环):控制小车移动
- 转向环(外环):控制转向
code复制转向环
│
▼
速度环
│
▼
直立环
│
▼
电机
4.2 直立环实现
直立环是PD控制:
c复制int Vertical(float Med, float Angle, float gyro_Y)
{
return Vertical_Kp*(Angle-Med) + Vertical_Kd*gyro_Y;
}
参数整定经验:
- 先设Kd=0,逐渐增大Kp直到小车能勉强站立但大幅摆动
- 然后增加Kd抑制摆动,直到小车能稳定站立
- 典型值范围:
- Kp:40-60
- Kd:5-10
4.3 速度环实现
速度环是PI控制:
c复制int Velocity(int Target, int encoder_left, int encoder_right)
{
static int Encoder_Integral;
int Encoder_Err = (encoder_left + encoder_right) - Target;
// 低通滤波
Encoder_Err = 0.7*Encoder_Err + 0.3*Encoder_Err_Last;
Encoder_Err_Last = Encoder_Err;
// 积分限幅
Encoder_Integral += Encoder_Err;
Encoder_Integral = LIMIT(Encoder_Integral, -20000, 20000);
return Velocity_Kp*Encoder_Err + Velocity_Ki*Encoder_Integral;
}
参数整定技巧:
- 先设Ki=0,调Kp使小车被推后能缓慢回位
- 再调Ki消除稳态误差
- 典型值:
- Kp:0.05-0.2
- Ki:0.00001-0.0001
4.4 转向环实现
转向环是PD控制:
c复制int Turn(int gyro_Z, int RC)
{
return Turn_Kp*RC + Turn_Kd*gyro_Z;
}
参数建议:
- Kp:3-8
- Kd:0.1-0.5
4.5 中断服务函数
控制算法在MPU6050的中断中执行:
c复制void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line5) != RESET)
{
// 1. 读取传感器数据
Encoder_Left = Read_Speed(2);
Encoder_Right = Read_Speed(4);
mpu_dmp_get_data(&Pitch, &Roll, &Yaw);
MPU_Get_Gyroscope(&gyrox, &gyroy, &gyroz);
// 2. 三环PID计算
Velocity_out = Velocity(Target_Speed, Encoder_Left, Encoder_Right);
Vertical_out = Vertical(Velocity_out + Med_Angle, Pitch, gyroy);
Turn_out = Turn(gyroz, Turn_Speed);
// 3. 电机输出
MOTO1 = LIMIT(Vertical_out + Turn_out, -100, 100);
MOTO2 = LIMIT(Vertical_out - Turn_out, -100, 100);
Load(MOTO1, MOTO2);
EXTI_ClearITPendingBit(EXTI_Line5);
}
}
5. 调试与优化
5.1 调试步骤
-
硬件检查阶段:
- 确认所有电源电压正常
- 检查电机转向是否正确
- 验证编码器计数方向
-
软件调试阶段:
- 先调直立环(Kp, Kd)
- 再加入速度环(Kp, Ki)
- 最后加入转向环
-
参数整定技巧:
- 每次只调整一个参数
- 调整幅度控制在±20%
- 记录每次调整的效果
5.2 常见问题解决
-
小车加速倒地:
- 检查电机极性是否接反
- 确认角度方向定义是否正确
-
高频抖动:
- 降低Kd值
- 检查MPU6050安装是否牢固
-
向一侧偏移:
- 调整机械重心
- 检查编码器计数是否对称
-
响应迟钝:
- 增加Kp值
- 检查电池电压是否充足
5.3 性能优化
-
采样率优化:
- MPU6050采样率设置为100-200Hz
- 控制周期5-10ms
-
滤波处理:
- 对编码器速度进行低通滤波
- 对陀螺仪数据进行滑动平均
-
功耗优化:
- 不使用时关闭OLED显示
- 降低MPU6050采样率
6. 项目扩展
完成基础平衡功能后,可以进一步扩展:
-
蓝牙遥控:
- 通过HC-06模块实现手机控制
- 添加前进/后退/转向指令
-
路径跟踪:
- 增加红外或超声波传感器
- 实现自动避障功能
-
状态显示:
- OLED显示角度、速度等信息
- LED指示灯显示工作状态
-
上位机调试:
- 通过串口发送数据到PC
- 使用Python绘制实时曲线
这个项目让我深刻体会到理论与实践的结合有多重要。最初我以为只要把PID公式实现出来就能成功,结果光是让小车站稳就调了一周参数。最难忘的是当小车第一次自主保持平衡的那一刻——所有的调试痛苦都值了。建议每个想做平衡车的朋友都要有耐心,从硬件组装到软件调试,每一步都可能遇到意想不到的问题,但这正是嵌入式开发的乐趣所在。