ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103避障小车源码解析:红外+超声波融合,状态机与差速转向实战

STM32F103避障小车源码解析:红外+超声波融合,状态机与差速转向实战 简介这份资源是一套完整的STM32F103避障智能小车源码工程面向嵌入式初学者、电子设计竞赛参赛者以及机器人爱好者。项目以STM32F103VC为主控结合HC-SR04超声波传感器与CDS5516舵机实现了距离检测、转向控制与避障决策等核心功能可帮助读者理解从传感器驱动到PWM舵机控制的完整闭环。压缩包共201个文件体积4.73MB包含大量HAL/标准库风格的.h/.c源码、Keil工程文件uvprojx、uvoptx、编译产物axf、hex、o、crf以及链接脚本和调试配置便于直接打开工程进行编译与烧录验证。目前已有2600余人学习下载。源码中主程序、传感器驱动、舵机控制和中止服务函数等模块划分清晰并附带初始化配置与错误处理代码适合作为学习STM32外设驱动、嵌入式编程规范和避障算法落地的参考范例。 如果你正在找一套能直接跑起来的 STM32F103 避障智能小车源码同时又希望不只是烧录进去看个热闹而是能看懂每一行逻辑、知道引脚为什么这么接、以后能按自己的需求改出一套新方案那这篇文章就是冲这个目标写的。这两年我陆陆续续帮学生、竞赛队友调过好几台 STM32F103 小车红外避障、超声波避障、循迹、蓝牙遥控都做过。说实话避障小车这个项目在嵌入式入门和电子竞赛里属于“性价比极高”的一类硬件成本低、逻辑链路完整、传感器和电机执行器的配合刚好能把 GPIO、定时器、PWM、中断这些核心外设全过一遍。文章会把整套源码的设计思路、硬件接线、关键代码流程、避障策略和排坑经验拆开讲清楚适合刚学完单片机基础、想做一个完整项目的同学也适合准备工创赛、智能车竞赛但还缺一套稳定底板的参赛队伍参考。1. 方案与思路拆解1.1 避障小车到底在解决什么问题很多人一开始把避障小车想简单了觉得“检测到障碍就拐弯”不就行了。真做起来你会发现问题其实是一串连环问检测到什么算“检测到”左右两个传感器都触发时往哪边转转多久才能避开而不是原地打转前方没障碍但侧面贴墙时怎么走这些问题的背后对应的是传感器选型、阈值标定、电机差速控制、状态机设计这几块内容每一步都牵扯到实际硬件表现。从开发角度看避障小车本质上是一个“感知—决策—执行”的闭环系统。传感器负责感知环境主控根据感知结果做决策电机驱动和车轮执行决策结果。源码里最重要的不是哪一行代码多巧妙而是这个闭环是否稳定、响应是否够快、遇到边界情况是否有兜底策略。1.2 为什么选 STM32F103 做主控STM32F103 在今天看来不算新但作为小车主控依然很能打。72MHz 主频、最多 80 多个 GPIO、多路定时器、ADC、串口、I2C、SPI 全套外设处理一个避障小车的逻辑绰绰有余跑到后面想加蓝牙、加 OLED 显示、加无线模块资源也够用。更重要的是生态成熟。市面上便宜大碗的 F103C8T6 核心板十几块钱就能拿到教程、例程、源码一抓一大把遇到问题搜一下基本都能解决。对比 ArduinoF103 的代码更贴近“真正的嵌入式开发”——你需要自己看数据手册、配置寄存器或操作库函数、处理时钟树和引脚复用这个过程对能力提升的帮助远大于“调用现成库跑个 Demo”。对比更高端的 F4/H7 系列F103 的功耗和成本更低做小车完全够用不会因为杀鸡用了牛刀而心疼。1.3 传感器方案红外、超声波怎么搭配避障方案里最常用的传感器就两类红外避障传感器和超声波测距模块。红外避障模块常见的是三线制 TTL 输出原理是红外发射管发射光束遇到障碍物反射回来被接收管接收模块内部比较器输出高低电平。它最大的优点是便宜、响应快、逻辑简单直接读 GPIO 就行缺点是探测距离近一般可调范围在 2~30cm 左右而且容易受环境光干扰黑色物体吸光会让它“瞎掉”。超声波模块HC-SR04测距更准确量程在 2cm~400cm不受颜色影响但响应速度慢、有最小盲区而且对斜面的反射不太友好。我最终的方案是“红外 超声波融合”前方用一路 HC-SR04 负责中远距离探测左右两侧各用一路红外避障模块负责近距离补充。超声波负责“看得远”红外负责“防贴脸”两者数据互补避障的成功率明显比单用任何一种都高。2. 硬件搭建与电路设计细节2.1 最小系统板与供电方案主控我选的是 STM32F103C8T6 最小系统板板上自带 8MHz 晶振、复位电路、LDO 稳压和 USB 转串口拿来就能用。如果你手上只有裸芯片那得自己搭最小系统供电、晶振、复位、BOOT 引脚、下载电路这五样缺一不可。供电是整个小车最容易翻车的地方我单独说一句。电机启动瞬间电流很大直接和主控共用一个电源很容易把电压拉垮导致单片机复位、传感器误触发。可靠的方案是电池组给电机驱动模块供电驱动模块再引出 5V 给主控板或者电池直接给主控和电机驱动分别供电但必须共地。共地这个问题非常关键不共地的话传感器信号和电机驱动逻辑电平没有参考基准会出现各种诡异现象。我在电机电源入口并联了一个 470uF 电解电容和一个 104 陶瓷电容实测对抑制电压跌落很有帮助。注意电机驱动模块的“逻辑电源”和“电机电源”往往是分开的引脚别把电机电源接到逻辑电源上否则驱动芯片容易烧。2.2 电机驱动PWM 调速与引脚分配电机驱动我用的是 TB6612FNG它比 L298N 体积小、压降小、发热低内部是 MOS 管 H 桥控制逻辑简单通过两个方向引脚AIN1/AIN2控制正反转通过 PWM 引脚控制速度。如果手头只有 L298N 也能用逻辑一样就是功耗大一些。PWM 输出我选择了 TIM3 的通道 1 和通道 2对应引脚 PA6、PA7。PA6 控制左侧电机转速PA7 控制右侧电机转速方向引脚用 PB12~PB15。为什么不用 PA1、PA3 这类也能输出 PWM 的引脚我踩过一个坑PA3 在 F103 上是 TIM2_CH4 的复用引脚但它同时也是 USART2 的 RX。如果你既想在 PA3 上输出 PWM又想让这块板子的串口 2 做调试日志输出就会产生功能冲突。实际调车时最需要的就是串口打印日志看到数据才能判断传感器和决策逻辑是否正常。PWM 频率也值得说。TB6612 支持的 PWM 频率范围比较宽我工程里设置的 10kHz电机运行很平稳。如果是 L298N建议降到 1kHz 左右频率太高驱动芯片响应不过来反而会出现电机嗡嗡叫但不转的情况。占空比方面我做了软启动处理启动时 PWM 从 0 逐步加到目标值避免小车猛地蹿出去。2.3 传感器接线与 GPIO 注意事项接线表直接给你抄作业就行模块引脚接 STM32HC-SR04 TrigPA4通用输出HC-SR04 EchoPA5通用输入左红外避障 OUTPA0通用输入右红外避障 OUTPA1通用输入左电机 PWMPA6TIM3_CH1右电机 PWMPA7TIM3_CH2电机方向引脚PB12~PB15这里有个 F103 新手必踩的坑关于 GPIO 能不能承受 5V 电压的问题。F103 大部分 IO 是 5V 容忍的但并不是所有引脚都是——模拟引脚如 PA0~PA3 在某些配置下和部分特殊引脚需要查数据手册确认。简单粗暴的做法是凡是外部模块输出 3.3V 电平的直接接外部模块输出 5V 电平的尽量加电平转换或者串电阻分压。红外避障模块的工作电压如果是 5VOUT 输出高电平可能就是 5V虽然大多数 F103 引脚号称容忍 5V但为了长期稳定我建议在信号线上串一个 1kΩ 电阻心里踏实。3. 软件框架与源码核心实现3.1 模块划分不要让 main.c 变成屎山我见过太多人做小车把逻辑全堆在 main 函数里几百行代码看似能跑加一个功能就崩一片。这套源码从开始就按模块划分好了main.c系统初始化、主循环调度motor.c / motor.h电机初始化、正反转控制、速度设置ultrasonic.c / ultrasonic.hHC-SR04 测距infrared.c / infrared.h红外传感器读取与滤波avoid.c / avoid.h避障状态机与决策逻辑debug.c / debug.h串口/OLED 调试输出模块化的好处是每个文件只干一件事出了问题能快速定位。避障逻辑想改不用翻电机驱动代码想换传感器只需改对应模块的函数接口。后面加蓝牙遥控也只需要在主循环里增加一个指令解析分支不动原有结构。3.2 电机控制PWM 初始化与调速函数TIM3 输出 PWM 的初始化代码如下标准外设库风格要点是配置 GPIO 复用推挽输出、定时器时钟源、PWM 模式、分频和占空比void Motor_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // PA6、PA7 配置为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 方向引脚 PB12~PB15 配置为通用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 999; // 10kHz72MHz/(9991)72kHz? TIM_TimeBaseStructure.TIM_Prescaler 7; // 72MHz/(71)9MHz9MHz/10009kHz TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }这里的分频参数大家别直接抄重点说一下计算方式APB1 定时器时钟默认是 72MHz想要 10kHz 的 PWM就是72MHz / (预分频1) / (自动重装载值1) 10000。我取预分频为 7实际分频后 9MHz自动重装载值为 999结果是 9kHz。这个频率不是固定的工程里标 10kHz 也对关键是你得会自己算换一块板子、换一个外设时钟配置时才知道怎么调整。电机控制接口封装成两个函数让上层只关心“左轮速度多少、右轮速度多少”不关心寄存器void Motor_SetSpeed(int left_speed, int right_speed) { if (left_speed 0) { GPIO_SetBits(GPIOB, GPIO_Pin_12); GPIO_ResetBits(GPIOB, GPIO_Pin_13); TIM_SetCompare1(TIM3, left_speed); } else { GPIO_ResetBits(GPIOB, GPIO_Pin_12); GPIO_SetBits(GPIOB, GPIO_Pin_13); TIM_SetCompare1(TIM3, -left_speed); } if (right_speed 0) { GPIO_SetBits(GPIOB, GPIO_Pin_14); GPIO_ResetBits(GPIOB, GPIO_Pin_15); TIM_SetCompare2(TIM3, right_speed); } else { GPIO_ResetBits(GPIOB, GPIO_Pin_14); GPIO_SetBits(GPIOB, GPIO_Pin_15); TIM_SetCompare2(TIM3, -right_speed); } }速度值范围 0~999 对应占空比 0~100%实际测试发现PWM 值低于 250 时电机几乎不转我给的最低有效值设在 300尽量避免“设置了速度但车不走”的疑惑。3.3 超声波测距时序与实现HC-SR04 的工作时序很简单Trig 引脚拉高 10us 以上模块自动发射 8 个 40kHz 脉冲然后 Echo 引脚输出高电平高电平持续时间就是超声波往返时间。距离计算公式是距离(cm) 高电平时间(us) / 58这个系数来自声速 340m/s往返距离 340 * t_us / 1000000 * 100 cm单程距离再除以 2算下来大约是 t_us / 58。uint32_t Ultrasonic_GetDistance(void) { uint32_t time_us 0; GPIO_SetBits(GPIOA, GPIO_Pin_4); // Trig 拉高 Delay_Us(15); GPIO_ResetBits(GPIOA, GPIO_Pin_4); // Trig 拉低 while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5) 0); // 等待 Echo 变高 TIM2-CNT 0; TIM_Cmd(TIM2, ENABLE); while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5) 1); // 等待 Echo 变低 time_us TIM2-CNT; TIM_Cmd(TIM2, DISABLE); return time_us / 58; }这段是我早期用的阻塞式版本逻辑清晰适合先跑通功能。它的缺点是测距期间 CPU 被占住一次测距最长可能等 20ms 以上对应 4m 量程如果后续还要同时处理编码器或者无线模块就得改成“启动测量 — 定时器输入捕获 — 中断里算时间”的非阻塞思路或者用定时器超时中断做超时保护防止 Echo 一直不来导致卡死在 while 里。3.4 主循环与状态调度主循环采用“非阻塞顺序轮询”的思想每个周期只做一次测距、一次决策、一次速度刷新。超声波测距一次约 10~20ms加上红外读取和状态判断整个循环控制在 30ms 左右对人眼和避障场景来说响应足够快。while (1) { distance Ultrasonic_GetDistance(); left_ir GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); right_ir GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); Avoidance_Decide(distance, left_ir, right_ir); Delay_Ms(20); }顺带提一下热量词里有人搜“STM32F103 FreeRTOS”如果后续功能变多比如要同时跑蓝牙协议、OLED 刷新、多个传感器轮询裸机主循环会显得手忙脚乱那时可以把这套状态机逻辑移植到 FreeRTOS 的一个任务里用队列或信号量在任务间传递传感器数据。但对避障小车这个项目来说裸机状态机完全够用没必要为了用 RTOS 而上 RTOS。4. 避障算法与路径规划实战4.1 避障逻辑从“无脑转弯”到状态机避障的核心不只是一个 if-else而是一个状态机。我定义的简单状态包括GO_FORWARD前进、TURN_LEFT左转、TURN_RIGHT右转、TURN_BACK掉头、STOP停止。每次主循环根据传感器数据判断当前处于哪个状态然后执行对应动作。状态转移规则前方距离大于 30cm且左右红外都没有触发保持前进。前方距离在 20~30cm 之间减速慢行向没有障碍的一侧微调方向。前方距离小于 20cm停车此时检查左右红外只有左侧障碍右转。只有右侧障碍左转。两侧都有障碍掉头两轮反转。红外传感器被触发且前方测距还没更新时优先按红外结果执行避障动作。这个规则看似简单但实际跑起来会遇到一个经典问题小车转向后可能一直处于“前方有障碍”的状态永远转不出去或者进入左右反复横跳的死循环。解决办法是给转向动作加一个“时间锁”——执行转向时记录转向开始时间持续至少 400ms期间不重新决策保证车身转到足够角度后再恢复判断。4.2 动态避障与路径规划向 Bug 算法取经热搜词里有“动态避障小车路径规划”这个点在小车场景下确实值得聊。动态避障和静态路径规划最大的区别是环境不是在开始前就已知的而是边走边探测所以很难用 Dijkstra、A* 这种全局规划算法直接硬怼。当然如果小车有里程计和陀螺仪可以建图做全局规划但对于 F103 这种入门小车更合适的是“反应式避障 趋向目标”的策略。我在后期改进版里加入了“目标导向”逻辑思路借鉴了移动机器人经典的 Bug 算法小车先大致朝向目标点直行遇到障碍物就沿障碍边缘绕行在满足一定条件后脱离障碍继续朝目标走。对单车来说实现起来不复杂关键是维护一个“朝向角”信息用电子罗盘或编码器估算左右轮里程差这个需要额外加硬件。如果只是用红外和超声波做纯反应式避障建议把策略命名为“走廊式避障”让小车尽量贴着障碍边缘行走而不是一遇到障碍就掉头跑远——掉头跑远很容易卡在墙角。实现方式是当左侧红外触发时不是直接右转而是右转一个固定角度后继续前进同时保持右侧距离墙在安全范围内。4.3 差速转向转弯为什么不掉头差速转向是履带式和两轮差速小车最常用的转向方式。转向时让一侧轮子减速或反转另一侧保持速度靠左右轮速差实现转弯。这里有一个细节转弯半径由轮速差决定轮速差越大转弯半径越小。所以在源码里我定义了几个梯度场景左轮 PWM右轮 PWM效果直行400400匀速前进慢速左偏400300小半径左转快速左转200400大角度左转原地掉头400-400绕中心旋转要注意的是PWM 占空比和实际车轮转速并不是线性关系尤其是在电池电压下降后同样的 PWM 占空比对应转速会明显降低。这就要求源码里不能写死某个 PWM 值作为“转弯到位的保证”而是根据状态机时间锁来判断是否完成转向动作。5. 常见问题与排错记录5.1 电机不转或只有嗡嗡声这是新手最容易遇到的情况。电机有声音但轮子不转原因基本三个一是 PWM 频率和驱动芯片不匹配L298N 配 20kHz 以上 PWM 就可能反应不过来降到 1kHz 左右立刻解决二是驱动芯片的逻辑电源没接很多 L298N 模块除了 12V 电机电源还要接 5V 逻辑电源三是使能引脚EN A/EN B没拉高电机被锁死。排查顺序建议先给方向引脚人为输出高/低电平看电机是否正反转再测驱动模块逻辑电源最后查 PWM 波形。把变量范围一步步缩小别上来就怀疑代码。5.2 红外传感器误触发红外避障模块出厂默认灵敏度比较高经常出现面前空无一物却检测到障碍的情况。原因往往是模块的电位器阈值调得太高或者环境光里的红外成分干扰了接收管。解决办法是实测标定把模块放在桌面上旋转电位器让它在 10cm 处检测到白纸、离开 15cm 后输出翻转。注意黑色的物体吸光能力很强同一套阈值在白墙和黑色桌腿上的表现完全不同条件允许的话给探头做个遮光罩减少侧面环境光的影响。5.3 小车避障时反复横跳频发“左转—右转—左转”的循环说明状态机缺少滞回逻辑。传感器在前方 20cm 处触发避障小车左转后前方障碍消失了又立即切回前进状态结果刚前进又检测到障碍再次右转——整个过程车身没有实际位移只是在原地摆动。这个我在前文提过解法就是转向时间锁和距离滞回。距离滞回的意思是触发避障的阈值是 20cm但解除避障的阈值要放大到 30cm而不是传感器刚从 19cm 变成 21cm 就恢复前进。状态切换要有“迟钝感”机械系统才有时间反应。5.4 小车跑着跑着突然重启这个问题十有八九是电源问题。电机堵转或者急加速瞬间电流可能到 2A 以上电池电压被瞬间拉低主控复位。我之前用两节 18650 电池串联供电没加电容一上坡就重启后来在电机驱动电源端并联 470uF 电容并把主控供电从驱动模块的 5V 输出取电问题就消失了。另外建议在代码里加上独立看门狗IWDG防止程序异常跑飞时小车失控。初始化看门狗并周期性喂狗相当于给系统加了一道保险丝即使电路干扰导致程序跑飞也能在几百毫秒内自动复位。5.5 超声波测量距离明显偏大或为 0先确认 Trig 和 Echo 有没有接反接反的话模块不会正常响应。其次HC-SR04 两次触发间隔必须大于 60ms否则上一次的余波会干扰下一次测量。如果 Echo 一直为高大概率是模块电源不稳HC-SR04 对供电要求比较高5V 供电如果纹波大测距结果会飘。提示超声波模块不要在空气流动性强的环境比如风扇旁边测试气流扰动会对声波传播造成影响测出来的距离不稳定。6. 源码背后的心得与扩展建议这套源码调通之后我最大的体会是避障小车真正难的不是“跑起来”而是“稳定地跑”。同样的代码在平坦地面可能表现完美换到瓷砖缝隙多、光线变化大的环境问题立刻暴出来。所以建议大家在拿到源码后专门花时间做一次“阈值标定”——把超声波触发距离、红外灵敏度、转向时间锁这三个参数全部实测一遍记录不同场景下的表现再根据结果调整代码里的宏定义。后续扩展的话有几个方向我觉得很值得做。一是加 OLED 显示屏把传感器数据、当前状态实时打印出来调试效率翻倍二是加蓝牙模块手机遥控和自动避障之间切换变成半自动平台三是上编码器电机测实际轮速做闭环控制这是往智能车竞赛方向走的关键一步四是研究低功耗设计把 F103 的停机模式用起来靠外部中断唤醒做电池供电的长时间待机巡检小车。源代码结构已经按模块拆好了这些扩展都只需要在对应模块里加内容不用推翻重来。本文还有配套的精品资源点击获取
返回列表