☰
STM32红外循迹避障小车程序调试与状态机设计
2026/10/15 18:52:57 网站建设 项目流程

简介:这份资源是面向嵌入式初学者与智能小车爱好者的STM32红外循迹避障小车完整程序,围绕路径跟踪与障碍物避障两大核心功能展开,适合用于课程设计、竞赛备赛或自学练手。压缩包共92个文件,约318KB,以C语言源码与头文件为主体,包含35个.h、33个.c,另有汇编启动文件、Keil工程文件、HEX烧录文件及readme说明,覆盖初始化配置、中断服务、传感器数据处理与电机控制等模块。目前已有10787人学习下载,热度较高。读者可从中获得一套可直接编译调试的工程框架,理解红外信号采集、PWM调速与避障决策的实现思路,并借助工程目录与说明文档快速上手实物验证,积累硬件驱动与实时系统开发经验。

1. 红外循迹避障小车:为什么你的程序一上电就跑偏

做嵌入式入门项目,红外循迹避障小车几乎是绕不开的一道坎。硬件搭好了,电机能转,传感器指示灯也亮,可程序烧进去之后,小车要么原地打转,要么冲出赛道,要么遇到障碍物直接撞上去。问题往往不在硬件,而在程序逻辑——红外对管的阈值判断、PWM调速的占空比映射、避障优先级和循迹优先级的冲突处理,这三块只要有一块没理顺,车就“不听话”。

这个标题讲的是基于STM32的红外循迹避障小车程序,核心是用STM32的GPIO读取红外传感器状态,用定时器输出PWM控制电机,再通过状态机协调循迹和避障两种行为。它适合已经学过STM32基础外设、想找一个综合项目练手的人,也适合带学生做课程设计的工程师。下面按“传感器怎么读→电机怎么控→逻辑怎么编→坑怎么避”的顺序,把可复现的方案拆开讲。

2. 红外传感器与电机驱动:先让轮子听你的话

2.1 红外对管的两种输出形式与GPIO配置

常见红外循迹模块分两种输出:数字量和模拟量。数字量模块自带LM393比较器,输出高低电平,STM32端配成上拉输入或浮空输入即可;模拟量模块输出随反射距离变化的电压,需要接ADC。循迹场景下,数字量足够用,因为只需要判断“黑线”和“白底”两个状态。

我一般把红外传感器的DO引脚接到STM32的GPIO上,配置成上拉输入。为什么上拉?因为多数模块输出是开漏或弱上拉,STM32内部再上拉一次,能避免悬空时读到随机值。以常见的五路循迹模块为例,从左到右依次接PA0到PA4,避障红外接PA5。

// gpio.c 红外传感器GPIO初始化 void IR_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉,防止悬空误读 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); }

这段代码把PA0到PA5全部设为上拉输入。参数说明:GPIO_MODE_INPUT是输入模式,GPIO_PULLUP启用内部上拉电阻,GPIO_SPEED_FREQ_LOW对输入引脚无实际影响,但保持低速度可以降低噪声。注意,如果你的模块是模拟输出,这里要改成GPIO_MODE_ANALOG并配合ADC使用,不能直接读高低电平。

读传感器时,黑线反射弱,红外接收管导通程度低,多数模块输出高电平;白底反射强,输出低电平。但不同模块逻辑可能相反,所以上电后先串口打印一遍五路状态,手动把车放在黑线和白底上各读一次,确认极性再写逻辑。这一步省不得,我见过太多人直接抄代码,结果模块极性和代码假设相反,车一上电就疯跑。

2.2 TB6612与L298N的选型差异及PWM接线

电机驱动常用L298N和TB6612。L298N便宜、耐压高,但压降大,电池电压低时电机没劲;TB6612效率高、发热小,适合锂电池供电。循迹小车一般用两节18650或四节镍氢,电压在6到8伏之间,TB6612更合适。如果手头只有L298N,也能用,但要注意它的使能跳线帽——很多人忘了拔掉跳线帽,PWM根本进不去,电机只有全速和停两种状态。

以TB6612为例,PWMA接STM32的TIM1_CH1(PA8),PWMB接TIM1_CH4(PA11),AIN1、AIN2、BIN1、BIN2接普通GPIO控制方向。STBY接高电平使能。

// motor.c PWM初始化与电机控制 void Motor_PWM_Init(void) { TIM_HandleTypeDef htim1; TIM_OC_InitTypeDef sConfigOC = {0}; __HAL_RCC_TIM1_CLK_ENABLE(); htim1.Instance = TIM1; htim1.Init.Prescaler = 71; // 72MHz/72 = 1MHz计数频率 htim1.Init.Period = 999; // 1MHz/1000 = 1kHz PWM频率 htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim1); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; // 初始占空比0 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_4); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4); }

参数说明:Prescaler=71把72MHz主频分频到1MHz,Period=999让PWM频率为1kHz。1kHz是电机驱动的常用频率,太低电机会啸叫,太高驱动芯片发热。Pulse决定占空比,范围0到999,对应0%到100%。实际调试时,我把Pulse设到300左右电机才开始转,这是死区,低于这个值电机不响应,所以代码里要加一个最小占空比补偿。

方向控制用GPIO写高低电平,比如AIN1=1、AIN2=0为正转,反之为反转。这里有个血泪经验:换向时一定要先让PWM归零,再切换方向引脚,否则瞬间电流冲击可能烧驱动芯片。我一般写一个Motor_Set(speed, dir)函数,内部先关PWM,延时1毫秒,再设方向,最后开PWM。

3. 循迹与避障的状态机:别让两个任务打架

3.1 五路循迹的偏差计算与舵机/差速转向

五路循迹模块输出五个数字量,从左到右记为L2、L1、M、R1、R2。常见偏差计算方法是加权求和:L2权重-2,L1权重-1,M权重0,R1权重1,R2权重2。偏差值范围-2到2,0表示居中。根据偏差调整左右轮速度差,偏差越大,差速越大。

// track.c 五路循迹偏差计算 int Track_GetError(void) { int error = 0; if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == BLACK) error -= 2; // L2 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == BLACK) error -= 1; // L1 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2) == BLACK) error += 0; // M if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_3) == BLACK) error += 1; // R1 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_4) == BLACK) error += 2; // R2 return error; }

逻辑说明:BLACK是宏定义,根据实际模块极性设为0或1。偏差为负表示车偏左,需要左轮减速或右轮加速;偏差为正则相反。实际调车时,我用比例控制就够了,不需要PID,因为循迹对响应速度要求不高,比例系数Kp取30到50之间,通过试凑确定。比如基础速度500,左轮速度=500 - Kperror,右轮速度=500 + Kperror,最后限幅到0到999。

注意,如果五路全黑或全白,说明车冲出赛道或悬空,这时候不能继续按偏差走,要进入丢线处理:全黑时停车或原地转向找回,全白时减速直行一小段再判断。这个逻辑不写,车一旦丢线就会乱冲。

3.2 避障优先级与状态切换的防抖处理

避障红外一般装在车头,检测到障碍物输出低电平(或高电平,看模块)。当避障触发时,无论循迹偏差是多少,都要优先执行避障动作:停车、后退、转向。但这里有个坑——如果避障和循迹同时触发,且避障信号有抖动,车会在“避障转向”和“循迹回正”之间反复横跳,看起来像抽搐。

解决办法是加状态机和防抖。状态机设三个状态:TRACK、AVOID、RECOVER。TRACK下正常循迹;避障触发且持续超过50毫秒,进入AVOID;AVOID执行后退和转向,持续300毫秒后进入RECOVER;RECOVER下低速直行,如果避障信号消失且循迹有偏差,回到TRACK。

// state.c 状态机与防抖 typedef enum { TRACK, AVOID, RECOVER } CarState; CarState state = TRACK; uint32_t avoid_tick = 0; void Car_Update(void) { uint8_t obs = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5); // 避障信号 switch (state) { case TRACK: if (obs == OBSTACLE) { if (HAL_GetTick() - avoid_tick > 50) { // 防抖50ms state = AVOID; avoid_tick = HAL_GetTick(); } } else { avoid_tick = HAL_GetTick(); Track_Run(); // 正常循迹 } break; case AVOID: Motor_Set(0, 0); // 先停 HAL_Delay(100); Motor_Set(400, -1); // 后退 HAL_Delay(200); Motor_Set(400, 1); // 右转 HAL_Delay(300); state = RECOVER; break; case RECOVER: Motor_Set(300, 1); // 低速直行 if (obs != OBSTACLE && HAL_GetTick() - avoid_tick > 300) { state = TRACK; } break; } }

参数说明:防抖时间50毫秒是经验值,太小滤不掉抖动,太大避障反应迟钝。AVOID里的延时用HAL_Delay会阻塞整个循环,实际产品中应该用非阻塞计时,但入门项目这样写能快速跑通。RECOVER的300毫秒是让车离开障碍物区域,避免刚退出又立刻触发。

这里的关键是:避障优先级高于循迹,但避障结束后不能立刻回到循迹,否则车还在障碍物旁边,循迹偏差可能把车又带回障碍物。RECOVER状态就是给车一个“冷静期”。

4. 避坑与排查:调车时最容易翻车的五个地方

4.1 现象:上电后电机不转,但驱动板指示灯亮

原因:TB6612的STBY引脚没接高电平,或者L298N的使能跳线帽没拔。很多人以为接了PWM就能调速,实际上使能脚没使能,芯片处于待机状态。

解决:用万用表测STBY电压,如果是0伏,直接接3.3V或5V。L298N则检查ENA、ENB跳线帽是否拔掉,PWM线是否插在使能脚上。

4.2 现象:小车直线行驶时左右轮速度不一致,越走越偏

原因:两个电机的死区占空比不同,或者轮子摩擦力不同。代码里给左右轮相同的PWM值,实际转速不一样。

解决:在代码里加左右轮补偿系数。比如左轮Pulse = speed * 0.95,右轮Pulse = speed * 1.05,具体值通过实测调整。更彻底的办法是加编码器做闭环,但入门项目用补偿系数就够了。

4.3 现象:循迹时车在黑线边缘来回摆动,像喝醉

原因:比例系数Kp太大,或者传感器响应有延迟。Kp太大时,偏差一出现就猛打方向,车冲过黑线又反向猛打。

解决:先把Kp降到10,观察车是否能缓慢回正,再逐步加大到刚好不摆动为止。另外,传感器离地高度要一致,一般1到2厘米,太高会误判,太低会刮地。

4.4 现象:避障触发后车后退撞到后面东西

原因:后退时间太长或后退速度太快。AVOID状态里HAL_Delay(200)后退200毫秒,如果速度400,车可能后退十几厘米。

解决:缩短后退时间到100毫秒,或者降低后退速度到300。同时,如果车尾也装避障传感器,后退时也要检测后方障碍。

4.5 现象:烧录后车不动,串口无输出

原因:STM32的BOOT引脚状态不对,或者程序卡在HAL_Delay里。还有一种可能是电机驱动抢了下载引脚,比如PA9、PA10被复用。

解决:检查BOOT0是否接地,串口TX、RX是否接对。如果用了PA9、PA10做电机控制,换到其他引脚。另外,在main函数开头加一句串口打印,确认程序跑起来了。

5. 进阶技巧:用定时器中断做非阻塞调度

前面状态机里用了HAL_Delay,实际调车时你会发现,避障后退的200毫秒里,循迹传感器完全没在读,如果障碍物突然消失,车还在傻退。更好的做法是用定时器中断做时间片调度,每1毫秒进一次中断,在中断里更新状态机的计时器,主循环只负责读传感器和更新PWM。

具体做法:配置一个TIM2,预分频到1kHz,开启更新中断。中断里维护track_tick、avoid_tick、recover_tick三个计数器,主循环根据计数器判断状态切换,不再用HAL_Delay。

// tim.c 1ms定时器中断 void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); if (avoid_tick < 0xFFFFFFFF) avoid_tick++; if (recover_tick < 0xFFFFFFFF) recover_tick++; } }

这样主循环里判断if (avoid_tick > 50)即可,不会阻塞。参数说明:TIM2的预分频值设为71,周期设为999,和PWM定时器一样,但注意不要和PWM共用同一个定时器,否则中断频率和PWM频率会互相干扰。

另一个技巧是给电机加软启动。直接给满占空比,车会猛冲,轮子打滑,循迹瞬间丢线。我一般在Motor_Set里做斜坡:目标速度分10步,每步延时5毫秒,逐步加到目标值。这样车起步平稳,循迹成功率明显提高。

最后说一个验证方法:把车放在黑线赛道上,用手挡住避障传感器,观察车是否先避障再回循迹;然后松开手,看车能否在1秒内回到循迹状态。如果超过1秒,说明RECOVER时间太长或循迹Kp太小。反复调这三者,直到车在赛道和障碍物之间切换自如。

我调这类小车的习惯是:先让车能直线走,再让车能循迹走,最后加避障。顺序反了,三个问题搅在一起,排查起来就是黑匣子。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询