轮腿机器人竞赛实战复盘:从机械结构到PID与视觉识别的工程优化
趁着比赛刚结束,记忆还在热乎劲儿,我把这次浙江轮腿赛从备赛、调试到上场的完整过程写下来。拿第四名,止步省二,说不遗憾是假的,但复盘之后发现,成绩背后暴露出来的技术问题才是真正值得记录的。
这篇文章不打算写成情绪宣泄,而是作为一名开发者的视角,把轮腿机器人从机械结构、运动控制、视觉识别到整机联调的实战经验完整拆开。如果你也准备参加机器人竞赛,或者正在做轮腿相关的项目,这篇文章应该能帮你少踩不少坑。
从拿到第四名离场,到回学校重新整理代码和调试记录,我意识到一个事实:第四名和省一等奖之间的距离,不是运气,而是从稳定性、容错性到临场决策力的一整套工程差距。
下面这篇文章,就是这套差距的完整复盘。
1. 轮腿机器人竞赛到底在比什么
1.1 轮腿机器人的技术定位
轮腿机器人,简单理解就是“轮子加腿”的复合结构。它结合了轮式移动的高效性和腿式结构的越障能力。
从结构类型上看,常见的方案有几类:
| 结构方案 | 特点 | 适用场景 |
|---|---|---|
| 四轮 + 前后摇臂 | 结构简单,稳定性最高 | 平地竞速、轻度越障 |
| 两轮自平衡 | 体型小,控制难度高 | 狭窄通道、姿态展示 |
| 四轮 + 主动腿关节 | 可以主动迈步、抬腿越障 | 高障碍、台阶、复杂地形 |
| 轮毂电机 + 悬挂 | 机械效率高,响应快 | 高速巡航、赛道类任务 |
我们这次采用的是“四轮 + 前后摇臂 + 后轮独立驱动”的方案,属于偏保守但可靠度较高的设计。从最终得分来看,机械结构的可靠性是整个比赛的基础,但它不是决定排名的唯一因素。
1.2 竞赛评分维度拆解
很多第一次参加轮腿赛的队伍,会把精力全部放在“跑得够不够快”上。但实际评分维度要复杂得多。
根据我们赛前拿到的评分细则,大致可以分为几个方向:
- 任务完成度:是否在限定时间内完成所有规定动作,这是权重最高的一项。
- 稳定性:机器人在运行过程中是否出现摔倒、卡死、冲出赛道等情况。
- 识别准确率:对场地中的标识、颜色、数字或物体进行识别的正确率。
- 速度与完成时间:在保证完成度和稳定性的前提下,用时越短分数越高。
- 现场应变:如果赛场环境与训练环境不一致,机器人能否通过参数调整或决策逻辑快速适应。
换句话说,轮腿竞赛比拼的是一个系统工程能力。单项突出不够,必须全面稳健。
1.3 为什么我们拿到了第四名
我们队最终的成绩是第四名,拿到省级二等奖。
复盘整个比赛过程,我们的速度和越障能力其实都在前三名的水平线上,真正拉开差距的是两个地方:
- 稳定性不够:排名前三的战队在连续两轮比赛中几乎没有出现机器人摔倒和识别失误,而我们在第二轮出现了一次过障碍时重心偏移导致的卡壳,耽误了关键时间。
- 环境适应性不足:赛场的光线强度和地面摩擦系数与训练场地存在差异,我们的视觉识别参数和运动控制参数没有来得及快速调整,导致部分动作出现迟滞。
这些问题的根源不是某一个模块的缺陷,而是赛前测试没有按照“比赛环境”的标准来执行。这一点,我会在后面用一整个章节详细展开。
2. 赛前准备:环境搭建与关键参数约定
2.1 实验平台与系统说明
先说明我们队伍使用的软硬件环境,这里写出来供大家参考,具体版本需要根据你手头的设备灵活调整。
硬件部分:
- 主控板:STM32F407 开发板(运动控制)
- 协处理器:树莓派 4B(视觉识别与上层决策)
- 电机:直流减速电机 × 4,带霍尔编码器
- 姿态传感器:MPU6050(六轴陀螺仪 + 加速度计)
- 摄像头:USB 免驱摄像头,720P,60FPS
- 电源:11.1V 3S 锂电池
软件部分:
- 嵌入式开发:STM32CubeIDE + HAL 库
- 视觉处理:Python 3.8 + OpenCV 4.5
- 通信方式:串口(UART)进行数据透传
- 调试工具:Serial Studio(串口数据可视化)、VNC Viewer(远程桌面)
需要说明的是,这套组合并不是什么稀缺硬件,STM32 加树莓派是竞赛圈非常经典的主从架构。主控负责实时性要求高的运动控制,协处理器负责算力需求大的视觉任务,两者通过串口通信协作。
2.2 软件工具链配置
如果你的环境和我类似,建议按下面顺序配置:
# 树莓派端(视觉处理) sudo apt update sudo apt install python3-pip pip3 install opencv-python numpy pyserial # Windows 端(串口调试辅助) # 安装串口调试助手,或使用 Python pyserial 编写调试脚本STM32 端建议直接用 STM32CubeMX 生成工程骨架,然后集成 MPU6050 驱动和 PID 控制代码。这里有一个非常实用的建议:在正式写控制逻辑之前,先把串口通信协议定好,否则后期联调会非常痛苦。
2.3 赛前必须维护的参数文档
这是我认为本次比赛中最应该早做的一件事:建一个参数配置表。
轮腿机器人的参数数量远超普通小车。我们最后统计了一下,机械尺寸参数、PID 参数、视觉 HSV 阈值、速度上限、障碍判断阈值加起来超过 60 个。如果没有一份清晰的参数文档,到了赛场根本不知道改了哪个参数会导致什么结果。
我建议的参数文档格式如下:
| 参数名 | 当前值 | 作用范围 | 最后修改日期 | 备注 |
|---|---|---|---|---|
| pid_balance_kp | 22.5 | 平衡环 P | 2025-03-10 | 负载增加后需要上调 |
| pid_balance_ki | 0.15 | 平衡环 I | 2025-03-10 | 过大容易振荡 |
| hsv_red_low | (0, 120, 70) | 红色标识识别 | 2025-03-12 | 依赖场地光线 |
| max_speed | 750 | 最大轮速 | 2025-03-13 | 单位 encoder 值 |
这份表不仅帮助我们在联调时快速定位问题,也在赛场上帮助我们快速恢复参数。比赛间隙只有几十分钟,靠脑子记 60 个参数根本不可能。
2.4 时间规划与任务拆解
按照我们三个月的备赛周期,时间分配大致如下:
- 第 1 个月:机械结构搭建 + 电机驱动调试
- 第 2 个月:运动控制闭环(平衡、转向、速度控制)
- 第 2.5 个月:视觉识别模块开发并与主控联调
- 最后两周:场地模拟测试 + 参数调优 + 应急预案
这个节奏整体来说是合理的,但我们在最后两周犯了一个错误:过度追求新功能,而没有把已经实现的功能做回归测试。结果就是,我们在赛前三天修改了视觉 HSV 阈值,导致原先稳定的识别逻辑出现波动,而这个问题直到比赛前一天晚上才发现。
3. 核心模块拆解:从机械到运动控制
3.1 机械结构设计中的重心控制
轮腿机器人的机械结构设计,核心不是“结实”,而是“重心”。
我们这次采用的方案,把电池放置在下底盘中央,主控板和树莓派分层安装在电池上方,摄像头安装在前支架顶部。整体重心控制在底盘平面投影的中心区域,这是保证平衡控制有效的前提。
如果你自己设计轮腿结构,需要重点注意以下三个原则:
- 重心越低越好:重心越低,机器人抗倾覆能力越强,平衡控制越容易。
- 左右严格对称:左右质量差会导致转向偏航,控制上需要额外补偿。
- 轮距和轴距的比例要适中:轮距太窄容易侧翻,轴距太长影响转向灵活性。
3.2 姿态解算:MPU6050 数据融合
姿态解算是轮腿机器人平衡控制的第一环。通过 MPU6050 获取三轴加速度和三轴角速度,然后通过合适的融合算法得到机器人的俯仰角。
我们使用的是互补滤波算法。相比卡尔曼滤波,互补滤波计算量小、调参简单,在 STM32 上运行非常合适。
核心思路是:高频段信任陀螺仪的角速度积分,低频段信任加速度计的姿态解算,通过一个系数 α 进行融合。
// 文件路径:Src/attitude.c // 互补滤波姿态解算核心代码 #define ALPHA 0.96f // 互补滤波系数,需要根据实际传感器调整 #define RAD_TO_DEG 57.29578f float dt = 0.005f; // 采样周期 5ms float angle_gyro = 0.0f; // 陀螺仪积分角度 float accel_angle = 0.0f; // 加速度计解算角度 float angle_output = 0.0f; // 最终姿态角 void Attitude_Update(float gx, float gy, float gz, float ax, float ay, float az) { // 1. 通过加速度计解算俯仰角,注意传感器安装方向 accel_angle = atan2f(-ax, az) * RAD_TO_DEG; // 2. 陀螺仪积分:gx 是绕 X 轴的角速度,即俯仰角速度 angle_gyro += gx * dt; // 3. 互补滤波融合 angle_output = ALPHA * (angle_output + gx * dt) + (1.0f - ALPHA) * accel_angle; }这里有一个非常重要的细节:加速度计解算的角度在运动剧烈时噪声很大,陀螺仪积分长期运行会产生漂移。因此互补滤波系数的选择需要权衡响应速度和噪声抑制。我们最终把 ALPHA 设置为 0.96,也就是 96% 信任陀螺仪、4% 信任加速度计,在平衡车上表现比较稳定。
3.3 串级 PID 平衡控制
轮腿机器人的运动控制采用的是经典的串级 PID 结构:外环为角度环,内环为角速度环(或速度环)。
串级 PID 的优势在于,外环输出作为内环的目标值,内环可以更快地抑制扰动。比如机器人受到外部冲击时,角速度会瞬间变化,内环可以快速响应,不需要等待外环计算。
// 文件路径:Src/motion_control.c // 串级PID控制核心代码(角度环 + 角速度环) typedef struct { float target; float actual; float kp; float ki; float kd; float integral; float previous_error; float output; } PID_t; // 位置式 PID 计算 float PID_Calculate(PID_t *pid, float target, float actual) { float error = target - actual; pid->integral += error; if (pid->integral > 1000.0f) pid->integral = 1000.0f; if (pid->integral < -1000.0f) pid->integral = -1000.0f; float derivative = error - pid->previous_error; pid->previous_error = error; pid->output = pid->kp * error + pid->ki * pid->integral + pid->kd * derivative; return pid->output; } // 串级控制入口 float target_angle = 0.0f; // 目标角度,0 为直立 float current_angle = 0.0f; // 姿态解算得到的当前角度 float current_gyro = 0.0f; // 当前角速度 PID_t angle_pid = {0.0f, 0.0f, 18.0f, 0.05f, 0.5f, 0.0f, 0.0f, 0.0f}; PID_t gyro_pid = {0.0f, 0.0f, 2.5f, 0.0f, 0.05f, 0.0f, 0.0f, 0.0f}; float target_gyro, motor_output; void MotionControl_Loop(void) { // 外环:角度环,输出为角速度目标 target_gyro = PID_Calculate(&angle_pid, target_angle, current_angle); // 内环:角速度环,输出为电机 PWM motor_output = PID_Calculate(&gyro_pid, target_gyro, current_gyro); // 输出到电机驱动,注意左右电机方向相反 Motor_SetPWM(motor_output); }在调参过程中,我建议按这个顺序:
- 只调角度环 P:让机器人能“硬扛”住不倒,即使会振荡。
- 加入角速度环:抑制振荡的核心,让动作变得平顺。
- 再逐项加入 I 和 D:消除静差,提升动态响应。
- 最后联调转向和速度:在直立稳定基础上叠加运动控制。
3.4 电机驱动与编码器反馈
轮腿机器人对电机响应速度要求很高。我们使用的是带霍尔编码器的直流减速电机,通过正交解码获取轮子转速和位置信息,用于速度闭环。
电机驱动我们采用常见的 H 桥驱动方案,通过 PWM 控制电机的速度和方向。
// 文件路径:Src/motor.c // 电机 PWM 输出核心代码(简化示意) void Motor_Init(void) { HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 左电机 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); // 右电机 HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL); // 编码器 } void Motor_SetPWM(int16_t pwm_value) { if (pwm_value > 1000) pwm_value = 1000; if (pwm_value < -1000) pwm_value = -1000; if (pwm_value >= 0) { __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pwm_value); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 0); } else { __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, -pwm_value); } }需要注意的是,电机 PWM 如果设置过大,会导致电流急剧上升,主控板电压跌落,甚至直接引起单片机复位。我们就在联调时遇到了这个问题,后来通过在电源端加装大电容并限制最大 PWM 值解决了。
4. 视觉识别与决策逻辑
4.1 视觉方案的整体架构
视觉部分运行在树莓派上,使用 OpenCV 进行图像处理,识别结果通过串口发送给 STM32。整体流程如下:
摄像头采集 → 图像预处理 → 颜色/目标识别 → 坐标计算 → 串口发送 → STM32 决策竞赛中需要识别的目标通常包括红色标识、绿色区域、数字标签等,颜色识别是最基础也是最可靠的方案。
4.2 基于 HSV 的颜色识别
颜色识别最常用的方法是把图像从 BGR 转换到 HSV 颜色空间,然后通过设定阈值提取目标颜色区域。
HSV 相比 BGR 对光照变化更鲁棒,但也不是完全免疫。这一点在比赛中给我们造成过麻烦,后面会专门讲。
# 文件路径:vision/color_detect.py # 颜色识别核心代码 import cv2 import numpy as np def detect_red_area(image): """ 检测图像中的红色目标区域,返回目标中心坐标和面积 """ # BGR 转 HSV hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # 红色的 HSV 范围,需要根据场地实际情况调整 lower_red1 = np.array([0, 120, 70]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([170, 120, 70]) upper_red2 = np.array([180, 255, 255]) # 红色在 HSV 中分布在 0° 和 180° 两端,需要两次掩膜 mask1 = cv2.inRange(hsv, lower_red1, upper_red1) mask2 = cv2.inRange(hsv, lower_red2, upper_red2) mask = cv2.bitwise_or(mask1, mask2) # 形态学操作去除噪声 kernel = np.ones((5, 5), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 寻找轮廓 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) == 0: return None, 0 # 取最大轮廓 max_contour = max(contours, key=cv2.contourArea) area = cv2.contourArea(max_contour) if area < 500: # 过滤小面积噪声 return None, 0 # 计算中心坐标 M = cv2.moments(max_contour) if M["m00"] == 0: return None, 0 cx = int(M["m10"] / M["m00"]) cy = int(M["m01"] / M["m00"]) return (cx, cy), area # 测试代码 if __name__ == "__main__": cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame = cap.read() if not ret: break center, area = detect_red_area(frame) if center is not None: cv2.circle(frame, center, 5, (0, 255, 0), -1) cv2.putText(frame, f"Area: {area}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow("Frame", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这个代码的核心是两点:
- 红色的 HSV 范围需要分两段,因为 HSV 色相环中红色跨越了 0° 分界线。
- 必须做形态学操作,否则二值图像中的孤立噪点会严重影响识别结果。
4.3 串口通信协议
视觉识别结果需要传给主控 STM32 做决策。我们使用了固定长度、带帧头帧尾的协议格式,保证通信的可靠性。
协议格式如下:
帧头(0xAA) + 数据长度(1字节) + 数据区 + 校验和(1字节)数据区定义了我们可能需要的识别信息:
| 字节索引 | 含义 | 说明 |
|---|---|---|
| 0 | 识别类型 | 0-红色/1-绿色/2-数字/3-无目标 |
| 1-2 | 目标中心 X | 0-640,高字节在前 |
| 3-4 | 目标中心 Y | 0-480,高字节在前 |
| 5 | 目标面积百分比 | 0-100 |
Python 发送端代码:
# 文件路径:vision/serial_send.py # 串口发送识别结果 import serial import struct def send_detect_result(ser, target_type, center_x, center_y, area_percent): """ 向 STM32 发送识别结果 """ data = bytearray() data.append(0xAA) # 帧头 data.append(7) # 数据长度:类型1 + X坐标2 + Y坐标2 + 面积1 + 校验1 data.append(target_type) data.extend(struct.pack('>H', center_x)) data.extend(struct.pack('>H', center_y)) data.append(area_percent) # 校验和:除帧头外所有字节累加 checksum = sum(data[1:]) & 0xFF data.append(checksum) ser.write(data) # 用法示例 ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=0.1) send_detect_result(ser, 0, 320, 240, 35)STM32 端接收时,需要对校验和进行同样的计算,不一致就丢弃这一帧数据。这样能避免噪声带来的误触发。
4.4 基于状态机的任务决策
识别到目标之后,机器人需要决定下一步动作。我们用了一个简单的状态机来管理任务流程。
状态定义如下:
SEARCH:搜索目标,原地旋转或沿指定路径运动。APPROACH:发现目标,调整方向并靠近。OVERCOME:执行越障或指定动作。FINISH:所有任务完成,停车。
// 文件路径:Src/task_state.c // 任务状态机核心代码 typedef enum { TASK_SEARCH = 0, TASK_APPROACH, TASK_OVERCOME, TASK_FINISH } TaskState; TaskState current_state = TASK_SEARCH; void Task_StateMachine(int target_type, int center_x, int center_y, int area_percent) { switch (current_state) { case TASK_SEARCH: // 慢速旋转搜索目标 Motor_SetPWM(200); Motor_SetPWM(-200); // 如果检测到目标面积足够大,进入靠近状态 if (area_percent > 10) { current_state = TASK_APPROACH; } break; case TASK_APPROACH: // 根据目标中心坐标调整方向 int error = center_x - 320; // 图像中心 320 int turn_speed = error * 0.3f; Motor_SetPWM(400 + turn_speed); Motor_SetPWM(400 - turn_speed); // 如果目标足够大,说明已经靠近,执行指定动作 if (area_percent > 50) { current_state = TASK_OVERCOME; } break; case TASK_OVERCOME: // 执行越障或避障动作,具体逻辑根据任务定制 Motor_SetPWM(800); Motor_SetPWM(800); // 延时或检测到完成标志后结束 if (动作完成) { current_state = TASK_FINISH; } break; case TASK_FINISH: Motor_SetPWM(0); Motor_SetPWM(0); break; } }状态机的优势在于逻辑清晰、易于调试。每一轮比赛,我们都可以通过串口日志打印当前状态,快速定位问题出在哪一个环节。
5. 整机联调与赛道验证
5.1 联调顺序
模块单独调好后,最怕的就是合并在一起时“互相打架”。我们总结了一套联调顺序,可以避免大部分低级问题:
- 先调通信:确认树莓派和 STM32 串口收发正常,数据帧解析正确。
- 再调视觉识别:在静止状态下确认 Python 端的识别结果准确、稳定。
- 然后调运动控制:在无视觉输入的情况下,验证平衡、前进、转向的基本指令。
- 最后做整机闭环:把视觉输出接入状态机,观察完整任务流程是否通顺。
每一步都要记录日志。我们使用了串口日志 + SD 卡存储的方式,每轮测试跑完,就可以回放机器人内部的状态变化,找出异常点。
5.2 赛道测试记录
每次赛道测试,至少跑三遍完整流程。记录表大概长这样:
| 测试轮次 | 任务完成情况 | 是否摔倒/卡住 | 总用时 | 视觉识别成功次数 | 问题描述 |
|---|---|---|---|---|---|
| 第 1 轮 | 全部完成 | 否 | 63s | 6/6 | 转向时有轻微摆尾 |
| 第 2 轮 | 全部完成 | 是(1次越障失败) | 71s | 5/6 | 过障碍时重心偏移 |
| 第 3 轮 | 部分完成 | 是(冲出赛道) | 88s | 4/6 | 光线变化导致识别失败 |
通过这张表,我们能够快速判断问题是机械、控制还是视觉。
5.3 稳定性优化的三个关键点
整个联调阶段,我们主要优化了三个方面:
1. 降低重心
初期摄像头安装过高,导致高速转向时离心力明显,机器人有翻车趋势。后来将摄像头支架降低了 3 厘米,并将电池从最上层下移到底盘上,整个机器人的过弯稳定性有了质的提升。
2. 增大脚轮减震
轮腿机器人在穿过减速带时,冲击力会直接影响姿态传感器的读数。我们在每个轮轴上增加了橡胶减震垫,并在代码里对姿态数据做了中值滤波,有效减少了过障碍时的瞬间漂移。
3. 动态调整 PID 增益
简单固定 PID 在平地跑得好,但在上坡、下坡或撞击后响应不佳。我们后来加入了一个简单的模糊逻辑:根据机器人的倾斜角度动态调整角度环的 P 值,角度偏差越大,P 值越大,让机器人能在更强的外力下恢复平衡。
// Src/motion_control.c // 动态调 P 值的简化思路 float angle_error = fabs(target_angle - current_angle); if (angle_error > 15.0f) { angle_pid.kp = 25.0f; // 大偏差时增大刚度 } else if (angle_error > 5.0f) { angle_pid.kp = 20.0f; // 中等偏差 } else { angle_pid.kp = 17.0f; // 小偏差时保持柔顺 }6. 比赛现场最可惜的几个失误
以下是本次比赛真实出现的失误,也是我们复盘后最想分享给后来者的经验。
6.1 光线变化导致视觉阈值失效
这是最致命的一个问题。
训练场地使用的是室内日光灯,光线均匀且偏白。而比赛场地靠近窗边,下午时段有太阳光斜射,部分地面区域产生了强烈的反光和高光区域。
我们的 HSV 阈值是在训练场地下标定的。到了比赛现场,第一次测试红色识别率就下降到了 70% 左右,部分区域甚至完全识别不到。
解决方案:当时没有完美的补救方法,只能临时调整 HSV 上下限。但因为参数改得太急,又导致绿色目标误识别成红色。赛后我们想到的正解是,赛前做一次“光照鲁棒性标定”,即在不同光照条件下采集多组 HSV 范围,做并集处理。
6.2 状态机缺少超时保护
我们的状态机在正常情况下工作正常,但比赛现场出现了一个训练时没有遇到过的情况:机器人到达目标附近后,因为视觉识别出现一次帧丢失,导致状态机认为“目标还没找到”,重新回到了 SEARCH 状态。
这一退,直接让机器人转了一圈,白白浪费了 8 秒。
如果状态机里加一个超时强制转移机制,比如“在 APPROACH 状态下连续 3 秒检测不到目标,默认执行 OVERCOME”,就不会出现这个问题。
6.3 负载变化导致的 PID 参数失效
比赛要求机器人携带一个指定的负载物(模拟运输任务)。我们训练阶段用的负载物约为 200g,但比赛现场的负载物偏重,接近 350g。
负载增加后,机器人整体的转动惯量变大,原来的 PID 参数出现明显的过冲和振荡。在近距离转弯时,甚至出现了一次推头现象。
这个问题本质上还是参数鲁棒性不足。赛前应该做“负载范围测试”,覆盖可能出现的最重和最轻负载,并制定对应的参数列表。
6.4 排故时间不足
比赛当天的排故时间非常有限。我们因为临时调整视觉参数和 PID 参数,花费了大量时间在复测上,导致第二轮比赛时机器人电量不足,个别动作出现了供电跌落引起的重启。
这里最值得反思的是:比赛现场不该再去做大范围调参,应该只做“参数切换”。也就是说,赛前准备多套完整参数,现场根据环境选择最接近的一套,最多微调。
6.5 常见问题排查清单
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 开机后无法直立 | 姿态角度方向反了 | 检查 MPU6050 安装方向和代码中的符号 |
| 机器人高频振荡 | 角度环 P 过大或角速度环 P 过小 | 减小角度环 P,增大角速度环 P |
| 视觉识别频繁丢失 | HSV 阈值过窄 | 扩大 HSV 范围,多次采样标定 |
| 串口数据乱码 | 波特率不一致或共地问题 | 统一波特率,确保两个设备共地 |
| 电机突然停转 | 电源供电不足 | 加装大电容,降低最大 PWM 限幅 |
| 状态机卡死 | 没有超时保护 | 增加超时强制跳转逻辑 |
7. 如果重来一次:工程建议清单
这次比赛虽然没有拿到最理想的名次,但回头看,很多问题其实是可以在备赛阶段就避免的。下面是我们在赛后会优先执行的改进方案,也推荐给准备参赛的队伍。
7.1 提前进行“比赛模式”演练
所谓“比赛模式”,就是模拟比赛的真实流程:限定 10 分钟进场调试时间、使用比赛现场的光照条件、跑完整的三轮任务、中间只允许调整参数不能改代码。
这个演练应该在赛前两周至少执行三次。
7.2 参数模板化与快速恢复
把所有参数按照不同场地、不同光照、不同负载条件编写成多套配置模板。比赛现场通过一个 switch 语句或配置文件快速切换。
// Src/param_config.h // 参数模板切换 typedef struct { float balance_kp; float balance_ki; float balance_kd; float gyro_kp; float gyro_kd; int hsv_red_low[3]; int hsv_red_high[3]; } RobotParam; const RobotParam PARAM_INDOOR = { 18.0f, 0.05f, 0.5f, 2.5f, 0.05f, {0, 120, 70}, {10, 255, 255} }; const RobotParam PARAM_OUTDOOR = { 20.0f, 0.05f, 0.6f, 3.0f, 0.08f, {0, 100, 60}, {12, 255, 255} }; const RobotParam PARAM_HEAVY_LOAD = { 24.0f, 0.06f, 0.4f, 2.8f, 0.04f, {0, 120, 70}, {10, 255, 255} };7.3 硬件冗余与防呆设计
比赛中最怕硬件出问题,但硬件问题恰恰最容易通过冗余设计来规避。建议至少做到:
- 准备两套电机驱动板。
- 准备两组编码器备件。
- 电源接口做防反接保护。
- 所有连接器接口打胶固定,防止震动松脱。
7.4 串口日志是排错第一手资料
比赛现场无法像实验室那样反复观看机器人行为。唯一能还原现场的就是日志。建议所有关键事件都打日志:
- 每次状态切换。
- 每次视觉识别结果。
- PID 输出超限。
- 电压过低警告。
8. 总结:从省二的遗憾里收获了什么
第四名这个成绩,让我们止步省二,确实遗憾。但说实话,比赛结束后的复盘让我意识到,这个结果并不冤枉。
排名前三的队伍,未必在某个单项上比我们强多少。他们的优势在于“系统性”和“鲁棒性”,所有的模块都能在变化的环境下稳定工作,而我们的系统在实验室条件下表现优秀,一旦环境变化就暴露出诸多脆弱点。
轮腿机器人竞赛,表面上比的是机器人谁跑得快、谁越障能力强,本质上比的是工程能力——能不能设计出一套在非理想环境下仍然可靠运行的完整系统。
这次比赛给我最大的收获,是建立了一套从备赛到临场的工程方法论:参数文档化、状态机设计、环境适应性测试、比赛模式演练。这些方法不只是用于竞赛,放在任何嵌入式或机器人项目中都适用。
如果你正在准备类似比赛,我建议你从第一天开始就养成为每项参数做记录的习惯,从第一轮测试开始就模拟比赛环境,从第一个功能完成就开始做回归测试。不要等到比赛前一周才考虑稳定性。
希望这篇复盘能对你有帮助。如果后续有时间,我会继续分享更多轮腿机器人的控制和视觉细节,也可以聊聊如何把竞赛代码改造成更通用的工程代码。比赛虽然结束了,但技术上还有太多可以折腾的地方。
一起加油。
