拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32平衡小车开发全攻略:从PID算法到电机控制的嵌入式实践

最近在整理项目资料时发现很多同学对STM32平衡小车这个经典项目既向往又畏惧。向往的是它融合了传感器、电机控制、PID算法等多个核心知识点是检验嵌入式学习成果的绝佳实践畏惧的是网上教程要么过于零散要么直接甩出代码让人无从下手。本文将彻底解决这个问题从零开始手把手带你完成平衡小车软件部分的全部开发即使你是刚接触STM32的“草履虫”级新手也能跟着一步步搭建起来。本文不仅会提供完整的、可直接复用的代码更会深入讲解每一行代码背后的设计思路和原理。你将掌握MPU6050姿态解算、直流电机PWM驱动、PID控制器设计与参数整定等核心技能最终得到一个能够自主站立、抵抗干扰的平衡小车。无论是用于课程设计、毕业设计还是作为个人技术能力的证明这套方案都极具价值。1. 项目核心概念与软件架构在动手写代码之前我们必须先理解平衡小车是如何“思考”和“行动”的。它的核心目标只有一个像不倒翁一样在受到外力倾斜时能迅速驱动轮子朝倾斜方向运动从而产生一个反向力矩将自己“扶正”。1.1 核心控制原理倒立摆模型平衡小车在控制理论上可以抽象为一个“倒立摆”模型。摆杆车身是倒立的其支点车轮与地面的接触点可以水平移动。控制的目标就是通过移动支点即驱动车轮使倒立的摆杆车身保持竖直平衡。软件系统的任务就是持续测量车身的倾斜角度Angle和倾斜角速度Gyro然后通过一个控制算法我们使用PID计算出需要给电机施加多大的速度PWM值从而产生合适的力矩来纠正倾斜。1.2 软件系统架构设计一个稳定可靠的平衡小车软件系统通常包含以下层次清晰的模块硬件驱动层负责与最底层硬件打交道。MPU6050驱动通过I2C总线读取原始加速度计和陀螺仪数据。电机驱动生成PWM信号控制电机速度和方向通常使用定时器的PWM输出功能。编码器读取通过定时器的编码器接口模式读取电机转速反馈用于速度控制。数据处理层对原始数据进行加工得到可用的状态信息。姿态解算融合MPU6050的加速度和陀螺仪数据通过互补滤波或卡尔曼滤波算法计算出准确、稳定的车身俯仰角Angle和角速度Gyro。速度计算根据编码器脉冲数和时间计算出小车的实际移动速度。控制算法层系统的大脑根据当前状态计算控制量。直立环PD控制以角度Angle为控制目标快速响应车身倾斜维持平衡。这是平衡的基础。速度环PI控制以小车速度为控制目标防止小车在平衡过程中因为持续朝一个方向加速而跑飞。它通过微调小车的平衡角度设定值来实现。转向环控制处理遥控器的转向指令让两个轮子产生速度差来实现转弯。应用层协调各个模块处理用户输入。主循环以固定频率如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驱动与姿态解算这是平衡小车的“眼睛”负责感知自身的倾斜状态。硬件连接I2C1MPU6050.VCC-3.3VMPU6050.GND-GNDMPU6050.SCL-PB6MPU6050.SDA-PB7MPU6050.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 / (7191) / (1991) 500Hz? 这里应为20kHz注意计算 // 目标20kHz: 72M / (711) / (491) 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; } // 根据方向设置GPIOAIN2/BIN2这里假设另一控制线接普通IO口 // 例如if(dir DIR_FORWARD) {AIN20;} else {AIN21;} // ... // 设置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) { // 配置TIM172M/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将速度环和转向环的输出先置零。只给PKp将Ki和Kd设为0Kp给一个较小的值如20。用手拨动小车它应该会朝你拨动的方向转动但无法保持平衡会来回震荡。逐渐增大Kp直到小车在受到扰动后能自己快速回到中间位置但可能会在平衡点附近高频抖动震荡。加入DKd逐渐增大Kd如从0.5开始阻尼效果会抑制抖动。Kd太大会导致响应迟钝太小则抑制不了震荡。目标是找到一个值让小车受到扰动后能平稳、无超调地回到平衡点。此时在悬空状态下小车应该能基本保持一个固定角度如0度。第二步调速度环PI参数先P后I将小车放在地上用手扶住。给一个较小的速度环Kp如1.0Ki0。此时松开手小车可能会缓慢地向一个方向加速跑掉。调节Kp目标是让小车在平衡的基础上能够抵抗缓慢的倾斜即速度环起作用。如果小车往一个方向跑说明速度环纠正力度不够适当增大Kp如果小车在原地剧烈前后抖动说明Kp太大应减小。加入KiKi用于消除静差。如果小车在平衡时仍有缓慢的漂移可以加入很小的Ki如0.001来抑制。Ki绝对不能大否则会引起系统震荡。最终效果小车能在原地基本保持平衡轻轻推它一下它会移动一小段距离然后自己停下来。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外设使用的全面练习更是对自动控制原理的深刻实践。调试参数的过程可能会充满挫折但当小车颤颤巍巍立起来的那一刻所有的努力都是值得的。建议你将本文的代码作为骨架先确保它能跑起来然后尝试修改参数、增加功能例如尝试用卡尔曼滤波替换互补滤波或者为小车加上循迹、避障模块。真正的学习发生在你动手解决每一个具体问题的时候。如果在实践中遇到任何问题欢迎在评论区交流讨论共同进步。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门