简介:这套工程以STM32F103系统板为核心,配合电机驱动、红外或超声波等传感器模块,构成完整的避障小车硬件平台。源码采用C语言基于标准库函数编写,摒弃复杂的寄存器操作,结构清晰、注释完整,便于初学者对照实物理解初始化、传感器采集、避障决策与电机控制等流程。压缩包约92.06MB,内含完整源码、工程文件及说明文档,包括Keil工程、源代码和接线说明,导入集成开发环境后即可编译运行,能够快速复现实地避障效果;读者按文档接线并烧录后,即可看到小车根据传感器信息自动转向或停止,获得直观的学习反馈。该工程源自作者课设实战,已经过运行验证,适合完整复刻或二次开发;目前已有62人学习/浏览,适用于课程设计、毕业设计、工程实训、大创竞赛等场景,也可作为学习嵌入式C语言编程的练手项目。作者长期从事嵌入式开发,遇到使用问题可通过CSDN私信交流。 做嵌入式开发这些年,我始终觉得,stm32配合库函数和C语言这套组合,是最适合入门、也最容易出成果的一条路。前阵子拿一块STM32F103C8T6最小系统板,配上超声波传感器、红外避障模块和直流电机驱动,最后让一辆小车在走廊里自己跑起来、不会一头撞上墙,这个"基于stm32库函数进行c语言编程,最终实现避障小车的实地运行"的工程,看起来不花哨,但信息量其实很大——从GPIO到定时器,从电平操作到状态机逻辑,几乎把32位单片机里最核心的外设和编程思想都过了一遍。
避障小车这个项目,本质上解决的是"嵌入式系统如何把真实世界的距离数据变成机械运动"这个问题。它特别适合正在做课程设计或毕业设计的学生,也适合刚学完C语言、想看看单片机里到底怎么编程的同学。因为整个项目可以拆分得很细,每一步都能独立验证,不会一上来就被复杂的功能压垮。这篇就来完整复盘一下我的实现过程和踩坑记录。
1. 项目整体设计与模块拆解
1.1 系统架构与工作流程
小车系统说白了就四部分:主控、感知、执行、供电。主控用的是STM32F103C8T6,也就是大家常说的"蓝丸"板子,性价比高,72MHz主频对这类低速小车绰绰有余。感知部分选了两个模块:一个HC-SR04超声波测距模块负责正前方较远距离(2cm到4m)的障碍探测,另外配两个红外避障模块装在车头左右两侧,负责近距补盲——超声波对细小的桌腿、黑色物体经常失效,红外模块刚好能兜底。执行部分是经典的L298N驱动板加两个直流减速电机,前轮用万向轮支撑,构成三轮结构,转向原理就是左右轮差速。
供电方面我强烈建议电机和逻辑分开供电。电机用7.4V锂电池组,主控和传感器用一个小的5V降压模块从同一块电池取电,注意两者必须共地。为什么必须分开?这个我在第四章会详细讲,但先记住结论:混供必出事。
整体工作流程就是:主控不断发射超声波、接收回波,算出前方障碍距离,同时读取两个红外模块的电平状态。如果前方距离大于设定阈值(我设的是30cm),小车就保持直行;一旦距离小于阈值,就根据左右红外模块的状态决定向左还是向右转,避开障碍后恢复直行。这个流程听起来简单,但实现时的先后顺序、优先级处理、防抖机制,远比你想的复杂。
1.2 为什么选择库函数而不是寄存器或HAL
动手之前我纠结过一个问题:到底用标准外设库、HAL库,还是干脆纯寄存器操作?最后选了传统库函数(标准外设库),原因有三点。
第一,教材和网上的参考资料极多。尤其是江科大、野火这些教程体系都以标准库为主,遇到不懂的函数,随便一搜就能找到完整例子,对新手太友好了。第二,库函数的封装程度适中。它比寄存器直观,你不必对着GPIO_CRL寄存器抠半天,但比HAL库又更接近底层,能清楚看到每个外设是怎么被一步步"使能"和配置的。第三,对答辩或者课程汇报来说,标准库代码的可读性好,老师问到底层原理时你也讲得明白。
HAL库我建议等你把标准库跑通、理解了寄存器配置后再去碰。HAL的好处是官方长期维护、可以用CubeMX图形化配置快速生成工程,但缺点是太"黑盒",一旦出了问题,追踪起来要翻很多层封装。寄存器方式效率高、代码体积小,可学习曲线陡,对一个避障小车来说完全没有必要。
2. 库函数开发环境与工程搭建
2.1 Keil5工程组织与宏定义
我用的开发环境是Keil5(MDK-ARM),工程组织基本沿用标准库的套路。新建工程后,建议参照官方库结构把源码文件分类放好:
- User:main.c、stm32f10x_it.c(中断服务函数可以放这里)
- Library/Core:core_cm3.c、startup_stm32f10x_md.s(启动文件,注意根据芯片容量选择,我用的是中等容量)
- Library/PeriphSrc:标准外设库的src目录,里面是各外设的.c源文件
- Library/PeriphInc:对应的头文件
- HARDWARE:自己写的传感器、电机驱动模块文件,比如bsp_ultrasonic.c、bsp_motor.c
工程里最关键的是两个宏定义。我用的是STM32F10X_MD(中等密度),配合USE_STDPERIPH_DRIVER,这两个宏缺任何一个都会导致编译报错,或者外设库函数找不到声明。很多人在这一步卡了很久,现象是编译时提示stm32f10x.h里没有定义GPIO等等,十有八九就是宏没加对。在Keil的C/C++选项卡里把这两个宏填进去就行,这个小问题至少帮我答疑了十几次。
2.2 基础外设配置:GPIO、PWM、串口与延时
工程量最大但其实最固定的是四个外设的初始化:GPIO、定时器、串口和SysTick。GPIO配置本身没太多技术含量,PushPull输出、开漏输入这些概念对着库函数查就行。但我推荐一个技巧:在初始化每个引脚前,先想清楚它的电压和电流路径。输出给谁?输入从哪里来?想清楚再选择上拉还是下拉。比如红外避障模块是输出低电平表示检测到障碍,那IO口要配置成上拉输入,这样没检测到时读到高电平,检测到时读到低电平,逻辑非常干净。
PWM配置我用定时器2的通道1和通道2,产生两路频率约10kHz的PWM分别控制左右电机速度。学习库函数时很多人分不清"预分频PSC"和"自动重装ARR"的关系。我这样理解:定时器时钟先经过PSC分频得到计数频率,再从0数到ARR,数满一轮就触发一次更新事件。系统时钟72MHz下想让PWM频率接近10kHz,可以把PSC设为71,此时计数频率是72MHz/(71+1)=1MHz,也就是每1微秒计一次数;再把ARR设为99,一个周期就是100微秒,对应10kHz。这个计算过程建议亲手推一遍,后面调速度会很有用。
串口我配置成115200-8-N-1,主要用来打印调试信息。跑车的时候,所有决策变量都通过它实时观察,这是排查问题最重要的窗口。SysTick延时也值得一提,标准库没有现成的delay函数,我封装了一个基于SysTick的毫秒延时,底层就是那几条语句,但注意SysTick的时钟源是AHB的八分频,也就是9MHz,所以给重载值赋值时要按9MHz去算。这个坑如果不注意,延时时间会差8倍,小车转向就会"摆过头"或者"根本没转够"。
3. 避障功能的C语言实现
3.1 超声波测距时序与计时实现
HC-SR04测距的原理不复杂,核心就是测声波往返时间。操作流程是:给Trig引脚一个至少10微秒的高电平触发脉冲,模块内部会自动发出8个40kHz的方波,然后Echo引脚会输出一个高电平,宽度等于声波从发出到回收的时间。距离计算公式是:
距离(cm) = 高电平时间(us) / 58
为什么是58?因为声速约340m/s,声波走了个来回,距离要除以2,再结合单位换算,算下来就是1cm距离对应约58微秒的高电平时间。工程上直接用这个系数完全够用。
实现时用输入捕获其实比较繁琐,我的做法是让定时器3自由计数,当Echo引脚变高时记录当前计数值t1,变低时再记录t2,两者差值就是高电平持续微秒数。前提是定时器计数频率为1MHz,一个计数值正好对应1微秒。这里有容易忽略的细节:HC-SR04的回波长度最长约23ms,对应4米量程,所以我把TIM3的PSC设为71,计数器最大值设成65535,确保它能容纳完整的回波时间而不会溢出翻转。
3.2 状态机避障策略设计
避障逻辑我推荐用状态机而不是一大堆if嵌套。原因很简单:状态机把"小车现在处于什么状态"显式保存下来,每个状态下只处理自己该处理的动作,可读性和扩展性都好太多。
我设计的四个状态是:直行GO、左转TURN_LEFT、右转TURN_RIGHT、后退BACK。主循环每50ms执行一次决策:
- 直行状态:测一次前方距离。如果大于30cm,继续保持直行;如果小于30cm,再看左红外状态,左红外检测到障碍就进入右转状态,否则进入左转状态。
- 转向状态:持续转向并计时。左转设置左轮低速、右轮高速,右转则反过来。转向持续600ms后重新测距,若前方距离已大于30cm就切回直行,否则继续转向。
- 后退状态:当左右红外同时检测到障碍且正前方距离特别近(小于10cm)时进入,说明小车被堵死了,先后退400ms再重新决策。
这套逻辑对付大多数墙面和桌腿环境足够了。如果想让小车显得更"聪明",可以在转向状态里增加动态阈值:离得越近,转的角度越大。转角本质就是转向持续时间,把目标距离和转向时间做成线性关系,代码里无非是几行赋值,但实际效果提升非常明显。
3.3 核心代码片段与解释
测距函数的核心部分我贴出来,方便直接参考:
uint32_t GetDistance(void) { uint32_t t1 = 0, t2 = 0; GPIO_SetBits(GPIOA, GPIO_Pin_8); // Trig拉高 Delay_us(20); GPIO_ResetBits(GPIOA, GPIO_Pin_8); // Trig拉低 TIM_Cmd(TIM3, ENABLE); // 启动定时器 while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_9) == 0) { t1 = TIM_GetCounter(TIM3); if (t1 > 40000) break; // 超时保护 } while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_9) == 1) { t2 = TIM_GetCounter(TIM3); if (t2 - t1 > 40000) break; // 超时保护 } TIM_Cmd(TIM3, DISABLE); TIM_SetCounter(TIM3, 0); return (t2 - t1) / 58; // 单位:cm }这段代码有两个点必须注意。第一,两个while循环里都加了超时保护,防止模块没对准或者接线松动导致程序死循环。跑车最怕的就是程序一卡,小车直接撞墙。第二,返回值用整数除法,距离小于1cm时返回0,所以后面判断时要把0当成异常值处理,不能让小车在0cm距离下做出错误决策。
避障状态机的一段代码如下:
void AvoidRun(void) { uint32_t dist = GetDistance(); switch (car_state) { case GO: Motor_SetSpeed(LEFT_SPEED, RIGHT_SPEED); if (dist < 30) { if (LeftIR_Read() == 0) car_state = TURN_RIGHT; else car_state = TURN_LEFT; } break; case TURN_LEFT: Motor_SetSpeed(LEFT_LOW, RIGHT_HIGH); Delay_ms(600); if (GetDistance() > 30) car_state = GO; break; case TURN_RIGHT: Motor_SetSpeed(LEFT_HIGH, RIGHT_LOW); Delay_ms(600); if (GetDistance() > 30) car_state = GO; break; } }实际调试时,我把转向时间做成了全局变量,用串口下发调整,慢慢找小车在不同地面上的最佳转角参数。这个思路比改一次代码烧录一次效率高太多了,强烈建议你也这么干。
4. 实地运行阶段的调试与问题排查
4.1 我以为能跑,结果翻车的几个瞬间
纸上谈兵阶段一切顺利,一到实地就被打脸,这是这个项目里我印象最深的部分。第一个问题也是最容易坑新手的:供电不足。最开始我用同一个5V输出给STM32、传感器和电机驱动板供电,结果小车一加速,单片机就重启。原因很典型——电机启动电流很大,导致电压瞬间跌落,低于单片机复位阈值。解决办法就是第一章说的电机和逻辑分离供电,而且必须共地。记住,肯定要共地,不然信号参考电位不一致,传感器读数会乱跳。
第二个问题是超声波模块的检测盲区。HC-SR04官方量程2cm到4m,但2cm到4cm这段测出来经常乱跳,因为回波的起始部分会被发射端信号干扰。实际使用中阈值设成30cm,盲区影响不大,但正对一堵墙起步时,第一次测距可能返回一个异常大的值,小车会直直往前冲。我的处理办法是连续测三次取中间值,同时如果返回值大于400cm或者等于0,直接判定为无效数据,沿用上一次的有效距离。
第三个问题是轮子打滑。在瓷砖地面上,左右电机即使PWM占空比相同,实际转速也常有差异,小车走不直。解决办法有两个:一是调整PWM基准值,给转速慢的一侧加一点补偿,我在代码里定义了LEFT_SPEED和RIGHT_SPEED两个常量,实测调整到左侧比右侧多3%才走直;二是底盘加一点配重或者换橡胶圈轮胎,从物理上增加摩擦力。
第四个问题是转向角度不稳定。原因很多:延时不准、电池电压下降导致转速变化、地毯和瓷砖摩擦系数不同。这个只能靠多次实测标定。我日常的流程是先跑5组数据,记录小车在不同延时下的实际转角,做成一张表,再选中间值作为固定参数。
4.2 常见问题速查表
我整理了这个项目里最容易遇到、搜索引擎上天天有人问的问题,放一张表:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 编译报错提示没定义GPIO或RCC | 工程没有添加USE_STDPERIPH_DRIVER和STM32F10X_MD宏 | 在Keil的C/C++选项卡里添加宏定义 |
| 下载时提示No STM32 Target Found | 调试器没接好、SWDIO/SWCLK接反、芯片没上电 | 检查接线和供电,按住复位键再点下载,或换用ST-Link |
| 超声波测距一直返回0 | Echo引脚配置错误或Trig脉宽太短 | 检查GPIO输入输出模式,Trig高电平保持至少10微秒 |
| 小车原地打转 | 左右电机接线反了 | 对调一侧电机线或交换PWM输出 |
| 原地不动但代码在运行 | 电机驱动板使能脚没拉高 | 检查L298N的ENA/ENB引脚是否置高 |
| 转向过头或不足 | 转向延时和实际转速不匹配 | 用串口下发参数,实测调整延时 |
| 运行时单片机频繁复位 | 供电电压不稳 | 电机独立供电,并与逻辑部分共地 |
4.3 高效调试的两个小技巧
调试这个项目,我的习惯是"启动即打印"。主循环最开始就用串口把所有关键变量打出来:前方距离、左右红外状态、当前状态机状态、左右电机PWM值。这样即使小车跑飞了,串口数据也能告诉你它当时在想什么,比瞪着眼睛猜快得多。
另一个小技巧是用板载LED做状态指示。我在代码里把PC13引脚接到一个LED,在不同状态机状态下让LED闪烁不同频率:直行常亮、左转快闪、右转慢闪。跑起来的时候不需要盯着屏幕,远远看LED就知道小车内部状态,排查逻辑问题非常直观。这个习惯我后来带到其他项目里也一直沿用。
5. 写在最后:给后来者的一点建议
这个项目做完,我最大的体会是:避障小车不算难,但"在实验室里能动"和"在实地稳定跑"完全不是一回事。前者考的是能把代码调通,后者考验的才是对一个系统级项目的掌控能力——供电、机械、传感、逻辑,每一个环节都得踏踏实实验证。
如果你也准备做类似的东西,我的建议是严格按照这个顺序推进:先把超声波测距数值用串口打印稳定,再让小车直行2米不走偏,然后单独实验左转和右转的延时参数,最后才把所有逻辑合进状态机里。千万别上来就写"完整避障主循环",不然出了问题,你根本分不清是传感器、电机还是逻辑环节的锅,排查起来会非常痛苦。
这个工程的扩展空间也不小。你可以在现有状态机框架里加一个蓝牙遥控模式,或者接上蓝牙模块用手机控制小车;也可以加一个循迹模块,让小车在"避障"和"循迹"两种工作模式之间切换,我后来就是在同一套框架上做的扩展。库函数已经帮你把底层寄存器封装好了,剩下的,其实就是C语言编程和系统设计能力的发挥空间。
本文还有配套的精品资源,点击获取