很多同学在制作单片机平衡车时往往只停留在通过串口打印角度数据的阶段虽然能看到姿态数据的变化但车子就是站不起来。这种情况通常是因为只完成了数据采集却没有实现真正的闭环控制。本文将带你从串口调试升级到完整的平衡车控制系统掌握PID算法、电机驱动、传感器滤波等核心技术。1. 平衡车系统架构与核心原理1.1 平衡车工作原理两轮平衡车的核心原理是基于倒立摆模型。当车身向前倾斜时控制系统需要驱动车轮向前加速产生一个反向的力矩来抵消倾斜反之亦然。这个过程需要实时检测车身姿态并快速响应任何延迟都可能导致系统失稳。1.2 系统组成模块一个完整的平衡车系统包含以下几个关键部分主控单元STM32F103C8T6等单片机负责数据处理和控制算法执行姿态传感器MPU6050提供三轴加速度和角速度数据电机驱动TB6612模块控制直流编码电机的转速和方向编码器用于测量电机实际转速实现速度闭环电源管理为各模块提供稳定供电1.3 控制层次结构平衡车控制系统采用双闭环结构内环速度环通过编码器反馈实现电机转速精确控制外环姿态环通过MPU6050数据维持车身平衡 两个环路的PID参数需要分别整定协同工作。2. 硬件选型与电路设计2.1 主控芯片STM32F103C8T6这款单片机具有丰富的外设资源特别适合平衡车项目3个通用定时器TIM2、TIM3、TIM4用于PWM输出和编码器接口1个高级定时器TIM1用于系统时序控制多个USART接口用于调试和通信充足的GPIO引脚连接各种传感器2.2 MPU6050姿态传感器MPU6050集成了3轴加速度计和3轴陀螺仪通过I2C接口与单片机通信。在实际使用中需要注意加速度计测量的是重力加速度在各轴上的分量可用于计算倾斜角陀螺仪测量的是角速度通过对时间积分可以得到角度变化两种传感器各有优缺点需要数据融合才能获得稳定的姿态数据2.3 TB6612电机驱动模块TB6612是双H桥电机驱动芯片具有以下特点最大输出电流1.2A连续3.2A峰值内置过热保护和低压检测电路支持PWM频率高达100kHz接线示意图VM → 电机电源7-12V VCC → 逻辑电源3.3V/5V GND → 共地 AIN1/AIN2 → 电机A方向控制 PWMA → 电机A速度控制 AO1/AO2 → 电机A输出 BIN1/BIN2 → 电机B方向控制 PWMB → 电机B速度控制 BO1/BO2 → 电机B输出 STBY → 使能引脚高电平有效2.4 霍尔编码器电机JGB37-520电机参数工作电压6-12V减速比30:1编码器类型霍尔传感器脉冲数每转11个脉冲减速前编码器接口说明VCC编码器电源3.3V-5VGND地线C1、C2A、B相脉冲输出3. 软件开发环境搭建3.1 开发工具链配置推荐使用STM32CubeIDE进行开发它集成了STM32CubeMX配置工具和Eclipse开发环境。安装步骤从ST官网下载STM32CubeIDE安装时选择适合的芯片系列支持包创建新工程时选择STM32F103C8Tx芯片配置时钟树将系统时钟设置为72MHz3.2 工程文件结构Balance_Car/ ├── Core/ │ ├── Inc/ // 头文件 │ ├── Src/ // 源文件 │ └── Startup/ // 启动文件 ├── Drivers/ │ ├── CMSIS/ // Cortex-M核支持 │ └── STM32F1xx_HAL_Driver/ // HAL库 ├── Hardware/ │ ├── MPU6050/ // 姿态传感器驱动 │ ├── Motor/ // 电机控制 │ ├── Encoder/ // 编码器接口 │ └── OLED/ // 显示模块 └── Middlewares/ // 中间件3.3 关键外设初始化代码// 系统时钟配置 void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; // 配置HSE振荡器 RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; HAL_RCC_OscConfig(RCC_OscInitStruct); // 配置系统时钟 RCC_ClkInitStruct.ClockType RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_2); } // PWM定时器配置 void PWM_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; // TIM2用于电机PWM输出 htim2.Instance TIM2; htim2.Init.Prescaler 71; // 1MHz计数频率 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 999; // 1kHz PWM频率 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim2); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 0; // 初始占空比0% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(htim2, sConfigOC, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2); }4. 传感器数据处理与姿态解算4.1 MPU6050数据读取MPU6050通过I2C接口通信需要先初始化并校准// MPU6050初始化 uint8_t MPU6050_Init(void) { uint8_t check, data; // 检查设备ID HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, WHO_AM_I_REG, 1, check, 1, 1000); if (check ! 0x68) return 1; // 唤醒MPU6050 data 0x00; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, PWR_MGMT_1_REG, 1, data, 1, 1000); // 设置陀螺仪量程 ±2000°/s data 0x18; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, GYRO_CONFIG_REG, 1, data, 1, 1000); // 设置加速度计量程 ±8g data 0x10; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, ACCEL_CONFIG_REG, 1, data, 1, 1000); // 设置低通滤波器 5Hz data 0x06; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, CONFIG_REG, 1, data, 1, 1000); return 0; } // 读取原始数据 void MPU6050_Read_Data(MPU6050_Data* mpu_data) { uint8_t recv_data[14]; HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, ACCEL_XOUT_H_REG, 1, recv_data, 14, 1000); mpu_data-accel_x (int16_t)((recv_data[0] 8) | recv_data[1]); mpu_data-accel_y (int16_t)((recv_data[2] 8) | recv_data[3]); mpu_data-accel_z (int16_t)((recv_data[4] 8) | recv_data[5]); mpu_data-temp (int16_t)((recv_data[6] 8) | recv_data[7]); mpu_data-gyro_x (int16_t)((recv_data[8] 8) | recv_data[9]); mpu_data-gyro_y (int16_t)((recv_data[10] 8) | recv_data[11]); mpu_data-gyro_z (int16_t)((recv_data[12] 8) | recv_data[13]); }4.2 一阶互补滤波算法单纯使用加速度计或陀螺仪都有明显缺点互补滤波可以结合两者优点// 互补滤波姿态解算 float Complementary_Filter(float accel_angle, float gyro_rate, float dt) { static float angle 0; float alpha 0.98; // 滤波系数 // 加速度计角度计算俯仰角 accel_angle atan2f(accel_y, accel_z) * 180 / PI; // 互补滤波公式 angle alpha * (angle gyro_rate * dt) (1 - alpha) * accel_angle; return angle; } // 姿态解算任务 void Attitude_Update_Task(void) { MPU6050_Data mpu_data; static uint32_t last_time 0; uint32_t current_time HAL_GetTick(); float dt (current_time - last_time) / 1000.0f; if (dt 0.01) { // 100Hz更新频率 MPU6050_Read_Data(mpu_data); // 转换为实际物理量 float accel_y mpu_data.accel_y / 4096.0f; // ±8g量程 float accel_z mpu_data.accel_z / 4096.0f; float gyro_y mpu_data.gyro_y / 16.4f; // ±2000°/s量程 // 计算角度 current_pitch Complementary_Filter(accel_y, accel_z, gyro_y, dt); last_time current_time; } }4.3 卡尔曼滤波进阶方案对于要求更高的应用可以使用卡尔曼滤波typedef struct { float angle; // 角度估计值 float bias; // 陀螺仪零偏估计 float P[2][2]; // 误差协方差矩阵 float Q_angle; // 过程噪声协方差 float Q_bias; // 陀螺仪零偏噪声 float R_measure; // 测量噪声协方差 } Kalman_Filter; float Kalman_Update(Kalman_Filter* kf, float new_angle, float new_rate, float dt) { // 预测步骤 kf-angle dt * (new_rate - kf-bias); kf-P[0][0] dt * (dt * kf-P[1][1] - kf-P[0][1] - kf-P[1][0] kf-Q_angle); kf-P[0][1] - dt * kf-P[1][1]; kf-P[1][0] - dt * kf-P[1][1]; kf-P[1][1] kf-Q_bias * dt; // 更新步骤 float y new_angle - kf-angle; float S kf-P[0][0] kf-R_measure; float K[2] {kf-P[0][0] / S, kf-P[1][0] / S}; kf-angle K[0] * y; kf-bias K[1] * y; float P00_temp kf-P[0][0]; float P01_temp kf-P[0][1]; kf-P[0][0] - K[0] * P00_temp; kf-P[0][1] - K[0] * P01_temp; kf-P[1][0] - K[1] * P00_temp; kf-P[1][1] - K[1] * P01_temp; return kf-angle; }5. PID控制算法实现5.1 位置式PID控制器位置式PID适合平衡车这种需要精确控制的场合typedef struct { float kp; // 比例系数 float ki; // 积分系数 float kd; // 微分系数 float integral; // 积分项 float prev_error; // 上一次误差 float integral_limit; // 积分限幅 float output_limit; // 输出限幅 } PID_Controller; float PID_Calculate(PID_Controller* pid, float setpoint, float measurement, float dt) { float error setpoint - measurement; // 比例项 float proportional pid-kp * error; // 积分项带限幅和抗饱和 pid-integral error * dt; if (pid-integral pid-integral_limit) pid-integral pid-integral_limit; else if (pid-integral -pid-integral_limit) pid-integral -pid-integral_limit; float integral pid-ki * pid-integral; // 微分项采用测量值微分减少设定值突变的影响 float derivative pid-kd * (measurement - pid-prev_error) / dt; pid-prev_error measurement; // 计算总输出 float output proportional integral - derivative; // 输出限幅 if (output pid-output_limit) output pid-output_limit; else if (output -pid-output_limit) output -pid-output_limit; return output; }5.2 双闭环PID控制结构平衡车需要姿态环和速度环协同工作// 全局变量定义 PID_Controller angle_pid; // 角度环PID PID_Controller speed_pid; // 速度环PID float target_angle 0; // 目标角度平衡位置 float target_speed 0; // 目标速度 // 双环PID控制函数 void Balance_Control(void) { static int32_t left_encoder_total 0, right_encoder_total 0; static uint32_t last_time 0; uint32_t current_time HAL_GetTick(); float dt (current_time - last_time) / 1000.0f; if (dt 0.005) { // 200Hz控制频率 // 读取编码器值速度环反馈 int16_t left_speed Encoder_Get_Speed(LEFT_MOTOR); int16_t right_speed Encoder_Get_Speed(RIGHT_MOTOR); float average_speed (left_speed right_speed) / 2.0f; // 速度环PID计算外环 float speed_output PID_Calculate(speed_pid, target_speed, average_speed, dt); // 速度环输出作为角度环的设定值 float angle_setpoint target_angle speed_output; // 角度环PID计算内环 float angle_output PID_Calculate(angle_pid, angle_setpoint, current_pitch, dt); // 输出到电机 Motor_Set_Speed(LEFT_MOTOR, angle_output); Motor_Set_Speed(RIGHT_MOTOR, angle_output); last_time current_time; } }5.3 自适应PID参数调整针对不同倾斜角度采用不同的PID参数// 自适应PID参数 void Adaptive_PID_Tuning(float current_angle) { float abs_angle fabsf(current_angle); if (abs_angle 10.0f) { // 大角度状态快速回正优先比例项 angle_pid.kp 25.0f; angle_pid.ki 0.1f; angle_pid.kd 0.8f; } else if (abs_angle 5.0f) { // 中等角度平衡过渡 angle_pid.kp 15.0f; angle_pid.ki 0.5f; angle_pid.kd 1.2f; } else { // 小角度精细平衡优先微分项 angle_pid.kp 8.0f; angle_pid.ki 1.0f; angle_pid.kd 2.5f; } }6. 电机控制与编码器接口6.1 编码器接口配置STM32的定时器编码器模式可以自动计数AB相脉冲// 编码器接口初始化 void Encoder_Init(TIM_HandleTypeDef* htim) { TIM_Encoder_InitTypeDef encoder_config {0}; encoder_config.EncoderMode TIM_ENCODERMODE_TI12; encoder_config.IC1Polarity TIM_ICPOLARITY_RISING; encoder_config.IC1Selection TIM_ICSELECTION_DIRECTTI; encoder_config.IC1Prescaler TIM_ICPSC_DIV1; encoder_config.IC1Filter 0; encoder_config.IC2Polarity TIM_ICPOLARITY_RISING; encoder_config.IC2Selection TIM_ICSELECTION_DIRECTTI; encoder_config.IC2Prescaler TIM_ICPSC_DIV1; encoder_config.IC2Filter 0; HAL_TIM_Encoder_Init(htim, encoder_config); HAL_TIM_Encoder_Start(htim, TIM_CHANNEL_ALL); } // 获取编码器速度 int16_t Encoder_Get_Speed(Motor_Type motor) { static int16_t last_count[2] {0, 0}; static uint32_t last_time[2] {0, 0}; uint32_t current_time HAL_GetTick(); int16_t current_count (motor LEFT_MOTOR) ? __HAL_TIM_GET_COUNTER(htim3) : __HAL_TIM_GET_COUNTER(htim4); // 处理计数器溢出 int16_t delta_count current_count - last_count[motor]; float delta_time (current_time - last_time[motor]) / 1000.0f; // 计算转速转/分钟 float speed (delta_count / (4.0f * 11.0f * 30.0f)) / delta_time * 60.0f; last_count[motor] current_count; last_time[motor] current_time; return (int16_t)speed; }6.2 电机驱动控制TB6612电机驱动控制函数// 电机控制函数 void Motor_Set_Speed(Motor_Type motor, float speed) { uint16_t pwm_value; GPIO_PinState in1, in2; // 速度限幅 if (speed MAX_MOTOR_SPEED) speed MAX_MOTOR_SPEED; if (speed -MAX_MOTOR_SPEED) speed -MAX_MOTOR_SPEED; // 设置方向和PWM值 if (speed 0) { in1 GPIO_PIN_SET; in2 GPIO_PIN_RESET; pwm_value (uint16_t)(speed / MAX_MOTOR_SPEED * 1000); } else { in1 GPIO_PIN_RESET; in2 GPIO_PIN_SET; pwm_value (uint16_t)(-speed / MAX_MOTOR_SPEED * 1000); } // 设置GPIO和PWM if (motor LEFT_MOTOR) { HAL_GPIO_WritePin(MOTOR_LEFT_IN1_GPIO_Port, MOTOR_LEFT_IN1_Pin, in1); HAL_GPIO_WritePin(MOTOR_LEFT_IN2_GPIO_Port, MOTOR_LEFT_IN2_Pin, in2); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, pwm_value); } else { HAL_GPIO_WritePin(MOTOR_RIGHT_IN1_GPIO_Port, MOTOR_RIGHT_IN1_Pin, in1); HAL_GPIO_WritePin(MOTOR_RIGHT_IN2_GPIO_Port, MOTOR_RIGHT_IN2_Pin, in2); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_2, pwm_value); } }7. 系统整合与主程序框架7.1 主程序任务调度采用时间片轮询的方式实现多任务调度// 任务时间定义 #define TASK_ATTITUDE_UPDATE 10 // 姿态更新 100Hz #define TASK_BALANCE_CONTROL 5 // 平衡控制 200Hz #define TASK_SPEED_CALC 20 // 速度计算 50Hz #define TASK_SERIAL_DEBUG 50 // 串口调试 20Hz // 主循环任务调度 int main(void) { // 系统初始化 HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); MX_TIM3_Init(); MX_TIM4_Init(); MX_I2C1_Init(); MX_USART1_UART_Init(); // 外设初始化 PWM_Init(); Encoder_Init(htim3); Encoder_Init(htim4); MPU6050_Init(); OLED_Init(); // PID参数初始化 angle_pid.kp 15.0f; angle_pid.ki 0.5f; angle_pid.kd 1.5f; angle_pid.integral_limit 1000.0f; angle_pid.output_limit 800.0f; speed_pid.kp 0.5f; speed_pid.ki 0.01f; speed_pid.kd 0.1f; speed_pid.integral_limit 500.0f; speed_pid.output_limit 200.0f; uint32_t task_timer[4] {0}; while (1) { uint32_t current_time HAL_GetTick(); // 姿态更新任务100Hz if (current_time - task_timer[0] TASK_ATTITUDE_UPDATE) { Attitude_Update_Task(); task_timer[0] current_time; } // 平衡控制任务200Hz if (current_time - task_timer[1] TASK_BALANCE_CONTROL) { Balance_Control(); task_timer[1] current_time; } // 速度计算任务50Hz if (current_time - task_timer[2] TASK_SPEED_CALC) { Speed_Calculation_Task(); task_timer[2] current_time; } // 串口调试任务20Hz if (current_time - task_timer[3] TASK_SERIAL_DEBUG) { Serial_Debug_Task(); task_timer[3] current_time; } } }7.2 调试信息输出通过串口输出关键数据用于调试// 串口调试任务 void Serial_Debug_Task(void) { static uint8_t debug_counter 0; if (debug_counter 5) { // 降低输出频率 printf(Pitch:%.2f, Speed:%d, PWM:%d\r\n, current_pitch, (Encoder_Get_Speed(LEFT_MOTOR) Encoder_Get_Speed(RIGHT_MOTOR)) / 2, __HAL_TIM_GET_COMPARE(htim2, TIM_CHANNEL_1)); debug_counter 0; } }8. PID参数整定与系统调试8.1 参数整定步骤PID参数整定需要循序渐进先调角度环将速度环输出设为0只调试角度环先将Ki和Kd设为0逐渐增大Kp直到车子开始轻微振荡然后加入Kd来抑制振荡逐渐增大直到系统稳定最后加入Ki来消除稳态误差但要小心积分饱和再调速度环角度环调好后再加入速度环同样先调Kp让车子能够维持在一定速度范围内加入Kd改善动态响应Ki要设置得很小避免积分项影响平衡8.2 常见问题排查问题1车子向一边倾斜检查MPU6050安装是否水平校准加速度计零偏检查电机机械结构是否对称问题2车子剧烈振荡减小角度环Kp系数增大角度环Kd系数检查控制频率是否足够高建议200Hz以上问题3响应迟钝容易倒下增大角度环Kp系数减小角度环Kd系数检查传感器数据更新频率问题4电机发热严重检查PWM频率是否合适建议1-10kHz降低PID输出限幅检查机械结构是否有卡滞8.3 安全保护机制在实际调试中需要加入安全保护// 安全监控函数 void Safety_Monitor(void) { // 角度过大保护 if (fabsf(current_pitch) 45.0f) { Motor_Stop(LEFT_MOTOR); Motor_Stop(RIGHT_MOTOR); // 触发软件复位或进入安全模式 } // 速度过快保护 float speed (Encoder_Get_Speed(LEFT_MOTOR) Encoder_Get_Speed(RIGHT_MOTOR)) / 2.0f; if (fabsf(speed) MAX_SAFE_SPEED) { // 逐渐减小目标速度 target_speed * 0.9f; } // 通信异常保护 static uint32_t last_mpu_time 0; if (HAL_GetTick() - last_mpu_time 100) { // MPU6050数据超过100ms未更新 Motor_Stop(LEFT_MOTOR); Motor_Stop(RIGHT_MOTOR); } }9. 进阶功能扩展9.1 无线遥控功能通过蓝牙或2.4G模块添加遥控功能// 串口命令解析 void UART_Command_Parser(uint8_t* data, uint16_t size) { if (data[0] 0xAA data[1] 0xBB) { // 帧头检测 int16_t speed_cmd (data[2] 8) | data[3]; int16_t turn_cmd (data[4] 8) | data[5]; // 设置目标速度 target_speed speed_cmd * 0.1f; // 缩放因子 // 差速转向 Motor_Set_Speed(LEFT_MOTOR, base_speed turn_cmd); Motor_Set_Speed(RIGHT_MOTOR, base_speed - turn_cmd); } }9.2 OLED显示状态信息添加显示屏实时显示系统状态// OLED显示任务 void OLED_Display_Task(void) { char buffer[20]; OLED_Clear(); sprintf(buffer, Pitch:%.1f, current_pitch); OLED_ShowString(0, 0, buffer); sprintf(buffer, Speed:%d, (Encoder_Get_Speed(LEFT_MOTOR) Encoder_Get_Speed(RIGHT_MOTOR)) / 2); OLED_ShowString(0, 2, buffer); sprintf(buffer, PWM:%d, __HAL_TIM_GET_COMPARE(htim2, TIM_CHANNEL_1)); OLED_ShowString(0, 4, buffer); OLED_Refresh(); }9.3 数据记录与分析添加SD卡模块记录运行数据// 数据记录函数 void Data_Logger(void) { static uint32_t log_counter 0; if (log_counter % 10 0) { // 降低记录频率 fprintf(log_file, %lu,%.3f,%.3f,%d,%d\n, HAL_GetTick(), current_pitch, target_speed, Encoder_Get_Speed(LEFT_MOTOR), __HAL_TIM_GET_COMPARE(htim2, TIM_CHANNEL_1)); } }10. 项目优化与生产注意事项10.1 硬件优化建议电源滤波在电机电源输入端加入大容量电解电容1000uF以上消除电压波动信号隔离I2C信号线远离PWM线必要时使用屏蔽线接地处理模拟地和数字地单点连接电机驱动地线要粗机械结构重心要低轮子与电机轴连接要牢固无间隙10.2 软件优化技巧浮点运算优化使用STM32的硬件FPU将浮点运算集中在短时间内完成中断优先级编码器中断优先级最高PWM更新次之串口调试最低内存管理使用静态分配避免动态内存分配减少内存碎片看门狗添加独立看门狗防止程序跑飞10.3 量产测试方案如果项目需要批量生产需要建立测试流程硬件测试单独测试每个模块功能软件烧录使用SWD接口批量烧录程序校准流程每个产品都需要进行传感器校准老化测试连续运行24小时检验稳定性通过本文的完整实现方案你的平衡车将不再只是简单地打印角度数据而是能够真正实现自主平衡。关键在于理解PID控制原理、掌握传感器数据处理技巧、以及具备系统调试能力。在实际项目中耐心调试和不断优化才是成功的关键。