☰
STM32智能小车黑线循迹与红外避障实战:从接线到调参全攻略
2026/10/17 11:35:04 网站建设 项目流程

简介:面向初学STM32与智能小车的开发者,一份以黑线循迹与红外避障为核心的综合实验资料,包含完整工程代码与详尽的函数封装,通过定时器配置电机并提供了前进、刹车、左转、右转、后退等基础动作接口,注释细致、方法直观,适合从零理解巡线和避障逻辑的入门学习者。压缩包共192个文件,以c源文件、h头文件、o编译中间文件为主,另有d工程文件以及uvprojx工程、hex烧录文件、map映射等配套内容,既能查看源码又可直接编译下载,整体大小仅6.49MB。已有1186人学习下载,说明这套资料在小车入门场景中具有实际参考意义。通过阅读代码和工程结构,可快速搭建实验环境,掌握红外传感器检测与电机控制配合的基本思路,为进一步扩展循迹避障功能打下基础。

1. 先让小车学会看路:黑线循迹与红外避障是两套感知逻辑

第一次把 ZYSTM32-A1 智能小车放到黑色电工胶带贴成的赛道上,多数人以为接好线、烧个 demo 就能跑起来;实际上一上电,小车要么在直线段原地转圈,要么对着障碍物硬撞上去。标题里的黑线循迹与红外避障,本质是两套相互独立的感知决策链路:循迹靠车底的红外对管读灰度,避障靠车头的红外模块检测障碍物,最后在同一个电机驱动逻辑里做优先级仲裁。这篇文章把传感器选型、接线、代码结构、参数标定和常见故障一次讲清楚,适合刚拿到套件还在对着原理图发懵的新手,也适合想把手上的示例改成能稳定跑完整圈的人。

2. 硬件与接线:引脚分配、供电顺序和传感器安装的三个细节

2.1 循迹模块与避障模块的选型逻辑

黑线循迹在套件里最常见的方案是反射式红外对管,比如 TCRT5000 这类光电传感器。它内部一个红外发射管、一个光电三极管,发射管朝地面打光,黑线吸收红外、白底反射红外,接收端收到的反射强弱就成了区分黑白的依据。这类模块有两种输出:一种板上带比较器,直接输出高/低电平,阈值靠一个蓝色电位器拧出来;另一种输出模拟电压,单片机内部 ADC 采集后软件定阈值。我的建议是优先用模拟输出,因为数字模块的电位器出厂位置不一定适合你的地面,现场拧电位器纯靠手感,而 ADC 采样可以在代码里做动态校准,后面第四章会具体讲。

红外避障模块同样走红外这条技术路线,但逻辑完全不同。循迹看的是反射强弱,避障看的是有没有物体把红外光反射回来,模块前方没有障碍时接收管收不到反射信号,输出一种电平;有障碍时反射信号到达,输出电平翻转。这里要特别留意模块上的灵敏度电位器,它决定触发距离,出厂默认位置通常照到 20 厘米就开始报警,对一个小车上赛道来说这个距离偏大,建议先调到 10 到 15 厘米的触发范围。

传感器的安装位置比型号更重要。循迹传感器要装在车底盘前缘,离地面 1 到 1.5 厘米左右,两个传感器的间距应该略小于黑线宽度。赛道黑线如果贴的是 2 厘米宽的电工胶带,两个对管间距取 1.5 厘米就合理,这样小车骑在线上的时候左右传感器一个在黑线区、一个在白底区,能自然获得左右偏差信号;间距如果比线宽还大,过弯时两个传感器可能同时压到白底,控制器就会丢失方向。避障传感器装在车头朝向前方,建议让探头略微向下倾斜 10 度到 20 度,保证检测区域集中在车头正前方 20 到 30 厘米位置,而不是照到地面。

2.2 引脚分配、供电顺序和共地问题

拿到套件后第一步不是烧代码,而是把每个模块的引脚认清楚。大多数 STM32 小车的硬件结构是主控板、电机驱动板、循迹模块、避障模块四块板分开的,需要逐个接线。我习惯把引脚关系先列成一张表,再照着接线,避免烧完代码才发现左右传感器接反了。

功能模块信号引脚STM32 引脚(常见接法)说明
左侧循迹传感器OUTPA1 (ADC1_CH2)模拟输出,读灰度
右侧循迹传感器OUTPA2 (ADC1_CH3)模拟输出,读灰度
左侧避障传感器OUTPB0 (GPIO)数字电平,检测障碍
右侧避障传感器OUTPB1 (GPIO)数字电平,检测障碍
左电机 PWMIN1/IN2PA6/PA7 (TIM3 通道)占空比控制速度
右电机 PWMIN3/IN4PB0/PB1 或其他定时器占空比控制转向差速
电池电压采样VIN 分压PC0 (ADC1_CH10)用于电压归一化

接线顺序上有一条血泪经验:先接逻辑供电,再接传感器供电,最后接电机电源。很多小车板子上的 STM32 和电机驱动是分开供电的,电机一启动,大电流会把电池电压瞬间拉低,如果地线没接好,主控会直接复位,表现就是小车一动就重启、串口疯狂掉线。接完线后用万用表量一下主控板 GND 和驱动板 GND 之间的电阻,接近 0 欧姆才算共地合格。

还要注意循迹模块的供电电压。这类模块一般支持 3.3V 和 5V 两种,但输出高电平的幅值不一样。如果主控是 3.3V 逻辑,而传感器供电 5V,数字输出高电平接近 5V,直接接到单片机 GPIO 上虽然有概率能读,但不规范。模拟输出模块则要把 ADC 参考电压对齐,我一般让传感器和主控用同一路 3.3V 供电,省去电平转换的麻烦。

注意:如果循迹模块是模拟输出,接 ADC 前先确认模块输出的最大值不超过 STM32 的 VREF。若供电 5V,输出也能到 5V,需要加电阻分压,否则 ADC 采到满量程后一路都是 4095,阈值会失效。

3. 代码落地:循迹阈值、避障去抖与主循环状态机的实现

3.1 灰度读取与 ADC 采样参数

循迹传感器输出的模拟量,直接用单次 ADC 读取会很不稳定,常见做法是连续采样多次取均值。下面这段代码是按 10 次采样、间隔 50 微秒的方式做软件滤波:

#define TRACK_LEFT_CH ADC_CH1 #define TRACK_RIGHT_CH ADC_CH2 #define SAMPLE_CNT 10 uint16_t get_track_value(uint32_t adc_ch) { uint64_t sum = 0; for (int i = 0; i < SAMPLE_CNT; i++) { sum += adc_read(adc_ch); // 连续采样取累加 delay_us(50); // 采样间隔,避开电容充放电 } return (uint16_t)(sum / SAMPLE_CNT); // 均值作为当前灰度值 }

这段代码的逻辑很清楚:对同一个通道连续读 10 次,累加后求平均。adc_read是 HAL 库或者寄存器版本的 ADC 转换函数,返回 0 到 4095 的 12 位原始值。SAMPLE_CNT取 10 是一个平衡点——取值太大会拖慢主循环频率,太小又滤不掉传感器本身的随机波动。50 微秒的间隔是为了让 ADC 采样电容充分放电后再采下一次,如果你去掉这个延迟,读出来的值经常会在固定几个数字之间跳动,这就是典型的采样保持时间不足。

实际跑起来以后,程序判断当前传感器看到的是黑还是白,靠的是阈值比较:

#define TRACK_THRESHOLD 1500 // 由校准过程得到,见第四章 uint8_t track_state(uint32_t adc_ch) { uint16_t val = get_track_value(adc_ch); if (val < TRACK_THRESHOLD) { return TRACK_ON_LINE; // 灰度值低 = 吸收红外 = 黑线 } else { return TRACK_ON_WHITE; // 灰度值高 = 反射红外 = 白底 } }

这段逻辑里有三个容易翻车的地方。第一,阈值比较的方向取决于你的模块设计:有的模块黑线输出低电平,有的输出高电平,不能照抄别人代码里的比较符号。第二,阈值不能拍脑袋定,必须按第四章的校准方法现场测,否则在深色木地板上跑和在白色瓷砖上跑会得出完全相反的结果。第三,白底和黑线的 ADC 值差通常有几倍,因此取中间值做阈值,抗干扰能力远强于取固定值。

3.2 红外避障读取与去抖机制

红外避障模块输出数字电平,看起来只要读一下 GPIO 就行,但直接读会吃大亏。障碍物边缘反射不稳定,电机振动、地面不平都会让电平在边界处反复翻转,程序容易误判。我一般会加一个简单的多数投票去抖:

#define OBSTACLE_GPIO GPIO_PIN_0 #define OBSTACLE_DETECTED 0 // 低电平触发,按实际模块修改 uint8_t get_obstacle_state(void) { uint8_t hit_cnt = 0; for (int i = 0; i < 5; i++) { if (gpio_read(OBSTACLE_GPIO) == OBSTACLE_DETECTED) { hit_cnt++; } delay_ms(2); // 相邻两次判断间隔 2ms } return hit_cnt >= 3; // 5 次里至少 3 次确认,才算有障碍 }

这里事实上做的是一个滑动窗口:5 次采样里 3 次以上检测到障碍才返回真值。delay_ms(2)的间隔不能省,如果 5 次采样挤在同 1 毫秒内完成,一旦遇到窄脉冲干扰,5 次结果可能全部是误报。OBSTACLE_DETECTED的值要对照你的模块说明书确认,常见低电平触发(有障碍拉低)和高电平触发两种都有。

这段去抖代码的执行频率也要控制。主循环里调用它的周期建议在 10 到 20 毫秒一次,太快没必要——小车轮子转一圈的时间都在几十毫秒级别,传感器以 2 毫秒间隔做确认,已经足够快。太慢也不行,车速稍快 100 毫秒的响应延迟对应的行驶距离就有好几厘米,可能撞上障碍车头才反应。

3.3 主循环状态机:先避障还是先循迹

把循迹和避障放在一起时,最核心的问题是执行的先后优先级。常见误区是先循迹后避障,结果小车贴线跑得正欢,前方突然冒出一个障碍物,它第一反应是继续循迹冲过去,等避障再来仲裁已经来不及。正确逻辑是每一轮循环先读避障,有障碍就立刻进入避障状态,没障碍才走循迹逻辑。

typedef enum { STATE_TRACKING, STATE_AVOIDING } car_state_t; car_state_t car_state = STATE_TRACKING; void state_machine_run(void) { uint8_t obs = get_obstacle_state(); switch (car_state) { case STATE_TRACKING: if (obs) { car_state = STATE_AVOIDING; } else { do_track_motion(); // 按循迹传感器差值决定直行或转向 } break; case STATE_AVOIDING: motor_set(0, 0); // 先刹车,停稳再转向 delay_ms(80); motor_set(-TURN_PWM, TURN_PWM); // 原地偏转,避开障碍 delay_ms(300); car_state = STATE_TRACKING; // 转向完成回到循迹 break; } }

这段状态机的关键是do_track_motion()和避障转向的实现方式。do_track_motion根据左右循迹传感器状态做差速转向:左循迹压白、右循迹压黑,说明车头偏左,需要右转修正,让占空比变成左边快、右边慢。差速转向比猛地打死方向更稳定,具体占空比参数第四章讲。

避障状态里我先motor_set(0, 0)刹车 80 毫秒,这是很多人省掉的步骤。如果检测到障碍后直接转向,轮子还在高速反转,重心惯性会让小车继续前冲,转向半径完全不可控。刹车 80 毫秒后原地偏转 300 毫秒,对常见小车的惯性和电机响应来说是一个比较稳妥的区间,轮径大、重量大的车要适当延长到 400 毫秒以上。

4. 参数标定与调参:阈值、占空比、灵敏度电位器的实操方法

4.1 循迹阈值:用串口实测,别拍脑袋

循迹阈值是整个循迹逻辑里最值得花时间的一项。每块传感器由于出厂差异、安装高度不同、地面材料不同,白底和黑线的 ADC 值都不一样。判断阈值是否合理的标准是:白底值减去黑线值,差值越大,系统越稳定;如果差值小于 300,说明传感器高度或照射角度有问题,调阈值也救不回来。

我启动时会把校准数据和串口打印合在一个函数里,实测给你的工程做参考:

void calibrate_track_threshold(void) { uint32_t white_sum = 0, black_sum = 0; // 1. 小车断电放在白底上,连读 20 次取均值 printf("Place car on WHITE, press key\r\n"); wait_key_press(); for (int i = 0; i < 20; i++) { white_sum += get_track_value(TRACK_LEFT_CH); delay_ms(50); } uint16_t white_avg = (uint16_t)(white_sum / 20); // 2. 移动到黑线上,再读 20 次 printf("Place sensor on BLACK, press key\r\n"); wait_key_press(); for (int i = 0; i < 20; i++) { black_sum += get_track_value(TRACK_LEFT_CH); delay_ms(50); } uint16_t black_avg = (uint16_t)(black_sum / 20); // 3. 阈值取中点,留出对称裕量 track_threshold = (white_avg + black_avg) / 2; printf("White=%d, Black=%d, Thresh=%d\r\n", white_avg, black_avg, track_threshold); }

采集次数取 20 次的原因是避免某一次地面杂质干扰污染均值,50 毫秒间隔留出了传感器响应时间。阈值取中点的道理很直接:白底值和黑线值各留一半的裕量,任何一侧受环境光干扰偏移一点都不会立刻翻转判断结果。给阈值再加一个滞回区间是更进阶的做法,判断黑线时用白底值*0.6 + 黑线值*0.4,判断白底时反过来,这样能避免阈值线附近的抖动,但对新手来说先取中点就够用。

校准时要特别注意光照条件。同一个传感器,白天太阳透过窗户照到地面和晚上开灯,实测值能差出 500 以上。因此我建议校准和正式跑车在同一个时段、同一个场地做。另外校准最好左右两个循迹传感器各做一遍,它们之间也可能有出厂差异。

4.2 占空比与转弯半径:差速转向的边界条件

电机 PWM 占空比决定速度,但占空比和实际转速不是线性关系。常见的低压直流电机存在一个启动死区,占空比低于 15% 到 20% 时,电机根本不转,推一下才动。因此直行占空比建议从 30% 到 45% 起步,低于这个区间电机响应慢,高于 60% 又会让小车在过弯时容易甩尾。转弯不推荐把一边电机完全停掉,一边 0% 一边 50% 的原地差速转向看起来转得快,但实际因为电机响应延迟,车头会顿一下再转,转向轨迹不连续。建议转弯时一边 20%、另一边 45%,转速差保持在 25% 左右,转向轨迹平滑,回正也容易。

转向时间用前面代码里的 300 毫秒做基准,但实际值跟你选的占空比和轮径强相关。我的调试习惯是先用 250 毫秒试跑一个直角弯,如果冲出线,就先减转弯占空比而非加转向时间。差速转向角度是占空比差和时间的积分,转向时间加长容易让车转成掉头。每改一次转向参数,就在赛道上做标记,把冲出线的点位和当前参数一起记录,而不是急着连续调参数。

电池电压对占空比的干扰是调参时最容易忽视的一块。一块满电的锂电池电压约 8.4V,用到保护板动作前约 7.2V,同一个 40% 占空比,实际转速差可能超过 20%。这意味着你上午调好的参数,下午电池电压跌了 1V,小车过弯就开始冲出去。我一般会给主控板接一个电池分压采样,跑完一圈用 ADC 读一下电压,低于某个值就换电池或者启动电压归一化。归一化的典型做法是让基础占空比乘以当前电压和标称电压的比值,电压低时补偿占空比,保持轮速基本一致,相关代码会在第六章给出。

4.3 避障灵敏度电位器与障碍物材质

避障模块上那个蓝色电位器直接决定触发距离,逆时针拧增大距离,顺时针拧减小距离,不同模块方向可能相反,以实测为准。我建议先把触发距离调到 10 到 15 厘米,不要更远。太远的话,小车经过赛道边上的挡板也会触发避障,导致它莫名其妙转弯绕开;太近的话,以 40% 占空比的车速,刹车加上转向的响应时间不够,车头会直接顶到障碍物。

还有一个玄学问题:障碍物的材质对红外反射率影响极大。白色纸箱反射率高,20 厘米外就能触发;黑色哑光塑料反射率极低,10 厘米内可能都触发不了。如果你只在调试时用白色纸箱当障碍物,比赛时遇到黑色的障碍物,原来的灵敏度参数就会失效。因此标定避障距离时,请用最终要避让的障碍物实体来调电位器,至少准备一个相近反射率的替代物。

电位器调过之后务必在代码里打一次障碍物状态,用串口确认触发距离是否合适。手拧电位器没有刻度,调多少只能靠输出状态判断,这就是为什么我要求校准过程必须接串口而不是对着指示灯猜。

5. 常见问题与避坑:五个让小车翻车的血泪踩坑记录

5.1 强光下循迹失灵:输出全变白底值

现象:小车白天在窗边跑,循迹传感器全部输出高电平,黑线怎么压都没反应,跑到直线段直接冲出去。

原因:反射式红外对管对环境光非常敏感。太阳光里含有大量红外成分,直接照到接收管上会把信号拉满,传感器的输出被环境红外淹没,黑线和白底的差异被吃掉。

解决:先给传感器加遮光罩,用热缩管或者黑色电工胶带把发射管和接收管的上方和侧面挡住,只留底部出光口。遮光后如果还未恢复,把传感器供电降到 3.3V 并同步降低 ADC 参考电压,相当于压低环境光的绝对影响。最彻底的方案是换用带调制解调功能的循迹模块,它只识别固定频率的红外脉冲,对环境光是天然免疫的。

5.2 电机不转,推一下才转

现象:上电后程序正常跑,串口打印状态也在切换,但电机就是不动;用手拨一下轮子,它才开始转。

原因:占空比落在电机启动死区里。直流电机启动需要克服静摩擦力,10% 的占空比提供的平均电压不足以让线圈建立旋转磁场,电机处于临界状态。

解决:把直行占空比提高到 30% 以上。如果一定要用低占空比慢速运行,可以在启动瞬间输出一个 30% 占空比的启动脉冲,持续 50 毫秒后回落到目标占空比,相当于给电机一个额外的转动力矩,代码上在motor_set里加一个启动判断即可。

5.3 转弯冲出赛道:车头指向反方向

现象:过第一个弯道时小车顺利转过去,到第二个弯道却朝反方向打,直接冲出赛道。

原因:左右循迹传感器接反,或者是左右电机 PWM 通道定义反了。第一个弯道勉强转对是巧合,第二个弯道偏差累积后彻底暴露。这类问题在调试初期最隐蔽,因为你看到的不是彻底不工作,而是行为间歇性错误。

解决:写一个自检函数,上电后手动按住左右传感器分别给高电平,观察串口打印的状态字是否为01或10。再用手轻轻拨动小车左右偏转,观察电机转向是否与修正方向一致。自检如果发现左右反了,优先交换代码里的引脚宏定义,而不是去改接线,避免在硬件上引入新接触不良。

5.4 电池电压下降后行为飘移

现象:充满电时小车跑得很好,跑了十分钟后开始出现转向过度、直行变慢,最后完全冲线。

原因:锂电池从满电 8.4V 放到 7.2V,电压变化超过 14%,电机在同一占空比下转速明显下降。而 ADC 参考电压如果直接由电池供电,传感器的阈值也会跟着漂。两个因素叠加,原来标定的参数全部失效。

解决:给电池电压加一个分压采样,在代码里对占空比做归一化补偿。参考电压也应使用 STM32 内部的参考通道(如 VREFINT),避免用电池直接做 ADC 基准。电压低于阈值就通过串口提示换电池,不要硬跑。相关补偿代码我在第六章里给出。

5.5 避障误报:对着地面频繁急刹车

现象:小车在平整地面上正常循迹,隔几秒就突然刹车转向,但正前方根本没有障碍物。

原因:避障模块的安装角度太平,指向了前方地面。红外光打到地面后反射回来,被接收管误认为前方有障碍。深色地板反射率低不容易触发,但浅色抛光瓷砖反射率很高,就会产生这种反直觉的误报。

解决:把避障传感器的探头向上抬起,让光轴朝向正前方而不是斜下方,检测区集中到距离车头 10 到 30 厘米的位置。调整后再用串口观察一次状态字,确认地面反射不再干扰。如果调整后还是误报,就需要在灵敏度电位器上缩小触发距离,把地面微弱反射排除在触发阈值之外。

5.6 传感器数据乱跳:示波器看到一根毛刺

现象:循迹和避障的数据偶尔出现单次跳变,状态切换异常,但静态测试一切正常,电机一转就出问题。

原因:电机电刷产生的火花在电源线上制造了大量尖峰干扰,如果传感器电源和电机电源没有隔离,干扰直接叠加到传感器的供电上,让 ADC 采样值突变,也让 GPIO 读到的电平瞬间翻转。

解决:传感器和主控供电走一路,电机驱动走另一路,两者只在电池总端共地。另外在传感器电源引脚旁边加一个 100 微法电解电容和 0.1 微法陶瓷电容,形成 LC 滤波。多数套件出厂时没有这个电容,自己焊一个成本极低,但能解决一半以上的数据乱跳问题。

6. 验证与进阶:用串口日志给小车装一个“黑匣子”

把代码烧进去、小车能跑起来只是第一步,真正做出稳定一圈不偏线的小车,靠的是可复盘的数据。我强烈建议你在工程里加一组事件日志,把每一次决策记录到内存缓冲区里,跑完一圈后用串口回放出来,对着赛道位置找出每次冲线的触发点。

日志结构可以用一个简单的环形缓冲实现:

typedef struct { uint32_t tick; // 系统节拍,单位 ms uint16_t track_l; // 左循迹 ADC 原始值 uint16_t track_r; // 右循迹 ADC 原始值 uint8_t obs_state; // 避障状态: 0 无,1 有 uint8_t decision; // 决策码: 0 直行 1 左转 2 右转 3 避障 } log_event_t; #define LOG_LEN 64 log_event_t log_buf[LOG_LEN]; uint8_t log_wr_idx = 0; void log_record(uint16_t tl, uint16_t tr, uint8_t obs, uint8_t dec) { log_event_t *e = &log_buf[log_wr_idx]; e->tick = get_tick_ms(); e->track_l = tl; e->track_r = tr; e->obs_state = obs; e->decision = dec; log_wr_idx = (log_wr_idx + 1) % LOG_LEN; // 环形写指针 }

环形缓冲不像串口实时打印那样会拖慢主循环,它是把数据先存到内存,跑完一圈再批量发出来。日志长度 64 条对一圈 20 秒的赛道足够,每 200 毫秒记一条,可以覆盖完整决策过程。decision字段留了扩展空间,你可以把状态机的每个分支都分配一个独立决策码,比如 4 表示刹车、5 表示回正,这样回放日志时一眼就能看出小车在哪个环节做了错误判断。

跑完一圈后,把日志内容按逗号分隔格式输出,然后复制到表格软件里,按照tick对齐就能画出传感器值和决策码的时间轴。我曾经遇到一个小车只在某个弯道冲线的问题,静态看代码和参数都合理,后来通过日志发现每次冲线前track_r在 200 毫秒内连续跳动了四次,说明右侧传感器在这个位置被地面反光干扰了,实际处理不是调占空比,而是给传感器加遮光罩。

这个验证流程还带出一个习惯:无论改阈值还是改占空比,都要留一组日志作为改参数前的基准。前后两组日志对比,就能量化参数变动带来的影响,而不是靠感觉说“好像稳了一点”。电压归一化的补偿代码也建议放进主循环,通过日志里的tick和电压采样值确认补偿是否生效。

从那以后,我每次调车都强制走一遍串口日志加赛道标记的流程,先看数据再改代码,而不是反复烧录新参数去碰运气。这个习惯帮我省下了大量盲调的时间,希望帮到你。

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

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

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

立即咨询