1. 红外寻迹传感器基础认知

红外寻迹传感器是智能车、轮式机器人和自动化搬运设备中最常用的传感器之一。它的核心任务是帮助机器人识别地面上的引导线,通常是黑色线条或白色线条,从而让机器人能够沿着预定轨迹自主行走。相比摄像头视觉方案,红外寻迹传感器具有成本低、响应速度快、电路简单、开发门槛低等优点,非常适合入门玩家和高校电子设计竞赛使用。

在学习驱动开发之前,需要先建立几个重要概念。第一,寻迹传感器检测的是“反射率差异”,而不是颜色本身。白色地面反射红外线能力强,黑色线条吸收红外线能力强,传感器正是利用这种反射差异来判断自身是否位于线上。第二,常见的红外寻迹传感器输出分为两类:数字量输出和模拟量输出。数字量输出只给出“检测到黑线”或“未检测到黑线”两种状态,模拟量输出则能给出连续的电压值,代表反射强度。第三,驱动红外寻迹传感器的本质,就是让主控芯片正确读取这些电信号,并把它们转换成软件能够理解的状态。

从工程实践角度看,红外寻迹传感器驱动不仅仅包含“读一个 GPIO 高低电平”这么简单。一个完整、可靠的驱动还需要考虑信号消抖、阈值标定、环境光干扰、多路传感器协同以及异常状态处理等问题。本文将从基本原理出发,逐步深入到 STM32、51 单片机、Arduino、树莓派等多个平台的驱动实现,并介绍循迹控制算法、标定方法和调试技巧。

本文面向的读者包括:正在准备电子设计竞赛的在校学生、需要快速搭建循迹小车的工程师,以及希望系统理解红外传感器驱动机制的嵌入式开发者。阅读本文前,建议具备 C 语言基础,了解 GPIO 输入输出、ADC 采集和单片机定时器中断等基本概念。如果对这些知识还不熟悉,也可以先通读一遍,再结合具体代码动手实践。

2. 红外寻迹传感器工作原理深度解析

红外寻迹传感器的物理基础是红外光在不同颜色表面上的反射差异。传感器内部通常包含一个红外发射管和一个红外接收管。发射管负责向外发射波长为 940nm 左右的红外光,接收管负责检测从地面反射回来的红外光强度。当红外光照射到白色或浅色表面时,大部分光被反射回来,接收管接收到较强的信号;当红外光照射到黑色表面时,大部分光被吸收,反射回来的信号明显减弱。

这里需要特别说明一个常见误区:很多人认为黑色线条是因为“颜色深”才被检测到,实际上核心机制是反射率的差异。一个表面反射率高,不代表它一定是纯白色;同样,黑色表面如果非常光滑,也可能产生镜面反射,导致接收管收到异常强的信号。因此在实际项目中,传感器安装高度、角度和地面材质都会显著影响检测效果,这正是驱动层需要提供阈值调节和软件滤波的重要原因。

接收管输出的是连续变化的模拟信号,其大小与反射光强度相关。对于大多数应用场景,我们只关心“在线”和“离线”两种状态,因此传感器模块内部通常集成一个电压比较器,将模拟信号与阈值电压比较后输出数字信号。以广泛使用的 LM393 双电压比较器为例,传感器模块板上会有一个电位器用于调节参考电压。当接收管输出电压高于参考电压时,比较器输出高电平;低于参考电压时输出低电平。设计者可以根据实际地面环境旋转电位器,设定合适的判断阈值。

除了数字量输出外,许多模块还保留了模拟量输出引脚。模拟量输出直接取自接收管的分压网络,能提供更丰富的信息。例如,在循迹过程中,模拟量不仅可以区分“黑”和“白”,还能反映传感器距离边界线的远近程度。利用这一特性,可以实现更平滑的循迹控制,而不只是简单的开关式控制。高级循迹算法往往会同时采集多路模拟量,计算出机器人偏离中心线的连续误差量,再交给 PID 控制器进行修正。

从信号时序上看,红外发射管通常连续工作,接收管实时输出。某些低功耗设计中,发射管会采用脉冲驱动方式,以降低平均功耗并提高抗干扰能力。脉冲驱动时,接收端需要与发射端同步解调,这通常由专用红外接收芯片完成。普通的寻迹传感器模块为了电路简单,一般使用直流驱动方式,让发射管持续发光。直流方式的缺点是容易受环境中其他红外光源干扰,例如阳光中的红外成分、白炽灯以及遥控器的红外信号。因此在室外或强光环境下,需要增加遮光罩或采用调制解调方案。

3. 硬件组成与典型电路分析

一个典型的红外寻迹传感器模块由以下部分组成:红外发射管、红外接收管、限流电阻、分压电阻、电压比较器和输出接口。下面分别说明各部分的作用。

红外发射管一般选择发射波长为 880nm 到 980nm 的 LED,常见型号有 IR333、5mm 红外发射管等。发射管需要串联一个限流电阻,以控制工作电流。假设供电电压为 5V,发射管正向压降约为 1.2V,希望工作电流为 20mA,则限流电阻应为 (5V - 1.2V) / 20mA = 190Ω,实际可选择 220Ω。电流越大,发射光越强,检测距离越远,但功耗也越高,且会加速 LED 老化。

接收管可以是光敏电阻、光电二极管或光电三极管。光电三极管由于内部具有放大作用,灵敏度更高,更适合近距离反射检测。常见型号有 3DU5C、PT334 等。接收管与固定电阻构成分压电路,反射光越强,接收管等效电阻越小,输出端电压越低;反之则电压越高。这个分压信号随后被送入比较器的同相输入端或反相输入端。

LM393 是双路差分比较器,内部有两个独立的比较器。在寻迹模块中,一路输入接参考电压,由电位器调节;另一路输入接接收管的分压信号。当信号电压超过参考电压时,比较器输出端通过上拉电阻输出高电平,否则输出低电平。LM393 是开漏输出结构,因此必须在输出引脚外接上拉电阻才能得到完整的高电平,否则输出引脚只能被拉低,无法主动输出高电平。这个细节在自行设计电路或排查故障时非常重要,很多“传感器一直输出低电平”的问题都是因为缺少上拉电阻引起的。

完整的典型电路连接如下:VCC 接 5V 电源,GND 接地;红外发射管通过限流电阻接 VCC 和 GND;接收管与分压电阻串联后,中间节点接入 LM393 的输入引脚;电位器一端接 VCC,另一端接 GND,中间抽头接 LM393 的另一个输入引脚;LM393 输出端通过 10kΩ 上拉电阻接 VCC,同时引出 DO 数字输出引脚;接收管分压节点同时引出 AO 模拟输出引脚。

对于多路传感器,例如四路或八路循迹模块,通常是把多个单路电路集成在一块 PCB 上,共享电源和地,独立输出各路的 DO 和 AO 信号。多路模块会在布局上把各路发射接收管并排排列,间距根据赛道线宽设计。一个重要的工程细节是:相邻传感器之间可能存在红外串扰,即一路发射管的红外光被另一路接收管接收到。为降低串扰,模块设计时会在传感器之间设置隔板或物理隔离槽,驱动软件也可以采用分时点亮发射管的方式进一步减少干扰。

4. 常见传感器模块与选型建议

市面上常见的红外寻迹传感器模块可以按照输出路数、输出类型和集成度进行分类。理解这些差异有助于在项目中做出合适的选型。

单路模块,如基于 TCRT5000 的反射式光电传感器模块,集成度较高,体积小,适合空间紧凑的场合。TCRT5000 内部封装了红外发射管和光电三极管,并带有可见光滤波功能,可以有效减少环境光影响。单路模块通常引出 VCC、GND、DO、AO 四个引脚。它的优点是成本低、使用灵活,缺点是搭建多路系统时需要自行组装和固定,机械一致性较难保证。

多路集成模块是循迹小车的主流选择。两路模块可以区分左右偏离,四路模块能提供更细的分辨率,五路模块在循迹小车中非常常见,八路模块则适合宽赛道或需要更强抗干扰能力的场景。以五路模块为例,五组传感器一字排开,中间一路作为中线参考,两侧各两路分别表示不同程度的偏离。五路模块通常还会集成电源指示灯和各路状态指示灯,方便现场观察与调试。

从输出类型看,数字量模块使用简单,只需读取 GPIO 电平即可。但数字量只能提供开关信息,循迹控制比较粗糙,容易出现小车在边界线上来回振荡的问题。模拟量模块能输出连续的反射强度信息,配合连续控制算法可以实现更平滑的循迹,但需要主控芯片具备足够的 ADC 通道。对于资源紧张的芯片,可以采用多路复用 ADC 或外接模拟开关来扩展通道。

选型时还需要关注供电电压范围。常见模块支持 3.3V 到 5V 供电,但输出高电平的幅值与供电电压相关。如果主控芯片是 3.3V 系统,而传感器是 5V 供电,输出高电平可能达到 5V,直接连接会损坏主控引脚。此时需要使用分压电阻或电平转换模块进行电压适配。5V 供电的传感器输出高电平通常接近 5V,但很多 3.3V 单片机引脚并不耐受 5V,这一点在实际接线时必须提前确认。

传感器安装高度同样影响选型。大多数寻迹模块的最佳检测距离在 5mm 到 15mm 之间。距离太近,发射光可能无法充分反射;距离太远,反射信号减弱,信噪比下降。安装时还应避免发射接收窗口被灰尘、油污遮挡。建议在结构设计上采用可调节支架,允许现场微调高度和角度,这样可以显著提升不同地面材质下的适应能力。

5. 驱动开发通用基础

无论使用哪种主控芯片,红外寻迹传感器驱动都可以抽象为几个通用步骤:初始化外设、读取传感器状态、处理信号噪声、输出结构化结果。下面先讨论这些通用环节,再在各平台章节中给出具体实现。

对于数字量传感器,驱动开发的第一步是配置 GPIO 输入模式。传感器输出接到主控芯片的普通 GPIO 引脚,配置为上拉输入或浮空输入。由于传感器模块的输出端通常已经带有上拉电阻,主控端使用浮空输入即可,但为了增强抗干扰能力,也常常在主控内部启用上拉。读取时直接调用 GPIO 读取函数即可获得高低电平。需要注意的是,不同平台对电平的定义可能不同,例如 Arduino 使用 HIGH 和 LOW,STM32 使用 GPIO_PIN_SET 和 GPIO_PIN_RESET,51 单片机直接判断引脚状态,代码移植时要注意对应关系。

数字量传感器最常见的噪声问题是边界抖动。当传感器恰好处于黑白交界处时,输出会在高低电平之间快速跳变。如果软件直接根据单次读取结果做判断,机器人会频繁改变运动方向,产生剧烈抖动。解决方法是软件消抖,常见手段包括多次读取取多数、延迟确认和状态保持。例如,连续读取 5 次,如果其中至少 3 次为低电平,才判定为检测到黑线,否则维持上一次状态。软件消抖牺牲了少量响应速度,但能显著提升稳定性。

对于模拟量传感器,需要使用 ADC 采集电压。驱动开发需要完成 ADC 外设初始化、采样通道配置和读数换算。ADC 采样值通常包含噪声,常见处理手段有滑动平均滤波、中值滤波和一阶低通滤波。滑动平均滤波实现简单,适合消除周期性噪声;中值滤波对偶发尖峰脉冲抑制效果好;一阶低通滤波计算量小,适合实时性要求高的场景。实际项目中也可以组合使用,例如先中值滤波去除尖峰,再做滑动平均平滑输出。

还有一个关键工程问题:传感器标定。同一型号的传感器在不同地面、不同安装高度下,输出的绝对电压可能差异很大。驱动层应当提供标定接口,让系统在运行前采集白地和黑线的基准值,再据此计算判断阈值。常见的阈值计算方法有固定阈值、中值阈值和自适应阈值。固定阈值实现最简单,但在环境变化时容易失效;中值阈值取白值和黑值的平均值;自适应阈值则根据历史数据动态调整,更适合长时间运行的机器人。

驱动层向外提供的接口应当尽量统一。建议设计如下接口:初始化函数负责 GPIO 和 ADC 配置;读取函数返回原始状态或电压值;滤波函数对原始数据进行处理;状态判断函数根据阈值输出“在黑线上”或“在白地上”的布尔结果。通过分层设计,上层循迹算法只需要关心状态判断结果,而不需要处理底层硬件细节,这大大降低了耦合度,也方便在不同平台之间移植。

6. STM32 驱动实现:初始化与数字量读取

STM32 是循迹小车项目中最常用的 32 位微控制器之一。本节以 STM32F103C8T6 和 STM32 HAL 库为例,介绍红外寻迹传感器的驱动实现。首先配置 GPIO 输入。假设一路数字量传感器接到 PB0 引脚,单路传感器接到 PB1 引脚,多路传感器依次接到更多引脚。

使用 STM32CubeMX 配置时,将对应引脚设置为 GPIO_Input 模式,无需上下拉或使能内部上拉,然后生成初始化代码。初始化函数由 CubeMX 自动生成,我们只需在 main 函数中读取引脚状态即可。下面给出最基本的读取示例。

/* main.c */
#include "main.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();

    while (1)
    {
        if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
        {
            /* 检测到黑线,执行左转或右转逻辑 */
        }
        else
        {
            /* 位于白地,保持前进 */
        }
        HAL_Delay(1);
    }
}

上面的代码虽然能够工作,但缺乏消抖处理。在传感器位于边界线附近时,单次读取结果可能不稳定。我们可以在驱动文件中封装一个读取函数,内部实现多次采样和多数表决。以下是一个实用的数字量读取驱动示例。

/* ir_sensor.c */
#include "ir_sensor.h"
#include "main.h"

/* 连续采样 5 次,若有至少 3 次为低电平则判定检测到黑线 */
uint8_t IR_ReadDigital(GPIO_TypeDef *port, uint16_t pin)
{
    uint8_t low_count = 0;
    for (uint8_t i = 0; i < 5; i++)
    {
        if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_RESET)
        {
            low_count++;
        }
        HAL_Delay(1);
    }
    return (low_count >= 3) ? 1U : 0U;
}

对应头文件声明如下。

/* ir_sensor.h */
#ifndef IR_SENSOR_H
#define IR_SENSOR_H

#include <stdint.h>
#include "stm32f1xx_hal.h"

uint8_t IR_ReadDigital(GPIO_TypeDef *port, uint16_t pin);

#endif /* IR_SENSOR_H */

对于必须在极短时间内响应的高速循迹场景,5 次采样会引入最多 5ms 的延迟。如果系统对延迟敏感,可以将采样间隔缩短到 100 微秒,或者改用定时器中断中周期采样,又或者借助硬件的施密特触发输入直接消除部分抖动。STM32 的 GPIO 本身不具备施密特触发配置,但外部可以增加小电容进行硬件滤波,通常在每个输入引脚对地并联 100nF 电容,就能有效滤除高频毛刺。

另一种更优雅的方案是使用定时器周期采样,在定时器中断服务函数中读取所有传感器引脚,并在中断里完成消抖和状态更新。这样做的好处是采样时刻精准,不会像 HAL_Delay 那样阻塞主循环。下面给出定时器中断驱动的示例框架。

/* 在 TIM3 溢出中断中周期采样(假设每 1ms 中断一次) */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3)
    {
        /* 更新传感器状态,并在驱动层维护消抖计数 */
        IR_SensorPoll();
    }
}

采用中断驱动后,上层控制逻辑只需要读取驱动模块维护的最新状态,而无需关心采样时序。这种把采集与处理分离的设计,是嵌入式驱动开发中非常重要的工程实践。它能够让主循环专注于控制算法和通信任务,避免因阻塞式延时导致系统响应变慢。

7. STM32 ADC 驱动:模拟量采集与滤波

当使用模拟量输出的红外寻迹传感器时,需要通过 ADC 采集各路电压。STM32F103 内置 12 位逐次逼近型 ADC,采样分辨率为 4096 级,完全满足循迹传感器的需求。以 PA0 作为第一路模拟输入为例,使用 CubeMX 将 PA0 配置为 ADC1_IN0,并使能 ADC 连续转换模式或由定时器触发转换。下面给出基本的 ADC 轮询读取函数。

/* adc.c */
#include "adc.h"
#include "main.h"

extern ADC_HandleTypeDef hadc1;

uint16_t ADC_ReadChannel(uint32_t channel)
{
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.Channel = channel;
    sConfig.Rank = ADC_REGULAR_RANK_1;
    sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5;
    HAL_ADC_ConfigChannel(&hadc1, &sConfig);

    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 100);
    return (uint16_t)HAL_ADC_GetValue(&hadc1);
}

单次 ADC 采样往往带有噪声,直接使用会导致循迹控制输出抖动。常见的做法是在驱动层实现滑动平均滤波。下面给出一个长度为 8 的滑动平均滤波器实现,每次调用将最新采样值加入缓冲区并计算平均值。

/* filter.c */
#include "filter.h"

#define FILTER_WINDOW 8

static uint16_t filter_buffer[FILTER_WINDOW] = {0};
static uint8_t filter_index = 0;

uint16_t Filter_MovingAverage(uint16_t sample)
{
    uint32_t sum = 0;
    filter_buffer[filter_index] = sample;
    filter_index = (filter_index + 1) % FILTER_WINDOW;

    for (uint8_t i = 0; i < FILTER_WINDOW; i++)
    {
        sum += filter_buffer[i];
    }
    return (uint16_t)(sum / FILTER_WINDOW);
}

滑动平均滤波会引入相位延迟,窗口越大延迟越明显。如果机器人速度较快,过大的滤波窗口会让控制响应变得迟钝。此时可以考虑使用一阶低通滤波(也称指数移动平均),它占用内存更小,响应更快。计算公式为:滤波值 = 上次滤波值 + 系数 × (本次采样值 - 上次滤波值)。系数通常取 0.1 到 0.3 之间。系数越小,滤波越强,响应越慢。下面给出实现代码。

/* 一阶低通滤波 */
float Filter_LowPass(float current, float previous, float alpha)
{
    return previous + alpha * (current - previous);
}

如果环境中存在偶发尖峰干扰,例如电机启动瞬间的电磁干扰,滑动平均滤波可能无法完全消除尖峰的影响。此时可以在滑动平均之前加入中值滤波。中值滤波的典型做法是连续采样 3 次,取中间值;或者采样 5 次,取第 3 大的值。中值滤波对孤立尖峰具有极强的抑制能力,但计算量略大。下面给出 3 点中值滤波的简易实现。

uint16_t Filter_Median3(uint16_t a, uint16_t b, uint16_t c)
{
    uint16_t tmp;
    if (a > b) { tmp = a; a = b; b = tmp; }
    if (b > c) { tmp = b; b = c; c = tmp; }
    if (a > b) { tmp = a; a = b; b = tmp; }
    return b;
}

实际工程中,建议将中值滤波和滑动平均组合使用:先做 3 点中值滤波剔除尖峰,再做 8 点滑动平均平滑输出。这样既能抗突刺,又能抑制随机噪声,是循迹传感器模拟量采集的可靠方案。

8. 51 单片机驱动实现

51 单片机虽然性能有限,但在教学和低成本项目中仍有广泛应用。STC89C52、STC15 系列以及增强型 STC8 系列都可以驱动红外寻迹传感器。数字量驱动非常简单,直接读取引脚即可。以 STC89C52 为例,假设传感器输出接 P2 口的 P2.0,读取代码如下。

/* main.c - 51 单片机数字量读取 */
#include <reg52.h>

sbit IR_PIN = P2 ^ 0;

void delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for (i = 0; i < ms; i++)
    {
        for (j = 0; j < 123; j++);
    }
}

void main(void)
{
    while (1)
    {
        if (IR_PIN == 0)
        {
            /* 检测到黑线 */
        }
        else
        {
            /* 位于白地 */
        }
        delay_ms(1);
    }
}

51 单片机的 IO 口为准双向口,读取输入前需要先向端口写 1,否则引脚被钳位在低电平,无法正确读取外部信号。上面的代码中,由于上电复位后 P2 口默认输出高电平,因此直接读取通常可以工作。但如果程序中之前对该引脚输出过低电平,再次读取前必须写 1。这是 51 单片机驱动开发中最容易被忽略的细节之一。更稳妥的做法是在初始化时明确将引脚置高。

void io_init(void)
{
    IR_PIN = 1; /* 准双向口读输入前先写 1 */
}

对于多路传感器,可以把所有传感器接到同一个端口的不同位,例如 P1 口的 P1.0 到 P1.7 接八路传感器。读取时一次性读入整个端口值,再按位解析。下面给出八路读取示例。

#include <reg52.h>

void main(void)
{
    unsigned char sensor_state;
    P1 = 0xFF; /* 读输入前先全部置高 */
    while (1)
    {
        sensor_state = P1; /* 一次性读取八路状态 */
        /* 按位判断各路状态,0 表示检测到黑线 */
        if ((sensor_state & 0x01) == 0)
        {
            /* 第 0 路检测到黑线 */
        }
        if ((sensor_state & 0x02) == 0)
        {
            /* 第 1 路检测到黑线 */
        }
    }
}

STC15 和 STC8 系列支持真正的推挽输出和 ADC 功能。如果使用模拟量传感器,可以利用片内 ADC 进行采集。以 STC8H 系列为例,内部集成 12 位 ADC,通过配置相应寄存器即可完成采样。由于不同型号寄存器和库函数存在差异,具体实现请参考对应芯片手册。对于只需要数字量的循迹项目,使用 51 单片机配合多路传感器模块已经足够完成基础巡线任务。

9. Arduino 驱动实现

Arduino 平台凭借丰富的库和简单的编程模型,成为快速验证红外循迹传感器方案的理想选择。Arduino Uno、Nano 和 Mega 都具备足够的数字 IO 和模拟输入通道。假设单路数字量传感器接在数字引脚 2,读取代码如下。

/* Arduino 单路数字量读取 */
const int irPin = 2;

void setup()
{
    pinMode(irPin, INPUT_PULLUP);
    Serial.begin(9600);
}

void loop()
{
    int state = digitalRead(irPin);
    if (state == LOW)
    {
        Serial.println("Black line detected");
    }
    else
    {
        Serial.println("On white ground");
    }
    delay(50);
}

这里使用 INPUT_PULLUP 模式启用了 Arduino 内部上拉电阻。对于开源极驱动模块,内部上拉可以增强信号稳定性。多路数字量传感器可以分别接在多个数字引脚上,用数组统一管理。下面给出三路数字传感器的读取示例,对应左、中、右三个传感器。

const int leftPin = 2;
const int centerPin = 3;
const int rightPin = 4;

void setup()
{
    pinMode(leftPin, INPUT_PULLUP);
    pinMode(centerPin, INPUT_PULLUP);
    pinMode(rightPin, INPUT_PULLUP);
    Serial.begin(9600);
}

void loop()
{
    int left = digitalRead(leftPin);
    int center = digitalRead(centerPin);
    int right = digitalRead(rightPin);

    Serial.print("L:");
    Serial.print(left);
    Serial.print(" C:");
    Serial.print(center);
    Serial.print(" R:");
    Serial.println(right);
    delay(20);
}

Arduino 的模拟输入通道可以直接读取传感器的模拟量。将传感器 AO 引脚接到 A0,使用 analogRead 函数即可获得 0 到 1023 之间的数值,对应 0V 到 5V 电压。下面给出模拟量采集与简单阈值判断的示例。

const int analogPin = A0;
const int threshold = 512; /* 阈值,需要根据现场标定 */

void setup()
{
    Serial.begin(9600);
}

void loop()
{
    int value = analogRead(analogPin);
    if (value < threshold)
    {
        Serial.println("Black line detected");
    }
    else
    {
        Serial.println("On white ground");
    }
    Serial.println(value);
    delay(20);
}

需要注意的是,analogRead 的默认采样精度是 10 位,对于循迹完全够用。如果想要更高的采样速度,可以直接操作 ADC 寄存器,或使用更高效的库。Arduino 还支持通过定时器产生精确的采样节拍,但多数循迹项目使用 main loop 加 delay 即可满足要求。如果机器人速度很快,建议减少 delay 时间,并配合软件消抖,避免控制指令抖动。

10. 树莓派与 Linux 平台驱动

树莓派等 Linux 单板计算机虽然不是实时控制的最佳选择,但在需要视觉处理、远程通信或复杂决策的机器人项目中,常常会用树莓派作为上位机,通过 GPIO 读取红外寻迹传感器。树莓派的 GPIO 电平为 3.3V,且引脚不耐受 5V,因此连接 5V 供电的传感器时必须使用分压电阻或电平转换模块。

在 Linux 下操作 GPIO 有多种方式,包括 sysfs、libgpiod 和 Python 的 RPi.GPIO 或 gpiozero 库。以 Python 的 RPi.GPIO 库为例,读取单路数字量传感器的代码如下。

import RPi.GPIO as GPIO
import time

IR_PIN = 17

GPIO.setmode(GPIO.BCM)
GPIO.setup(IR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

try:
    while True:
        state = GPIO.input(IR_PIN)
        if state == GPIO.LOW:
            print("Black line detected")
        else:
            print("On white ground")
        time.sleep(0.05)
except KeyboardInterrupt:
    pass
finally:
    GPIO.cleanup()

使用 gpiozero 库可以让代码更简洁。gpiozero 提供了面向对象的接口,并支持回调机制。下面给出使用 gpiozero 读取传感器的示例。

from gpiozero import DigitalInputDevice
from time import sleep

ir_sensor = DigitalInputDevice(17, pull_up=True)

while True:
    if ir_sensor.is_active:
        print("Black line detected")
    else:
        print("On white ground")
    sleep(0.05)

树莓派本身没有 ADC,如果传感器输出模拟量,需要外接 ADS1115、MCP3008 等模数转换芯片。以 ADS1115 为例,它是 16 位四通道 I2C 接口的 ADC,能够提供较高精度的电压采集。Python 可以使用 Adafruit 提供的库进行读取。下面给出基于 ADS1115 的模拟量采集示例。

import time
import board
import busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

i2c = busio.I2C(board.SCL, board.SDA)
ads = ADS.ADS1115(i2c)
channel = AnalogIn(ads, ADS.P0)

while True:
    print("Voltage: %.3f V" % channel.voltage)
    time.sleep(0.1)

需要强调的是,Linux 不是一个实时操作系统,进程调度会引入不可控的延迟。如果机器人速度较快,直接用 Python 读取传感器并控制电机会导致循迹精度不足。常见做法是在树莓派上使用实时内核,或者将循迹控制交给 STM32 等 MCU,树莓派只负责顶层任务,通过串口或 I2C 与底层控制器通信。这种上位机加下位机的分层架构在实际项目中非常普遍。

11. 多路传感器与循迹控制算法

多路红外寻迹传感器组合在一起,可以构成循迹控制的状态输入。常见的有两路、三路、四路、五路和八路方案。两路传感器分别位于小车左右两侧,通过比较两侧状态判断偏离方向;三路方案增加中间一路,用于检测是否位于线上;五路和八路方案能提供更细的偏离量化信息。

以三路传感器为例,左中右三路输出共有八种理论状态。实际循迹中常见状态如下:中间检测到黑线而左右未检测到,说明小车位于线上,应直行;左侧检测到黑线,说明小车向右偏离,应左转;右侧检测到黑线,说明小车向左偏离,应右转;中间和最左同时检测到黑线,说明轨道出现左弯或小车偏向左侧,应较大幅度左转。根据状态查表决定转向是初学者最容易理解的控制方式。下面给出三路状态机的伪代码框架。

typedef enum
{
    LINE_CENTER = 0,
    LINE_LEFT,
    LINE_RIGHT,
    LINE_HARD_LEFT,
    LINE_HARD_RIGHT,
    LINE_LOST
} line_state_t;

line_state_t Line_GetState(uint8_t left, uint8_t center, uint8_t right)
{
    if (center == 1 && left == 0 && right == 0) return LINE_CENTER;
    if (left == 1 && center == 0 && right == 0) return LINE_RIGHT;
    if (left == 0 && center == 0 && right == 1) return LINE_LEFT;
    if (left == 1 && center == 1 && right == 0) return LINE_HARD_RIGHT;
    if (left == 0 && center == 1 && right == 1) return LINE_HARD_LEFT;
    if (left == 1 && center == 1 && right == 1) return LINE_LOST;
    return LINE_CENTER;
}

状态机方案的优点是逻辑直观、调试方便,缺点是控制输出是离散的,容易造成小车左右摆动,尤其在高速行驶时。为解决这个问题,需要引入连续误差信号。五路或八路传感器可以按照传感器位置为每路分配权重,将各路状态加权求和得到连续的位置误差。例如五路传感器从左到右权重设为 -2、-1、0、+1、+2,误差等于当前检测到黑线的传感器权重之和。误差为 -2 表示严重偏左,+2 表示严重偏右,0 表示居中。

加权求和的具体实现如下:对于每路传感器,如果检测到黑线,则把该路权重累加到误差变量中。如果同时多路检测到黑线,说明轨道较宽或传感器间距较小,此时取多路权重的平均能够更准确地反映中心位置。实际项目中,还应考虑“全黑”和“全白”两种异常状态。全黑可能表示传感器全部进入黑线区域,全白可能表示传感器全部离开赛道。这两种情况需要根据上一时刻的状态进行方向保持或紧急停止。

12. PID 循迹控制进阶

当循迹机器人需要以较高速度稳定行驶时,单纯的状态机或比例控制往往不够。PID 控制器利用位置误差的比例、积分、微分三个分量计算转向修正量,是循迹控制中应用最广泛的算法之一。

位置式 PID 的数学表达式为:输出 = Kp × e + Ki × Σe + Kd × (e - e_prev)。其中 e 是当前误差,即传感器加权位置与目标位置的差值;Kp 为比例系数,决定响应快慢;Ki 为积分系数,用于消除稳态误差;Kd 为微分系数,能够预测误差变化趋势,抑制超调。在循迹中,通常只需要 PD 控制即可,因为循迹的核心是快速响应位置偏差,积分项容易产生累积导致振荡。下面给出一个位置式 PD 控制器的 C 语言实现。

typedef struct
{
    float kp;
    float kd;
    float last_error;
    float output;
} pd_controller_t;

float PD_Update(pd_controller_t *ctrl, float error)
{
    float derivative = error - ctrl->last_error;
    ctrl->output = ctrl->kp * error + ctrl->kd * derivative;
    ctrl->last_error = error;
    return ctrl->output;
}

PID 参数整定是循迹项目中最耗时的环节之一。一般流程是先整定 Kp:从小值开始逐步增大,直到机器人能够对偏差做出明显响应,出现轻微振荡时再略微回调。然后整定 Kd:从小值开始逐步增大,直到振荡被有效抑制。最后根据实际情况决定是否加入积分项。积分项只在需要消除固定偏移时使用,且需要设置积分限幅,防止积分饱和导致控制失效。

转向修正量通常映射为左右电机的差速值。以双轮差速小车为例,左轮速度 = 基准速度 - 转向修正量,右轮速度 = 基准速度 + 转向修正量。需要根据电机的 PWM 范围和转向方向对输出进行限幅,确保修正量不会超出物理可实现范围。此外,控制周期应保持稳定,建议在定时器中断中固定周期执行循迹控制,避免主循环执行时间变化导致控制不一致。

除了传统 PID,还可以使用模糊控制等智能算法。模糊控制不需要精确的数学模型,将误差和误差变化率模糊化后,通过规则库推理出转向输出。模糊控制在环境变化较大、难以精确标定的场景下具有一定优势,但设计和调试复杂度也更高。对于大多数循迹项目,调试良好的 PD 控制器已经能够满足需求。

13. 传感器标定、校准与自适应阈值

阈值设定是模拟量循迹系统的核心问题。如果阈值设置过高或过低,都会导致黑线误判或漏判。固定阈值在实验室环境表现良好,但一旦更换场地、光照变化或传感器老化,就可能失效。因此,一个健壮的驱动应该包含标定和自适应阈值机制。

标定的基本方法是:将传感器分别置于白色地面和黑色线上,采集多组模拟量数据,计算白值和黑值的平均值,阈值取两者的中点。下面给出标定函数示例,它采集 N 次白值和黑值并计算平均值。

void Sensor_Calibrate(uint16_t *white_value, uint16_t *black_value)
{
    uint32_t white_sum = 0;
    uint32_t black_sum = 0;
    const uint8_t samples = 50;

    /* 等待用户将传感器置于白色地面 */
    for (uint8_t i = 0; i < samples; i++)
    {
        white_sum += ADC_ReadChannel(ADC_CHANNEL_0);
        HAL_Delay(10);
    }
    *white_value = (uint16_t)(white_sum / samples);

    /* 等待用户将传感器置于黑色线上 */
    for (uint8_t i = 0; i < samples; i++)
    {
        black_sum += ADC_ReadChannel(ADC_CHANNEL_0);
        HAL_Delay(10);
    }
    *black_value = (uint16_t)(black_sum / samples);
}

标定完成后,阈值可以取白值和黑值的平均值。但实际使用中,传感器在循迹过程中读取到的值会随地面灰尘、环境光等因素缓慢漂移。自适应阈值机制能够动态跟踪这种漂移。一种简单有效的方法是维护一个长期滑动平均,记录最近一段时间内的最大值和最小值,根据这两者动态计算阈值。

uint16_t Adaptive_Threshold(uint16_t current)
{
    static uint16_t max_value = 2000;
    static uint16_t min_value = 1000;

    if (current > max_value) max_value = current;
    if (current < min_value) min_value = current;

    /* 缓慢向中间收缩,适应环境缓慢变化 */
    max_value = max_value - (max_value - current) / 32;
    min_value = min_value + (current - min_value) / 32;

    return (uint16_t)((max_value + min_value) / 2);
}

这种自适应阈值方法在路面材质均匀、黑白对比明显时效果很好。但如果机器人长时间只行驶在白色区域而没有经过黑线,min_value 和 max_value 会逐渐趋同,导致阈值失真。因此,更稳健的方案是在标定阶段确定基准阈值,运行阶段仅做小幅动态修正,并设置修正范围限制。同时,启动时进行一次完整标定,运行中定期微调,可以兼顾准确性和适应性。

14. 抗干扰与可靠性设计

红外寻迹传感器的可靠性受多种因素影响,包括环境光、电机电磁干扰、电源噪声和传感器串扰。要构建稳定的循迹系统,必须从硬件和软件两方面进行抗干扰设计。

环境光干扰是最大的挑战之一。太阳光和白炽灯都含有丰富的红外成分,会使接收管输出增高,甚至饱和,导致传感器无法区分黑白。硬件上可以给传感器增加遮光罩,减少环境光直接照射;选择带可见光滤波功能的接收管,如 TCRT5000;适当降低安装高度,让有用反射信号占据主导。软件上可以通过采样环境光基准并做差分计算来抑制缓变的环境光,但这种方法对快速变化的光照效果有限。

电机是循迹小车上的主要干扰源。电机启停和 PWM 调速瞬间会产生较大的电流波动和电磁辐射,通过电源线和空间耦合干扰传感器。应对措施包括:传感器电源与电机电源分离或加 LC 滤波;电机两端并联电容吸收火花;传感器信号线远离电机驱动线;在传感器电源引脚就近放置去耦电容。软件上,可以在电机切换状态后的短暂时间内暂停采样,避开干扰尖峰。

电源噪声是另一个常见问题。当电机加速或堵转时,电池电压会瞬间跌落,如果传感器供电不稳定,输出就可能错误翻转。解决方法是使用独立的稳压电源给传感器供电,例如 7805 或 AMS1117 稳压后单独供电,并在传感器模块的 VCC 与 GND 之间并联 10uF 和 100nF 电容。

多路传感器之间的串扰问题也不容忽视。相邻发射管的红外光可能被彼此接收管拾取,造成误判。物理上可以在传感器之间加装隔板,或拉开传感器间距。软件上可以采用分时选通发射管的方式,同一时刻只点亮一路发射管,采样完成后立即关闭,再点亮下一路。这种方式要求硬件支持独立控制发射管电源,对于集成模块不容易实现,因此在模块选型和 PCB 设计阶段就应考虑。

15. 低功耗与资源优化

在电池供电的循迹机器人中,功耗是一个必须关注的指标。红外发射管持续点亮会消耗可观电流。以单路 20mA 计算,八路传感器就是 160mA,对电池续航影响很大。降低功耗最直接的方法是降低发射管电流,代价是检测距离缩短。因此需要在检测距离和功耗之间权衡。

更有效的低功耗手段是脉冲驱动发射管。发射管以一定占空比间歇工作,例如每 10ms 点亮 1ms,平均电流降为持续点亮的十分之一,同时发射管在点亮瞬间可以通过较大电流获得更强的瞬时发光强度。接收端需要与发射同步采样,在发射管点亮的窗口内读取接收管电压。这种方式可以显著延长电池寿命,并且由于发射管脉冲发光,还有助于抗环境光干扰。下面给出脉冲驱动的软件框架示例。

void Sensor_PulsedRead(void)
{
    /* 点亮发射管 */
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
    delay_us(50); /* 等待光路稳定 */

    /* 采集接收管电压 */
    uint16_t sample = ADC_ReadChannel(ADC_CHANNEL_0);

    /* 关闭发射管 */
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);

    /* 后续滤波与状态判断 */
    (void)sample;
}

代码中的 delay_us 需要根据具体芯片实现微秒级延时,可以使用定时器或空循环。STM32 HAL 库中可以使用 DWT 计数器或简单的指令循环。需要注意的是,脉冲宽度和间隔需要与 ADC 采样时间匹配,确保在发射管点亮期间完成采样。

除了传感器功耗,主控芯片的资源优化也很重要。在定时器中断中执行滤波和控制时,应避免使用浮点运算,尽量使用定点整数运算,以减少中断服务程序的执行时间。以固定点数代替浮点数,把除法替换为移位操作,都能显著降低 CPU 占用。对于需要长时间待机的机器人,还可以让主控进入低功耗睡眠模式,由定时器或外部中断唤醒执行循迹,进一步延长续航。

16. 完整工程实战:智能小车循迹系统

本节以一个双轮差速循迹小车为例,整合前面各章的知识,给出一个可运行的完整工程框架。硬件配置为:STM32F103C8T6 主控、五路模拟量红外寻迹传感器、两路电机驱动模块(如 TB6612 或 L298N)、编码器用于速度反馈(可选)。软件采用定时器周期执行循迹控制,主循环负责状态机和非实时任务。

整个工程分为几个模块:传感器驱动模块 ir_sensor.c/h,负责 ADC 采集、滤波和位置误差计算;PD 控制器模块 pd_ctrl.c/h,负责根据误差计算转向修正量;电机驱动模块 motor.c/h,负责 PWM 输出和左右轮速控制;定时器服务模块提供稳定的控制周期。下面先给出传感器驱动模块的核心实现。

/* ir_sensor.c */
#include "ir_sensor.h"
#include "main.h"
#include "filter.h"

#define SENSOR_NUM 5
#define FILTER_WINDOW 8

/* 五路传感器对应的 ADC 通道 */
static uint32_t channel_list[SENSOR_NUM] =
{
    ADC_CHANNEL_0, ADC_CHANNEL_1, ADC_CHANNEL_2,
    ADC_CHANNEL_3, ADC_CHANNEL_4
};

/* 各路原始值和滤波值 */
static uint16_t raw_value[SENSOR_NUM] = {0};
static uint16_t filtered_value[SENSOR_NUM] = {0};

/* 标定的白值和黑值 */
static uint16_t white_value[SENSOR_NUM] = {0};
static uint16_t black_value[SENSOR_NUM] = {0};

void IR_SensorInit(void)
{
    /* ADC 初始化由 CubeMX 生成 */
}

void IR_SensorUpdate(void)
{
    for (uint8_t i = 0; i < SENSOR_NUM; i++)
    {
        raw_value[i] = ADC_ReadChannel(channel_list[i]);
        filtered_value[i] = Filter_MovingAverage(raw_value[i]);
    }
}

int8_t IR_GetPositionError(void)
{
    static const int8_t weight[SENSOR_NUM] = {-2, -1, 0, 1, 2};
    int8_t error = 0;
    uint8_t detected = 0;

    for (uint8_t i = 0; i < SENSOR_NUM; i++)
    {
        uint16_t threshold = (white_value[i] + black_value[i]) / 2;
        if (filtered_value[i] < threshold)
        {
            error += weight[i];
            detected++;
        }
    }
    if (detected == 0)
    {
        /* 全部在白地,保持上次方向,这里简单返回 0 */
        return 0;
    }
    return error;
}

位置误差通过加权求和得到,范围从 -2 到 +2。接下来实现 PD 控制器。为了简化,控制器内部使用整数运算。考虑到误差只有几个离散值,PD 控制器的参数可以通过实验整定。下面给出 PD 控制器的实现。

/* pd_ctrl.c */
#include "pd_ctrl.h"

#define KP 30
#define KD 18

static int8_t last_error = 0;

int16_t PD_Update(int8_t error)
{
    int16_t p_term = KP * error;
    int16_t d_term = KD * (error - last_error);
    last_error = error;
    return p_term + d_term;
}

电机驱动模块负责将 PD 输出映射为左右电机的 PWM。假设转向修正量正值为向右转,基准速度为 60(PWM 占空比百分比),则左轮速度 = 基准速度 + 修正量,右轮速度 = 基准速度 - 修正量。左右轮速度需要限幅在 0 到 100 之间。下面给出电机控制函数。

/* motor.c */
#include "motor.h"
#include "main.h"

#define PWM_MAX 100
#define PWM_MIN 0

void Motor_SetSpeed(int16_t left_speed, int16_t right_speed)
{
    /* 限幅 */
    if (left_speed > PWM_MAX) left_speed = PWM_MAX;
    if (left_speed < PWM_MIN) left_speed = PWM_MIN;
    if (right_speed > PWM_MAX) right_speed = PWM_MAX;
    if (right_speed < PWM_MIN) right_speed = PWM_MIN;

    /* 根据符号设置方向,输出 PWM */
    if (left_speed >= 0)
    {
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_SET);
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, left_speed);
    }
    else
    {
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_0, GPIO_PIN_RESET);
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, -left_speed);
    }

    if (right_speed >= 0)
    {
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_SET);
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, right_speed);
    }
    else
    {
        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_RESET);
        __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, -right_speed);
    }
}

主控制循环放在定时器中断中执行,保证周期稳定。下面给出定时器回调函数,其中 TIM3 每 5ms 触发一次,完成采样、误差计算、PD 控制和电机输出。

/* 主控制逻辑 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3)
    {
        int8_t error;
        int16_t steering;
        int16_t base_speed = 60;
        int16_t left_speed;
        int16_t right_speed;

        IR_SensorUpdate();
        error = IR_GetPositionError();
        steering = PD_Update(error);

        left_speed = base_speed + steering;
        right_speed = base_speed - steering;
        Motor_SetSpeed(left_speed, right_speed);
    }
}

这个框架结构清晰、易于扩展。标定功能可以在上电初始化阶段通过按键触发,采集白值和黑值后存入 flash;也可以每次开机先进行短时自动标定。工程中还应该加入看门狗、按键输入和串口调试输出,以便在实际调试中快速定位问题。

17. 常见问题排查与调试方法

红外循迹系统在开发过程中会遇到各种问题,掌握系统的排查方法能大幅缩短调试时间。下面列举常见问题及其解决思路。

问题一:传感器输出始终为高或始终为低。首先检查供电是否正常,用万用表测量 VCC 与 GND 之间电压是否为 5V 或 3.3V。然后检查输出引脚是否有上拉电阻,尤其是 LM393 开漏输出必须接上拉。再用手遮挡传感器,观察输出是否变化,如果不变化,可能是接收管或比较器损坏。还可以用手机摄像头观察发射管是否发光,因为手机摄像头能感知红外光,发射管正常工作时在手机画面中应显示为一个亮点。

问题二:传感器能检测到黑线,但在边界处输出频繁抖动。这是典型的阈值问题或噪声问题。先调节电位器,使黑线和白地的输出区分更明显;再在软件中加入消抖,或缩短传感器与地面的距离。如果抖动仍然严重,检查附近是否有强干扰源,例如电机引线是否与信号线平行捆绑,必要时加屏蔽或重新走线。

问题三:小车循迹时左右摆动严重。可能是比例增益过大或微分增益不足。先减小 KP,观察摆动是否减弱;再适当增大 KD 抑制超调。也可能是传感器采样周期过长,导致控制滞后,可以缩短控制周期。还要检查传感器安装是否左右对称,机械不对称会引入固定的偏置误差。

问题四:小车在弯道处容易冲出赛道。可能是传感器检测范围太窄,车速过快,或微分增益不足。可以提高传感器路数,或降低进入弯道的速度,或增大微分项提前响应误差变化。在结构上,把传感器组前移也能提供更早的转弯预判。

调试工具方面,串口打印是最基础的手段。将各路传感器状态或 ADC 值周期性发送到上位机,用串口助手观察数值变化,可以直观了解传感器行为。更进一步,可以使用带显示功能的调试环境,例如在 OLED 屏上实时显示误差和传感器状态。对于高速控制,示波器或逻辑分析仪可以观察传感器输出波形和控制输出波形,帮助定位时序问题。做好记录,将不同参数下的表现整理成表格,能够更快收敛到合适的参数组。

18. 总结与展望

本文系统梳理了红外寻迹传感器驱动开发的完整知识体系。从红外反射原理、典型硬件电路和模块选型开始,详细介绍了 STM32、51 单片机、Arduino 和树莓派等平台下的驱动实现,并深入讨论了数字量与模拟量采集、滤波、标定、自适应阈值、PID 控制、抗干扰和低功耗设计等关键工程问题。最后通过一个完整的双轮循迹小车工程,把各个模块串联起来,展示了驱动层、控制层和执行层的协同工作方式。

红外寻迹传感器虽然原理简单,但要做到稳定可靠的循迹,需要综合考虑硬件、软件和控制算法的多方面因素。尤其要重视传感器标定、信号滤波和抗干扰设计,这些细节往往决定了系统能否从实验室演示走向实际运行。

随着视觉传感器成本下降,摄像头循迹逐渐流行,但红外寻迹传感器在低成本、低算力和高实时性要求下仍然具有不可替代的优势。将红外寻迹传感器与编码器、IMU 融合,可以实现更精确的定位和控制。未来,随着边缘 AI 的普及,红外传感器阵列也可以与轻量级神经网络结合,实现更复杂的路径识别与决策。理解底层驱动原理,始终是构建这些高级系统的基础。希望本文能够帮助读者扎实掌握红外寻迹传感器驱动开发,并将这些方法应用到自己的项目中。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐