
简介2023年电赛E题参考资料包面向全国大学生电子设计竞赛参赛者围绕“基于STM32F1的自动追光云台”赛题提供完整工程与实现剖析重点覆盖光感检测、云台电机控制与追踪算法等核心环节。压缩包共199个文件约6.74MB以C语言源文件.c/.h、编译中间产物.o/.crf及Keil工程配置文件.uvprojx/.uvoptx为主同时包含汇编启动代码.s、调试辅助脚本及烧录固件.hex/.axf目录结构清晰适合直接打开工程对照学习。该资源已有7803人浏览学习热度较高可帮助备赛者快速定位关键技术难点。内容涵盖光敏传感器信号采集、步进/伺服电机驱动、PID追踪调节等模块配合完整源码与工程配置读者既能理解整体工作流程也能针对姿态解算或转动精度进行局部调优是电赛E题备赛与方案比对的实用参考。1. 2023年电赛E题参考先搞清题目到底在考什么2023年电赛E题表面上是“追光”实际上考的是视觉识别、坐标映射、闭环控制三件事的串联。很多人拿到题目第一反应是上神经网络结果四天三夜全耗在调模型上最后连舵机都没转起来。这个参考资源要解决的就是这个问题用一套成熟的视觉识别加PID控制方案把“看到激光点”变成“控制云台跟上激光点”的完整链路。适合准备电赛的学生、想快速搭一套视觉追踪Demo的开发者以及被“识别到了但控制不住”卡住的人。先说明白E题不需要高深算法把颜色识别和PID调好就能拿分。2. 系统框架与核心选型从赛题到实物的映射2.1 硬件链路先画明白摄像头、云台、主控分别干什么做任何电赛题目第一步不是写代码而是把硬件链路在纸上画出来。2023年电赛E题的物理实体是两套二自由度云台一套挂红色激光笔作为“追踪方”一套挂绿色激光笔作为“被追踪方”外加一块A4打印纸作为运动区域。整个系统的闭环逻辑是视觉模块拍下红色激光点和绿色激光点算出两个点的像素坐标偏差把偏差交给控制算法转成舵机PWM增量让红色云台跟着绿色云台走。这个参考资源里的典型方案是视觉模块用自带MCU的摄像头模组云台用两个金属齿舵机组成的二自由度结构主控用普通的开发板。视觉模块负责采集图像、识别激光点、输出坐标主控负责接收坐标、跑PID、输出PWM。为什么这么拆因为把视觉识别和电机控制分开调试时可以单独验证每一段先确认坐标输出稳定再调舵机跟随最后才联调。串在一起调出了问题你根本不知道是识别丢了目标还是PID发疯。我见过一组同学用树莓派加USB摄像头算法用OpenCV的霍夫圆检测找激光点思路没错但树莓派启动慢、供电要求高赛场电源一波动就重启翻了车。反而是用带MCU的摄像头模组每次上电三秒内就能出坐标稳得多。2.2 选型对比算力不是越高越好稳定和简单才是关键四天三夜的比赛时间调试成本比硬件成本贵得多。下面是E题常见方案选型对比都是参赛队伍里出现过的路子模块方案A本资源采用方案B方案C视觉识别带MCU的摄像头模组自带颜色阈值识别固件树莓派OpenCV普通摄像头上位机PC处理主控普通单片机开发板树莓派直接输出PWM单片机接收上位机坐标云台舵机金属齿舵机扭矩大不回弹塑料齿舵机便宜但容易扫齿步进电机精度高但重量大供电方案舵机独立6V供电主控5V供电共地所有模块共用一个电源双电源隔离选型理由很简单E题核心是“控制”不是“识别得多花哨”。把识别放在专用模组上主控只做控制逻辑整个系统的实时性和稳定性都好掌握。树莓派方案看着高级但比赛现场环境光一变OpenCV参数就要重新调根本来不及。供电是最容易被忽略的坑。舵机启动瞬间电流能到1A以上如果和主控共用电源电压跌落会导致单片机复位。我一般会把舵机电源和逻辑电源分开最后共地避免地电位差导致串口通信乱码。这个细节在后面的排查章节还会再提。2.3 坐标映射与坐标系统一先定基准再写代码把像素坐标转成舵机角度之前必须先统一坐标系。摄像头画面里x向右增大y向下增大而云台左右舵机的角度是顺时针为正俯仰舵机是抬头为正。如果不做映射经常出现“目标在画面右边云台却往左转”的诡异现象。常见的做法是先手动让云台回中此时激光点落在画面中央把画面中心像素坐标记为原点。然后云台向左转10度记录激光点像素坐标向右转10度再记录一次。用这两组数据算出像素到角度的比例系数。这个系数不需要很精确后面PID会消除稳态误差但方向必须一致。我在调的时候会把画面坐标系的y轴取反再送进PID这样云台上仰对应画面中目标下移逻辑才顺。这个参考资源里给出的系数计算是这样的假设画面宽度320像素云台左右范围正负15度那么比例系数就是15除以160约等于0.094度每像素。俯仰方向同理但通常俯仰范围比水平小一些系数会不同。3. 视觉识别模块矩形框、靶标与激光点的坐标提取3.1 颜色空间与阈值标定为什么用LAB而不是RGBE题里激光点识别最大的干扰来自环境光和纸张上的印刷色。RGB颜色空间对亮度极其敏感同一个红色激光点在画面亮部和暗部拍出来RGB分量能差出一倍。LAB颜色空间把亮度和颜色分开L通道管亮度A通道管红绿B通道管黄蓝这样识别颜色的时候可以只盯着A和B通道看亮度变化的影响就小得多。阈值标定是这套参考方案里最“玄学”的一步但做得好不好直接决定识别稳定性。我的操作习惯是在赛题要求的场景下用视觉模组的阈值编辑器先截取包含激光点的区域分别统计L、A、B三个通道的均值和上下浮动范围然后把阈值设置成“均值加减三倍标准差”。下面这段代码是标定用的核心逻辑# 阈值标定辅助脚本采集激光点ROI区域的LAB统计值 import sensor import image sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240分辨率帧率更高 sensor.skip_frames(30) sensor.set_auto_whitebal(False) # 关闭自动白平衡颜色才稳定 roi (120, 80, 60, 60) # 手动画框框住激光点 sample_count 10 l_sum a_sum b_sum 0 for i in range(sample_count): img sensor.snapshot() stat img.get_statistics(roiroi) l_sum stat.l_mean() a_sum stat.a_mean() b_sum stat.b_mean() print(L均值: %d, A均值: %d, B均值: %d % (l_sum // sample_count, a_sum // sample_count, b_sum // sample_count))这里有个关键动作关闭自动白平衡。自动白平衡会根据场景色温自动调整颜色通道增益导致同一个激光点在不同时刻识别出的颜色不一致。视觉模组的自动白平衡默认是开着的标定前必须关掉颜色阈值才不会飘。ROI区域要在画面里框住完整的激光点光斑别框太大否则把背景也统计进去了。标定完均值之后上下限怎么设我一般取均值偏差20左右。比如A通道均值是60那阈值范围就设成40到80。太宽会把相近颜色的物体框进来太窄则环境光稍微一变动就会丢失目标这个值需要你在实际场景里试几次才能找到平衡点。3.2 激光点识别与坐标输出形态学处理加最大连通域阈值标定好之后下一步就是把符合阈值的像素点聚成一个目标。直接用find_blobs会得到很多小碎块比如远处观众衣服上一小块红色反光都会变成一个噪声块。可行的做法是先做腐蚀去噪再做膨胀恢复光斑尺寸然后只保留面积最大的那个连通域认定它就是我们要找的激光点。下面是一段实际的激光点识别代码可以直接烧进视觉模组运行import sensor import image # 这里的阈值需要按3.1的标定结果替换 THRESHOLD_RED (20, 60, 40, 80, 30, 70) THRESHOLD_GREEN (20, 60, -70, -40, 20, 60) sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240平衡分辨率与帧率 sensor.set_vflip(True) # 根据摄像头安装方向决定 sensor.skip_frames(50) sensor.set_auto_whitebal(False) def find_laser(img, threshold): # 先用阈值过滤出候选像素 blobs img.find_blobs([threshold], pixels_threshold10, area_threshold10, mergeTrue, margin5) if not blobs: return None # 取面积最大的连通域作为激光点 largest max(blobs, keylambda b: b.pixels()) if largest.pixels() 30: return None # 面积太小大概率是噪声 img.draw_cross(largest.cx(), largest.cy(), color(255, 255, 255)) img.draw_rectangle(largest.rect(), color(255, 255, 255)) return (largest.cx(), largest.cy()) while True: img sensor.snapshot() red_pos find_laser(img, THRESHOLD_RED) green_pos find_laser(img, THRESHOLD_GREEN)这段代码里有两个参数值得注意。pixels_threshold是单块连通域至少要包含的像素数低于这个数的直接丢掉area_threshold是区域面积下限。这两个值设太小会引入噪声设太大会把距离远的激光点丢掉。E题场景下摄像头离A4纸大概40到60厘米激光点光斑在画面上直径约5到8像素pixels_threshold设10比较合适。mergeTrue的意思是相邻的候选块只要间距小于margin像素就合并成一个大块。这个参数对激光点识别非常重要光斑中心高亮、边缘暗有可能会被阈值切分成内外两圈不合并的话会误判成两个目标。margin设5像素足够把同一个光斑拉回来。最后那个pixels() 30的判断是我后加的“后悔药”。有一次测试时桌面上的红色储物盒反光被识别成目标那个反光块面积比激光点大好几倍直接抢走了“最大连通域”的位置云台一下甩过去了。加了面积上限和下限双重约束之后这类误识别就很少出现了。3.3 坐标输出与串口协议帧头校验一个都不能少视觉模组识别出坐标之后要把坐标发给主控。串口是最简单的通信方式但裸发两个数字很容易出问题。比如主控只收到了一个字节剩下的数据错位坐标就全乱了。所以一套带帧头和校验的串口协议是必须的。下面是我常用的帧格式import time from machine import UART uart UART(3, 115200, timeout_char200) def send_coord(uart, x, y): # 帧格式: 帧头 类型 X高字节 X低字节 Y高字节 Y低字节 校验和 type_id 0x01 # 0x01表示红色激光点坐标0x02表示绿色 buf bytearray(7) buf[0] 0xAA # 帧头固定0xAA buf[1] type_id buf[2] (x 8) 0xFF buf[3] x 0xFF buf[4] (y 8) 0xFF buf[5] y 0xFF buf[6] (buf[1] buf[2] buf[3] buf[4] buf[5]) 0xFF uart.write(buf) # 主循环里调用 while True: img sensor.snapshot() pos find_laser(img, THRESHOLD_RED) if pos: send_coord(uart, pos[0], pos[1]) time.sleep_ms(10) # 约100Hz发送频率校验和是前六个字节相加取低八位。主控端收到一帧数据后先判断帧头是不是0xAA再算一遍校验和两个都通过才认为是有效坐标。这样即使串口干扰造成某个字节错误这一整帧也会被丢掉而不是拿错误坐标去控制云台。在赛场那种几十块电源板堆在一起的电磁环境下这个校验逻辑能救你一次。发送频率控制在100Hz就够。舵机的响应带宽也就几十赫兹发太频繁反而占用主控处理时间。另外视觉模组在识别到目标时才发送没识别到就什么都不发。主控那边要做的超时判断在后面的避坑章节会专门讲。4. 云台控制与PID调参让舵机“指哪打哪”的调参路径4.1 从像素误差到舵机角度归一化和限幅是保命设计主控拿到坐标后工作才真正开始。控制的第一步是把“目标在画面中的位置”和“画面中心”的偏差算出来。这个偏差是像素值不能直接当舵机PWM增量用需要先做归一化和限幅。假设画面宽度320像素云台水平机械范围是正负30度。如果把整个画面宽度映射到正负30度那目标在画面边缘时舵机要瞬间摆30度动作太猛视觉模组画面会因为云台振动而抖动反而更不容易识别目标。我一般会把舵机工作范围压缩到正负15度像素偏差限幅在正负80像素左右超出部分直接截断。# 像素误差转舵机角度增量 MAX_PIXEL_ERROR 80 # 像素误差限幅 MAX_ANGLE_STEP 15 # 最大角度步进 def pixel_error_to_angle(err_x, err_y): # 限幅防止误差过大导致舵机猛转 err_x max(-MAX_PIXEL_ERROR, min(MAX_PIXEL_ERROR, err_x)) err_y max(-MAX_PIXEL_ERROR, min(MAX_PIXEL_ERROR, err_y)) angle_x err_x / MAX_PIXEL_ERROR * MAX_ANGLE_STEP angle_y err_y / MAX_PIXEL_ERROR * MAX_ANGLE_STEP return angle_x, angle_y这里为什么限幅这么重要因为视觉识别偶尔会跳一帧比如激光点被遮挡后又出现瞬间的像素误差到了200多。如果不过限幅直接映射舵机会猛地甩一下整个云台都会跟着振动。限幅之后误差再大也就输出15度云台动作平滑测出来反而更快到位。注意这里返回的是角度增量不是绝对角度。主控里会维护一个当前角度的变量每次更新时累加增量。这样做的好处是即使某一帧输出有问题也只是在正确位置附近小幅偏移不会出现“归零然后重新转”的跳变。位置式PID配合增量式输出是舵机云台控制里最稳的组合。4.2 位置式PID与死区处理不抖动是调参的第一原则E题的云台追踪评价标准里不只看能不能跟上还看稳不稳。舵机停在目标附近时如果一直在微调激光点就会呈现高频抖动评委一眼就能看出来不行。要消除这种高频抖动第一道工序是加死区。死区的含义是当误差小于某个像素值时认为“已经到位了”输出直接置零。E题场景下死区设3到5像素比较合适。如果用1像素舵机会在目标附近来回摆动肉眼看到的就是激光点在颤抖如果用10像素误差还没到死区就停了追踪精度变差。这个值需要现场调试但5像素是个比较稳的起点。下面是一份基础的增量式PID实现我在多个项目里都用过这套结构class IncrementalPID: def __init__(self, kp, ki, kd, deadband4, integral_limit60): self.kp kp self.ki ki self.kd kd self.deadband deadband self.integral_limit integral_limit self.last_err 0 self.integral 0 def compute(self, err, dt0.02): # 死区内不计算直接输出0 if abs(err) self.deadband: self.last_err err self.integral 0 return 0 # 积分项分离只有误差持续存在时才积分 self.integral err * dt self.integral max(-self.integral_limit, min(self.integral_limit, self.integral)) derivative (err - self.last_err) / dt output self.kp * err self.ki * self.integral self.kd * derivative self.last_err err return output # 初始化水平舵机和俯仰舵机分开调参 pid_x IncrementalPID(kp0.25, ki0.01, kd0.08) pid_y IncrementalPID(kp0.25, ki0.01, kd0.08)积分项为什么要设上限这是血泪经验。如果目标被遮挡误差持续存在积分项会不断累积等到遮挡解除积分项已经大到离谱云台会飞一样地甩过去完全失控。加上积分限幅之后最坏情况也就是输出最大值不会破坏系统稳定性。微分项的作用是抑制超调但副作用是放大噪声。如果视觉坐标本身有跳动微分项会把跳动放大成控制输出来回波动。所以我一般把kd设得很小优先保证平滑而不是追求极致响应速度。一个参考起点是kp在0.2到0.3之间ki在0.005到0.02之间kd不超过0.1。具体数值要等上电实测后再调。4.3 三个调参阶段从定死区到补积分步骤不能乱调PID最忌讳一步到位妄想一组参数直接完美。我在E题备赛时总结了一套固定流程照着走基本都能调出来。第一阶段锁定死区。先关闭I和D只保留PP设0.15。观察云台在目标停止后的行为。如果云台在目标附近快速抖动说明P太大或死区太小增大死区或减小P如果云台缓慢漂移停不下来说明P太小需要加大。这一步的目标是让云台能在目标静止后2秒内稳定下来。第二阶段调P和D的配合。P从0.15往上加每加0.05就测试一次阶跃响应。现象是云台快速转过去如果超过目标位置再弹回来几下叫超调需要加D或者减P。加D对超调的抑制立竿见影但D太大会导致高频振动需要来回试几次。这个阶段的通过标准是手动快速移动目标云台能在1秒内跟上且超调次数不超过一次。第三阶段补I消除静差。如果云台停下后激光点和目标之间始终差几个像素这叫稳态误差是舵机死区或机械回程差导致的。加上I从0.005开始加观察目标静止时激光点会不会慢慢爬向目标。I太大会在目标附近来回振荡看到振荡就退回一半重新确认。调参阶段调整对象观察指标通过标准第一阶段死区、KP目标静止时的云台行为2秒内稳定不抖第二阶段KD配合KP阶跃响应1秒内跟上超调不超过1次第三阶段KI消除静差稳态像素误差静止偏差小于等于死区宽度这套流程的核心逻辑是先让系统“稳得住”再让它“跟得上”最后才让它“瞅得准”。跳过任何一步参数都会相互影响容易调两天都调不出来。5. 避坑与常见问题排查复位、曝光、舵机抖动的解决记录5.1 坑一红色激光点和纸质靶标颜色撞车云台乱跑现象红色激光点识别正常但A4纸上的红色印刷靶标也被识别成目标云台在两个红色区域之间来回跳完全分不清该跟哪个。原因阈值设置得太宽。激光点颜色是高度饱和的纯红纸质印刷的红色在LAB空间里其实偏暗A通道数值没那么高。如果阈值下限设置过低纸质红色就会混进来。解决把A通道的下限往上抬直到只有激光点能通过阈值。实操方法是用视觉模组的阈值编辑器加一个实时预览窗口手动调A通道下限一边看预览一边观察检测结果。另一个有效手段是加面积约束纸质靶标在正常距离下成像面积比激光点大很多面积上限可以直接排除掉。这条经验后来在绿色激光点上同样适用绿色靶标和绿色激光的A通道分布差异更大收窄阈值就能解决。5.2 坑二环境光一变阈值当场失效现象在实验室调好的阈值第二天带去比赛现场识别不出激光点了或者噪声暴增。原因比赛场地的灯光是荧光灯色温和实验室白炽灯差异很大。关闭自动白平衡之后画面各通道的绝对值变化巨大旧阈值自然失效。解决这是E题最常见的“翻车”现场之一。处理办法是到场后不要急着跑题目先花20分钟用阈值编辑器重新标定。我在标定时会连续采样20帧把激光点位置固定取每帧的LAB均值再求平均比单帧采样可靠得多。另外把摄像头曝光时间固定下来而不是用自动曝光。自动曝光会随画面亮度波动导致激光点过曝成白色或欠曝看不见。设置固定曝光后至少三天内环境不变阈值就不用反复调。5.3 坑三舵机低速时咔咔响激光点在目标附近高频抖动现象追踪精度看起来还行但云台一直在轻微左右摆动伴随时有时无的咔咔声拍出来视频很不美观。原因舵机进入“极限环”状态。P项和D项的输出让舵机在目标位置附近来回试探每次修正幅度又不足以跨过死区舵机就在死区边缘反复震荡。另一个机械因素是舵盘螺丝没拧紧齿间隙让微小运动被放大。解决先把死区从4像素提高到7像素抖动一般能直接消失。如果还有轻微抖检查舵盘固定螺丝用手轻轻扳动舵盘看有没有旷量有旷量就用螺丝胶固定并把螺丝拧紧。最后检查D项是不是设置太大D会把坐标噪声放大成控制量适当降低KD也是常见手段。5.4 坑四一上电云台猛转然后复位或者视觉模组反复重启现象系统上电瞬间云台像“抽搐”一样转到底再回中多试几次还会出现主控重启、激光笔亮度变暗。原因舵机启动电流峰值太大。云台从静止到猛转的瞬间如果电源输出能力不足电压会被拉低到单片机复位阈值以下整个系统就重启了。激光笔和主控共用电源时电压跌落也会让激光驱动电路输出不稳。解决这是供电设计问题不是代码能修的。舵机要单独供电我一般用一节6V电池直接供电主控和视觉模组用5V稳压电源两组电源只在GND端相连。实测这个方案能解决百分之九十五的复位问题。如果舵机扭矩大启动电流超过2A还要在舵机供电端并联一个大容量电容470uF起步瞬间电流由电容先顶着。5.5 坑五目标被胳膊挡住一秒云台瞬间甩飞现象追踪过程中操作者伸手调整绿色激光笔红色云台突然向一个方向猛甩手移开后云台已经找不到目标了。原因目标短暂丢失期间视觉模组没有发送坐标主控保持上一帧的误差值继续积分。一秒的时间足够让积分项冲到上限目标重新出现时PID输出已经是最大值云台猛烈摆动。解决主控要设计“丢目标处理”逻辑。串口连续超过200毫秒没有收到有效帧就认为目标丢失此时把PID积分清零输出归零云台保持在最后有效位置附近。目标恢复后误差会从当前位置重新计算即使之前跑偏了也只是慢慢调整回来不会猛甩。这个逻辑属于“悲观采样”宁愿目标丢失时不动也不要在错误方向上越走越远。6. 从“能跑”到“稳定”三个验证流程与边角优化6.1 稳定性验证跑数据说话别靠感觉调完就交调参是否结束不能凭“看着还行”来判断。我在每次调完PID之后会做三个固定测试全部通过才认为这套系统可以上场。第一个测试是定点精度测试。把绿色激光点固定在A4纸中心让红色云台从边缘位置追踪过来等待5秒测量红色激光点和绿色激光点之间的像素距离。通过标准是误差小于死区宽度也就是画面里两颗光斑基本重合。第二个测试是阶跃响应测试。手动改变绿色云台输入让绿点快速跳到另一个位置记录红点从开始运动到第一次到达目标附近的时间。通过标准是1秒内到位且不产生超过一次的来回摆动。第三个测试是十五分钟压力测试。连续追踪运动轨迹15分钟每30秒记录一次坐标发送帧率板子温度和舵机温度。测试项目测试方法通过标准定点精度绿点固定红点从外侧追踪并保持5秒像素误差不超过死区宽度阶跃响应绿点位置跳变观察红点跟随过程1秒内到位超调不超过1次持续压力连续运行15分钟记录帧率和舵机温度不掉帧舵机不过热很多时候“静态很稳”的云台在持续追踪时就掉链子。压力测试期间我会一直盯着视觉模组的调试画面一旦出现连续多帧找错目标说明阈值还是有问题需要回去重新调阈值而不是调PID。这个细节很有用视觉问题会表现为控制问题排查时要从信号源头一层层往外查。6.2 边角优化目标坐标低通滤波让云台动作更平滑E题的绿色激光点不会静止总是要画出各种路径。如果直接把每一帧的像素坐标拿去计算误差坐标的随机跳动会被PID放成云台的细微颤动。一个低成本优化是加一阶低通滤波让目标坐标输出得更平滑。# 目标坐标一阶低通滤波 alpha 0.4 # 滤波系数越大越平滑但延迟越高 target_smooth_x target_smooth_y 0 def smooth_target(raw_x, raw_y): global target_smooth_x, target_smooth_y # 首次赋值用原始值后续滑动平均 if target_smooth_x 0: target_smooth_x, target_smooth_y raw_x, raw_y else: target_smooth_x alpha * raw_x (1 - alpha) * target_smooth_x target_smooth_y alpha * raw_y (1 - alpha) * target_smooth_y return target_smooth_x, target_smooth_yalpha取值0.4到0.5比较平衡。取太大滤波效果不明显取太小云台动作变得迟钝追踪快速移动目标时会有明显滞后。这个参数要在实际追踪测试中感受没有统一最优值。滤波后的坐标再送给PID云台动作明显比滤波前平滑一个档次。6.3 调试环境标准化拍照记录每个实验条件每次调参之前把摄像头位置、云台角度、A4纸位置、灯光角度固定好拍一张照片存下来。赛场上很多诡异问题追查到最后发现是A4纸歪了5度或者摄像头被碰了1毫米。环境一变化像素坐标和物理坐标的映射关系就变了之前调好的参数自然失效。我自己的习惯是在测试桌的桌角和云台底座上用胶带做标记每次实验开始前先检查所有标记是否对齐。这套资源的代码包里也包含了图像预览和阈值快照工具方便每次调试前快速确认环境条件没有变化。从那以后我每次调视觉都强制先做一遍环境对准再动代码。希望帮到你。本文还有配套的精品资源点击获取