资讯动态

2024电赛H题视觉小车实战:从图像处理到PID闭环调试

发布时间:2026/10/14 20:56:58 来源:尧图企业网站定制
简介面向2024年电子设计竞赛H题参赛者的完整代码包聚焦信号处理与STM32微控制器硬件控制适合具备C语言与嵌入式基础、正在备赛的高校电子与计算机类学生。压缩包约114.28MB内容以C语言源码与STM32相关工程文件为主围绕赛题核心模块展开涵盖信号采集、滤波与频谱分析等功能的程序实现以及基于STM32的外设驱动、控制逻辑与数据交互代码可帮助理解从模拟输入到数字处理再到外设响应的完整链路。已有5720人学习下载适合对照赛题要求进行代码研读、模块移植与在线调试。除主程序外包内还包含独立的功能拆分文件便于分模块梳理算法与硬件控制流程减少底层驱动和信号处理框架的重复搭建可显著提升备赛效率与方案完整性。对于希望在电赛H题中快速搭建原型、核实关键算法细节的读者这套代码是一份可直接落地的实践参考资料。1. 2024年电赛H题这类视觉小车代码的核心不在代码在闭环2024年电赛H题的方向是自动行驶小车要求小车在指定赛道上完成循迹、识别标记、停车等动作。很多同学拿到代码包第一件事就是烧录结果车不走或者乱走于是觉得“代码有问题”。实际上这类题的代码从来不是跑通的是调通的。摄像头看到什么、主控怎么理解、电机怎么响应这三者之间任何一环没对齐车就会翻。这篇笔记适合正在备赛的参赛队也适合想入门视觉小车的人。我会把一套能稳定跑完赛道的代码拆开讲从图像处理、串口协议到PID控制最后给出调车阶段的排查清单。这套写法不挑具体板子OpenMV或者K210配STM32都能用重点是你得知道每行代码在帮小车回答什么问题。2. 赛题代码从哪里下手先搭系统再写代码2.1 硬件选型决定了代码长什么样视觉小车的代码结构很大程度上是硬件方案决定的不是你想怎么写就怎么写。常见的方案有两类一类是单芯片方案直接用K210或者树莓派Pico外加摄像头模块所有逻辑写在一块板子上另一类是双芯片方案用OpenMV或者K210做视觉采集与识别把结果通过串口发给STM32或者GD32做主控由主控去驱动电机和舵机。我在备赛时用的是后一种方案。理由很简单2024年H题这类赛题不仅要循迹还要判断岔路、识别停止线甚至可能需要区分颜色标记。视觉任务越复杂越需要把图像处理和实时控制分开。视觉芯片跑图像算法时中断和时序会受很大影响如果这时候还让它控制电机出问题的概率会翻倍。双芯片方案里主控只需要关心串口里收到的数字然后调速、转向逻辑非常干净调试时也容易定位是视觉的问题还是控制的问题。代码组织同样受这个方案影响。视觉端的代码只负责回答三个问题赛道在哪里、车偏了多少、前面有没有特殊标记。主控端的代码也只回答两个问题当前应该多快、应该往哪边转。你不要在一段代码里既做图像识别又做PID运算那样后面没法调。2.2 主控与外设的代码分层感知、决策、执行把代码分成三层这是我从往届优秀方案里总结出来的经验自己用也顺手。感知层是视觉代码处理摄像头画面输出偏差值和标记信息。决策层在主控里根据感知数据决定目标速度和目标转向角。执行层再往下用PID去驱动电机和舵机让实际速度和转向逼近目标值。分层的好处是在赛前调试时可以单独验证每一层。比如车不动你先看执行层手动给一个PWM值看电机转不转电机转了再看决策层手动往串口发一个偏差值看车轮有没有对应动作最后才查视觉层。按这个顺序排查十分钟就能定位问题而不是像个黑匣子一样瞎试。很多队伍拿到别人的代码后一上来就改PID参数结果车还是不走就是因为他们跳过了执行层和决策层的验证直接在最高层调参。2.3 最小可跑通的工程骨架一个能跑的工程文件不需要很多但结构必须清晰。我一般会在视觉芯片里放一个main.py里面包含初始化、主循环主控端放一个main.c或者main.py里面包含串口接收、PID计算、电机输出。下面是一个典型的主控端主循环骨架你可以照着把工程搭起来。int main(void) { // 初始化串口1接收视觉模块的偏差数据 uart_init(UART1, 115200); // 初始化定时器产生PWM控制电机和舵机 timer_init(TIM2, 1000); // 1kHz 控制频率 // 初始化OLED用于调试显示 oled_init(); while (1) { // 1. 从串口读取视觉数据解析出偏差值 if (uart_receive_frame(frame) OK) { target_speed calculate_speed(frame.deviation, frame.marker); target_steer calculate_steer(frame.deviation); // 2. 把目标值交给PID控制器 pid_set_target(speed_pid, target_speed); pid_set_target(steer_pid, target_steer); } // 3. 跑一遍PID并更新PWM // 这一行在定时器中断里做也可以效果差别不大 pid_process_1000hz(speed_pid, motor_read_speed()); pid_process_1000hz(steer_pid, servo_read_angle()); } }这个骨架的重点是主循环只做两件事收数据、跑控制。任何延时、打印、按键扫描如果在这个循环里做太多都会拖慢控制频率。比如你用延时函数加一个按键消抖假设每次消抖20毫秒控制频率就直接从1kHz掉到50Hz小车高速过弯时必翻车。我一般把按键扫描放到定时器中断里主循环保持纯净。主控端的代码解释完了但整个系统真正的难点在视觉端。下面这一章我们来处理视觉感知这也是整个H题代码里最容易翻车、最需要反复调的部分。3. 视觉感知模块图像怎么变成赛道参数3.1 用LAB阈值分割提取赛道边界2024年H题的赛道背景通常是浅色地面赛道线是深色或者彩色。视觉端第一步是把赛道从背景里分离出来我用的方法是在LAB色彩空间做阈值分割。为什么不用RGBRGB三个通道对光照变化特别敏感同一段赛道在阳光下和阴影里颜色差异很大阈值很难统一。LAB的L通道是亮度A和B通道是色彩赛道和背景通常在A通道或者B通道上有稳定的差异把亮度干扰丢掉阈值就好调很多。OpenMV的find_blobs就是干这个的。下面这段代码里我定义了深色赛道线的LAB阈值然后提取所有符合阈值的色块再从中挑出面积最大的一个作为赛道目标。import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) # RGB565比灰度图保留更多颜色信息 sensor.set_framesize(sensor.QVGA) # 320x240够用且处理速度快 sensor.skip_frames(30) sensor.set_auto_whitebal(False) # 关闭白平衡防止颜色漂移 # 赛道线阈值LAB色彩空间的下限和上限 # 这个值需要在不同光照下反复采集标定 THRESHOLD [(10, 40, -20, 30, -10, 20)] def find_track(): img sensor.snapshot() blobs img.find_blobs(THRESHOLD, pixels_threshold100, area_threshold100) if blobs: # 挑选面积最大的色块作为主赛道区域 max_blob max(blobs, keylambda b: b.area()) img.draw_rectangle(max_blob.rect()) return max_blob return None这段代码里有几个关键点。pixels_threshold和area_threshold用于过滤噪点如果设得太小画面里的杂散反光会被当成赛道设得太大远处的赛道就丢了因为远处赛道的面积本来就小。我一般先用100起步实测后图像里只有一处大色块那就说明过滤条件合理。这些参数直接影响小车能不能看到远处弯道需要在现场实际光线条件下标定不是在实验室调好就完事的。3.2 计算车体位置偏差与弯道方向有了赛道色块下一步是把它变成一个数——偏差值。偏差值代表小车中心线和赛道中心线之间的横向偏移量单位用像素。OpenMV的色块对象提供了cx()和cy()方法分别返回色块中心点的横纵坐标。用画面中心线的x坐标减去色块中心的x坐标就得到了偏差。但只用色块中心有个问题车已经上了赛道色块往往集中在画面下方中心点反映的是近处赛道位置对远处弯道不敏感。我一般改用色块的质心加权计算把色块下半部分和上半部分分别处理近处偏差和远处方向信息糅合在一起。具体做法是把画面分成上下两半分别计算色块中心然后近处偏差占权重大远处方向占权重小得到一个合成偏差。def compute_deviation(blob, img_width320): # 画面下半部分占60%权重上半部分占40% mid_y blob.cy() # 色块中心y坐标 if blob.cy() img_width * 0.4: # 赛道太远偏差参考意义弱放大返回 return (blob.cx() - img_width // 2) * 1.5 # 近处赛道直接用中心偏差 return blob.cx() - img_width // 2这里乘1.5是为了让远处赛道出现偏差时小车能提前做出反应。弯道识别太晚是这类视觉小车最常见的问题总是在进弯的一瞬间才打方向车身姿态已经救不回来了。提前放大偏差相当于让转向的灵敏度在远处就开始生效。这个系数不是定死的你的赛道如果弯道急可以调到2.0弯缓则调到1.2。我在调车时通常先在直道上确认偏差输出为0再进弯道看偏差是否平滑变化而不是跳变。3.3 串口通信协议发送什么、多久发一次视觉端算出了偏差值接下来要通过串口发给主控。通信协议我建议用固定帧格式不要发裸数据。裸数据的坑在于偏差值是多字节数字主控端解析时一旦收到半截数据后面的帧全错位。加了帧头、帧尾和校验之后主控可以检测到错位并重新同步。我常用的帧格式是帧头0xAA随后是数据长度、偏差值的高字节和低字节、特殊标记值最后是校验和。偏差值范围是-100到100用有符号16位整数表示特殊标记用来告知主控是否识别到停车线或岔路标志。发送频率要稳定一般每50毫秒发送一帧也就是20Hz。太快会挤占串口缓冲太慢主控控制跟不上。这一段代码我一般放在主循环里不单独开线程。# 组装串口数据帧AA 03 [偏差高8位] [偏差低8位] [标记] def send_frame(uart, deviation, marker): dev_int int(deviation) dev_high (dev_int 8) 0xFF dev_low dev_int 0xFF data bytearray([0xAA, 0x03, dev_high, dev_low, marker]) # 最后一个字节是校验和累加所有前序字节 checksum sum(data) 0xFF data.append(checksum) uart.write(data)marker的值可以预先约定好0表示无标记1表示停车线2表示岔路3表示终点。主控端根据marker决定是继续跑、减速还是停车。这里有个细节视觉端识别到标记后不能只发一帧因为主控可能刚好没收漏了。我一般的做法是持续发3到5帧同一个标记直到主控确认收到。确认机制可以是在主控端收到标记后回传一个ACK字节视觉端收到ACK才清掉当前状态。这个双向确认机制在比赛现场特别管用能避免小车明明看到了停车线却不减速的情况。4. 电机控制与PID参数让小车跟着代码跑起来4.1 差速转向控制的思路小车转向方式决定了控制代码的复杂度。常见的H题小车底盘有两种舵机转向和差速转向。舵机转向是前轮偏转后轮驱动控制模型简单偏差直接映射到舵机角度就行。差速转向是两个后轮独立驱动靠左右轮转速差实现转向转弯半径可以非常小甚至原地转圈。我比较推荐差速底盘因为赛道上复杂的S弯和U形弯舵机底盘往往转向半径不够差速底盘可以通过一个轮正转一个轮反转完成小半径掉头。代价是控制代码复杂一些需要同时管理两个电机的速度和转向。下面这套代码是我常用的差速PID结构速度环和转向环分离。速度环控制整体前进速度转向环控制左右轮速度差。两个环互不干扰调参时可以分开整定。4.2 速度环与转向环的分离式PID实现实际写代码时转向环的输出是一个修正量叠加到基础速度上。左轮速度等于基础速度减去转向修正右轮速度等于基础速度加上转向修正。这样小车直行时偏差为0两边速度一样偏差变大时一边加速一边减速自然形成转向效果。我这里用了一个简单的增量式PID输出是修正量而不是绝对值。增量式的好处是突然拔掉电源再上电时不会有巨大的积分冲击车不会猛地弹出去。class IncrementalPID: def __init__(self, kp, ki, kd, integral_limit50): self.kp kp self.ki ki self.kd kd self.last_error 0 self.integral 0 self.integral_limit integral_limit def update(self, error, dt0.02): # 增量式PID输出是本次修正量叠加到目标值上 p_term self.kp * error self.integral error * dt # 积分限幅防止积分饱和导致过冲 if self.integral self.integral_limit: self.integral self.integral_limit elif self.integral -self.integral_limit: self.integral -self.integral_limit i_term self.ki * self.integral d_term self.kd * (error - self.last_error) / dt self.last_error error return p_term i_term d_term # 转向修正量 steer_pid IncrementalPID(kp1.2, ki0.05, kd0.08) # 基础速度单位是编码器每秒计数值需要根据底盘实测 base_speed 400 # 偏差从视觉端来正数代表偏右负数代表偏左 deviation get_deviation_from_serial() steer_correction steer_pid.update(deviation) left_speed base_speed - steer_correction right_speed base_speed steer_correction这段代码里dt默认0.02秒对应控制频率50Hz。如果主循环里用了任何有可能超过20毫秒的操作这个频控就不准了。我一般会在主循环开头记录时间戳每次计算时用实际的时间差而不是固定值。另外左右轮速度是编码器目标值实际给到电机的PWM还要经过一个电流环或速度环这个取决于你的电机驱动板。如果驱动板自带速度闭环你只需要写PWM占空比就行。4.3 三个必调的参数Kp、Ki、Kd的整定顺序PID参数整定是所有调车环节里最玄学的部分但很多队伍翻车不是因为不会调而是因为上来就调Ki。我的整定顺序从来都是Kp优先Ki其次Kd最后。第一步把Ki和Kd都设成0只调Kp。从0.5开始让小车在直道上跑看偏差波动。如果小车左右摆动越来越厉害说明Kp太大减小到0.3再试。如果小车反应迟钝冲出赛道才打方向说明Kp太小加大到0.8。第二步加入Ki主要解决稳态偏差——车始终偏向一边的固定偏移抵消底盘重心不对称或电机装配误差。Ki从0.02起步每次加0.01。第三步加Kd抑制超调Kd相当于提前踩刹车弯道出口处如果车身回摆明显就加大Kd。Kd从0.05起步注意Kd过大在直道上会引发高频抖动。这套顺序听起来简单实际调车时每一轮都要跑完一整圈才有效果。抄别人的PID参数是不能用的因为底盘、轮胎摩擦力、电池电压全都不一样。电池电量下降时同样的PWM跑出来的实际速度会变低你的PID参数在满电时刚好低电量时可能变得很迟钝。所以正式比赛前我会把电池充到额定电压再调参并且调完就用同一批电池。5. 调车阶段的避坑与排查代码没错车不走怎么办5.1 现象阈值调了一下午赛道提取还是花屏这是视觉端最常见的坑。表现是画面里识别出来的色块一会大一会小有时把反光的白色地面也当成赛道有时真正的赛道反而丢了。原因大概率是白平衡没有关闭。OpenMV和K210的摄像头默认会自动调整白平衡画面颜色会跟着环境光线实时变化你上午标定的阈值下午换了光照环境就不适用了。解决方法是初始化时显式关闭白平衡和自动曝光sensor.set_auto_whitebal(False)和sensor.set_auto_exposure(False)。然后重新在比赛现场实际光照下标定阈值。如果现场光线确实变化大可以多做几组阈值在代码里设定一个按键切换逻辑现场按一下切换到合适的组。我之前干过一件事把三组阈值存成数组用小车上一个按键轮询切换然后OLED上显示当前是第几组现场调车效率提升非常明显。5.2 现象串口有数据电机没反应这类问题往往是主控端串口解析的锅不是电机的问题。你先用USB转TTL接在主控串口上手动往里面发一帧数看电机动不动。如果动了说明执行层没问题再看串口配置。常见原因是波特率不匹配视觉端是115200主控端初始化为9600数据全是乱码。还有一个我踩过的坑是串口空闲中断配置错误导致一帧数据要等超时才能被处理主控制器反应慢半拍车看起来就像“轴”一样。解决方法是先确认波特率一致然后打印主控收到的原始数据看是否和视觉端发送的字节一致。看着不对再逐字节排查帧格式看是不是帧头被前面多余字符挤掉了。5.3 现象直道稳、弯道冲出去这是控制参数的时间尺度问题。直道稳定说明偏差小的时候反馈控制没问题弯道冲出赛道往往是控制频率太低或者转向修正量太小。先用示波器或者调试输出看进弯时偏差值变化正常情况下弯道偏差应该平滑增大如果偏差阶跃变化说明视觉端丢帧主控收到的是上一次的旧数据。再说控制频率如果你的主控主循环里有延时函数或者OLED刷新控制频率可能掉到10Hz以下弯道里反馈一遍要100毫秒车早就飞出去了。解决方法是把OLED刷新放到一个低频分支里比如每100次主循环刷新一次控制代码保持高频执行。转向修正量太小也好解决把基础速度从400降到300跑一圈看效果如果弯道能过说明是修正量不够加大转向环的Kp系数而不是硬撑速度。5.4 现象重启后参数全丢某些参数如果存在主控的RAM里断电就清零。比赛现场有个常见的场景车跑完一圈回来接线碰掉又重新插上结果发现原来调好的PID参数变成了出厂值车又开始鬼畜摆动。这种情况说明你的参数是硬编码在初始化函数里的每次上电都重置。解决办法是把参数存到芯片的Flash或者EEPROM里。也可以更简单把关键参数定义在一个单独的头文件里每次重新编译烧录就能固化。如果你用的是开源的固件或者别人的代码先确认参数存储位置别改完参数不保存就上电。5.5 现象停车线识别到了但车不减速这类问题在2024年H题里特别多因为停车线不是一个纯色块它是横向的条纹区域和赛道线颜色相似但方向不同。很多队伍沿用找赛道线的方法找停车线找到的是一根斜线甚至一块光斑色块面积不够大直接被过滤器丢了。解决办法是用两次判断。第一次用宽的阈值找大面积浅色区域判断画面下边缘是否有横向宽带第二次在带内检测多条纵向暗线同时出现3条以上才算停车线。识别逻辑并不复杂关键是你得把“停车线”单独建模不要指望一个find_blobs能同时处理赛道和停车线。这个判断一旦误触发会导致小车在赛道中间突然刹车比不刹车更糟。6. 验证方法让小车“每次都跑出同一圈”才算调完调车到最后很多队伍陷入了玄学——这次跑完了下次又不行也不知道改了什么。我最常做的一件事是给小车建立“重复性基线”同一赛道、同一电量、同一阈值组连续跑五圈记录每圈完成时间和是否冲出赛道。如果五圈结果基本一致说明系统是可控的。如果忽快忽慢、这次过弯下次冲出说明还有偶发问题没解决这时候不要继续调参数回去查硬件连接和代码时序。进阶方向上如果基础循迹已经稳定可以试着加入两点增强。第一点是动态阈值根据画面上半部分的平均亮度实时调整阈值边界这能显著提升阳光移动时的适应性。第二点是前瞻距离调整把偏差计算从当前帧改成前瞻几帧的加权平均相当于让小车“看更远”过S弯时提前量更足。这两个功能不要同时加先加一个跑一版验证没问题再加另一个。我的习惯是每调一次参数就备份一个版本命名里带日期和效果摘要比如“baseline_0518_fast5laps”或者“turn_better_lowbatt”。比赛前夜绝不改大逻辑只微调阈值和PID。这套习惯帮我避免过很多次“昨天还好好的今天全乱了”的情况。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑