☰
STM32智能小车红外循迹实战:原理、硬件与代码全解析
2026/10/19 15:26:04 网站建设 项目流程

简介:面向STM32入门及智能车爱好者的红外循迹小车完整工程,基于HAL库实现,覆盖GPIO输入配置、外部中断、PID调速和UART串口监控等关键开发环节。项目以四个红外传感器检测路面黑线为核心,详细演示硬件连接、中断服务函数编写与电机纠偏逻辑,既适合毕业设计参考,也适合竞赛前快速搭建原型。压缩包共72个文件,以45个.h头文件与18个.c源文件为主,配合Keil工程配置(.uvprojx/.uvoptx)、CubeMX初始化文件(.ioc)、启动文件(.s)、hex烧录文件及一个bat清理脚本,整体仅515KB,目录结构清晰,便于按模块阅读与修改;代码采用模块化封装,注释完整,硬件引脚需根据实际连接调整,门槛较低。目前已有5634人学习下载,对希望从寄存器编程转向HAL库开发或正在调试循迹算法的读者,是一份有实际价值的参考示例。 这个标题一看就知道是玩到第二步了:车架、电机、HAL库点灯这些基础已经打通,这期给小车装上“眼睛”——红外循迹。红外循迹算是智能小车从“能动”跨向“能自己走”的第一道坎,成本低、逻辑直观、调试起来也很有成就感。这篇文章我会把这套方案从原理、硬件选型、CubeMX配置到完整代码逻辑全部拆开讲,顺便把我在实际调试中踩过的坑都摆出来,给正在搞第一辆循迹小车的朋友一个可以直接抄作业的参考。项目基于STM32F103C8T6最小系统板,HAL库开发,传感器用TCRT5000五路循迹模块,电机驱动是TB6612,这套组合在各类竞赛和小车入门里非常常见。

1. 项目整体思路与方案选型

1.1 红外循迹到底解决什么问题

循迹小车的核心任务就一句话:让车沿着地面上的一条线自动行驶。这条线通常是黑色胶带贴在白色地面,或者反过来白色线贴在深色地面。要实现这个效果,小车需要不断检测自己相对黑线的位置偏移,然后通过左右轮差速把偏移纠正回来。

红外循迹是目前最简单、最便宜、也最适合入门的位置检测方案。它不需要复杂的图像处理,不需要大量算力,几个传感器加上一个比较器就能给出“在线/离线”的数字信号,单片机只需读GPIO电平就能判断当前位置。这也是为什么工创赛、电子设计竞赛、物联网毕设里,循迹小车几乎都是红外方案的天下。

1.2 为什么选红外而不是摄像头

做循迹也有人用OpenMV、K210加摄像头做视觉循迹,或是用灰度传感器走模拟量,但红外方案在多数场景下依然是最优解:

方案成本开发难度环境适应性适合场景
红外循迹(数字量)低(模块几块钱一个)低,读GPIO即可怕强光,深色/浅色对比明显即可入门、竞赛初赛
灰度循迹(模拟量)中中,需要ADC和阈值标定可识别灰度等级需要连续位置反馈时
摄像头视觉循迹高高,涉及图像处理和通讯受光线影响也比较大,但灵活性高需要识别复杂路径、二维码、路障时

对小车的“第一版”来说,红外数字量方案能在最短时间里跑通闭环控制,把差速转向的逻辑练熟。等你把这套玩明白了,后续再往视觉方案迁移,控制部分的代码照样能用,只是换了“眼睛”而已。

1.3 从“能跑”到“跑得稳”的三步走

我习惯把循迹小车的实现拆成三层:

  1. 感知层:红外传感器模块持续检测地面反射,输出5路数字信号。
  2. 决策层:单片机根据5路信号组合,判断当前相对黑线的位置状态(居中、偏左、偏右、丢线)。
  3. 执行层:根据位置状态输出对应占空比的PWM波,控制左右电机差速转向。

很多新手一上来就盯着传感器怎么调,或者代码怎么写,结果忽略了最关键的决策逻辑设计。事实上,决定小车跑得稳不稳的,不是传感器精度,而是状态判断和转向力度之间的配合。这一步需要在实测中反复调参,没有一劳永逸的参数。

2. 红外循迹原理读透再动手

2.1 TCRT5000反射式红外传感器的工作方式

市面上常见的循迹模块核心器件是TCRT5000,一个集成了一对红外发射管和光电晶体管的反射式传感器。模块工作的时候,发射管持续发出红外光(波长约950nm),光线照射到地面后发生反射,反射光被接收管检测到,接收管的等效电阻会随光照强度的变化而变化。

如果地面是浅色(比如白色),红外光反射率高,接收管接收到大量反射光,模块板上比较器输出低电平,同时模块上的指示灯点亮。如果地面是深色(黑色),红外光被吸收,接收管接收不到反射光,比较器输出高电平,指示灯熄灭。模块上的电位器用于调节比较器阈值,实际效果是控制“多黑才算黑”。

2.2 灵敏度、安装高度与传感器间距

这是调试红外循迹最核心的三个参数,也是新手最容易忽略的地方。

灵敏度调节:模块上蓝色电位器顺时针拧,阈值变高,传感器更容易判断为“深色”(即更严格)。判断标准是:把传感器悬空对准白色地面,指示灯应该亮;对准黑线,指示灯应该灭。如果两个状态灯没有明显变化,就说明阈值不合适。建议在不开车、上电状态下,用一张白纸和一截黑胶带分别贴在传感器下面做测试,调好再装车。

安装高度:TCRT5000的推荐检测距离是5mm到20mm,超过这个范围信号会呈指数级衰减。装车时传感器表面离地面10mm到15mm比较理想。太近了容易剐蹭地面凸起或胶带接头,太远了检测不到。这个高度直接决定了后续循迹的稳定性。

传感器间距:五路传感器中间的传感器负责“居中”,左右两侧的传感器负责“纠偏”。整车探测宽度最好能覆盖黑线的宽度。比如黑线宽度2cm,五个传感器一字排开,间隔1.5cm到2cm,总探测范围6cm到8cm,这样既不会漏线,也不会因为探测范围太宽而频繁误判。

我把安装做个示意图说明:

车头朝下俯视 车辆前进方向 →(上方是车头) [左1] - [左2] - [中间] - [右2] - [右1] \_______ 黑线位置 ______/

车正对黑线时,中间传感器压线,左右两侧传感器在白色地面上,这是最理想的状态。如果车偏了,黑线就会移动到某一个侧边传感器下方,对应传感器输出低电平,程序据此判断该往哪边转。

2.3 数字量还是模拟量,这是个取舍

TCRT5000模块一般引出两个信号引脚:DO(数字量输出)和AO(模拟量输出)。DO是经过LM393电压比较器处理后的数字信号,直接输出高/低电平;AO输出的是传感器原始电压值,反映反射光的强度。

对于入门方案,直接用DO就够了。但有一点要注意:DO输出的“黑/白”判断是二值的,无法区分“已经偏了一大半”和“刚刚偏出去一点”。所以纯DO方案只能做离散的转向控制,路面偶尔有色差、反光差异时容易抖动。

如果你手头模块带AO引脚,其实把它接在STM32的ADC上会更爽:你可以实时读到每一路的反射强度,通过ADC值做更细的偏置判断,甚至做比例转向。我建议第一版先用DO跑通,等觉得“不够顺滑”了,再升级到ADC读取AO,代码改动量并不大,收益却很直观。

3. 硬件接线与CubeMX工程配置

3.1 模块选型与接线清单

我用的是五路TCRT5000循迹模块,它把五个传感器集成在一块PCB上,引出5路DO和一个电源接口,省去了自己搭电路的麻烦。如果你买的是单个传感器,也可以自己拼成多路,但焊接和供电都要自己处理,工程量会大一些。

整体接线参考如下:

模块/外设信号引脚STM32引脚说明
循迹模块 VCC3.3V3.3V直接用STM32板载3.3V供电
循迹模块 GNDGNDGND共地
循迹 左1 DOOUT1PB0输入
循迹 左2 DOOUT2PB1输入
循迹 中 DOOUT3PB2输入
循迹 右2 DOOUT4PB3输入
循迹 右1 DOOUT5PB4输入
TB6612 AIN1/AIN2方向PA4/PA5左电机方向
TB6612 BIN1/BIN2方向PA6/PA7右电机方向
TB6612 PWMAPWMPA8TIM1_CH1
TB6612 PWMBPWMPA9TIM1_CH2
TB6612 STBY使能PA10高电平使能

上面是我的接线习惯,你不一定照搬,只要记住几个原则:传感器DO引脚选用普通GPIO输入即可;电机PWM一定要选带定时器输出比较通道的引脚;STBY如果接了,记得给高电平,否则电机永远不转。

3.2 CubeMX配置要点:GPIO与PWM

打开STM32CubeMX,芯片选STM32F103C8T6,按以下步骤配置:

  1. 配置时钟:RCC中HSE选择Crystal/Ceramic Resonator,Clock Configuration里把系统时钟拉到72MHz,注意APB1总线的定时器时钟是72MHz(虽然没有倍频,但这里也不需要太高频率),APB2也是72MHz。
  2. 配置GPIO输入:PB0到PB4,全部设置为GPIO_Input。考虑到模块输出是推挽数字信号,不需要内部上拉,设置为No pull即可。但如果你的模块是开漏输出型,就一定要把内部上拉打开,否则读不到高电平。
  3. 配置PWM:以TIM1为例,Channel1和Channel2都选择PWM Generation CHx。参数设置:Prescaler设为71,Counter Period设为999,这样PWM频率为72MHz / (71+1) / (999+1) = 720Hz,频率偏低但适合普通直流电机,如果电机有噪音可以调成72-1和999,频率就到1kHz了。实际我常用Prescaler=71,Period=1999,频率360Hz左右,电机响应和噪音平衡得比较好,你可以按需微调。
  4. 配置调试端口:如果是买的最小系统板,通常只有SWD下载口,CubeMX里默认Debug选项为Serial Wire,这里必须保持为Serial Wire,否则下载一次程序后第二次连不上。

生成代码后,main.c里需要手动加PWM启动代码。HAL库不会自动启动所有外设,需要自己在main函数里调用HAL_TIM_PWM_Start,这个操作很多人会忘。

3.3 电源问题别忽视

五路传感器模块如果每个传感器工作电流按20mA算,五路就是100mA,加上STM32最小系统板和电机驱动逻辑部分的电流,直接从USB取电容易供不上。我建议:

  • 传感器模块VCC直接接3.3V,因为它的比较器输出高电平可能是3.3V,接5V虽然也能工作,但DO高电平会抬高到5V,如果STM32引脚不耐5V,就有烧毁风险。
  • 动力电源(电池)单独给TB6612的VM供电,逻辑电源VCC接STM32的5V或3.3V,具体看模块要求。TB6612的STBY和AIN、BIN都是逻辑电平,必须和STM32共地。
  • 实测中发现,如果电池电压掉到6V以下,电机转速明显下降,转向力度变小,循迹会变得迟钝。所以电池电量也是影响循迹效果的一个隐藏变量。

4. 核心代码实现与逻辑讲解

4.1 传感器读取与状态映射

HAL库读取GPIO就一句HAL_GPIO_ReadPin(),但直接裸读五路会让主循环代码变得很乱。我习惯把五路合并成一个uint8_t状态值,每一位代表一个传感器:

#define SENSOR_LEFT1 0x01 // PB0 #define SENSOR_LEFT2 0x02 // PB1 #define SENSOR_MID 0x04 // PB2 #define SENSOR_RIGHT2 0x08 // PB3 #define SENSOR_RIGHT1 0x10 // PB4 uint8_t read_line_status(void) { uint8_t status = 0; if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) status |= SENSOR_LEFT1; if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) status |= SENSOR_LEFT2; if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET) status |= SENSOR_MID; if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3) == GPIO_PIN_RESET) status |= SENSOR_RIGHT2; if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_4) == GPIO_PIN_RESET) status |= SENSOR_RIGHT1; return status; }

注意这里我判断的是GPIO_PIN_RESET,因为模块输出低电平表示检测到黑线。如果你的模块逻辑相反,把RESET改成SET就行,这是最容易被拷贝代码后坑到的地方。

4.2 电机控制接口封装

电机控制放在一个独立函数里,主循环只调用接口,不直接操作定时器,这样后续想换成闭环速度控制也方便:

void motor_set(int16_t left_speed, int16_t right_speed) { if (left_speed >= 0) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, left_speed); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, -left_speed); } if (right_speed >= 0) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, right_speed); } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, -right_speed); } }

这个函数的好处是,在整个循迹逻辑里我只需要关心“左轮转多快、右轮转多快”,至于方向引脚怎么组合,全部封装在底层。后续如果需要加编码器做闭环,也只需要替换这个函数的实现,不影响上层逻辑。

4.3 五种循迹状态的转向策略

五路循迹的状态最多有32种组合,但实际运行中常见状态就那么几种,我用一个switch处理主要状态,剩下的统一按丢线急停或原地旋转处理:

状态位组合含义策略
00010(中间检测到)车正对黑线直行,左右轮同速
01100(偏右)黑线在传感器阵列偏左右轮提速/左轮减速,向左修正
00110黑线在中间偏左轻微右转修正
11000(偏左)黑线在传感器阵列偏右左轮提速/右轮减速,向右修正
10010黑线在中间偏右轻微左转修正

注意状态值来自前面的bit定义,比如“中间检测到”就是SENSOR_MID为1,其他位为0,也就是二进制00010,而“偏右”意味这黑线往右跑,右侧传感器先压到线,对应的是SENSOR_RIGHT1为1,这要看你怎么摆放传感器命名。我上面定义RIGHT1是最右边,那么SENSOR_RIGHT1置位就是最右侧检测到线,车需要往右,修正方向就让左侧轮速提高。

转向力度建议不要一上来就原地打死,先用差速小角度修正。基础版本我这样定义:

#define BASE_SPEED 680 // 基础占空比,60%~70%左右 #define TURN_SPEED 300 // 差速补偿值 void handle_line_following(uint8_t status) { switch (status) { case SENSOR_MID: motor_set(BASE_SPEED, BASE_SPEED); break; case SENSOR_MID | SENSOR_LEFT2: motor_set(BASE_SPEED - TURN_SPEED, BASE_SPEED + TURN_SPEED); break; case SENSOR_LEFT2 | SENSOR_LEFT1: motor_set(BASE_SPEED - TURN_SPEED, BASE_SPEED + TURN_SPEED); break; case SENSOR_MID | SENSOR_RIGHT2: motor_set(BASE_SPEED + TURN_SPEED, BASE_SPEED - TURN_SPEED); break; case SENSOR_RIGHT2 | SENSOR_RIGHT1: motor_set(BASE_SPEED + TURN_SPEED, BASE_SPEED - TURN_SPEED); break; default: // 丢线情况:先减速,尝试原地转向找回黑线 if (status & (SENSOR_LEFT1 | SENSOR_LEFT2)) motor_set(-BASE_SPEED, BASE_SPEED); else if (status & (SENSOR_RIGHT1 | SENSOR_RIGHT2)) motor_set(BASE_SPEED, -BASE_SPEED); else motor_set(0, 0); break; } }

我的策略是:检测到中间传感器压线时,直行;检测到黑线在左侧传感器范围时,减小左轮速度同时加大右轮速度,让车向左回正;反之向右回正。这里的TURN_SPEED不是固定死的,它的值直接决定了修正的强度。如果小车过弯时总是冲出赛道,就增大TURN_SPEED或减小BASE_SPEED;如果小车沿线走蛇形大幅摆动,就减小这个差值。

4.4 完整主循环逻辑

主循环里就三件事:读状态、处理循迹、加延时。这个延时比较有讲究,太短会导致传感器抖动被直接放大,太长会导致转向响应滞后。我试下来2ms到10ms的延时比较合适,既不影响实时性,又能滤掉一部分传感器毛刺:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM1_Init(); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_10, GPIO_PIN_SET); // STBY使能 while (1) { uint8_t status = read_line_status(); handle_line_following(status); HAL_Delay(5); } }

这里有几个容易出问题的地方:

  • 上电后电机没反应,先检查STBY引脚有没有拉高。TB6612的STBY不是默认使能的,很多模块需要外部拉高才工作。
  • 用了HAL_TIM_PWM_Start后,如果中途想改变占空比,用__HAL_TIM_SET_COMPARE宏,不要重新调用PWM_Start。重复Start在某些HAL版本里会导致输出异常。
  • 如果电机一边转一边不转,检查另一路的PWM通道配置和引脚是否正确。TIM1的CH1是PA8,CH2是PA9,但如果你的板子有重映射或者引脚冲突,需要回到CubeMX里确认。

5. 常见问题排查与调试经验

5.1 传感器“全亮全灭”的排查

五路循迹模块上电后,指示灯可能出现的情况和对应解法我整理成一张表:

现象可能原因排查思路
所有灯常亮阈值太低,白色地面也被判定为“黑”顺时针微调电位器,或检查模块供电是否正常
所有灯常灭阈值太高,黑线也反射不过去逆时针微调电位器,检查安装高度是否过高
某一路灯一直不变该路传感器损坏或遮挡把模块拿到白纸和黑线上分别测试,判断是硬件还是阈值问题
指示灯闪烁不定模块供电不足或电位器处于临界点检查3.3V供电,单独接一个稳压模块;稍微调一下阈值让状态更分明

实测里最常见的问题就是电位器没调好,导致传感器把白色地面当成黑线,小车一上电就在原地疯狂打转。不要嫌麻烦,上电后用一张白纸和黑胶带挨个传感器测试,确定每路都能正确区分黑白再装车。

5.2 走线抖动与钝化策略

循迹小车跑起来之后最常见的现象是“画龙”——车沿着线走但是左右大幅摆动,轨迹不是平滑曲线,而是锯齿状。主要原因有这几个:

  1. 速度太快:转向修正的速度跟不上偏移累积的速度,车冲出线才检测到偏差,于是大幅回正,又冲过中线。解决方法是降低BASE_SPEED。
  2. 采样频率不适:如果主循环延时太短(比如1ms以内),传感器噪声会被放大,系统频繁切换转向状态;如果延时太长(比如50ms),车早就偏出去了。5ms到10ms是经验值。
  3. 转向修正力度过强:TURN_SPEED设置过大,车一检测到偏移就猛烈修正,导致过冲。可以适当减小差值,或者引入“分级转向”思路,即检测到中间偏侧时小力度修正,检测到最外侧传感器时大力度修正。
  4. 传感器间距过宽或过窄:间距宽了,车只有在偏得比较多时才能检测到;间距窄了,黑线细的时候中间传感器可能抓不住线。按黑线宽度调整。

我自己的调试顺序是:先低速(BASE_SPEED = 400)测试直线和直角弯,确认逻辑方向正确,再逐步提速,每提一次速度就重新观察车的走线形态,看是过弯冲出还是直线抖动,针对性调TURN_SPEED。

5.3 串口打印辅助调试

没有上位机辅助,全靠肉眼看小车转向,很多时候只能靠猜。我强烈建议在调试阶段接一个串口,把状态值打出来看。用HAL库配置一个UART,在main循环里把status的二进制输出出来:

uint8_t msg[32]; sprintf((char *)msg, "status: %d\r\n", status); HAL_UART_Transmit(&huart1, msg, strlen((char *)msg), 100);

这样你能直观看到小车在直道、弯道时,五路传感器分别是什么状态。我在调一个直角弯时发现,车过弯的瞬间状态会跳变到0x00,也就是五路全丢线,说明转向修正偏慢,车已经彻底冲出线了。这时候加一个记忆逻辑:只要上一次状态是偏左,那么丢线时默认继续向左转,直到重新看到黑线。

这个“丢线记忆”功能很实用,尤其对直角弯和十字路口,处理逻辑是记住丢失前一拍的状态,按原方向继续旋转而不是傻傻停下。

6. 个人体感与后续扩展

6.1 实际测试中的几个心得

红外循迹这个项目,看起来简单,真正调起来能把人整得没脾气。以前我在实验室调一辆五路循迹小车,第一次下地的时候车一上电就是360度原地转圈,原因就是电位器拧太过了,五个传感器把白色地面全判成黑色。后来我总结出一条原则:先调传感器,再调电机方向,最后调转向参数,每步单独验证完再组合测试。

调电机方向也有一个坑:左右电机安装方向相反,导致同一组AIN/BIN信号下,两个轮子转向相反。如果你发现小车不是直行,而是原地打转,先检查两个电机是否同向。最简单的验证方法:用固定占空比(比如300)驱动两个电机,把车架空,看两个轮子是否都往前走。如果不一致,要么调整电机线序,要么在代码里把某个方向引脚逻辑反过来。

6.2 还能怎么继续升级

第一版红外循迹跑通之后,升级路径其实很清晰:

  • 用ADC采模拟量替代数字量:五路AO接定时器的ADC采样,可以拿到0到4095的连续灰度值,根据反射强度做线性偏差计算,转向力度可以连续变化,小车走线会顺滑很多。
  • 引入PID控制:当有了连续偏差值,就可以套一个位置式PD控制器,把偏差映射成左右轮差速,这是很多纯数字量方案无法做到的。
  • 加编码器做速度闭环:给电机配上霍尔编码器,用STM32定时器编码器模式采集转速,再用PID把左右轮实际速度稳在目标值。这样即使电池电压变化、坡道负载变化,车速依然稳定。
  • 和K210通讯扩展视觉:如果后续要参加竞赛,识别红绿灯、数字、二维码,可以用串口把K210的识别结果传给STM32,循迹仍然由红外负责,STM32在红外基础上叠加视觉规则处理。

红外循迹是整个智能小车DIY链路里最值得反复打磨的一环。它不复杂,但把感知、决策、执行三个环节的配合吃透之后,再看后面更复杂的视觉、导航、机械臂控制,思路都会清晰很多。我到现在调试新平台时,还会先用一套红外循迹验证电机和差速逻辑,跑通了再往上叠功能。

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

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

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

立即咨询