公司动态

大一单人挑战智能车竞赛:蚂蚁搬家赛题全流程技术备赛记录

📅 2026/8/30 17:43:37
大一单人挑战智能车竞赛:蚂蚁搬家赛题全流程技术备赛记录
大一的时候一句话描述了很多东西没有队友、没有学长全程带路、没有充足的预算甚至没真正调过摄像头。当我在社团备赛群里看到“蚂蚁搬家”这个赛题时第一反应是名字挺有意思第二反应是这个东西真的能在大一一个人做出来吗后来我不仅做了还把它做成了我大学阶段最完整的项目之一。这篇文章不是比赛复盘也不是鸡汤而是一份面向硬件的备赛记录里面包含赛题理解、技术方案、图像处理思路、PID控制、状态机设计以及一个人独自排查问题时踩过的坑。希望能给同样准备智能车竞赛、尤其是准备一个人起步的同学一点参考。1. 背景理解什么是“蚂蚁搬家”赛题如果你还没拿到当年的赛题手册第一件事一定是去官网和官方公众号把《竞赛规则》和对应组别的赛题手册下载下来。不同届别、不同组别的规则细节每年都会有调整下面说的是我这届备赛时对“蚂蚁搬家”这个任务的一般理解具体参数请一定以你们那一年的手册为准。通俗地说“蚂蚁搬家”考察的是智能车在一张固定场地上的任务完成能力小车需要从起点出发在场地中沿指定路径或者自由巡视找到代表“蚂蚁”的标志物然后把它们运送到指定的“巢穴”区域。听起来像是物流搬运实际上它把智能车竞赛最经典的几个技术点全部串了起来视觉感知识别赛道边界、识别蚂蚁标志物的位置运动控制稳定地巡线、转向、调速不能压线、不能冲出场地机械执行通过翻板、推杆、机械爪等机构完成蚂蚁的收集和投放逻辑调度什么时候找、什么时候搬、什么时候放需要用状态机来管理。这个赛题难就难在它不是单一技能考核而是一个“感知—决策—执行”的闭环。对大一新生来说真正的难点不是某一个模块而是把所有模块串起来之后整个系统能不能稳定地一遍跑完。我当时选择“蚂蚁搬家”的原因也很简单它比纯竞速组更有“项目感”代码量适中又能在硬件上有不少发挥空间。一个人备赛虽然累但整个链路掌握下来之后你会发现自己对嵌入式开发的理解会被拉高一大截。2. 一个人也要做的整体技术方案一个人备赛最容易犯的错误就是拿到车模就开始乱接线、乱写代码结果调了三天发现连需求都没理清楚。我建议在动手之前先把下面这张功能拆解图画在纸上哪怕只是简简单单的几个方框。整体系统可以拆成四层感知层摄像头负责采集场地图像编码器负责测量车轮转速决策层主控芯片读取图像数据跑图像处理和路径规划然后给出目标速度和目标转角执行层电机驱动模块根据PWM控制直流电机转速舵机根据PWM控制前轮转角供电与通信层电池提供能源经过稳压模块给主控、摄像头、传感器供电同时通过串口或无线模块把调试数据传到电脑上位机。一个人的情况下我建议按“最小可跑”原则分阶段推进阶段目标验收标准第一阶段车能通电电机能转舵机能打角用手控制PWM占空比车能前后动第二阶段摄像头图像能传到上位机电脑能实时看到灰度图或二值图第三阶段能稳定巡线简单环道上不掉线第四阶段能识别蚂蚁标志物控制台打印出标志物坐标第五阶段完成抓取和投放动作整个流程能在场地连续跑通这样划分的原因是每个阶段都只引入一个新的不稳定因素。智能车是最典型的多变量耦合系统如果你同时调电机PID和摄像头图像出问题的时候你根本不知道是哪个环节引起的。3. 环境准备与硬件选型如果你的实验室有传承下来的硬件优先用现成的。一个人备赛最不该浪费时间的事就是纠结“哪块板子更高级”。我们当年用的是一套基于STC8系列主控的方案配的是入门级灰度摄像头和普通直流减速电机。这套组合的好处是资料多、便宜、坏了不心疼。常规环境清单如下车模根据赛题手册选择指定车模规格我们用的是普通三轮或四轮小车底盘主控STC8系列或者其他你熟悉且有库的单片机常见的有STC8G、STC8H、TC264等摄像头入门级灰度摄像头分辨率160x120或者更小都够用关键是要能拿到实时图像编码器安装在电机后面用来测速做闭环控制舵机负责转向型号需要能承受转向力矩电机驱动模块可以控制电机正反转和调速电池与稳压7.2V或11.1V锂电池组加稳压模块给主控和传感器供电调试工具USB转TTL下载器、逻辑分析仪、串口屏或者直接单片机串口发数到电脑。这些硬件的具体型号请以你们实验室库存和比赛规则为准我不写死避免误导。但有一点要提醒如果条件允许尽量给摄像头和电机驱动单独供电或者至少做好电容滤波。电机启动瞬间电流很大经常会把单片机直接拉复位。我们第一次上电调车车一动屏幕就黑排查了半天才发现是电源设计出了问题。开发环境一般是Keil或IAR加上官方的下载调试器。建议一开始就把工程目录整理清楚我用的结构大概是这样project/ ├── doc/ # 赛题手册、数据手册、笔记 ├── hardware/ # 原理图、接线图 ├── firmware/ │ ├── app/ # 主逻辑例如巡线、识别、状态机 │ ├── driver/ # 单片机外设驱动摄像头、编码器、PWM │ ├── algorithm/ # PID、图像处理 │ └── main.c └── tools/ # 上位机、图像分析脚本一个人写项目代码目录清晰是后期能坚持调下去的重要前提。4. 图像处理与蚂蚁识别4.1 摄像头图像二值化与去噪“蚂蚁搬家”的第一关是把摄像头采集到的灰度图变成好处理的二值图。摄像头看到的东西可以理解成一张每个像素都有亮度值的矩阵比如160宽x120高的灰度图每个像素是0到255的亮度值。简单做法是固定阈值二值化设定一个阈值THRESHOLD亮度大于阈值的像素置为白色小于阈值的置为黑色。这样赛道边界、蚂蚁标志物都会以黑白块的形式呈现。核心思路如下import cv2 import numpy as np def binarize(image, threshold130): gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY_INV) # 用小核做开运算去噪再做闭运算补洞 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) binary cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) binary cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return binary固定阈值在比赛场地光线稳定的情况下很好用但如果比赛中场地不同区域反光不一样固定阈值就会出问题。这时候更稳妥的做法是自适应阈值也就是统计整张图像的亮度分布或者分区域计算阈值。代码里我用THRESHOLD_INV表示反二值化目的是把暗色标志物变成白色前景因为场地通常是浅色“蚂蚁”标志或赛道边界往往是深色。如果你做的是反色场景记得把参数反过来调。4.2 用轮廓特征识别蚂蚁标志二值化之后我们需要在图像中找出“什么是蚂蚁”。如果赛题用的是圆形或者近似圆形的标志物最保守的识别方案是“面积 圆形度 半径”。圆形度的计算方法是圆形度 4 * π * 面积 / 周长^2圆的圆形度接近1四边形、细长形状的圆形度会明显偏低。我们可以利用这个特征过滤掉大量干扰区域。下面这段Python代码可以在电脑上用一张实拍图片快速验证识别思路验证完再移植到单片机上。import cv2 import numpy as np def find_ants(image, min_area80, max_area8000, min_radius5): binary binarize(image) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) ants [] for cnt in contours: area cv2.contourArea(cnt) if area min_area or area max_area: continue perimeter cv2.arcLength(cnt, True) if perimeter 1e-6: continue circularity 4 * np.pi * area / (perimeter * perimeter) (x, y), radius cv2.minEnclosingCircle(cnt) if circularity 0.6 and radius min_radius: ants.append((int(x), int(y), int(radius))) return ants这段代码放到电脑上配合比赛场地照片可以直接看出哪个阈值、哪个圆形度参数最合适。强烈建议准备一小段场地实拍视频跑一遍这个脚本把参数定下来再动手移植到嵌入式环境。单片机上实现时轮廓提取会麻烦一些但完全可以用“面积统计 边界点计算圆形度”的简化方法不一定要用完整的OpenCV。4.3 单片机端巡线偏移量计算识别到蚂蚁之后小车怎么沿着赛道走是另一个核心问题。最经典的方案是“图像中线法”在摄像头画面底部到中部选取若干行每一行找到赛道左边界和右边界取中点对所有行的中点求平均就得到了赛道中心线的横向偏移。这个偏移量可以直接喂给舵机PD控制器用来打角。单片机端用C语言实现大致是这样的思路#define IMAGE_W 160 #define IMAGE_H 120 #define THRESHOLD 130 uint8_t image[IMAGE_H][IMAGE_W]; // 二值图1表示白色前景 // 返回值负值偏左正值偏右-1表示丢线 int compute_line_offset(void) { int sum 0; int count 0; for (int row IMAGE_H - 1; row IMAGE_H / 2; row--) { int left -1, right -1; for (int col 0; col IMAGE_W; col) { if (image[row][col] 1) { if (left 0) left col; right col; } } if (left 0 right 0) { sum (left right) / 2; count; } } if (count 0) { return -1; // 这一帧丢线了 } return sum / count - IMAGE_W / 2; }丢线处理是巡线里最容易翻车的地方。如果某一行没有找到边界我建议先用上一行的有效中线做补偿如果连续多行都找不到就进入“丢线保护”状态让车减速并朝最后一次有效偏移方向打角。5. 控制层PID与运动控制5.1 速度闭环为什么要用PID电车在下地跑的时候如果只给固定的PWM占空比车轮转速会受电池电压和地面摩擦影响忽快忽慢。编码器的主要作用就是测量实际转速然后用PID把实际转速拉回到目标值。PID的核心思想是根据“目标值—当前值”的误差按比例、积分、微分三项叠加计算输出。比例项决定响应快慢积分项消除稳态误差微分项抑制超调。智能车这种系统速度可以用PI转向可以用PD。下面是一个精简但完整的PID结构体实现可以直接作为工程里的基础模块使用typedef struct { float target; float kp; float ki; float kd; float integral; float last_error; float out_limit; float integral_limit; } PidCtrl; void pid_reset(PidCtrl *pid) { pid-integral 0.0f; pid-last_error 0.0f; } float pid_update(PidCtrl *pid, float current, float dt) { float error pid-target - current; pid-integral error * dt; if (pid-integral pid-integral_limit) pid-integral pid-integral_limit; if (pid-integral -pid-integral_limit) pid-integral -pid-integral_limit; float d_error (dt 0.0f) ? (error - pid-last_error) / dt : 0.0f; pid-last_error error; float out pid-kp * error pid-ki * pid-integral pid-kd * d_error; if (out pid-out_limit) out pid-out_limit; if (out -pid-out_limit) out -pid-out_limit; return out; }使用的时候只需要初始化一次目标值和三个系数然后在控制中断里不断调用pid_updatePidCtrl speed_pid { .target 1.0f, // 单位按你的编码器换算 .kp 0.5f, .ki 0.02f, .kd 0.0f, .last_error 0.0f, .integral 0.0f, .out_limit 100.0f, .integral_limit 30.0f }; float duty pid_update(speed_pid, current_speed, dt);5.2 转向PD控制舵机控制最重要的不是速度而是平稳。如果直接用偏移量线性映射到舵机角度车往往会左右剧烈摇晃因为误差一变转向角度就突变。所以舵机一般用PD控制公式是steer kp * offset kd * (offset - last_offset) / dtoffset就是前面图像处理算出的赛道中线偏移量。这里kd尽可能不要省它相当于阻尼能有效抑制转向抖动。实际调参时我会先让kd0只加kp直到车能过弯但开始抖动然后逐渐加kd消除抖动直到车在直道稳定、弯道不冲出赛道。调PID参数不要一次给满。一个人的精神状态消耗不起反复炸车建议用上位机把速度曲线、转向输出、误差值记录下来对着曲线调参而不是靠肉眼猜测。6. 端到端任务状态机设计“蚂蚁搬家”最大的特点是流程多所以主程序不能写成从头到尾一条大直线。我用了状态机来管理整个任务流程这是一个人备赛时最能节省精力的设计。状态机可以简单理解成一个多分支的switch-case每个case代表一个阶段阶段之间通过标志位跳转。我的状态划分大致是这样enum TaskState { STATE_INIT, STATE_TRACK, STATE_FIND_ANT, STATE_CAUTION, STATE_CATCH, STATE_CARRY, STATE_DROP, STATE_FINISH }; enum TaskState state STATE_INIT; void task_loop(void) { switch (state) { case STATE_INIT: if (start_flag) { state STATE_TRACK; } break; case STATE_TRACK: track_line(); if (find_ant_flag) { state STATE_CAUTION; } break; case STATE_CAUTION: // 降速继续用摄像头寻找蚂蚁精确位置 set_speed(0.2f); if (ant_centered_flag) { state STATE_CATCH; } break; case STATE_CATCH: catch_action(); // 电动推杆或翻板执行收集 if (catch_done_flag) { state STATE_CARRY; } break; case STATE_CARRY: track_line(); if (reach_nest_flag) { state STATE_DROP; } break; case STATE_DROP: drop_action(); if (drop_done_flag) { state STATE_FINISH; } break; case STATE_FINISH: stop_car(); break; } }这套状态机的核心思路是“每个时刻只干一件事”。你在STATE_TRACK阶段不需要同时判断“要不要下电机关机械爪”你只要专注巡线进入STATE_CAUTION后再切换成低速识别模式。这样代码逻辑简单也方便单独测试每个状态。状态转移条件最好不要依赖单一帧的判定结果。比如find_ant_flag如果摄像头某一帧误判一下状态就会跳错。我习惯用连续计数比如连续三帧都检测到蚂蚁才认为真的找到了连续五帧都确认到了巢穴位置才执行投放。这个“滤波”思路在嵌入式视觉里非常常用。7. 调试排错一个人踩过的坑以下是我在一个人备赛过程中真实遇到过的几类问题按出现频率排序。一个人调试时最怕问题出来了不知道从哪下手所以我把现象、原因和排查思路整理成了一张表问题现象常见原因排查思路电机一启动单片机就重启电源瞬间压降过大主控供电不足独立供电、加大电容、检查稳压模块电流余量图像在电脑上看全是雪花点摄像头信号线松动或供电不足用示波器看信号波形更换线材单独给摄像头供电二值化后赛道边缘忽宽忽窄固定阈值不适合多变光线改自适应阈值减少画面反光区域直道正常弯道冲出跑道舵机Kp太小或车速太快先降速再增大方向Kp最后补Kd巡线时左右剧烈摆动舵机Kd为0误差噪声被放大给偏移量做滤波增加Kd限制角速度蚂蚁标志识别漏检圆形度阈值过严、面积范围不对用场地实拍图离线调试参数机械爪每次都差一点抓不进执行机构位置有误差控制逻辑在固定点触发改成连续检测标志物相对位置到位后延时执行跑着跑着突然停车程序卡死、看门狗没喂或者电池欠压打开看门狗串口打印当前状态检查电池电压我记得最揪心的一次是车在场上跑一圈后突然停下指示灯也不亮。排查了一整晚最后发现是一根负责电机驱动板逻辑供电的杜邦线在车震动中松了导致驱动板进入保护状态。从那以后我的原则是所有接插件必须用扎带或热熔胶固定重要信号线直接焊接或者压接。一个人备赛没有队友帮你看线接线可靠性就是唯一的兜底。另外一个建议是善用串口打印。主控每一次状态切换我都把当前状态、偏移量、速度、识别结果通过串口发到上位机。跑车之前先在场地把车架起来空跑一遍看串口数据是否正常。等确认上层逻辑没问题再放下去实跑这样能省下大量反复弯腰捡车的时间。8. 一个人也要有的工程习惯很多人觉得智能车比赛比的是谁调参调得多实际上比的是谁的系统更稳定、谁的问题定位更快。一个人备赛工程习惯尤其重要。我总结了几条非常实在的经验第一写调试笔记。不要相信自己的短期记忆。我调PID时经常改一个参数就试跑一圈跑完如果不记下午就完全忘了哪个参数是“当前最优”。我后来用了一个最简单的表格里面记录日期、状态、参数、现象、结论。这张表后来成了我比赛前调优的最重要依据。第二做模块化封装。摄像头驱动、编码器读取、PID计算、状态机逻辑全部拆成不同文件。不要全部堆在main.c里。哪怕你写的代码只是给自己用也要让一个礼拜后的自己能看懂。一个人备赛最痛苦的场景之一就是代码写到一半连自己都找不出上次的逻辑。第三版本管理很重要。哪怕不用Git也要经常备份工程。最稳妥的做法是在每次试跑前复制一份完整的工程文件夹命名为20240518_track_ok_backup。就算后面调崩了也能回到上一个稳定版本。用Git也行但对大一新人来说先养成“改代码前备份”的习惯更重要。第四不要硬扛。一个人备赛容易陷入死磕状态一个Bug调三天还没结果。建议给自己设一个时间上限比如一个疑难问题超过三小时没进展就去翻官方论坛、看B站公开调试视频、查CSDN上的同类问题。把别人的经验拿来验证比自己熬夜盲试效率高太多。智能车竞赛本身有很丰富的社区资料官方公众号“智能车竞赛”也会发规则解读和技术分享这些公开资料就是一个人最好的“隐形队友”。第五注意安全。锂电池充电要有人在场烙铁焊接注意通风跑车时手不要伸进电机齿轮传动区域。一个人搞项目出任何安全问题都没有队友在旁边帮忙所以安全底线必须守住。9. 关于大一、一个人和“蚂蚁搬家”如果现在有人问我大一一个人选“蚂蚁搬家”这个赛题值不值得我会说值得但前提是你能接受这个过程非常“孤独且琐碎”。所谓孤独是指从焊板子到查资料从调图像到改机械结构所有事情都要自己决定、自己承担结果。所谓琐碎是指你真正花在“写代码”上的时间可能只有三分之一剩下时间都在查线、换电池、改阈值、看波形、量电压。但正是因为一个人你必须逼着自己把整个系统的每一个环节都弄懂而不是像有队友时那样各管一段。我大一备赛那段时间最常用到的反而不是复杂的算法而是扎实的基础知道怎么看电路图知道PWM占空比和电机转速的大致关系知道摄像头图像每帧的像素数据怎么读进内存知道怎么用串口把变量发出来看。这些都是肉眼可见的成长。等到大二再回头看你会发现当初那些让你焦虑到整晚睡不着的Bug绝大多数都是简单的接线问题、阈值问题和参数问题。如果你也准备一个人挑战智能车我的建议很简单选一个你真正感兴趣的赛题把项目拆成最小可跑的步骤先把车跑起来再一步步加功能。别怕代码写得丑别怕第一次用烙铁烫到手也别怕把车模撞坏。只要还能从场地里把车捡回来你就在往前走。这篇文章既是备赛记录也是写给当年那个独自坐在实验室里的自己。如果你的情况和我类似希望它能帮你少走一点弯路也让你知道一个人并不等于什么都做不成。