在实际嵌入式开发项目中,平衡小车是一个经典的综合性实践案例,它融合了传感器数据采集、姿态解算、电机控制、PID算法和系统稳定性设计等多个核心知识点。很多开发者,尤其是刚接触STM32或实时控制系统的初学者,往往在硬件搭建后,面对纷繁复杂的软件逻辑感到无从下手,不知道如何将陀螺仪、加速度计的数据转化为稳定的控制信号,更不清楚PID参数该如何调试。
本文将以STM32微控制器为核心,面向有一定C语言和单片机基础的开发者,手把手带你完成平衡小车软件系统的构建。我们将从最基础的工程框架搭建开始,逐步实现传感器数据读取、姿态融合(以互补滤波为例)、直立环PD控制、速度环PI控制以及最终的电机PWM输出。文章会详细解释每一段代码背后的物理意义和控制逻辑,并提供可复现的代码片段、参数调试方法以及关键的排错思路。学习完成后,你将能够理解双轮自平衡系统的基本控制原理,并具备独立编写和调试一套简易平衡小车控制程序的能力。
1. 理解平衡小车的核心控制原理
在动手写代码之前,必须建立清晰的物理模型和控制逻辑概念。平衡小车本质上是一个倒立摆系统,其控制目标是让车体在竖直方向保持稳定。
1.1 系统组成与控制闭环
一个典型的STM32平衡小车软件系统包含以下几个关键部分,它们共同构成了一个或多个控制闭环:
- 传感器模块:通常采用MPU6050(六轴传感器),提供三轴加速度和三轴角速度数据。这是系统的“感官”,用于感知车体的倾斜状态和旋转速度。
- 姿态解算模块:原始传感器数据存在噪声且不能直接反映角度。此模块通过算法(如互补滤波、卡尔曼滤波)融合加速度计和陀螺仪数据,计算出相对准确的车体倾角(Pitch)和角速度。
- 控制决策模块:这是系统的大脑。它根据目标角度(通常为0度,即竖直)、当前实际角度和角速度,通过控制算法(通常是PID)计算出需要施加给电机的控制量。为了更好的性能,通常会采用串级PID,内环(直立环)快速响应角度变化,外环(速度环)维持长期稳定。
- 执行器模块:通常由STM32的定时器产生PWM波,驱动电机驱动板(如TB6612、DRV8833)来控制两个直流减速电机的正反转和转速,从而产生使车体恢复平衡的力矩。
- 反馈回路:电机运动后,车体姿态发生变化,又被传感器捕捉到,形成闭环。
整个控制流程可以简化为:传感器数据 -> 姿态解算 -> PID控制器 -> PWM输出 -> 电机运动 -> 影响姿态 -> 新的传感器数据。
1.2 姿态解算:从数据到角度
MPU6050输出的加速度计数据可以用于计算倾角,但动态情况下受线性加速度影响很大;陀螺仪积分可以得到角度,但存在零漂,积分误差会随时间累积。因此需要融合两者。
互补滤波是一种简单有效的融合算法,其核心思想是:在高频部分信任陀螺仪(响应快),在低频部分信任加速度计(长期稳定)。公式如下:
角度 = α * (上一角度 + 陀螺仪角速度 * dt) + (1 - α) * 加速度计角度其中,α是一个介于0和1之间的滤波系数,dt是采样周期。α越接近1,越信任陀螺仪。
1.3 控制算法:PID与串级PID
直立环(角度环):目标是让车体保持竖直。输入是目标角度(0度)和当前角度的偏差。使用PD(比例-微分)控制器。比例项(P)负责“扶正”车体,偏差越大,纠正力越大;微分项(D)负责“抑制摆动”,角速度越大,抑制力越大,防止小车来回振荡。
直立环输出 = Kp1 * 角度偏差 + Kd1 * 角速度速度环(位置环):目标是让小车在平衡的同时,能稳定在某个位置,或者缓慢响应遥控指令。如果只有直立环,小车为了平衡可能会朝一个方向持续加速。速度环通过测量电机编码器的脉冲数(反映速度)来调节。通常使用PI(比例-积分)控制器。积分项(I)可以消除静态误差,使小车能真正停在某处。
速度环输出 = Kp2 * 速度偏差 + Ki2 * 速度偏差积分串级控制:速度环的输出,作为直立环目标角度的微调量。即:
最终目标角度 = 0度(原始平衡目标) + 速度环输出然后直立环根据这个最终目标角度与当前角度的偏差进行计算。这样,当小车需要向前移动以平衡时,速度环会给出一个微小的前倾目标,直立环为了“扶正”这个前倾的车体,就会驱动电机向前转,从而实现移动中的平衡。
2. 软件工程框架与准备工作
在开始具体模块编码前,一个清晰、模块化的工程结构至关重要。这能保证代码的可读性、可维护性,并方便调试。
2.1 开发环境与硬件准备
- 主控:STM32F103C8T6(或其他系列,如F4/F7,性能更佳)。
- 开发环境:Keil MDK-ARM 或 STM32CubeIDE。
- 调试工具:ST-LINK/V2 下载调试器。
- 核心硬件:
- MPU6050模块(带或不带DMP)。
- 带编码器的直流减速电机(编码器用于速度环反馈)。
- 电机驱动板(如TB6612)。
- 锂电池及降压模块(为STM32和驱动板供电)。
- 软件库:通常需要HAL库或标准库。本文示例基于HAL库,因其可移植性更好。
2.2 工程模块划分
建议在STM32CubeMX生成代码的基础上,创建以下模块文件夹:
/Project /Core /Drivers /User /App balance_task.c/h // 主控制任务,调度所有模块 pid.c/h // PID算法实现 /Bsp mpu6050.c/h // MPU6050驱动(I2C) encoder.c/h // 编码器读取(定时器编码器模式) motor.c/h // 电机PWM控制(定时器PWM输出) filter.c/h // 滤波算法(互补滤波等) /Utils delay.c/h // 精确延时(SysTick) usart.c/h // 调试串口打印使用STM32CubeMX进行图形化配置是高效的第一步。主要配置项如下表:
| 外设 | 模式 | 参数说明 | 用途 |
|---|---|---|---|
| SYS | Debug | Serial Wire | 启用SWD调试 |
| RCC | HSE | Crystal/Ceramic Resonator | 使用外部高速时钟 |
| I2C1 | I2C | 标准模式 (100kHz) 或快速模式 (400kHz) | 连接MPU6050 |
| TIM1 | PWM Generation CH1, CH2, CH3, CH4 | 预分频和周期决定PWM频率,如72M/72/1000=1kHz | 生成4路PWM驱动两个电机 |
| TIM2 | Encoder Mode | 编码器接口模式,上下沿计数 | 读取电机编码器A、B相 |
| TIM3 | Encoder Mode | 同上 | 读取另一个电机编码器 |
| TIM4 | Basic Timer | 预分频和周期决定中断频率,如72M/7200/100=10ms | 产生10ms定时中断,作为控制周期 |
| USART1 | Asynchronous | 波特率115200 | 调试信息输出 |
配置完成后生成代码,初始化工作就完成了大部分。
3. 核心驱动模块实现
控制逻辑建立在可靠的底层驱动之上。我们需要先确保能正确读取传感器数据和驱动电机。
3.1 MPU6050驱动与数据读取
MPU6050通过I2C通信。首先实现基本的读写函数。
// mpu6050.c #include "mpu6050.h" #include "i2c.h" // HAL库I2C头文件 #define MPU6050_ADDR 0xD0 // 默认地址,左移一位后为0xD0 #define SMPLRT_DIV_REG 0x19 #define PWR_MGMT_1_REG 0x6B #define ACCEL_CONFIG_REG 0x1C #define GYRO_CONFIG_REG 0x1B #define ACCEL_XOUT_H_REG 0x3B uint8_t MPU6050_Init(void) { uint8_t check; HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, 0x75, 1, &check, 1, 100); // 读取WHO_AM_I寄存器 if(check != 0x68) return 1; // 器件ID错误 // 唤醒MPU6050,设置时钟源 uint8_t data = 0x00; HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, PWR_MGMT_1_REG, 1, &data, 1, 100); // 设置陀螺仪量程 ±2000°/s data = 0x18; HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, GYRO_CONFIG_REG, 1, &data, 1, 100); // 设置加速度计量程 ±8g data = 0x10; HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, ACCEL_CONFIG_REG, 1, &data, 1, 100); // 设置采样率分频器 (1kHz/(1+9)=100Hz) data = 0x09; HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, SMPLRT_DIV_REG, 1, &data, 1, 100); return 0; } void MPU6050_Read_Accel(MPU6050_Data *mpu_data) { uint8_t buf[6]; HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, ACCEL_XOUT_H_REG, 1, buf, 6, 100); mpu_data->accel_x = (int16_t)(buf[0] << 8 | buf[1]); mpu_data->accel_y = (int16_t)(buf[2] << 8 | buf[3]); mpu_data->accel_z = (int16_t)(buf[4] << 8 | buf[5]); } void MPU6050_Read_Gyro(MPU6050_Data *mpu_data) { uint8_t buf[6]; HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, 0x43, 1, buf, 6, 100); // GYRO_XOUT_H mpu_data->gyro_x = (int16_t)(buf[0] << 8 | buf[1]); mpu_data->gyro_y = (int16_t)(buf[2] << 8 | buf[3]); mpu_data->gyro_z = (int16_t)(buf[4] << 8 | buf[5]); }注意:原始数据是int16类型,需要根据量程转换为物理量。例如,加速度计量程为±8g时,灵敏度为4096 LSB/g。所以
accel_x_g = accel_x_raw / 4096.0。陀螺仪量程为±2000°/s时,灵敏度为16.4 LSB/(°/s)。所以gyro_x_dps = gyro_x_raw / 16.4。
3.2 编码器速度测量
使用STM32定时器的编码器接口模式是最佳选择,硬件自动计数,不占用CPU。
// encoder.c #include "encoder.h" // 定时器2和3已由CubeMX配置为编码器模式 int32_t Get_Encoder_CNT(TIM_HandleTypeDef *htim) { int32_t cnt; cnt = (int32_t)(__HAL_TIM_GET_COUNTER(htim)); // 获取当前计数值 __HAL_TIM_SET_COUNTER(htim, 0); // 读取后清零,方便下次计算增量 return cnt; } // 在定时中断中调用,计算速度(脉冲数/控制周期) int32_t g_left_speed, g_right_speed; void Encoder_Calculate_Speed(void) { static int32_t left_last = 0, right_last = 0; int32_t left_now = Get_Encoder_CNT(&htim2); int32_t right_now = Get_Encoder_CNT(&htim3); // 简单差分得到速度,可加入低通滤波 g_left_speed = left_now - left_last; g_right_speed = right_now - right_last; left_last = left_now; right_last = right_now; }3.3 电机PWM驱动
通过改变定时器通道的占空比来控制电机速度和方向。
// motor.c #include "motor.h" // 电机PWM输出范围,取决于定时器的自动重载值ARR #define PWM_MAX 1000 #define PWM_MIN -1000 void Motor_Set_PWM(int16_t left_pwm, int16_t right_pwm) { // 限幅 if(left_pwm > PWM_MAX) left_pwm = PWM_MAX; if(left_pwm < -PWM_MAX) left_pwm = -PWM_MAX; if(right_pwm > PWM_MAX) right_pwm = PWM_MAX; if(right_pwm < -PWM_MAX) right_pwm = -PWM_MAX; // 根据正负值,控制两个电机的方向引脚和PWM占空比 // 假设驱动逻辑:IN1=1, IN2=0 正转; IN1=0, IN2=1 反转; PWM控制速度 if(left_pwm >= 0) { HAL_GPIO_WritePin(MOTOR_LEFT_IN1_GPIO_Port, MOTOR_LEFT_IN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(MOTOR_LEFT_IN2_GPIO_Port, MOTOR_LEFT_IN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, left_pwm); // CH1对应左电机PWM } else { HAL_GPIO_WritePin(MOTOR_LEFT_IN1_GPIO_Port, MOTOR_LEFT_IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_LEFT_IN2_GPIO_Port, MOTOR_LEFT_IN2_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, -left_pwm); } // 右电机同理,使用CH2 if(right_pwm >= 0) { HAL_GPIO_WritePin(MOTOR_RIGHT_IN1_GPIO_Port, MOTOR_RIGHT_IN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(MOTOR_RIGHT_IN2_GPIO_Port, MOTOR_RIGHT_IN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, right_pwm); } else { HAL_GPIO_WritePin(MOTOR_RIGHT_IN1_GPIO_Port, MOTOR_RIGHT_IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTOR_RIGHT_IN2_GPIO_Port, MOTOR_RIGHT_IN2_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, -right_pwm); } }4. 姿态解算与控制算法实现
底层驱动就绪后,进入核心算法层。
4.1 互补滤波姿态解算
// filter.c #include "filter.h" #include "math.h" float Complementary_Filter(float angle_acc, float gyro, float *angle, float dt, float alpha) { // angle_acc: 由加速度计计算出的角度(atan2(accY, accZ)) // gyro: 陀螺仪角速度(已转换为度/秒) // *angle: 上一周期的融合角度,同时作为输出 // dt: 采样周期(秒),如0.01 // alpha: 滤波系数,通常0.98-0.995 // 陀螺仪积分得到角度预测 float angle_gyro = (*angle) + gyro * dt; // 互补滤波融合 *angle = alpha * angle_gyro + (1 - alpha) * angle_acc; return *angle; } // 在应用层调用 MPU6050_Data mpu; float g_angle, g_gyro; // 全局变量,存储最终角度和角速度 void Attitude_Update(float dt) { float acc_angle; // 1. 读取原始数据 MPU6050_Read_Accel(&mpu); MPU6050_Read_Gyro(&mpu); // 2. 转换为物理量(示例,需根据实际量程调整) float acc_x = mpu.accel_x / 4096.0; float acc_y = mpu.accel_y / 4096.0; float acc_z = mpu.accel_z / 4096.0; float gyro_y = mpu.gyro_y / 16.4; // 假设绕Y轴旋转是俯仰角 // 3. 由加速度计算角度(弧度),再转为度 acc_angle = atan2(acc_x, acc_z) * 57.29578f; // 57.29578 = 180/PI // 4. 互补滤波 g_angle = Complementary_Filter(acc_angle, gyro_y, &g_angle, dt, 0.98); g_gyro = gyro_y; // 记录角速度用于PD控制 }4.2 PID控制器实现
实现一个通用的位置式PID控制器。
// pid.c #include "pid.h" void PID_Init(PID_TypeDef *pid, float kp, float ki, float kd, float max_out, float max_iout) { pid->Kp = kp; pid->Ki = ki; pid->Kd = kd; pid->max_out = max_out; pid->max_iout = max_iout; pid->integral = 0; pid->prev_error = 0; } float PID_Calculate(PID_TypeDef *pid, float target, float current) { float error = target - current; pid->integral += error; // 积分限幅,防止积分饱和 if(pid->integral > pid->max_iout) pid->integral = pid->max_iout; else if(pid->integral < -pid->max_iout) pid->integral = -pid->max_iout; float derivative = error - pid->prev_error; pid->prev_error = error; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; // 输出限幅 if(output > pid->max_out) output = pid->max_out; else if(output < -pid->max_out) output = -pid->max_out; return output; }4.3 主控制任务调度
将所有模块在定时中断中串联起来。假设我们使用TIM4产生10ms中断。
// balance_task.c #include "balance_task.h" #include "mpu6050.h" #include "filter.h" #include "encoder.h" #include "pid.h" #include "motor.h" PID_TypeDef pid_angle; // 直立环PD控制器 PID_TypeDef pid_speed; // 速度环PI控制器 float target_angle = 0.0; // 直立环目标角度 float target_speed = 0.0; // 速度环目标速度(通常为0,或由遥控给定) float motor_out; // 最终输出给电机的PWM值 void Balance_Init(void) { MPU6050_Init(); // 初始化PID参数,这些参数需要实际调试 PID_Init(&pid_angle, 200.0, 0.0, 1.2, 1000, 1000); // 直立环:P=200, D=1.2 PID_Init(&pid_speed, 10.0, 0.5, 0.0, 100, 1000); // 速度环:P=10, I=0.5 } void Balance_Task(void) { // 此函数在10ms定时中断中调用 // 1. 更新姿态 Attitude_Update(0.01); // dt = 10ms = 0.01s // 2. 更新编码器速度 Encoder_Calculate_Speed(); float current_speed = (g_left_speed + g_right_speed) / 2.0; // 取平均速度 // 3. 速度环PI控制:计算为平衡速度所需的角度补偿量 float angle_adjust = PID_Calculate(&pid_speed, target_speed, current_speed); // 4. 直立环PD控制:目标角度 = 平衡点 + 速度环补偿 float final_target_angle = target_angle + angle_adjust; motor_out = PID_Calculate(&pid_angle, final_target_angle, g_angle); // 5. 输出到电机(左右电机输出相同,用于平衡) Motor_Set_PWM((int16_t)motor_out, (int16_t)motor_out); }在stm32f1xx_it.c的中断服务函数中调用:
void TIM4_IRQHandler(void) { if(__HAL_TIM_GET_FLAG(&htim4, TIM_FLAG_UPDATE) != RESET) { __HAL_TIM_CLEAR_FLAG(&htim4, TIM_FLAG_UPDATE); Balance_Task(); // 核心控制任务 } }5. 系统调试、参数整定与问题排查
代码编写完成后,距离小车成功站立还有最关键的一步:调试。这是一个“理论->实践->观察->调整”的循环过程。
5.1 调试准备与安全措施
- 硬件保护:调试时,务必用东西撑住小车,让轮子悬空。防止参数错误导致小车突然猛冲,损坏硬件或伤人。
- 调试接口:确保串口(USART)正常工作,用于打印关键变量(角度、角速度、PWM输出、速度等)。可以使用
printf重定向或简单的发送函数。 - 参数记录:准备一个笔记本或电子表格,记录每次调整的参数和对应的现象。
5.2 PID参数调试步骤(“先直立,后速度”)
调试必须分步进行,遵循“先内环后外环”、“先比例后微分积分”的原则。
第一步:调试直立环(PD)
- 将速度环的PID参数全部设为0(
Kp=Ki=Kd=0),使其不工作。此时angle_adjust恒为0。 - 手持小车,使其倾斜,观察串口输出的
g_angle值。检查角度符号是否正确(前倾为正?后仰为负?)。如果不符,在计算acc_angle时取反。 - 先调P(比例):设定一个较小的
Kp(如50),D=0。用手轻轻推动小车,它应该会朝你推的反方向转动(试图“扶正”自己)。如果转动方向错误,将Kp改为负数。 - 逐渐增大
Kp,小车的回复力会变强。直到小车在平衡点附近开始高频振荡(“哆嗦”)。此时说明P值过大。 - 加入D(微分):将
Kp回调到振荡前的大小,然后逐渐加入Kd(如从0.5开始)。D的作用是抑制振荡。增加Kd,振荡应该减弱,小车变得更“沉稳”。但Kd过大会引入高频噪声,可能引起电机啸叫或抖动,需要配合软件低通滤波。
关键现象判断:当直立环PD参数基本合适时,用手使小车偏离平衡点后松开,小车应能快速、平稳地回到平衡点,且没有持续振荡。在悬空状态下,轻轻拨动轮子,小车应能抵抗扰动。
第二步:加入速度环(PI)
- 保持直立环参数不变。
- 先调速度环P:给
pid_speed.Kp一个很小的正值(如1.0)。此时,如果你用手轻轻转动一个轮子(给小车一个速度),小车为了消除这个速度,会通过速度环输出一个angle_adjust,使直立环的目标角度发生微小改变,从而让小车朝相反方向转动来“抵消”你给的速度。 - 测试小车在悬空时的“定住”能力:快速拨动一下轮子然后松开,看小车能否自己将轮子停回原处。如果不能,缓慢增大
Kp。 - 加入速度环I:如果小车在平衡时,轮子会缓慢地朝一个方向持续转动(积分饱和现象),说明存在静态误差。此时需要加入较小的
Ki(如0.1),让速度环能最终消除这个误差。Ki太大会引起系统超调或振荡。 - 微调:同时观察直立稳定性和速度环的纠偏能力,对两组参数进行微调。
5.3 常见问题与排查清单
调试过程中一定会遇到问题,下表列出了典型现象及排查思路:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 小车毫无反应,电机不转 | 1. 电机驱动板未供电或使能。 2. PWM输出引脚错误或未初始化。 3. 控制输出 motor_out恒为0。4. 中断未正确触发。 | 1. 检查驱动板电源和使能引脚电平。 2. 用示波器或万用表检查PWM引脚是否有输出。 3. 打印 g_angle和motor_out值,看计算逻辑。4. 检查定时器中断配置和优先级。 |
| 小车朝一个方向猛冲 | 1. 角度极性错误(前倾/后仰判断反)。 2. 电机输出极性错误(正转/反转接反)。 3. PID参数 Kp或Ki极大。 | 1. 手持小车,观察串口角度值,倾斜方向与数值正负应一致。 2. 单独测试电机驱动函数,确认PWM正负值与转向对应关系。 3. 将PID参数暂时设为零进行测试。 |
| 小车剧烈振荡(高频抖动) | 1. 直立环Kp过大。2. 微分项 Kd过小或为0。3. 控制周期 dt不稳定或过长。4. 传感器数据噪声大。 | 1. 减小Kp。2. 适当增加 Kd。3. 确保定时中断周期稳定(如10ms),检查是否有其他任务阻塞中断。 4. 对陀螺仪原始数据或融合后的角度进行低通滤波。 |
| 小车缓慢来回摆动(低频振荡) | 1. 微分项Kd过大,引入了相位滞后。2. 速度环 Ki过大。3. 机械结构松动。 | 1. 减小Kd。2. 减小速度环 Ki。3. 紧固电机、轮子等连接件。 |
| 角度值漂移严重 | 1. 陀螺仪零漂未校准。 2. 互补滤波系数 alpha不合适。3. 加速度计受振动干扰大。 | 1. 上电静止一段时间,读取陀螺仪输出,将其平均值作为零偏在程序中减去。 2. 适当增大 alpha(更信任陀螺仪),但会增加长期漂移。3. 考虑使用更复杂的卡尔曼滤波,或对加速度计数据进行滑动平均。 |
| 响应遥控指令迟钝 | 1. 速度环PID参数过小。 2. 控制输出被限幅。 3. 遥控指令处理有延迟。 | 1. 适当增大速度环Kp。2. 检查 pid_speed.max_out是否太小,限制了角度补偿量。3. 确保遥控数据接收和处理在控制周期内完成。 |
5.4 进阶调试技巧
- 数据可视化:如果条件允许,可以将STM32的传感器数据、中间变量通过串口发送到上位机(如
SerialChart、PlotJuggler或自己写的Python脚本)绘制曲线,直观观察系统响应。 - 参数固化:调试出稳定参数后,可以将其存储在STM32的Flash中,避免每次上电重新设置。
- 加入死区:在电机输出接近0时,设置一个死区(如
if(abs(pwm) < 10) pwm = 0),可以消除电机启动静摩擦带来的抖动。 - 软件低通滤波:对陀螺仪数据
g_gyro进行一阶低通滤波,可以平滑微分项,减少高频噪声的影响。
6. 从调试到稳定运行的最佳实践
当小车能在你手中稳定平衡后,可以考虑以下优化点,使其更可靠、更智能。
6.1 增加启动与保护机制
裸奔的控制循环很脆弱,需要增加安全逻辑。
// 在Balance_Task()开头或主循环中加入 void Balance_Task(void) { // 1. 上电自检/校准 static uint8_t is_calibrated = 0; if(!is_calibrated) { // 例如:等待1秒,计算陀螺仪零偏 // 或者等待角度稳定在较小范围内 if(/* 校准完成条件 */) { is_calibrated = 1; } else { Motor_Set_PWM(0, 0); // 校准期间电机停转 return; } } // 2. 倾角过大保护 if(fabs(g_angle) > 45.0) { // 如果倾角超过45度,认为即将摔倒 Motor_Set_PWM(0, 0); // 紧急停止 // 可以同时触发声光报警 return; } // 3. 正常控制逻辑... // Attitude_Update(...); etc. }6.2 优化控制周期与实时性
- 固定周期:确保
Balance_Task在精确的定时中断中执行,使用硬件定时器,不要用HAL_Delay。 - 中断优先级:控制任务中断的优先级应高于其他非实时任务(如串口接收)。
- 减少计算量:在中断服务函数中只做必要的计算和赋值,将复杂的调试打印移到主循环中。
6.3 扩展功能方向
当基础平衡实现后,你可以以此为平台,添加更多功能:
- 遥控控制:通过蓝牙(如HC-05/06)或2.4G模块(如NRF24L01)接收指令,修改
target_speed或target_angle,实现前后左右移动和转向。 - 循迹/避障:增加红外或超声波传感器,实现自动巡线或避障。
- 姿态升级:将互补滤波替换为卡尔曼滤波,获得更优的姿态估计。
- 无线调试:通过无线模块将实时数据发送到电脑,实现无线参数调试和监控。
- 上位机配置:编写一个简单的PC端工具,通过串口实时修改PID参数并观察效果,极大提升调试效率。
平衡小车项目是嵌入式软硬件结合的绝佳练手机会。它迫使你去理解传感器特性、掌握实时控制思想、亲手调试参数并分析系统响应。整个过程的关键在于耐心和系统性的调试方法:先确保每个模块单独工作,再逐步闭环;先调稳一个环,再加入下一个环;大胆假设,小心验证,仔细观察现象。当你看到小车颤颤巍巍地第一次自己站稳时,那种成就感就是对所有努力的最好回报。接下来,试着让它走起来,转起来,甚至跳个舞,你会发现这片天地更加广阔。