☰
STM32平衡小车开发全攻略:从PID算法到电机控制的嵌入式实践
2026/10/15 10:29:50 网站建设 项目流程

最近在整理项目资料时,发现很多同学对STM32平衡小车这个经典项目既向往又畏惧。向往的是它融合了传感器、电机控制、PID算法等多个核心知识点,是检验嵌入式学习成果的绝佳实践;畏惧的是网上教程要么过于零散,要么直接甩出代码让人无从下手。本文将彻底解决这个问题,从零开始,手把手带你完成平衡小车软件部分的全部开发,即使你是刚接触STM32的“草履虫”级新手,也能跟着一步步搭建起来。

本文不仅会提供完整的、可直接复用的代码,更会深入讲解每一行代码背后的设计思路和原理。你将掌握MPU6050姿态解算、直流电机PWM驱动、PID控制器设计与参数整定等核心技能,最终得到一个能够自主站立、抵抗干扰的平衡小车。无论是用于课程设计、毕业设计,还是作为个人技术能力的证明,这套方案都极具价值。

1. 项目核心概念与软件架构

在动手写代码之前,我们必须先理解平衡小车是如何“思考”和“行动”的。它的核心目标只有一个:像不倒翁一样,在受到外力倾斜时,能迅速驱动轮子朝倾斜方向运动,从而产生一个反向力矩将自己“扶正”。

1.1 核心控制原理:倒立摆模型

平衡小车在控制理论上可以抽象为一个“倒立摆”模型。摆杆(车身)是倒立的,其支点(车轮与地面的接触点)可以水平移动。控制的目标就是通过移动支点(即驱动车轮),使倒立的摆杆(车身)保持竖直平衡。

软件系统的任务就是持续测量车身的倾斜角度(Angle)和倾斜角速度(Gyro),然后通过一个控制算法(我们使用PID)计算出需要给电机施加多大的速度(PWM值),从而产生合适的力矩来纠正倾斜。

1.2 软件系统架构设计

一个稳定可靠的平衡小车软件系统通常包含以下层次清晰的模块:

  1. 硬件驱动层:负责与最底层硬件打交道。

    • MPU6050驱动:通过I2C总线读取原始加速度计和陀螺仪数据。
    • 电机驱动:生成PWM信号控制电机速度和方向(通常使用定时器的PWM输出功能)。
    • 编码器读取:通过定时器的编码器接口模式,读取电机转速反馈(用于速度控制)。
  2. 数据处理层:对原始数据进行加工,得到可用的状态信息。

    • 姿态解算:融合MPU6050的加速度和陀螺仪数据,通过互补滤波或卡尔曼滤波算法,计算出准确、稳定的车身俯仰角(Angle)和角速度(Gyro)。
    • 速度计算:根据编码器脉冲数和时间,计算出小车的实际移动速度。
  3. 控制算法层:系统的大脑,根据当前状态计算控制量。

    • 直立环PD控制:以角度Angle为控制目标,快速响应车身倾斜,维持平衡。这是平衡的基础。
    • 速度环PI控制:以小车速度为控制目标,防止小车在平衡过程中因为持续朝一个方向加速而跑飞。它通过微调小车的平衡角度设定值来实现。
    • 转向环控制:处理遥控器的转向指令,让两个轮子产生速度差来实现转弯。
  4. 应用层:协调各个模块,处理用户输入。

    • 主循环:以固定频率(如1kHz)运行,周期性执行数据采集、滤波、控制计算和输出。
    • 遥控接收:解析蓝牙、红外或无线模块的指令,改变小车的运行模式(如启动、停止、转向)。

数据流向:传感器数据->姿态解算->直立环PD+速度环PI+转向环->综合控制量->电机PWM输出。

理解了这个架构,我们写代码时就能做到心中有数,知道每个函数、每个变量在整个系统中扮演什么角色。

2. 开发环境与工程准备

工欲善其事,必先利其器。一个清晰的工程结构能极大提升开发效率。

2.1 硬件与软件环境清单

  • 主控芯片:STM32F103C8T6(核心板,性价比高,资源足够)。其他F1系列或F4系列也类似。
  • 姿态传感器:MPU6050(六轴,含三轴加速度计+三轴陀螺仪)。
  • 电机驱动:TB6612FNG或DRV8833双路电机驱动模块。
  • 电机与编码器:带减速箱的直流电机,配有AB相增量式编码器。
  • 电源:7.4V或11.1V锂电池,需搭配降压模块为单片机提供5V或3.3V。
  • 开发环境:Keil MDK-ARM 或 STM32CubeIDE。
  • 固件库:标准外设库(Standard Peripheral Library)或HAL库均可。本文示例基于标准库,因其更贴近底层,便于理解原理。使用HAL库的同学可以对照移植。
  • 调试工具:ST-Link下载器/调试器,串口助手(用于打印调试信息)。

2.2 创建工程与文件结构

在Keil中创建一个新工程,选择你的芯片型号(STM32F103C8)。然后,在工程目录下建立如下清晰的文件夹结构:

Balance_Car_Project/ ├── USER/ │ ├── main.c │ ├── stm32f10x_it.c │ └── system_stm32f10x.c ├── HARDWARE/ │ ├── mpu6050/ │ │ ├── mpu6050.c │ │ └── mpu6050.h │ ├── motor/ │ │ ├── motor.c │ │ └── motor.h │ ├── encoder/ │ │ ├── encoder.c │ │ └── encoder.h │ └── led/ (或其他指示灯) ├── SYSTEM/ │ ├── sys.c │ ├── usart.c (串口调试) │ └── delay.c (精确延时) ├── ALGORITHM/ │ ├── filter.c (滤波算法) │ ├── pid.c (PID算法) │ └── imu.c (姿态解算) ├── CORE/ └── FWLIB/ (存放标准外设库的src和inc)

在Keil的工程管理窗口中,也建立对应的分组(Groups),并将.c文件添加到相应分组中,同时设置好头文件包含路径(Include Paths)。这一步是基础,确保编译不报错。

3. 核心模块驱动与原理实现

接下来,我们逐一攻克各个核心模块。每个模块我们都遵循“原理说明 -> 硬件连接 -> 代码实现 -> 调试验证”的步骤。

3.1 MPU6050驱动与姿态解算

这是平衡小车的“眼睛”,负责感知自身的倾斜状态。

硬件连接(I2C1):

  • MPU6050.VCC->3.3V
  • MPU6050.GND->GND
  • MPU6050.SCL->PB6
  • MPU6050.SDA->PB7
  • MPU6050.AD0->GND(I2C地址为0x68)

驱动代码 (mpu6050.c/.h): 首先实现I2C的读写函数,然后初始化MPU6050(设置量程、采样率、使能等)。

// mpu6050.h 部分定义 #ifndef __MPU6050_H #define __MPU6050_H #include “sys.h” #define MPU6050_ADDR 0xD0 // 写地址,0x68左移一位 // 加速度计和陀螺仪原始数据结构体 typedef struct { short accel_x; short accel_y; short accel_z; short temp; short gyro_x; short gyro_y; short gyro_z; } MPU6050_RAW_DATA; void MPU6050_Init(void); u8 MPU6050_Read_Byte(u8 reg); void MPU6050_Write_Byte(u8 reg, u8 data); void MPU6050_Read_All(MPU6050_RAW_DATA *raw); #endif
// mpu6050.c 初始化函数示例 void MPU6050_Init(void) { I2C_Configuration(); // 先配置好I2C硬件 MPU6050_Write_Byte(MPU6050_RA_PWR_MGMT_1, 0x80); // 复位设备 delay_ms(100); MPU6050_Write_Byte(MPU6050_RA_PWR_MGMT_1, 0x00); // 唤醒,使用内部8M晶振 MPU6050_Write_Byte(MPU6050_RA_SMPLRT_DIV, 0x07); // 采样率分频 MPU6050_Write_Byte(MPU6050_RA_CONFIG, 0x06); // 低通滤波器设置 // 设置加速度计量程 ±2g MPU6050_Write_Byte(MPU6050_RA_ACCEL_CONFIG, 0x00); // 设置陀螺仪量程 ±250°/s MPU6050_Write_Byte(MPU6050_RA_GYRO_CONFIG, 0x00); }

姿态解算 - 互补滤波: 原始数据不能直接用,加速度计测得的Angle(通过atan2(accY, accZ)计算)长期稳定但动态响应慢、易受震动干扰;陀螺仪测得的Gyro动态响应快但存在漂移(积分后角度会发散)。互补滤波就是取两者之长。

// imu.c float angle, angle_dot; // 最终角度(度)和角速度(度/秒) float Q_angle = 0.001; // 加速度计置信度 float Q_gyro = 0.003; // 陀螺仪置信度 float dt = 0.005; // 采样周期,对应200Hz解算频率 float K1 = 0.05; // 一阶互补滤波系数 void IMU_Update(MPU6050_RAW_DATA *raw) { // 1. 转换原始数据为物理量 float accel_x = raw->accel_x / 16384.0; // 假设量程为±2g, 灵敏度16384 LSB/g float accel_z = raw->accel_z / 16384.0; float gyro_y = raw->gyro_y / 131.0; // 假设量程为±250°/s, 灵敏度131 LSB/(°/s) // 2. 由加速度计计算倾斜角(弧度) float acc_angle = atan2(accel_x, accel_z) * 57.29578f; // 弧度转度 // 3. 一阶互补滤波 angle = K1 * acc_angle + (1 - K1) * (angle + gyro_y * dt); // 4. 更新角速度(可直接用陀螺仪数据,或做简单滤波) angle_dot = gyro_y; // 或 angle_dot = (angle - angle_last) / dt; }

调试:将angle通过串口打印出来,用手缓慢转动模块,观察角度值是否在-90°到+90°之间平滑变化。这是后续所有控制的基础,务必先调准。

3.2 电机驱动与PWM生成

这是平衡小车的“手脚”,负责执行控制命令。

硬件连接(以TB6612和TIM3的CH1, CH2为例):

  • PWMA (AIN1)->PA6 (TIM3_CH1)
  • PWMB (BIN1)->PA7 (TIM3_CH2)
  • STBY-> 接高电平
  • AO1, AO2接电机A的两线
  • BO1, BO2接电机B的两线

代码实现 (motor.c/.h): 我们需要配置定时器为PWM输出模式,并编写函数来控制电机的速度和方向。

// motor.h #define MOTOR_A 0 #define MOTOR_B 1 #define DIR_FORWARD 0 #define DIR_BACKWARD 1 void Motor_Init(void); void Motor_Set_Pwm(int motor, int pwm);
// motor.c // PWM周期设置为20kHz左右,避免电机啸叫 void Motor_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 2. 配置GPIO为复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 3. 配置定时器基础:72M / (719+1) / (199+1) = 500Hz? 这里应为20kHz,注意计算 // 目标20kHz: 72M / (71+1) / (49+1) = 20kHz TIM_TimeBaseStructure.TIM_Period = 49; // 自动重装载值 ARR TIM_TimeBaseStructure.TIM_Prescaler = 71; // 预分频器 PSC TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // 4. 配置PWM模式 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比为0 TIM_OC1Init(TIM3, &TIM_OCInitStructure); // CH1 TIM_OC2Init(TIM3, &TIM_OCInitStructure); // CH2 TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); // 5. 启动定时器 TIM_Cmd(TIM3, ENABLE); } // 设置电机PWM,范围-1000 ~ +1000,内部会处理方向 void Motor_Set_Pwm(int motor, int pwm) { int pwm_abs = 0; u8 dir = DIR_FORWARD; // 限幅 if(pwm > 1000) pwm = 1000; if(pwm < -1000) pwm = -1000; // 判断方向并取绝对值 if(pwm >= 0) { dir = DIR_FORWARD; pwm_abs = pwm; } else { dir = DIR_BACKWARD; pwm_abs = -pwm; } // 根据方向设置GPIO(AIN2/BIN2),这里假设另一控制线接普通IO口 // 例如:if(dir == DIR_FORWARD) {AIN2=0;} else {AIN2=1;} // ... // 设置PWM占空比 if(motor == MOTOR_A) { TIM_SetCompare1(TIM3, pwm_abs * 50 / 1000); // 映射到0-ARR } else if(motor == MOTOR_B) { TIM_SetCompare2(TIM3, pwm_abs * 50 / 1000); } }

调试:编写一个测试函数,让电机正转、反转、停转,观察电机是否按预期动作。注意安全,最好将小车架起来,让轮子空转。

3.3 编码器读取与速度计算

这是速度环的“反馈传感器”,用于测量电机的实际转速。

硬件连接(以TIM2和TIM4的编码器接口模式为例):

  • 电机A编码器A相->PA0 (TIM2_CH1)
  • 电机A编码器B相->PA1 (TIM2_CH2)
  • 电机B编码器A相->PB6 (TIM4_CH1)
  • 电机B编码器B相->PB7 (TIM4_CH2)

代码实现 (encoder.c/.h): STM32的定时器硬件编码器接口可以自动根据AB相脉冲增减计数值,极大简化了软件设计。

// encoder.h extern int encoder_left, encoder_right; void Encoder_Init_TIM2(void); void Encoder_Init_TIM4(void); void Encoder_Read(void);
// encoder.c int encoder_left = 0, encoder_right = 0; void Encoder_Init_TIM2(void) { // ... GPIO和定时器时钟使能 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; // 定时器基础设置,ARR设置为最大值65535 TIM_TimeBaseStructure.TIM_Period = 65535; TIM_TimeBaseStructure.TIM_Prescaler = 0; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // 配置编码器接口模式:在TI1和TI2上计数 TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_ICStructInit(&TIM_ICInitStructure); TIM_ICInitStructure.TIM_ICFilter = 10; // 滤波器,防抖动 TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ClearFlag(TIM2, TIM_FLAG_Update); TIM_ITConfig(TIM2, TIM_IT_Update, DISABLE); TIM_SetCounter(TIM2, 32768); // 设置初始值在中间,防止向上或向下溢出 TIM_Cmd(TIM2, ENABLE); } // Encoder_Init_TIM4 类似 // 读取编码器值,并计算速度(在固定周期中断中调用) void Encoder_Read(void) { static int left_last = 0, right_last = 0; int left_now, right_now; left_now = TIM_GetCounter(TIM2); right_now = TIM_GetCounter(TIM4); // 计算差值,处理计数器溢出 encoder_left += (short)(left_now - left_last); encoder_right += (short)(right_now - right_last); left_last = left_now; right_last = right_now; // 可以将 encoder_left/right 清零,用于计算周期内的脉冲数 }

速度计算:在固定的时间间隔(如10ms)内,encoder_left的累计值就代表了电机在这段时间内转过的“相对位置”。将其除以时间,再根据编码器线数和减速比,就可以估算出轮子的实际转速。这个速度值将作为速度环的反馈。

4. 核心控制算法:PID与双环控制

这是平衡小车的“大脑”。我们采用经典的“串级PID”控制:内环是直立环(快),外环是速度环(慢)。

4.1 PID控制器实现

首先实现一个通用、易于调试的PID结构体和函数。

// pid.h typedef struct { float target; // 目标值 float actual; // 实际值 float err; // 当前误差 float err_last; // 上次误差 float err_sum; // 误差积分项 float Kp, Ki, Kd; // PID参数 float output; // 控制器输出 float output_max; // 输出限幅 float integral_max; // 积分限幅 } PID; void PID_Init(PID *pid, float kp, float ki, float kd, float max_out, float max_i); float PID_Calc(PID *pid, float target, float actual);
// pid.c void PID_Init(PID *pid, float kp, float ki, float kd, float max_out, float max_i) { pid->Kp = kp; pid->Ki = ki; pid->Kd = kd; pid->target = 0; pid->actual = 0; pid->err = 0; pid->err_last = 0; pid->err_sum = 0; pid->output = 0; pid->output_max = max_out; pid->integral_max = max_i; } float PID_Calc(PID *pid, float target, float actual) { pid->target = target; pid->actual = actual; pid->err = pid->target - pid->actual; // 积分项,并做限幅防止积分饱和 pid->err_sum += pid->err; if(pid->err_sum > pid->integral_max) pid->err_sum = pid->integral_max; if(pid->err_sum < -pid->integral_max) pid->err_sum = -pid->integral_max; // PID计算 pid->output = pid->Kp * pid->err + pid->Ki * pid->err_sum + pid->Kd * (pid->err - pid->err_last); // 输出限幅 if(pid->output > pid->output_max) pid->output = pid->output_max; if(pid->output < -pid->output_max) pid->output = -pid->output_max; pid->err_last = pid->err; return pid->output; }

4.2 直立环PD控制

直立环直接使用角度Angle和角速度Gyro进行PD控制。P项(比例)负责“扶正”的力度,D项(微分,即角速度)负责“抑制摆动”,提供阻尼。

// 全局变量 PID pid_angle; // 直立环PID float angle_target = 0; // 期望平衡角度,通常为0度(竖直) float balance_out; // 直立环输出 void Balance_Control(void) { float angle_now = get_angle(); // 从IMU获取当前角度 float gyro_now = get_gyro(); // 从IMU获取当前角速度 // 注意:这里将角速度作为微分项直接输入,是一种简化。 // 更标准的做法是将角度误差进行微分。 // 直立环通常只需要PD控制,I项容易引起震荡。 balance_out = PID_Calc(&pid_angle, angle_target, angle_now); // 实际上,D项通常用角速度负反馈:balance_out = Kp*angle_err + Kd*(-gyro) // 所以我们的PID结构需要稍作调整,或者单独写一个PD函数。 }

4.3 速度环PI控制

速度环的目标是让小车整体的移动速度趋于0。它通过微调angle_target来实现。比如,小车向前倾斜会向前走,如果一直向前走,速度环检测到正向速度,就会产生一个负的调整量给angle_target,让小车稍微向后倾斜一点,从而产生一个制动力来减速。

// 全局变量 PID pid_speed; // 速度环PID int speed_target = 0; // 期望速度,通常为0 float speed_out; // 速度环输出 void Speed_Control(void) { int speed_now = get_speed(); // 根据编码器计算当前速度 speed_out = PID_Calc(&pid_speed, speed_target, speed_now); // 速度环的输出用于调整直立环的目标角度 angle_target = speed_out; // 注意:这里需要乘以一个系数,将速度输出映射到角度变化量 // 例如:angle_target = speed_out * 0.001; }

4.4 控制量融合与输出

最终给电机的PWM值是直立环输出和转向环输出的叠加。

int motor_left_out, motor_right_out; int turn_out = 0; // 转向环输出,由遥控器控制 void Motor_Output(void) { // 1. 基础输出 = 直立环输出 + 速度环对角度的微调(已包含在angle_target中) // 2. 加上转向控制 motor_left_out = balance_out + turn_out; motor_right_out = balance_out - turn_out; // 转向时左右轮速度相反 // 3. 限幅并输出到电机 Motor_Set_Pwm(MOTOR_A, motor_left_out); Motor_Set_Pwm(MOTOR_B, motor_right_out); }

5. 系统整合与主程序流程

将所有模块整合到一个以固定频率运行的循环中,是系统稳定的关键。我们通常使用一个定时器中断来触发这个控制循环。

5.1 定时器中断设置

配置一个定时器(如TIM1),产生1ms或2ms的中断,作为整个系统的“心跳”。

// 在main.c或单独的定时器文件中 void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); control_flag = 1; // 设置标志位,主循环中检测 } } void Timer_Init(void) { // 配置TIM1,72M/7200/10 = 1kHz中断 (1ms) // ... 定时器初始化代码 TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE); NVIC_EnableIRQ(TIM1_UP_IRQn); TIM_Cmd(TIM1, ENABLE); }

5.2 主程序逻辑

主循环以查询标志位的方式,确保控制任务以精确的周期执行。

// main.c int main(void) { System_Init(); // 系统时钟、延时初始化 USART1_Init(115200); // 串口初始化,用于调试 MPU6050_Init(); // 初始化MPU6050 Motor_Init(); // 初始化电机PWM Encoder_Init(); // 初始化编码器接口 Timer_Init(); // 初始化控制周期定时器 PID_Init(&pid_angle, 100.0, 0.0, 1.2, 1000, 1000); // 直立环参数,需调试 PID_Init(&pid_speed, 10.0, 0.1, 0.0, 100, 1000); // 速度环参数,需调试 printf(“Balance Car System Start!\r\n”); delay_ms(1000); // 等待系统稳定 while(1) { if(control_flag == 1) { // 1ms到 control_flag = 0; // 1. 读取传感器数据 MPU6050_Read_All(&raw_data); // 2. 姿态解算 IMU_Update(&raw_data); // 3. 读取编码器(每1ms读一次,但速度计算可以每10ms做一次) Encoder_Read(); // 4. 速度计算(每10ms执行一次) if(speed_cnt++ >= 10) { speed_cnt = 0; Speed_Calculate(); // 根据encoder_left/right计算速度 Speed_Control(); // 速度环控制,更新angle_target } // 5. 直立环控制 Balance_Control(); // 6. 综合输出 Motor_Output(); } // 其他任务,如读取遥控器、状态指示等,可以放在这里 Remote_Process(); LED_Blink(); } }

6. 参数整定与调试技巧

这是让小车站起来的最后一步,也是最需要耐心的一步。务必先将小车用东西架起来,让轮子悬空!

6.1 调试步骤与顺序

第一步:调直立环PD参数(先P后D)

  1. 将速度环和转向环的输出先置零。
  2. 只给P(Kp):将Ki和Kd设为0,Kp给一个较小的值(如20)。用手拨动小车,它应该会朝你拨动的方向转动,但无法保持平衡,会来回震荡。
  3. 逐渐增大Kp:直到小车在受到扰动后,能自己快速回到中间位置,但可能会在平衡点附近高频抖动(震荡)。
  4. 加入D(Kd):逐渐增大Kd(如从0.5开始),阻尼效果会抑制抖动。Kd太大会导致响应迟钝,太小则抑制不了震荡。目标是找到一个值,让小车受到扰动后能平稳、无超调地回到平衡点。
  5. 此时,在悬空状态下,小车应该能基本保持一个固定角度(如0度)。

第二步:调速度环PI参数(先P后I)

  1. 将小车放在地上,用手扶住。
  2. 给一个较小的速度环Kp(如1.0),Ki=0。此时松开手,小车可能会缓慢地向一个方向加速跑掉。
  3. 调节Kp:目标是让小车在平衡的基础上,能够抵抗缓慢的倾斜(即速度环起作用)。如果小车往一个方向跑,说明速度环纠正力度不够,适当增大Kp;如果小车在原地剧烈前后抖动,说明Kp太大,应减小。
  4. 加入Ki:Ki用于消除静差。如果小车在平衡时仍有缓慢的漂移,可以加入很小的Ki(如0.001)来抑制。Ki绝对不能大,否则会引起系统震荡。
  5. 最终效果:小车能在原地基本保持平衡,轻轻推它一下,它会移动一小段距离然后自己停下来。

6.2 常见问题与排查思路

问题现象可能原因排查思路
小车一上电就猛转电机线接反;PWM极性设置反;角度正负号错误。1. 检查电机接线。2. 测试电机单独控制函数。3. 打印角度值,观察倾斜方向与电机转动方向逻辑是否正确(应朝倾斜方向转动)。
小车剧烈高频抖动直立环D参数太小;控制周期太慢;机械结构松动。1. 增大Kd。2. 检查定时器中断周期是否稳定在1-5ms。3. 紧固所有螺丝和接线。
小车缓慢向一个方向跑速度环未起作用或参数太小;机械重心不在中心。1. 检查速度环计算和输出是否正常。2. 适当增大速度环Kp。3. 调整电池等重物的位置。
角度值跳动大,无法稳定MPU6050数据噪声大;滤波参数不佳;传感器安装不牢固。1. 检查MPU6050的电源是否稳定。2. 调整互补滤波系数K1,增大则更信任加速度计(稳但慢),减小则更信任陀螺仪(快但漂)。3. 加固传感器。
编码器读数不准或为0编码器接线错误;定时器编码器模式配置错误;未处理计数器溢出。1. 用示波器或逻辑分析仪看AB相信号。2. 检查TIM的编码器接口函数调用。3. 在Encoder_Read中打印原始计数值检查。

7. 进阶优化与工程实践

当小车能基本站立后,可以考虑以下优化,让它更稳定、更智能。

7.1 软件滤波优化

  • MPU6050软件滤波:除了互补滤波,可以尝试卡尔曼滤波,其性能更优,但原理和调参更复杂。
  • 编码器速度滤波:速度计算时,可以对多次采样进行滑动平均滤波,使速度值更平滑。

7.2 控制算法增强

  • 积分抗饱和:在PID_Calc函数中我们已经做了积分限幅,这是防止“积分饱和”导致系统失控的关键。
  • 微分先行:只对测量值微分,不对设定值微分,可以避免设定值突变引起的输出抖动。
  • 变参数PID:可以根据误差大小切换不同的PID参数,实现更精细的控制。

7.3 功能扩展

  • 蓝牙遥控:集成HC-05/06模块,通过手机APP控制小车前进、后退、转向。
  • OLED显示:显示当前角度、角速度、PID参数、电池电压等信息,方便调试。
  • 掉电保护:检测电池电压,过低时报警并逐渐停止电机,防止电池过放。
  • 上位机调试:通过串口将关键数据(角度、速度、PWM输出)发送到电脑,用波形软件(如SerialChart、VOFA+)实时显示,这是调参的神器。

7.4 工程化建议

  • 模块化编程:就像本文的代码结构一样,每个功能独立成.c/.h文件,通过清晰的接口耦合,方便调试和移植。
  • 参数可配置:将PID参数、滤波系数等定义为全局变量,并预留串口修改接口,这样就不用每次修改都重新烧录程序。
  • 添加看门狗:防止程序跑飞,增加系统可靠性。
  • 代码注释与版本管理:详细注释关键代码,并使用Git等工具管理代码版本,记录每次参数修改的效果。

从MPU6050的数据读取到PWM波的输出,从互补滤波的融合到双环PID的协同,我们一步步构建了平衡小车的整个软件系统。这个过程不仅是对STM32外设使用的全面练习,更是对自动控制原理的深刻实践。调试参数的过程可能会充满挫折,但当小车颤颤巍巍立起来的那一刻,所有的努力都是值得的。

建议你将本文的代码作为骨架,先确保它能跑起来,然后尝试修改参数、增加功能,例如尝试用卡尔曼滤波替换互补滤波,或者为小车加上循迹、避障模块。真正的学习发生在你动手解决每一个具体问题的时候。如果在实践中遇到任何问题,欢迎在评论区交流讨论,共同进步。

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

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

立即咨询