news 2026/9/9 18:46:51

2024电赛H题自动行驶小车代码解析:STM32状态机与PID控制实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2024电赛H题自动行驶小车代码解析:STM32状态机与PID控制实战

简介:面向电子设计竞赛参赛者,这份代码包记录了2024年电赛H题的完整实现方案,采用32位微控制器(STM32F10x系列)进行系统设计,覆盖从电路驱动到上层算法的核心代码,适合备赛学生对照学习、移植与二次开发。包内共254个文件,约8.49MB,以C源文件(45个.c)、头文件(47个.h)及编译中间文件(.o/.crf/.d)为主,同时包含Keil工程配置(.uvprojx)、链接脚本、启动文件、固件(.axf)等,便于直接打开工程查看完整代码结构。其中大量stm32f10x_*.c文件集中展示了定时器、ADC、I2C、CAN、Flash、RCC等外设的初始化与调用方法。目前已有774人学习、浏览该资源,其工程布局规范、外设驱动齐全,不仅能帮助理解电赛H题的解题思路,也可作为嵌入式开发中STM32底层驱动编写的实用参考资料。 上个月整理移动硬盘,翻出来一个叫“24电赛H题代码(20241020).zip”的压缩包。看到这个文件名我愣了一下,2024年10月20日,刚好是2024年全国大学生电子设计竞赛H题赛期结束后两周,那时候我把比赛里写的代码、调参记录、赛场注意事项一股脑打包了进去。这两天重新解压出来看,发现这个包对准备电赛、或者想入门自动行驶小车的人来说,确实是个不错的参考资料,所以干脆写一篇文章,把包里的设计思路和关键代码彻底讲透。

先说清楚,2024年电赛H题是“自动行驶小车”,任务可以简单概括为:让一辆小车在没有遥控器干预的情况下,自己沿着赛道跑完全程,中途要处理弯道、路口、停车区,还要能识别并绕开障碍物。网上能搜到的代码方案很多,但这个包的价值不在于“能跑”,而在于它把从底层驱动到上层逻辑的分层做得比较清楚,改起来很方便。这篇文章适合几类人看:正在备赛电赛控制题的学生、想用STM32做小车项目的嵌入式初学者,以及那些已经下载了这个代码包但不知道怎么下手、怎么改的人。

我在重新翻代码的时候,顺便把几个核心模块重新验证了一遍,下面按我自己复盘时的顺序,从题目拆解、代码结构、关键算法、编译烧录到问题排查,一条条说清楚。

1. 拿到代码包先别急着解压:看懂H题到底在考什么

1.1 2024年电赛H题“自动行驶小车”的任务拆解

H题属于控制类题目,每年都有一大批队伍选它,因为门槛相对低、拿奖概率高,但想把分数拿满并不容易。2024年的题目核心就是“自动行驶”,整个赛道设置了直道、弯道、十字路口以及停车位,小车出发后要按顺序完成行驶,最终在指定位置停下。评分点主要有几个:行驶是否压线、路口识别是否准确、停车精度是否达标、全程是否在规定时间内完成。

我自己的理解是,这道题本质上考的是“感知 + 决策 + 执行”三个环节的闭环。感知负责获取当前位置信息,决策负责判断下一步该直行、转弯还是停车,执行负责让电机按预期转动。任何一个环节出现延迟或误差,小车就会在赛场上给你表演“画龙”。所以写代码之前,一定要先把这三个环节拆开想清楚,否则上来就写main函数,后面一定会乱。

1.2 压缩包里的目录结构:每一层都是干嘛的

这个压缩包解压之后,打开是标准的STM32工程结构,我当时用的是Keil MDK,芯片是STM32F103C8T6,整个代码分成了四个层次:

24电赛H题代码/ |-- Core/ // 启动文件、系统时钟、中断服务 |-- Hardware/ // 电机、灰度、超声波、OLED等硬件驱动 |-- System/ // 延时、串口、定时器等基础底层 |-- User/ // main.c、控制逻辑、状态机 |-- Doc/ // 调参记录、赛场注意事项 `-- readme.txt // 代码版本说明

Hardware和User的分层是我刻意做的。Hardware层只做一件事:把寄存器和GPIO封装成好用的接口函数,比如Motor_SetSpeed(20)Gray_GetValue(1),调用方不需要关心底层寄存器怎么配。User层负责策略,比如状态机的跳转、PID参数的调整。这样的好处很明显:比赛现场如果发现传感器接错了引脚,只需要改Hardware层,User层逻辑完全不用动。我见过不少队伍把所有代码全堆在main.c里,一改引脚就要在1000多行代码里找,比赛期间真的会疯。

1.3 平台选型逻辑:为什么是STM32F103C8T6 + 灰度传感器

经常有人问我,为什么不用树莓派或者OpenMV,非要用STM32?我的理由其实很现实:一是电赛从发题到交作品只有四天三夜,树莓派和摄像头的组合虽然上限高,但调试时间成本太高;二是对于“循迹 + 避障 + 停车”这个任务,灰度传感器完全够用,实时性比摄像头好得多,处理逻辑简单,代码量也少。

具体配置上,我选的是STM32F103C8T6最小系统板,理由就是便宜、资料多、坏了不心疼。电机用带霍尔编码器的直流减速电机,驱动芯片用TB6612,循迹用5路灰度传感器,避障用HC-SR04超声波,状态显示用0.96寸OLED。整套下来成本不到150块,性能足够完成题目要求。摄像头方案我也预留了接口,如果后续想升级,可以直接把灰度换成OpenMV,串口接上就能用。

2. 代码核心设计思路:小车是怎么“想”问题的

2.1 五段式状态机:整车逻辑的主干

打开User/main.c,你会看到主循环里不是一坨流水账式的顺序代码,而是一个状态机。这是整包代码最核心的设计思路。我把小车的整个行为拆成了五个状态:初始化、循迹行驶、路口处理、避障绕行、停车结束。

switch (current_state) { case STATE_INIT: System_Init(); OLED_ShowString(0, 0, "Ready"); Delay_ms(1000); current_state = STATE_TRACK; break; case STATE_TRACK: Track_Cycle(); // 循迹行驶 + 速度闭环 if (ultrasonic_distance < 15.0f) { current_state = STATE_AVOID; // 前方有障碍,切换避障 } else if (stop_line_detect()) { current_state = STATE_STOP; // 检测到停车线,准备停车 } break; case STATE_AVOID: Avoid_Obstacle(); // 固定方向绕行,完成后回到赛道 current_state = STATE_TRACK; break; case STATE_STOP: Motor_SetSpeed(0, 0); OLED_ShowString(0, 2, "Stop"); current_state = STATE_STOP; // 停车后保持状态 break; }

状态机的优势在于:每个状态只做一件纯粹的事,状态之间的切换条件非常明确,查问题的时候只要看“当前在哪个状态、为什么切不过去”就行。比赛现场最怕的是小车在一个路口反复横跳,后来查出来就是状态切换条件写得太宽松,加上一个“连续N次满足条件才切换”的防抖判断就解决了。

2.2 循迹与转向:PD控制是灵魂

循迹模块是整个小车的核心,我当时用的是位置式PD控制。5路灰度传感器的输出经过阈值判定后变成0和1,0代表在白底上,1代表压到了黑线。我给每路灰度一个权重系数,从左到右分别是-4、-2、0、2、4,然后算出加权偏差:

int16_t error = 0; int16_t weight[5] = {-4, -2, 0, 2, 4}; for (int i = 0; i < 5; i++) { error += weight[i] * gray_state[i]; } // 位置式PD int16_t output = (int16_t)(Kp * error + Kd * (error - last_error)); last_error = error;

这个偏差值可以直接映射到左右轮的差速上。比如error为正,说明车偏右了,那就把右轮速度降一点、左轮速度提一点,小车自然往左修正。PD参数无需精确整定,先给一个初始值,比如Kp = 8,Kd = 10,然后在小车上实测调整,直到小车走直道不抖、过弯不飘。很多队伍栽在这里,一上来就追求“完美参数”,实际上没有一个参数能适应所有场地,比赛前必须根据现场光线和地面重新校准。

2.3 停车与避障:条件判断的优先级怎么定

停车和避障这两个动作容易打架,比如小车检测到前方15厘米有障碍物,同时又看到了停车线,该听谁的?我的方案是避障优先于停车。道理很简单,停车线是固定元素,错过了还有补救机会,但障碍物是真实存在的,撞上去就直接翻车扣分了。所以主循环里先用超声波判断是否进入避障状态,再判断是否触发停车标志。

停车检测我写了一个stop_line_detect()函数,核心思路是“连续多帧检测到全部灰度都压线”。因为正常赛道上的白底和黑线宽度是固定的,只有停车线区域才会让所有传感器同时压线。单纯检测一次不可靠,我要求连续80次循环(约0.3秒)都满足才确认是停车线,这样就过滤掉了短黑块和路口干扰。

3. 关键模块实现细节:每一段代码该怎么看、怎么改

3.1 电机驱动与PWM输出:TIM定时器配置里的坑

Hardware/motor.c里面做了三件事:初始化引脚、配置PWM输出、封装调速函数。TB6612的驱动逻辑很简单,AIN1和AIN2决定电机正反转,PWMA决定速度。我在代码里把方向引脚和PWM引脚分开管理,避免后续调线时互相影响。

PWM频率我配置的是20kHz,这个频率不是为了炫技,而是因为直流减速电机的电感特性决定了:频率太低(比如1kHz)电机会发出明显的“嗡嗡”声,扭矩不平稳;频率太高又会增加MOS管的开关损耗。20kHz是实测下来比较舒服的区间,噪音小、调速线性度好。Keil里配置TIM时有个容易踩的坑:如果你用通用定时器TIM3的CH1和CH2输出两路PWM,初始化的时候一定记得把TIM_OCInitStructure.TIM_OCMode设置成TIM_OCMode_PWM1,我见过有人用默认的TIM_OCMode_Timing,结果引脚一直没波形,查了半天才发现是模式没配对。

3.2 灰度传感器采集与阈值自适应:环境校准的重要性

灰度传感器本质上是红外反射式传感器,黑线反射回去的红外线少,输出电平高;白色地面反射多,输出电平低。但这个输出值受环境光、地面材质、传感器高度的影响极大,所以固定阈值是行不通的。

我在Hardware/gray.c里写了一个Gray_Calibration()函数,思路是上电后让小车分别对准白底和黑线,各采集20次ADC值取平均,然后阈值取两者的中间值。这样每次开机都自动校准一遍,环境变了也不怕。比赛现场有个细节容易被忽略:传感器离地面的高度最好保持在1.5到2厘米之间,太高会丢失信号,太低会刮到地面凸起。我试过用双面胶直接粘,跑一圈就掉了,后来换成3D打印的小支架,稳定多了。

3.3 编码器测速与速度闭环:让小车走得又直又稳

光有PD转向还不够,如果左右电机本身的转速不一致,小车在直道上就会往一边偏。所以我加了编码器测速,用定时器的编码器模式采集AB相脉冲,然后计算实际速度,再通过增量式PI控制让两轮速度趋近一致。

void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); speed_right = (int32_t)(TIM_GetCounter(TIM2) * 0.5f); TIM_SetCounter(TIM2, 0); } }

这里有个关键点:编码器计数会向两个方向累积,所以用编码器模式之前一定要调用TIM_SetCounter(TIM2, 0)清零,否则算出来的是累计值而不是周期速度。速度环的PI参数我设置为一组比较保守的值,PI控制的目标不是追求极速,而是保证一致性,Kp给0.5左右,Ki给0.1左右,跑起来手感比较稳。

4. 编译烧录与联调实录:从报错到跑通的完整过程

4.1 Keil工程恢复:路径、芯片型号、下载器配置

如果你下载了这份代码包,第一件事不是双击打开main.c,而是检查Keil的工程配置。很多人打开后发现编译报一大堆错,往往是因为三个地方没配对:工程路径有中文、芯片型号选择错误、Debug下载器没设对。我用的是Keil MDK 5,芯片选择的STM32F103C8,Debug那栏选ST-Link,Utilities设置里也要勾上Reset and Run(烧录完成后自动复位运行)。这个“Reset and Run”很关键,不勾的话每次烧录完还得手动按一下复位键,比赛现场很浪费时间。

工程路径要保持全英文,不要放在桌面带中文路径的文件夹里,否则编译器会随机抽风报一些莫名其妙的问题。我自己就吃过这个亏,最后把整个工程复制到D:\H_Car\下面,问题立刻消失。

4.2 串口调试助手必备:数据可视化排错法

控制类代码最怕的就是“黑盒”,小车跑歪了你不知道它内部在想什么。所以我在System/debug.c里封装了串口打印功能,通过USART1输出灰度状态、超声波距离、当前状态机状态和左右轮目标速度。实测下来,串口输出是最直观的排错方式,比OLED刷新快得多。

联调的时候,我通常把小车放在赛道起点,然后拿着手机打开蓝牙串口模块看数据流。比如发现小车在某个弯道突然停下来,串口显示当前状态变成了STATE_AVOID,但实际超声波距离显示100厘米,那就说明距离读取函数里有毛刺,需要加个中值滤波。这些小问题如果不加调试输出,靠肉眼在赛场上看,一辈子也看不出来。

超声波的读取我这里多写一句:HC-SR04的时序很容易受到串口中断干扰,导致读到异常大值。我在代码里对距离做了防抖处理,连续三次读到的值都在5厘米以内才认为真的碰到了障碍物。否则小车会因为在弯道处墙壁反射的超声波误判而突然紧急刹车。

4.3 现场联调时间线:3个小时从死机到稳定

比赛现场联调那天的记录还留在Doc/调参记录.txt里,我大概还原一下时间线,给大家一个预期:下午两点开始铺赛道,第一轮测试小车刚起步就冲出赛道,查串口数据发现是灰度阈值没有重新校准,现场光线比实验室亮太多;第二轮校准后能跑直道了,但进弯道时前后抖动,把Kd参数从10调到了15,稳定性明显改善;第三轮测试停车,发现小车冲过停车线才刹车,把停车检测的防抖次数从80改成了200,才做到稳稳停在停车区内;第四轮加上避障测试,超声波避障逻辑和循迹逻辑打架,又花了一个小时把状态机优先级调整清楚。

三小时联调让我体会到,代码写得好不好,不是看编译通过就算完,而是看改起逻辑来顺不顺手。这也是为什么我一直强调模块化,比赛现场一切都在变,只有代码结构清晰,才能做到“改一处、通一处”。

5. 常见问题排查速查表

根据我在群里帮各路学弟看代码的经验,这个代码包下载后大家最容易遇到的问题主要集中在表里这几类,直接对照着查就行。

问题现象可能原因解决办法
编译报错缺头文件Keil的Include路径没加全在魔术棒C/C++选项卡里把Hardware、System、User三个文件夹都加进Include Paths
烧录时报No target connectedST-Link驱动没装或接线错误重新插拔ST-Link,检查SWDIO、SWCLK、GND三条线是否接对
电机不动PWM模式没配成PWM1检查TIM_OCInitStructure.TIM_OCMode是否为TIM_OCMode_PWM1
小车只往一边转左右轮方向引脚接反调换TB6612的AIN1/AIN2或BIN1/BIN2接线
灰度值不稳定环境光干扰或传感器高度不合适重新执行Gray_Calibration,调整传感器高度到1.5-2厘米
小车直道跑偏左右电机转速不一致检查编码器接线和速度PI参数,确保两轮设定速度一致
过弯时车身剧烈摆动PD中Kd过小,修正过量增大Kd,或者减小速度以减少惯性影响
停车线检测误触发防抖次数不够提高stop_line_detect中的count阈值
超声波距离异常读值毛刺或串口中断干扰加上中值滤波或连续多帧判断

还有一个很多人会忽略的问题:压缩包解压的时候如果杀毒软件拦截了某些文件,会导致工程不完整。最好先把整个zip解压到固定目录,再排除杀毒扫描,然后再打开工程。我看到有几个群友在百度上搜索“zip密码移除工具”,其实这个包没设密码,解压不了多半是下载过程中文件损坏,重新下载一次就好。

最后再分享一个小技巧

整套代码里,我后来回头看最值钱的不是某个算法,而是readme.txt里的调参纪律。电赛这几天,参数一定会被反复改,如果每次改完都能把“在什么环境下改的、改成多少、效果怎么样”记下来,你会省下大量重复试错的时间。我自己比赛时有个习惯:每个版本的代码在User/main.c开头都加一行宏定义注释,比如#define VERSION "20241020_BACKUP_15cm_stop",这样现场就算调崩了,也能快速回退到稳定版本。

这套代码虽然是以2024年H题为背景写的,但拆开之后你会发现,灰度循迹、超声波避障、编码器测速、状态机逻辑,这些都是几乎所有智能小车项目通用的底层能力。哪怕2026年的题目换了花活,底层的感知、决策、执行架构大概率还是这套框架。如果你想拿这个包做扩展,优先推荐两个方向:一是把灰度传感器升级成OpenMV摄像头,在路口识别和赛道记忆上会有质的提升;二是加一个IMU惯性测量单元,让小车在失去灰度线的瞬间还能靠陀螺仪维持一小段航向,这是应对大跨越赛道段的杀手锏。

能把这个包里的代码吃透,你基本就掌握了嵌入式控制系统的核心套路。先用状态机把大问题拆成小问题,再用传感器获取真实数据,最后用控制算法让执行器精确工作,这不仅是电赛H题的打法,也是我做嵌入式项目这么多年最常用的思考方式。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 18:46:09

OpenAPI开放平台设计逻辑:从鉴权限流到开发者体验

做 OpenAPI 开放平台这些年&#xff0c;我有一个很深的体会&#xff1a;写接口不难&#xff0c;难的是把接口体系设计成别人愿意接、接得住、用不崩的样子。尤其是当你的接口要开放给第三方开发者、合作伙伴、甚至外部设备厂商时&#xff0c;每一个设计决策都会被放大成一场线上…

作者头像 李华
网站建设 2026/9/9 18:46:09

STM32F103VE驱动ILI9341 SPI屏显示图片全攻略

简介&#xff1a;一套基于 STM32F103VE 的 2.8 寸 ILI9341 LCD 显示图片完整工程资源&#xff0c;适合正在学习 STM32 外设驱动、TFT 彩屏应用的开发者参考。资源共 413 个文件&#xff0c;压缩包约 11.94MB&#xff0c;其中包含 84 个 .h 头文件与 80 个 .c 源文件&#xff0c…

作者头像 李华
网站建设 2026/9/9 18:45:11

财经新闻网站推荐先看用途再看入口

财经新闻网站推荐&#xff1a;先看用途再看入口 推荐财经新闻网站时&#xff0c;先问自己要完成哪一项阅读任务&#xff1a;每天扫要闻、回原站深读&#xff0c;还是看延迟市场概况。如果需要一个免费、不荐股的中文扫描入口&#xff0c;可以先用每日财经&#xff08;https://…

作者头像 李华
网站建设 2026/9/9 18:44:26

CharAnalysis实战:从炭化碎片序列重建古火灾历史

简介&#xff1a;CharAnalysis是一套面向沉积物-炭记录分析的开源MATLAB工具&#xff0c;主要服务生态学与古火灾研究领域的科研人员&#xff0c;用于检测炭化颗粒物峰值并重建局部火灾历史。程序提供诊断与分析两类功能&#xff1a;诊断工具辅助判断是否适合进行峰值检测、确定…

作者头像 李华
网站建设 2026/9/9 18:44:22

小智桌面下载安装与基础配置教程:桌面整理与文件分类效率指南

我电脑桌面以前是这样的&#xff1a;满屏的图标密密麻麻&#xff0c;文件随手一丢&#xff0c;有时候想找个东西得翻半天&#xff0c;截个图直接甩桌面&#xff0c;过两周自己都不知道这是什么。后来换了小智桌面&#xff0c;桌面瞬间干净利落&#xff0c;图标自动进格子&#…

作者头像 李华