大家好,我是专注于嵌入式与智能控制开发的博主。最近,2026年全国大学生电子设计竞赛(电赛)的赛题方向备受关注,其中“车载平衡滚球运动”作为一个融合了经典倒立摆控制与移动机器人技术的综合性题目,对参赛者的机械设计、传感器融合、控制算法和系统集成能力提出了很高的要求。本文将围绕这一主题,提供一个从零开始的完整示例项目,涵盖系统设计、核心代码实现、PID调参技巧以及常见问题排查。无论你是初次接触电赛的新手,还是希望深化控制理论实践的开发者,都能通过本文搭建一个可运行、可调试的平衡滚球运动平台原型。
1. 项目背景与核心概念解析
1.1 什么是“车载平衡滚球运动”?
“车载平衡滚球运动”是一个典型的机电一体化控制问题。其核心目标是:设计一个可移动的小车(车体),在其顶部安装一个平板(平衡平台),平台上放置一个滚球。小车需要具备两个基本能力:
- 平衡能力:通过控制小车的左右移动,来抵消滚球在平板上的滚动趋势,使滚球始终稳定在平板中心附近。
- 运动能力:在保持滚球平衡的前提下,控制小车整体按照预定轨迹(如直线、曲线)运动,或完成指定任务(如将球运送到目标区域)。
这本质上是一个二级倒立摆问题的变体。一级倒立摆是控制摆杆不倒,而这里是控制平板上的球不滚落,同时车体本身也是一个被控对象,复杂度更高。
1.2 技术挑战与核心知识点
完成该项目,你需要掌握以下核心技术栈:
- 硬件平台:单片机(如STM32、ESP32)、电机驱动模块(如TB6612、DRV8833)、直流减速电机与编码器、姿态传感器(如MPU6050、陀螺仪加速度计模块)、测距传感器(如超声波、激光测距用于球位置检测)。
- 控制理论:PID控制算法是基础且核心的方法。通常需要两个PID环:内环用于快速响应,控制小车移动以平衡球;外环用于精确控制,使球到达目标位置。
- 传感器融合:如何准确、实时地获取滚球在平板上的位置(X, Y坐标)是小车实现平衡的关键。常见方案有摄像头视觉识别、红外阵列、电阻式触摸屏或多个测距传感器组合。
- 系统建模与仿真:在动手制作前,使用MATLAB/Simulink或Python进行动力学建模和算法仿真,可以大幅降低实物调试的难度和风险。
1.3 应用场景与学习价值
除了电赛,此类技术广泛应用于:
- 教育机器人:直观演示经典控制理论。
- 精密运输平台:在半导体、生物实验室中,平稳运输对振动敏感的物品。
- 自平衡机器人:是两轮自平衡车、独轮车等更复杂系统的基础。 掌握它,意味着你打通了从传感器数据采集、滤波处理、控制算法到电机驱动的全链路嵌入式开发能力。
2. 系统设计与环境准备
2.1 整体系统架构
我们设计一个简化版的系统,采用二维控制(仅考虑球在平板一条轴上的运动),便于理解和实现。
- 被控对象:滚球。
- 执行机构:由两个直流电机驱动的双轮小车。
- 传感器:
- 滚球位置检测:使用一个超声波传感器(HC-SR04)安装在平板一端,测量球到边缘的距离,间接推算球心位置。这是成本较低的方案。更优方案是使用线性CCD或摄像头。
- 车体姿态检测:使用MPU6050获取车体倾角(俯仰角),用于补偿平板不平带来的影响(本例简化,暂不考虑)。
- 电机速度反馈:电机编码器,用于实现速度闭环,使控制更平滑。
- 控制器:STM32F103C8T6(蓝桥杯/电赛常用核心板)。
- 控制算法:串级PID控制。
系统框图如下:
[目标球位置] --> [外环位置PID] --> [目标小车速度] --> [内环速度PID] --> [PWM驱动] --> [电机/小车] ^ | | | [超声波测距] <-- [滚球] <-- [平板] <-- [小车车体] <-- [编码器反馈] <-----------------------|2.2 硬件清单与连接
| 组件 | 型号/说明 | 连接至STM32 |
|---|---|---|
| 主控 | STM32F103C8T6 | - |
| 电机驱动 | TB6612FNG | PWMA: PA8, AIN1: PA9, AIN2: PA10 (电机A) PWMB: PB13, BIN1: PB14, BIN2: PB15 (电机B) |
| 直流减速电机 | 带编码器, 6V | 电机线接TB6612输出,编码器A相接PA0/PA1 (TIM2), B相接PA2/PA3 (TIM5) |
| 超声波 | HC-SR04 | Trig: PB0, Echo: PB1 |
| MPU6050 | GY-521模块 | SCL: PB6, SDA: PB7 (I2C1) |
| 电源 | 18650电池组 7.4V | 为TB6612供电,STM32由USB或稳压模块供电 |
2.3 软件开发环境
- IDE: Keil uVision 5 或 STM32CubeIDE
- 固件库: HAL库(本文示例基于HAL库,易于移植)
- 核心工具: STM32CubeMX(用于图形化配置引脚和生成初始化代码)
- 调试工具: 串口助手(用于打印调试信息、PID参数)
3. 核心控制原理与算法拆解
3.1 滚球位置感知
我们假设平板长度为 L(例如30cm)。超声波传感器安装在平板一端,测量值为distance_cm(球面到传感器的距离)。设球半径为 r。 则球心在平板上的位置ball_position(以平板中心为原点)可近似计算为:
ball_position = (L - distance_cm - r) - L/2值为正表示球在中心右侧,为负表示在左侧。需要定期(如10ms)采样并滤波(如均值滤波或卡尔曼滤波)以减少噪声。
3.2 串级PID控制算法
这是本项目的核心。我们采用两个PID控制器串联。
外环(位置环):
- 输入(误差):目标球位置(通常为0,即平板中心)与当前
ball_position的差值。 - 输出:期望的小车运动速度
target_velocity。这个速度是为了让球回到中心而需要小车移动的速度。 - 作用:实现“球到哪,车就跟到哪”的宏观平衡策略。P参数过大,小车会抖动;过小,反应迟钝。
- 输入(误差):目标球位置(通常为0,即平板中心)与当前
内环(速度环):
- 输入(误差):外环输出的
target_velocity与通过编码器计算出的当前小车实际速度current_velocity的差值。 - 输出:最终的PWM占空比,直接驱动电机。
- 作用:确保小车能精确、快速地达到外环要求的速度。它让电机响应更平滑,克服阻力,提高系统稳定性。
- 输入(误差):外环输出的
算法伪代码:
// 每10ms执行一次的控制循环 void Control_Loop() { // 1. 读取传感器 float ball_pos = Get_Ball_Position(); // 经过滤波的位置 float car_vel = Get_Car_Velocity(); // 编码器计算的速度 // 2. 外环PID计算(位置环) float target_vel = PID_Outer_Calculate(0.0, ball_pos); // 目标位置是0 // 3. 内环PID计算(速度环) float pwm_output = PID_Inner_Calculate(target_vel, car_vel); // 4. 输出PWM,驱动电机 Set_Motor_PWM(pwm_output); }3.3 PID参数整定思路
调参是成败关键。务必遵循“先内后外,先P后I再D”的原则:
单独调试速度环(内环):
- 将外环断开,直接给一个固定的
target_vel。 - P:从小到大增加,直到小车能快速响应速度指令,但开始出现轻微振荡。
- I:加入少许I,消除静差(例如给定速度后,实际速度稳定值略低于目标)。
- D:如果响应超调严重或振荡,加入少量D抑制。
- 目标:小车能平稳、准确地跟随阶跃速度信号。
- 将外环断开,直接给一个固定的
闭合外环调试位置环:
- 将内环参数固定,闭合外环。
- P:从小开始,观察小车是否能开始尝试平衡球(车会随着球动)。增大P直到系统开始发散(球来回滚动幅度越来越大)。
- 将此时P值的60%~70%作为初始值。
- I:如果球始终无法稳定在中心(存在稳态误差),加入很小的I。
- D:如果球在中心附近高频抖动,加入少量D。
- 关键:外环的响应速度必须慢于内环。如果外环变化太快,内环跟不上,系统就会崩溃。
4. 完整实战代码示例
4.1 STM32CubeMX工程配置
- 创建STM32F103C8工程。
- 时钟:配置为72MHz。
- 引脚:
- 电机PWM:配置TIM1_CH1 (PA8) 和 TIM1_CH4 (PB13) 为PWM Generation CHx。
- 电机方向:配置PA9, PA10, PB14, PB15为GPIO_Output。
- 编码器:配置TIM2和TIM5为Encoder Mode。
- 超声波:配置PB0为GPIO_Output (Trig), PB1配置为输入捕获(或EXTI中断),这里简化为GPIO_Input并配合定时器做延时测量。
- MPU6050:配置I2C1。
- 串口:配置USART1用于调试打印(PA9-TX, PA10-RX,注意与电机方向引脚冲突,需调整)。
- 生成代码。
4.2 PID控制器实现 (pid.c / pid.h)
// pid.h #ifndef __PID_H #define __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 output_min; float integral_limit; // 积分限幅 } PID; void PID_Init(PID *pid, float kp, float ki, float kd, float max_out, float i_limit); float PID_Calculate(PID *pid, float target, float actual); #endif// pid.c #include "pid.h" void PID_Init(PID *pid, float kp, float ki, float kd, float max_out, float i_limit) { pid->target = pid->actual = 0.0f; pid->err = pid->err_last = pid->err_sum = 0.0f; pid->Kp = kp; pid->Ki = ki; pid->Kd = kd; pid->output = 0.0f; pid->output_max = max_out; pid->output_min = -max_out; pid->integral_limit = i_limit; } float PID_Calculate(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_limit) pid->err_sum = pid->integral_limit; if (pid->err_sum < -pid->integral_limit) pid->err_sum = -pid->integral_limit; // 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_min) pid->output = pid->output_min; pid->err_last = pid->err; return pid->output; }4.3 主控制逻辑 (main.c 部分关键代码)
// main.c #include "main.h" #include "pid.h" #include "motor.h" #include "encoder.h" #include "ultrasonic.h" #include "mpu6050.h" #include <stdio.h> // 用于printf // 定义PID结构体 PID pid_outer; // 外环-位置环 PID pid_inner; // 内环-速度环 // 全局变量 float ball_position = 0.0f; float car_velocity = 0.0f; float target_position = 0.0f; // 目标:球在中心 // 定时器中断回调函数(10ms周期) void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { // 使用一个基本定时器TIM6产生10ms中断 // 1. 读取传感器数据 ball_position = Get_Filtered_Ball_Position(); // 自定义函数,包含超声波读取和滤波 car_velocity = Get_Car_Velocity_From_Encoder(); // 自定义函数,编码器脉冲换算速度 // 2. 外环PID计算:输入目标位置和球位置,输出目标车速 float target_velocity = PID_Calculate(&pid_outer, target_position, ball_position); // 3. 内环PID计算:输入目标车速和实际车速,输出PWM值 float pwm_out = PID_Calculate(&pid_inner, target_velocity, car_velocity); // 4. 设置电机PWM (pwm_out为正向右,为负向左) if (pwm_out >= 0) { Motor_Set_Direction(FORWARD); Motor_Set_PWM((uint32_t)pwm_out); } else { Motor_Set_Direction(BACKWARD); Motor_Set_PWM((uint32_t)(-pwm_out)); } // 5. 调试信息(通过串口打印,可注释掉以节省时间) // printf("Pos:%.2f, Vel:%.2f, PWM:%.2f\r\n", ball_position, car_velocity, pwm_out); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM_Init(); // 初始化PWM、编码器、基础定时器TIM6 MX_USART1_UART_Init(); MX_I2C1_Init(); // 初始化外环PID (参数需实际调试) PID_Init(&pid_outer, 1.5f, 0.01f, 0.2f, 50.0f, 100.0f); // 输出限幅±50,积分限幅100 // 初始化内环PID PID_Init(&pid_inner, 10.0f, 0.5f, 0.05f, 7200.0f, 500.0f); // PWM限幅对应ARR值 // 启动10ms定时器中断 HAL_TIM_Base_Start_IT(&htim6); while (1) { // 主循环可处理其他任务,如接收上位机指令改变target_position // 例如:让球运动到平板左侧5cm处 // target_position = -5.0f; HAL_Delay(100); } }4.4 传感器与执行器驱动示例
超声波读取函数(简化版,未使用中断):
// ultrasonic.c float Get_Ball_Position(void) { float distance_cm = 0.0f; // 发送10us高脉冲触发 HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(10); HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); // 等待回响引脚变高,开始计时 while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_RESET); uint32_t start_time = HAL_GetTick(); // 注意:此方法精度较低,建议用定时器捕获 // 等待回响引脚变低,结束计时 while(HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET); uint32_t end_time = HAL_GetTick(); // 计算距离 (声速340m/s) distance_cm = (end_time - start_time) * 0.034 / 2; // 单位ms,换算需调整 // 转换为球心位置 (假设平板长30cm,球半径2cm) const float L = 30.0f, r = 2.0f; float pos = (L - distance_cm - r) - L/2.0f; return pos; }编码器速度计算:
// encoder.c int32_t Get_Encoder_Counts(void) { // 读取TIM2的计数器值(注意计数器可能溢出) int32_t cnt = (int32_t)__HAL_TIM_GET_COUNTER(&htim2); __HAL_TIM_SET_COUNTER(&htim2, 0); // 读取后清零,计算增量 return cnt; } float Get_Car_Velocity_From_Encoder(void) { static int32_t last_count = 0; int32_t current_count = Get_Encoder_Counts(); int32_t diff = current_count - last_count; last_count = current_count; // 计算速度:diff(脉冲数) / 时间间隔(10ms) * 转换系数 // 转换系数 = (轮子周长) / (编码器每转脉冲数) const float wheel_circumference = 20.0f; // cm,假设 const int pulses_per_rev = 780; // 编码器分辨率 float vel_cm_per_s = (diff / 780.0f) * wheel_circumference * 100; // 乘以100是因为10ms=0.01s return vel_cm_per_s; }5. 系统调试与常见问题排查
调试是项目成功的关键,务必循序渐进。
5.1 分模块调试流程
- 电机与驱动测试:编写代码让电机正反转,确认PWM和方向控制正常。
- 编码器测试:旋转电机,通过串口打印编码器计数值,确认计数方向正确、数值连续。
- 超声波测试:固定传感器,测量不同距离的物体,验证读数准确性和稳定性。
- MPU6050测试:读取原始数据并计算姿态角,确认I2C通信正常。
- 速度环单独测试:给定目标速度,观察电机能否跟随,调整内环PID至响应快速无振荡。
- 整体开环测试:手动移动球,观察串口打印的
ball_position是否变化符合预期。 - 闭环调试:先用手扶住小车,让PID工作,感受电机出力方向是否正确(球往左滚,车应该往左追)。然后尝试缓慢放手。
5.2 常见问题与解决方案
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| 上电后电机狂转或不动 | 1. 电源接错或电压不足。 2. PWM输出引脚配置错误。 3. 电机驱动模块使能信号未拉高。 | 1. 用万用表检查电机驱动模块供电电压。 2. 用示波器或LED检查PWM引脚是否有输出。 3. 检查TB6612的STBY引脚是否接高电平。 |
| 编码器读数始终为0或不变 | 1. 编码器AB相接反或接触不良。 2. 定时器未配置为编码器模式。 3. 编码器供电问题。 | 1. 交换A、B相线序试试。 2. 在CubeMX中确认TIMx配置为“Encoder Mode”。 3. 检查编码器是否有5V或3.3V供电。 |
| 超声波测距不准或跳动大 | 1. 触发和回响信号受干扰。 2. 测量时序不精确(用HAL_GetTick精度低)。 3. 被测物体表面不规整。 | 1. 给Trig和Echo信号线加短延时和滤波电容。 2.强烈建议使用定时器输入捕获模式精确测量高电平时间。 3. 对连续多次采样进行中值滤波或均值滤波。 |
| 小球平衡不住,来回滚动幅度越来越大 | 1. 外环PID参数(尤其是P)过大,系统发散。 2. 传感器数据噪声大,导致控制指令混乱。 3. 内环(速度环)响应太慢,跟不上外环。 | 1.大幅减小外环P值,从非常小的值开始调。 2. 加强传感器数据滤波。 3. 先确保内环性能足够好(响应快且稳定)。 |
| 小球能平衡但始终偏离中心 | 存在稳态误差。 | 1. 适当增加外环的I(积分)参数。 2. 检查机械结构是否对称,平台是否水平。 3. 检查超声波安装是否歪斜,导致零点偏移。 |
| 系统在高频小幅抖动 | 微分项D或比例项P引入高频噪声。 | 1. 检查传感器数据是否本身有高频毛刺,加强滤波。 2. 尝试减小D参数,或对误差进行低通滤波后再进行D项计算。 3. 可能是P值仍偏大。 |
| 控制响应迟钝,球滚远后车才动 | 外环P值太小,或内环输出限幅太小。 | 1. 在系统稳定的前提下,逐步增大外环P值。 2. 增大内环PID的输出限幅,让电机有更大的出力。 |
5.3 高级调试技巧
- 串口绘图:将
ball_position,target_velocity,pwm_out等关键变量通过串口发送到电脑,使用SerialPlot、VOFA+等工具绘制实时曲线,直观观察系统响应和振荡情况。 - 参数整定助手:可以编写简单上位机,通过串口实时修改PID参数并下发给STM32,避免反复烧录程序。
- 加入“软启动”:在系统初始化或目标位置突变时,对
target_position进行斜坡变化,避免阶跃信号对系统造成过大冲击。
6. 优化方向与进阶扩展
完成基础平衡后,你可以从以下方向提升项目:
6.1 从一维到二维平衡
当前示例只控制一个方向(前后)。要实现真正的平衡,需要扩展到二维(X-Y平面)。
- 硬件:需要两个正交安装的超声波传感器,或一个摄像头进行图像处理。
- 控制:需要两组独立的串级PID控制器,分别控制X轴和Y轴。需要注意两个轴之间的耦合可能很弱,可以独立控制。
6.2 更优的球位置检测方案
- 摄像头+OpenMV/OpenCV:在平板上方架设摄像头,通过颜色识别或特征识别追踪球,精度高,能获取XY坐标。
- 电阻式触摸屏:将整个平板换成触摸屏,直接读取球压下的坐标,响应快,但成本高且需处理压力信号。
- 红外对管阵列:在平板下方布置网格状的红外发射接收管,通过遮挡判断球位置,成本适中,精度尚可。
6.3 先进控制算法
- 模糊PID:针对PID参数难以适应不同工况(如球速变化)的问题,可以用模糊规则动态调整PID参数。
- 线性二次型调节器(LQR):基于状态空间模型的最优控制器,性能通常优于PID,但需要精确的系统数学模型。
- 模型预测控制(MPC):更高级的算法,能够处理约束(如电机PWM限幅),但计算量大,对单片机性能要求高。
6.4 增加运动控制功能
在平衡的基础上,让小车带着球运动到指定地点。
- 路径规划:给定目标点坐标,规划小车行进路径。
- 上层控制器:在现有的平衡控制环之上,再增加一个“位置环”,根据小车当前位置和目标位置,计算出
target_position的设定值,从而引导球和小车一起移动。
6.5 工程化改进
- 状态机管理:引入状态机(如初始化、就绪、平衡、运动、错误等),使程序逻辑更清晰健壮。
- 遥控与交互:增加蓝牙或Wi-Fi模块,通过手机APP或电脑上位机遥控小车、设置目标点、实时调整参数。
- 安全保护:增加限位开关或软件限幅,防止球滚出平板边缘时小车失控猛冲。
从零搭建一个“车载平衡滚球运动”系统是一个极具挑战也收获满满的项目。它强迫你将理论(控制原理)与实践(嵌入式编程、硬件调试)紧密结合。本文提供的示例代码和调试方法是一个可行的起点,但真正的精髓在于调试过程中对系统行为的观察、分析和思考。记住,没有一次成功的调参,耐心和细致的观察比盲目修改参数更重要。建议你先在仿真软件中验证算法,再动手搭建硬件,可以节省大量时间和物料成本。希望这篇长文能为你备战电赛或开展个人项目提供扎实的帮助。如果在实践中遇到具体问题,欢迎在评论区交流讨论。