免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

基于STM32的红外循迹避障小车完整实战与调试指南

基于STM32的红外循迹避障小车完整实战与调试指南 简介一套基于STM32的红外循迹避障小车完整C程序工程适合嵌入式学习者、电子竞赛备赛者及智能小车爱好者作为实战参考。以STM32为主控结合红外循迹与避障传感器实现路径检测、障碍识别和自动转向控制涵盖SysTick延时、PWM调速、GPIO/ADC配置、中断处理等关键模块。资源共92个文件包含35个h头文件、33个c源文件以及启动文件、工程配置、hex烧录文件等整体仅318KB目录结构层次分明便于模块化阅读与移植。已有10777人学习说明该工程需求较大。程序包内提供工程模板、外设驱动、readme说明和烧录脚本并附Proteus仿真未成功提示与实物调试建议可帮助读者直接对照实物搭建环境快速验证循迹避障效果同时理解STM32中断、定时器与电机控制等嵌入式开发流程。 做智能小车这个项目在嵌入式学习路径里几乎是绕不开的一站。基于STM32的红外循迹避障小车简单说就是让小车沿着地面黑线跑遇到障碍物自动转向绕行听起来不复杂但真正要把它做得稳定——不丢线、不撞墙、不抖动、不跑飞——需要把传感器、电机控制、状态机这几块吃透。这篇文章我把自己的完整实战过程和踩过的坑放出来适合刚学完STM32标准库或者HAL库基础、想做一个能跑能躲的实物项目的同学参考。1. 项目整体设计与方案选型1.1 为什么主控选STM32先聊主控选型。市面上做循迹避障小车的方案很多51、Arduino、ESP32都能跑但STM32的性价比、外设资源和扩展空间是最均衡的。用51或者AVR的单片机做简单循迹逻辑其实够用可一旦你后面想加摄像头识别、PID自整定、串口调参、OLED显示这些功能51的资源就显得捉襟见肘。我选的是STM32F103C8T6最小系统板主频72MHzFlash 64KBRAM 20KB同价位段里非常能打。这颗芯片有好几个定时器既能输出多路PWM驱动电机又能做输入捕获去测超声波回波时间还能剩一个定时器做系统时间基准。整车的传感器、电机、电源状态管理都可以在一颗芯片上解决不需要外扩太多东西。还有一个很现实的原因STM32的学习资料、例程和教程量非常大遇到问题搜索一下基本都有方向。哪怕你只是想尽快过一个毕设或者课设选它也比冷门芯片稳妥得多。1.2 硬件清单与选型心得这套硬件组合是我实测下来比较顺手的价格也不贵模块型号/方案说明主控STM32F103C8T6 最小系统板通用蓝板即可红外循迹TCRT5000 × 4四路比两路判断更稳避障模块HC-SR04 超声波测距范围约3cm~4m电机驱动DRV8833 或 L298N我更推荐DRV8833电机TT马达带减速齿轮× 2 或 × 4两驱够用四驱更猛供电18650电池 × 2 AMS1117-3.3/5V降压电压必须稳其他小车底盘、杜邦线、万用表调试必备选型上有个要点驱动芯片我一开始用L298N便宜但压降大两个功率管加内部二极管导通压降实测能掉2V左右。电池电压本来就不高电机启动瞬间再压一下很容易让单片机关机重启。后来换成DRV8833导通内阻低很多同样的电池电压下电机转起来明显更有力体积也小适合小车这种紧凑布局。如果你的电机功率比较大或者只想用现成模块L298N也不是不行但最好给控制板单独稳压供电避免电机大电流把系统电压拖垮。2. 循迹模块原理、接线与阈值标定2.1 红外反射式循迹的工作原理TCRT5000内部是一个红外发射管加一个光电接收管发射管持续发出红外光照射到地面后反射光的强度取决于地面的颜色和材质。白色或浅色地面反射强接收管导通程度高输出电压低黑色地面吸收红外光反射弱输出电压高。所以循迹模块其实不是看见黑线而是通过反射率差异判断传感器下方是什么颜色。市面上常见模块分两种输出方式数字量输出内部带LM393比较器直接输出高低电平和模拟量输出。数字量的接GPIO就能读简单直接适合新手。模拟量的需要用ADC采样能拿到连续数值软件层面调节空间更大适合对稳定性有要求的场景。我自己用的TCRT5000模块上有电位器可以调比较阈值但实际用下来发现一个问题电位器调到一个固定位置后换个环境光线条件或者换块地面误判概率会明显上升。如果从零开始选型我建议直接买模拟量输出的版本配合ADC读取后面用软件自适应阈值调起来会舒服很多。2.2 接线与GPIO分配方案四路循迹传感器从左到右分别叫左外、左内、右内、右外。我用的接线方案传感器信号STM32引脚功能左外PA0ADC/GPIO输入左内PA1ADC/GPIO输入右内PA2ADC/GPIO输入右外PA3ADC/GPIO输入如果只是用数字量版本这几个引脚配置成上拉输入就行。如果用模拟量版本就配置成ADC输入通道。电源方面模块的VCC接3.3V不要接5V。虽然很多说明书写着5V供电但单片机GPIO容忍度里有些配置不能直接承受5V安全起见统一3.3V供电信号电平也和STM32匹配省去电平转换的麻烦。2.3 阈值标定循迹能不能跑稳的关键循迹程序最核心的不是代码逻辑而是阈值标定。很多同学第一次下地试车小车跑得歪歪扭扭甚至冲出去多半是阈值没调好。阈值标定步骤我总结下来分三步把小车放在白色地面分别读取四路传感器的原始ADC值记录下白底数值。把小车放在黑线上方再次读取四路原始值记录黑线数值。取白底和黑线数值的中间值作为判定阈值。举个例子如果某一路在白色地面上ADC读数约800黑线上约200那阈值可以设在500附近。但注意这个中间值不是严格数学平均因为红外模块在黑白边界附近的输出有一个过渡区如果实际使用中频繁抖动可以把阈值稍微偏向黑线一侧让识别黑线更保守。我实际测试中发现手动标定一次只能保证当天、当前光照条件下的效果。更好的做法是在程序里加一个自校准逻辑上电后小车原地读取几秒自动记录当前环境下的最高值和最低值然后计算动态阈值。这套思路不复杂但能明显缓解环境光变化带来的误判。3. 避障模块超声波测距与绕行策略3.1 HC-SR04测距原理与驱动思路HC-SR04是主动式超声波测距模块工作时MCU给Trig引脚一个至少10us的高电平脉冲模块内部会发射一串40kHz超声波同时Echo引脚输出高电平。高电平持续的时间就是声波从发射到碰到障碍物返回的总时间用这个时间乘以声速再除以2就是前方障碍物的距离。计算距离的公式很简单距离(cm) echo高电平时间(us) / 58。这个58是从声速推出来的经验系数常温下声速约340m/s往返时间对应距离换算过来就是1cm约等于58us。用STM32驱动HC-SR04有几种方式可以GPIO模拟输出Trig脉冲再用外部中断加定时器测量Echo高电平时间也可以用定时器输入捕获功能直接测脉宽。我推荐用输入捕获精度更高而且不占用CPU空转。接线建议HC-SR04引脚STM32引脚VCC5V模块需5V供电GNDGNDTrigPB0EchoPB1定时器输入捕获通道注意Echo输出也是5V电平如果直接接STM32引脚可能有耐压风险稳妥做法是串联一个1k电阻再进引脚或者用电阻分压降到3.3V。很多教程不提这个细节但实际批量玩下来直接怼5V虽然大多也能跑偶尔会出奇怪问题。3.2 避障策略设计方向优先而不是到处乱撞避障策略有几个层级。最简单的版本是前方距离小于某个值比如30cm就后退然后固定右转再继续前进。这种方案代码量少但逻辑很死板如果右边也有障碍小车会陷入原地反复转向的死循环。我建议用方向优先策略每次检测到前方障碍时超声波模块转头测左侧和右侧的距离或者用两个超声波分别装左右哪边空间大就往哪边转。如果两边都很窄说明真的过不去就后退然后掉头。有人会问一个超声波怎么测左右简单办法是舵机云台带着超声波摇头测完前方测左边测右边更省事的做法是小车正前方装一个超声波左右转向时通过运动状态来判断。我第一版用的舵机云台方案结构上多花点功夫但数据更直观调试也方便。还有一个常见坑超声波测距数据偶尔会跳变比如前方明明没东西距离突然从200cm掉到10cm。这种异常值如果不处理小车会突然急刹甚至误判转向。实测下来加一个简单的滑动窗口滤波或者连续三次读到近距离才认为是障碍的判断能过滤掉大部分干扰。3.3 测距数据的防抖处理我的处理方法是维护一个长度为5的环形缓冲每次新数据进来替换最旧的数据然后取中位数作为当前有效距离。中位数滤波比平均值滤波在这种场景下更可靠因为平均值会被某个极端大值或者小值拉偏而中位数在突变数据中依然能保持稳定。防抖逻辑的伪代码如下uint16_t distance_data[5]; // 环形缓冲 uint8_t data_index 0; uint16_t get_median_distance(void) { // 返回5次采样中的中位数 }在每次超声测距完成后把结果放入缓冲然后取中位数。这个值再用于避障判断。实测下来加了这层处理后小车误判率下降非常明显。4. 电机驱动与PWM调速控制4.1 两轮差速转向怎么理解两轮差速转向的核心逻辑很简单左轮和右轮速度不同小车就会向速度慢的一侧转弯。左轮慢、右轮快小车左转反之右转两轮同速小车直行。四驱车其实也差不多左右两侧各自同速一对轮子。循迹小车的转向不能用原地打轮的方式而是要根据传感器状态实时调整两侧PWM占空比的差值。差值越大转弯越急。我常用的转向策略传感器状态判定左轮PWM右轮PWM只有左内压线小幅左偏减速保持只有右内压线小幅右偏保持减速左外也压线大幅左偏明显减速加速右外也压线大幅右偏加速明显减速四路都在白底丢失黑线紧急搜索模式—4.2 定时器PWM输出配置STM32的PWM输出核心逻辑就是定时器计数器从0跑到ARR值和比较寄存器CCR的数值做比较决定输出高还是低。占空比 CCR / (ARR 1)。我平时用标准库以TIM2输出四路PWM为例void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // PA0~PA3 复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 分频让PWM频率落在10kHz以上 TIM_TimeBaseStructure.TIM_Period 999; // ARR TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz / (711) / (9991) 1kHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC2Init(TIM2, TIM_OCInitStructure); TIM_OC3Init(TIM2, TIM_OCInitStructure); TIM_OC4Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }这里有个经验PWM频率不要设太低低频率下电机会产生人耳可闻的啸叫声而且转速波动明显。我一般用10kHz以上或者直接超过20kHz人耳听不见电机运行也平稳。上面这个例子如果把Prescaler改成8ARR改成199就能得到20kHz左右的PWM频率。4.3 速度斜坡与电机死区补偿电机启动有个特点PWM占空比低于某个值的时候电机根本不转因为要克服静摩擦力和齿轮阻力。这个启动阈值每种电机都不一样不处理的话小车在小幅调整方向时会很突兀——要么没反应要么突然一冲。解决办法是加一个速度斜坡ramp逻辑。比如目标占空比是50%不要直接一步到位而是每次中断里把当前占空比往目标方向递增或递减一小步让电机平滑加速或减速。另外一个坑是左右电机个体差异。同样给50%占空比左轮可能跑得比右轮快小车会向一边偏。简单校正方法做一个测试模式把左右轮分别空载跑固定时间量出各自速度差然后在程序里给慢的一侧加一点偏置量。更精准的做法是给电机加编码器做闭环PID但在这个项目里固定偏置已经能满足大部分需求。5. 主控制程序循迹与避障的协同状态机5.1 状态机怎么设计把循迹和避障放在一起最容易出现的问题是打架小车正在循迹超声波检测到前方有障碍于是转向避障但避障结束之后小车已经离开黑线了这个时候是继续找线还是重新上电初始化我的做法是设计一个简单的状态机主要状态有三个TRACK正常循迹状态依据四路红外传感器实时调整转向。AVOID避障状态超声波检测到前方障碍且距离小于阈值时切入执行减速-转向-前进-回正动作。SEARCH寻线状态在避障结束或者循迹丢失后低速前进并扫描黑线找到后回到TRACK。三个状态的切换逻辑typedef enum { STATE_TRACK, STATE_AVOID, STATE_SEARCH } CarState; CarState current_state STATE_TRACK; void state_machine_run(void) { uint16_t distance get_median_distance(); switch (current_state) { case STATE_TRACK: if (distance OBSTACLE_THRESHOLD) { current_state STATE_AVOID; // 记录当前速度进入避障 } else { track_follow(); // 循迹逻辑 } break; case STATE_AVOID: avoid_obstacle(); // 绕行处理 if (is_black_line_found()) { current_state STATE_TRACK; } break; case STATE_SEARCH: search_black_line(); if (is_black_line_found()) { current_state STATE_TRACK; } break; } }避障结束时不能直接切回TRACK因为小车很可能已经偏离黑线很远了。我的做法是避障动作完成后先进入SEARCH状态让小车低速前进并用一侧的红外传感器扫描直到重新检测到黑线再切回TRACK。这样整体流程闭环小车不会因为一次避障就彻底跑丢。5.2 核心代码实现循迹部分的核心逻辑我用的是四路传感器状态组合判断。先说明一下我这里直接用GPIO读取数字电平1代表在黑线上0代表在白底上实际电平逻辑看模块设计uint8_t s1, s2, s3, s4; // 左外、左内、右内、右外 void track_follow(void) { s1 GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); s2 GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1); s3 GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2); s4 GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3); if (s2 s3) { // 内两路压线直行 set_motor_speed(BASE_SPEED, BASE_SPEED); } else if (s2 !s3) { // 车偏左右转修正 set_motor_speed(BASE_SPEED - TURN_OFFSET, BASE_SPEED TURN_OFFSET); } else if (!s2 s3) { // 车偏右左转修正 set_motor_speed(BASE_SPEED TURN_OFFSET, BASE_SPEED - TURN_OFFSET); } else if (s1 !s4) { // 左边外沿压线大角度右转 set_motor_speed(BASE_SPEED - BIG_TURN_OFFSET, BASE_SPEED BIG_TURN_OFFSET); } else if (!s1 s4) { // 右边外沿压线大角度左转 set_motor_speed(BASE_SPEED BIG_TURN_OFFSET, BASE_SPEED - BIG_TURN_OFFSET); } else { // 四路都不在线上进入SEARCH current_state STATE_SEARCH; } }这个逻辑写的是一种偏差分级思路内两路负责正常循迹外两路负责大偏差修正。很多教程里只用两路传感器效果也行但弯道半径稍大就容易丢线。四路方案的好处是能提前感知即将到来的弯道提前加大转向实测在普通赛道上跑得明显更顺。set_motor_speed这个函数里再叠加速度斜坡逻辑用全局变量记录当前左右轮占空比每次调用时往目标值靠拢每次递增不超过2%~3%。5.3 状态切换的细节处理状态切换中有一个容易忽略的问题当从TRACK切到AVOID时如果车速很快超声波距离判断的响应距离要留足余量。我实测中车速比较快的时候30cm的判断阈值基本来不及反应至少放大到40~50cm才稳妥。另外在AVOID状态下小车执行绕行动作时要控制转弯速度和幅度。如果转弯太大小车很容易整体脱离赛道范围转弯太小又可能撞到障碍物侧面。我一般让小车先减速到基础速度的40%左右再执行转向绕行完成后再慢慢恢复速度。还有一点AVOID状态下超声波传感器不要停下来不测要持续测距防止绕行过程中撞上侧面障碍物。可以把避障绕行设计成一个短暂的过程比如持续800ms到1.5s根据情况决定是回到循迹还是继续绕行避免小车卡在障碍物旁边震荡。6. 实操中遇到的常见问题与排查技巧6.1 ST-LINK连接失败的经典场面很多同学第一次下载程序就遇到 error: no stm32 target found! 包括我自己第一次也一样懵。这个报错基本都是硬件连接或者芯片状态问题排查顺序我建议如下确认SWDIO、SWCLK、GND三根线都连好并且和目标板共地。漏接GND是最高频原因。确认芯片BOOT0接GND低电平BOOT1随意但建议也接GND。如果还不行按住目标板上的复位键不放点击下载在开始下载瞬间松开复位键这个方法对很多诡异情况有效。检查驱动是否正常设备管理器里看到STM32 ST-LINK设备并且没有叹号。如果设备带感叹号可以重装ST-LINK驱动。最后考虑ST-LINK固件版本和Keil设置问题在Keil的Settings里把SW模式下频率从4MHz降到1MHz有时候能解决线太长导致的通信不稳定。如果确定接线没问题还有一种可能是芯片被锁死或者程序里占用了SWD引脚导致无法连接用ST-LINK Utility里的Full Chip Erase可以解决。6.2 循迹阈值漂移与误判循迹模块误判的典型场景是上午在朝阳的房间试车正常下午拉到灯光很亮的走廊小车开始画龙甚至直接冲出赛道。原因是环境光中的红外分量干扰了传感器。处理方案从简单到复杂给模块加遮光罩用热缩管或者黑色胶带挡住环境光直射。把系统放在室内灯光稳定的环境下运行避免阳光直射。用ADC版本做动态阈值自校准上电前几秒采集环境基线值。我最终采用的是动态阈值程序里设置了一个校准模式上电后如果按键按下小车原地不动连续采集500ms四路传感器的原始ADC值记录每路的最大值和最小值然后算出各自的阈值存到全局变量里。这样每次换场地、换光线条件不用改代码重新编译按一下按键就完成标定。6.3 跑起来抖动、偏离、供电不足小车跑起来左右抖动一个原因是PWM频率太低电机转速反馈滞后容易产生振荡。我在4.2节里说过把PWM频率提到10kHz以上情况会明显改善。另一个原因是左右轮速度不一致。如果没有编码器就得靠程序里的固定偏置去补偿。调试方法先让小车直线空跑观察它偏向哪边然后在set_motor_speed里给快的那侧减去一个固定值慢的那侧不动。反复调整这个偏置直到小车能直线跑出2米以上不明显偏航。还有供电问题。如果小车在转弯瞬间出现复位或者蜂鸣器乱叫大概率是电池电压瞬时跌落尤其使用L298N的时候更明显。排查方法用万用表量电机启动瞬间的电压如果低于4.5V就说明驱动压降太大或者电池内阻偏高。换DRV8833、用大倍率18650电池、在电源入口并联一个大容量的电解电容470uF以上都能缓解这个问题。6.4 循迹线识别不稳定与丢失循迹线识别不稳定的一个隐蔽原因是赛道黑线的宽度和传感器布局不匹配。TCRT5000模块的探测距离一般在0.5cm~1.5cm之间如果黑线太窄比如只有1cm四路传感器同时压线的情况很少循迹会非常吃力。检查方法把小车上电悬空用四路传感器在黑线上方慢慢移动观察每一路的输出变化。确保黑线宽度下至少有两路能稳定压住黑线否则需要调整传感器的间距或者选择更粗的黑线。普通赛道推荐黑线宽度1.5cm到2cm转弯处不要用直角尽量用小圆弧连接否则循迹系统很容易在尖角处丢线。关于SEARCH寻线模式我的建议是不要原地旋转找线那样容易转晕。更稳的方式是低速直行一小段同时左右小幅摆动车身让外沿传感器去扫线扫到就回正。如果前进了一小段还是没扫到再掉头180度反向找线一般都能救回来。我个人玩了这么多台小车之后最大的感受是这类项目的难点不在某一个模块怎么驱动而在系统层面的协同——传感器的响应速度、电机响应的跟手程度、状态机切换的时机每一环都在影响整车表现。建议拿到代码之后不要急着调速度先把循迹阈值标定好然后在小幅速度下把循迹跑稳再加上避障逻辑。只要每一步都验证过了整个系统基本不会出大问题。最后再分享一个小技巧调试时在串口把四路传感器状态和当前超声波距离打成一行ASCII输出配合上位机或者串口监视器你就能看到小车在跑的时候脑子里在想什么定位问题会快非常非常多。本文还有配套的精品资源点击获取
返回列表