尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

2024电赛H题自动行驶小车:STM32循迹与PID控制实战解析

2024电赛H题自动行驶小车:STM32循迹与PID控制实战解析 简介2024年电赛H题代码资源包面向电子设计竞赛参赛队伍聚焦信号处理类任务提供基于C语言与STM32微控制器的完整实现方案。资源针对H题中的信号采集、数据分离、滤波与特征识别等任务环节给出可运行的源代码模块便于选手直接编译调试或对比学习各模块间逻辑清晰有助于理解整体工程架构。压缩包大小约114.28MB内容以代码工程和说明文件为主虽未提供逐文件清单但目录结构清晰涵盖主控程序、外设驱动与算法子模块适合备赛期间快速定位关键部分。目前已有5707人学习下载适配具备基础嵌入式知识、正在冲刺电赛的本科生及指导教师。通过分析该代码读者可深入理解STM32外设配置与信号处理算法的工程化落地并借鉴比赛中的问题排查与优化思路有效提升备赛效率。 2024年电赛H题“自动行驶小车”光听名字好像不难可等自己真正开始写代码、上板调试才知道什么叫“看着简单跑起来全乱套”。这道题要求小车沿黑线行驶经过装货区、卸货区完成装卸再返回整个过程涉及循迹、测距、转向、舵机控制、站点识别几乎把嵌入式开发里最基础也最核心的模块都串了一遍。网上能搜到很多“2024电赛H题代码”但大多数只是贴一段主循环真正有价值的其实是背后的拆解思路和调试方法。这篇就把我当时从拿到题到最终跑通的完整过程写出来从硬件配置到核心算法再到现场踩坑给下一步要做类似赛题或想自己搞一台循迹小车的朋友做个参考。1. 赛题拆解与整体代码架构1.1 题目到底考了什么先说我对这道题的理解。2024年电赛H题的核心是“自动行驶小车”场地里有一条黑色引导线小车要从启动区出发沿黑线行驶到指定站点停车等待装货然后继续沿黑线前进到卸货区执行卸货动作最后再返回启动区。听起来很简单但实际考察的点非常密集循迹稳定性决定小车会不会冲出赛道站点识别准确性决定它能不能在正确的位置停车速度控制决定整个流程能不能在规定时间内跑完机械结构与舵机配合决定装卸动作是否可靠。再算上现场光照变化、地面反光、电池电压波动这些干扰因素这道题本质上考的不是“会不会写代码”而是“能不能用代码把一堆不确定的传感器数据变成稳定的控制决策”。1.2 模块化拆解才是唯一出路我见过不少队伍拿到题就开始写主循环把循迹、测距、舵机、显示全塞进一个while里最后改一个参数就要牵动全局比赛现场直接崩。我的做法是先按功能把整个系统拆成几个独立模块模块职责关键资源传感器采集模块读取灰度传感器阵列、编码器脉冲ADC、定时器编码器模式滤波与预处理滑动平均、异常值剔除、动态阈值纯软件运动控制模块电机PID速度环、转向PD环、PWM输出定时器PWM、编码器站点识别模块基于灰度全黑计数、编码器里程判断站点纯软件状态判断执行机构模块舵机抬升/放下装货卸货动作PWM舵机控制人机交互模块OLED显示状态、按键启动、蜂鸣器提示I2C、GPIO这样拆完以后每个模块独立测试最后再通过一个主状态机把它们串起来。别小看这一步模块化带来的最大好处是出问题时能快速定位比如小车转弯突然抖动我先查转向PID再查电机速度环几分钟就能锁定问题而不是对着几百行代码干瞪眼。2. 硬件底层与驱动配置2.1 主控选型和引脚分配思路H题绝大多数队伍都选STM32我用的是一块STM32F407核心板主频168MHz资源完全够用。选型时重点考虑三个因素定时器数量要够需要编码器接口和PWM输出ADC通道要够8路灰度传感器最好每路一个通道串口至少留一个用作调试输出。我的引脚分配方案是这样的TIM2、TIM3作为编码器输入模式接左右轮电机编码器A、B相TIM4的四个通道输出PWM其中两个给电机驱动TB6612两个给舵机ADC1的8个通道分别接8路灰度传感器输出I2C1接OLED屏幕SSD1306128x64USART1接串口转USB模块用来打印调试信息几个普通GPIO接按键、蜂鸣器、LED指示灯。这里有个很容易踩的坑编码器输入和PWM输出尽量使用不同定时器否则复用中断和通道映射会把自己绕晕。另外ADC的采样时间不要默认设置建议把采样周期拉长一点比如采样时间设置为84个周期以上能明显减少信号抖动。2.2 传感器和执行器的接线与供电细节灰度传感器我用的是TCRT5000反射式红外模块一共8路按等间距排成一条直线安装高度大概在1.5厘米到2厘米之间。这个高度很关键太高了检测不到黑线太低了遇到地面轻微不平就会误判。模块安装时我在外侧加了遮光罩避免环境光干扰。电机是带编码器的直流减速电机减速比1:30轮子直径6.5厘米左右。电机驱动选TB6612它的MOS管压降比L298N小很多发热也低适合电池供电的小车。舵机是常见的9g舵机负责装货和卸货时抬升或者倾倒的动作。供电是整个系统里最容易出问题的地方。我实测发现单独给主控板供电、单独给电机驱动供电、单独给舵机供电三路电源必须共地否则PWM信号和编码器信号都会受到严重干扰。舵机启动瞬间电流能到1A左右直接和主控共用3.3V会把单片机拉复位所以舵机必须单独接5V供电并且靠近舵机的位置并联一个470微法的电解电容。3. 核心算法与关键代码实现3.1 灰度采集、滤波与动态阈值灰度传感器的原始输出是模拟电压白底反光强电压高黑线吸收红外电压低。我通过ADC读回来的数值范围大概在800到3000之间12位ADC不能直接拿原始值当二值化信号用因为环境光变化和地面材质不同会让基准值漂移。我的做法是两段式处理。第一步做滑动平均滤波每路连续采样5次去掉最大最小值再取平均消除瞬间抖动。第二步做动态阈值开机的三秒钟让小车静止在起点读取8路通道的“当前白底基准值”然后每路阈值就是白底基准值减去一个固定offset。这个offset一般取300到500具体数值取决于安装高度和黑线宽度。uint16_t gray_raw[8]; uint16_t gray_filtered[8]; uint16_t white_base[8]; uint16_t threshold[8]; void gray_sample_filter(void) { uint16_t buf[8][5]; for (int t 0; t 5; t) { for (int ch 0; ch 8; ch) { buf[ch][t] adc_read(ch); } delay_ms(2); } for (int ch 0; ch 8; ch) { // 简单插入排序取中间值 for (int i 1; i 5; i) { uint16_t key buf[ch][i]; int j i - 1; while (j 0 buf[ch][j] key) { buf[ch][j 1] buf[ch][j]; j--; } buf[ch][j 1] key; } gray_filtered[ch] buf[ch][2]; // 取中值 } } void gray_update_threshold(void) { for (int ch 0; ch 8; ch) { threshold[ch] white_base[ch] - 400; } }动态阈值比固定阈值好用的地方在于每组传感器和地面都有自己的“白底电压”只要开机时自动校准一次后面无论换场地还是换车都不用手动调电位器。3.2 偏差计算与PID控制8路灰度传感器采集完以后要转换成一个表征“车头相对黑线的偏离程度”的数值。我用的方法是加权平均法给每路传感器赋予一个位置权重传感器的排列位置就是权重值。比如8路传感器从左到右权重是 -4、-3、-2、-1、1、2、3、4那么偏差计算公式为int16_t calc_error(void) { int16_t pos_weight[8] {-4, -3, -2, -1, 1, 2, 3, 4}; int32_t sum 0; int32_t cnt 0; for (int ch 0; ch 8; ch) { if (gray_filtered[ch] threshold[ch]) { // 检测到黑线 sum pos_weight[ch] * 1; cnt; } } if (cnt 0) { return last_error * 2; // 全丢线时保持上次偏差并放大 } if (cnt 8) { return 0; // 全黑说明到了站点或大十字路口 } last_error sum / cnt; return last_error; }这段代码有三个细节要注意。第一cnt为0表示所有传感器都没看到黑线可能是车冲出赛道也可能是到了大拐弯处线突然脱离视野此时不能直接返回0否则车会失去转向依据我选择保留上次偏差并放大让小车朝最后丢失黑线的方向继续转弯很多情况下能“救回来”。第二cnt为8表示8路全黑说明车头正对一个大片黑色区域这通常是站点停车标识或者十字路口此时单独标记事件而不是盲目纠偏。第三pos_weight用奇数配对不用0避免小车正好压在线上时偏差为0导致转向迟钝。PID控制分为两部分。巡线转向我用PD控制int16_t steering_pd(int16_t error) { int16_t p 12 * error; int16_t d 8 * (error - last_error); last_error error; int16_t out p d; if (out 120) out 120; if (out -120) out -120; return out; }同时左右轮的基础速度由速度环控制。速度环用PI编码器测速周期20毫秒目标速度设定为直线段每秒40厘米过弯时降到每秒20厘米。速度环输出作为PWM占空比的基准转向PD输出叠加在左右轮上左轮PWM 速度环输出 转向输出右轮PWM 速度环输出 - 转向输出。这样实现差速转向既保证平均速度稳定又让转向动作直接作用于两个轮子。速度环计算时有一个关键参数编码器脉冲到实际速度的换算。我的电机编码器是霍尔式的13线经过4倍频后单圈脉冲数是52电机减速比1:30也就是说车轮转一圈需要编码器输出1560个脉冲。车轮直径65mm周长约0.204米所以每个脉冲对应大约0.00013米。如果目标速度是0.4米/秒那么一个速度环周期20毫秒内应该测到约615个脉冲。有了这个换算关系速度环的误差单位就统一成“脉冲数/周期”比例和积分参数才有意义不会跟在网上抄的参数一样稀里糊涂。3.3 舵机控制与装货卸货动作舵机使用PWM控制我这边选择的周期是50Hz也就是20毫秒周期对应0.5毫秒到2.5毫秒的高电平脉冲宽度舵机角度0度到180度。在STM32上配置TIM4的PWM输出比较寄存器当计数周期为2000时比较值500对应0.5毫秒比较值2500对应2.5毫秒。装货动作和卸货动作本质是一组预置的舵机角度序列void loader_action_load(void) { servo_set_angle(servo_lift, 30); // 放低承载台等待装货 delay_ms(500); mcu_set_buzzer(1); delay_ms(200); mcu_set_buzzer(0); } void loader_action_unload(void) { servo_set_angle(servo_lift, 30); delay_ms(300); servo_set_angle(servo_tilt, 160); // 倾斜倒出货物 delay_ms(600); servo_set_angle(servo_lift, 90); servo_set_angle(servo_tilt, 90); }这类动作函数设计的要点是动作之间必须有延时等待不能让舵机连续大幅度摆动否则电流冲击大还容易机械卡死。同时所有动作完成后务必让舵机回到一个确定的角度这样下一次流程不会从错误姿态开始。4. 状态机、站点识别与主流程4.1 用状态机管理整个运行流程如果把所有逻辑都写在主循环里代码会变成一个谁也看不懂的大泥潭。我用经典的状态机模式定义五个运行状态typedef enum { STATE_IDLE, STATE_LINE_TRACK_1, STATE_LOAD, STATE_LINE_TRACK_2, STATE_UNLOAD, STATE_RETURN, STATE_FINISH } run_state_t; run_state_t g_state STATE_IDLE;主循环只负责两件事更新传感器数据然后根据当前状态调用对应的处理函数。状态之间的切换条件必须清晰明确我举个实际例子STATE_IDLE等待按键触发按下后蜂鸣器响一声进入STATE_LINE_TRACK_1STATE_LINE_TRACK_1小车从启动区沿黑线驶向装货区。当站点识别模块检测到装货站点、并且小车完全停止时切换STATE_LOADSTATE_LOAD执行装货动作延时2秒让现场人员放货然后进入STATE_LINE_TRACK_2STATE_LINE_TRACK_2沿黑线从装货区驶向卸货区到达后切换STATE_UNLOADSTATE_UNLOAD执行卸货动作延时1.5秒然后进入STATE_RETURNSTATE_RETURN反向或者继续绕行回到启动区识别到起点黑线标识后停车切换STATE_FINISHSTATE_FINISH蜂鸣器响三声停止一切运动。状态机的好处是每个状态下只关注“当前要干什么”和“满足什么条件才能去下一个状态”逻辑不会交叉排查问题也非常直观。4.2 站点识别与全黑判断H题的站点识别是最容易翻车的地方之一。我的实现方案是“灰度全黑连续计数 编码器里程确认”双重判定。先说灰度全黑连续计数。上一节提到当8路灰度全部检测到黑线时我把它记为“疑似站点事件”。但小车过十字路口时也会出现8路全黑直接切换状态会误判所以必须连续多次检测到全黑才算有效。我设置连续5次全黑每次循环大约50毫秒总共250毫秒才确认是站点这个时间长度能够区分普通路口和停车站。再说编码器里程确认。根据赛题场地分布装货区通常在距离起点一定路程的位置。我把起点到装货区的编码器累计脉冲数记录成变量每次检测到全黑事件时对比当前累计里程是否落在“预期站点区间”内。如果在区间之外就视为普通路口只做转向处理不计停。这个方法有效解决了“场地里多个路口形状相似”的问题。站点识别还需要解决停车精度问题。我的做法是进入站点前150毫秒就切换目标速度为0让车子靠惯性滑行一小段距离再停下同时PID的输出限幅到较小的值防止停车瞬间电机猛拽导致车身位置偏移。这个预减速距离需要实车测试我最终调到180毫秒才得到最佳停车位置。4.3 OLED显示与调试信息输出OLED屏幕平时显示当前状态、目标车速、实时偏差和编码器脉冲数。不要把OLED当成装饰它实际是你现场调车的“仪表盘”。我在状态函数里每隔200毫秒刷新一次屏幕显示内容类似下面这样void oled_display_status(run_state_t state) { char line1[20]; sprintf(line1, State:%d, (int)state); char line2[20]; sprintf(line2, Err:%d, (int)last_error); char line3[20]; sprintf(line3, Cnt:%d, g_site_counter); oled_show_string(0, 0, line1); oled_show_string(0, 4, line2); oled_show_string(0, 8, line3); }调试时串口输出比OLED更详细我用串口打印模式打印所有原始灰度值、滤波后数值、阈值、目标速度、实际速度、PID输出、站点计数。现场调车时连接蓝牙透传模块笔记本上开串口助手就能边跑边看数据。有一次小车在弯道反复冲出赛道我看了串口日志才发现是某一路上ADC偶发异常数值直接掉到阈值以下导致偏差计算错误这才针对性加了中值滤波。5. 常见问题与调优心得5.1 现场最容易踩的坑现象可能原因我的处理办法小车直道左右摆动转向PD的P过大或D过小先减半P值再逐步增大D值直到直道稳定弯道冲出赛道D过大导致转向滞后减小D增加弯道基础速度降低逻辑站点停车位置不准预减速时间太短把预减速从150ms加到200ms配合编码器里程修正舵机动作时小车抖动舵机与主控共电源舵机独立5V供电加470uF电容OLED显示闪烁I2C上拉不足给SCL/SDA加4.7k上拉减小通信速率灰度数值越跑越飘光源变化或传感器温度漂移增加动态校准用每次恢复白色区域时的基准值更新阈值小车启动瞬间窜出去速度环积分饱和启动时目标速度从0斜坡增加每秒加速度限定为0.2m/s²5.2 调试顺序建议调这辆小车一定要有顺序观念我建议严格按照“开环直行 → 开环转弯 → 闭环直行 → 闭环转弯 → 站点停车 → 完整流程”这个顺序推进。跳步是大忌我见过有队伍先调完整路径结果车连直线都跑不稳整体流程调了三天还是乱最后还是回到基础开始。调试时充分利用编码器数据和串口可视化。我在电脑上用串口助手实时画折线图的方式观察灰度波动一眼就能看出哪一路传感器信号异常。具体做法是把8路灰度原始值按顺序打印成一行分析曲线突变点很快就能发现阈值设置是否合理。PID参数整定遵循先P后D再I的顺序先给一个小P值让车能沿黑线走每次增加20%观察响应直到直道出现轻微振荡为止然后加D把振荡压下去最后加一点I弥补长距离直线段的稳态误差。最后控制车速也要讲究性价比。极限速度不一定能赢因为一旦冲出赛道就彻底报废。我最终把直线速度定在0.4m/s过弯降到0.18m/s全程跑完大约18秒虽然不算快但每个站点都能稳定停准。个人体会最深的还是那句老话电赛拼到最后不是拼谁代码写得炫而是拼谁的调试流程更系统、更经得起现场折腾。2024年H题把嵌入式比赛里最能拉开差距的“传感器处理 运动控制 状态逻辑”三块全考了一遍想靠背代码蒙混过关基本没有可能。如果你马上要参加下一次电赛或者自己想做一辆循迹小车我建议先把这篇文章里提到的每个模块单独搭一遍再合并联调整个过程走完你对嵌入式系统整体的把控能力会上一个台阶。本文还有配套的精品资源点击获取
返回列表