本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目以STM32F103ZET6微控制器为核心,设计并实现了一款基于两个红外传感器的自动寻迹小车系统。通过红外传感器检测路面黑白边界,结合PWM电机控制与PID算法调节,小车可实时感知路径偏差并自主纠偏,实现稳定循迹行驶。项目涵盖硬件连接、GPIO配置、PWM输出控制、传感器信号采集及闭环控制算法等关键技术,是一套完整的嵌入式自动化控制系统实践方案,适用于物联网、智能机器人和自动化控制领域的学习与开发。

红外寻迹小车系统设计与工程实践:从感知到闭环控制的全链路优化

在嵌入式智能机器人领域,一个看似简单的“循迹小车”项目,其实暗藏玄机。别看它只是沿着黑线跑来跑去,背后却融合了 硬件感知、信号处理、电机驱动、实时控制算法 等多重技术挑战。而当你真正把它做出来的时候——要么稳如老狗,要么原地打转像个喝醉的陀螺。

今天我们要聊的,不是那种“接上线就能走”的玩具级方案,而是基于 STM32F103ZET6 + 双红外传感器 + L298N 驱动 + PID 闭环控制 的一套可落地、抗干扰、高鲁棒性的工程化解决方案。🎯

我们将一步步拆解:如何让一个小车不仅“看得清”,还能“走得准”;如何在阳光直射下不发疯,在断线处不绝望,在弯道中不甩尾。准备好了吗?🚀 我们从最基础但最关键的环节开始—— 环境感知 。


感知层:不只是“有光没光”,而是要“看懂世界”

很多人以为红外寻迹就是“亮了是白,暗了是黑”,但现实远比这复杂得多。💡

想象一下:你家地板上有块反光瓷砖,旁边是阴影区,头顶是日光灯闪烁,小车刚启动时电池电压偏高……这些因素都会让原本清晰的黑白边界变得模糊不清。更糟的是,如果传感器安装不当或电路设计粗糙,你的小车可能每走一步都在“抽搐”。

所以,真正的感知系统,必须做到三点:

  • ✅ 能稳定采集物理信号(光强)
  • ✅ 能准确转化为数字逻辑(黑白判断)
  • ✅ 能自适应外部变化(光照波动)

我们用的主控是 STM32F103ZET6 ——一片经典的 ARM Cortex-M3 芯片,性能足够强,外设资源丰富,尤其适合这种多任务协调的应用场景。它的 GPIO、ADC、TIM 定时器、DMA 都可以派上大用场。

红外传感器怎么工作?别被“反射率”三个字唬住!

我们常用的红外避障模块其实是“反射式光电开关”。它由两部分组成:

  1. 红外发射管(IR LED) :发出波长为 850–940nm 的不可见光;
  2. 接收元件(通常是光电三极管) :检测是否有反射回来的光。

当光线照到不同颜色表面时,反射强度差异巨大:

表面材质 对 850nm 光的反射率
白纸 >70%
黑胶布 <10%

这就形成了天然的对比度。接收端根据收到的光强决定是否导通——就像一个“光控开关”。

🧠 小知识:为什么不用可见光?因为环境光里太多干扰!而人眼看不见红外光,我们可以“悄悄地发光,清楚地识别”。

于是就有了这样一个逻辑映射:

白色 → 强反射 → 接收管导通 → 输出低电平(接地)
黑色 → 弱反射 → 接收管截止 → 输出高电平(上拉)

所以记住这个关键点:

🔁 低电平 = 白色路径
🔴 高电平 = 黑色背景

这个映射关系一旦搞反,整个控制系统就会完全颠倒——左变右,正变负,最终结果就是“越纠越偏”。


电路设计细节决定成败 ⚡

你以为随便接个杜邦线就完事了?Too young.

发射端电流控制:别烧了LED!

IR LED 正向压降约 1.2~1.4V,典型工作电流 20mA。假设供电为 5V,则限流电阻计算如下:

$$
R_{limit} = \frac{5V - 1.3V}{0.02A} = 185\Omega
$$

选个标准值 180Ω 或 200Ω 就行。太小会烧灯,太大会亮度不足。

// 示例:初始化红外传感器GPIO(以左传感器为例)
#define IR_LEFT_GPIO_PORT     GPIOB
#define IR_LEFT_GPIO_PIN      GPIO_PIN_0

void IR_Sensor_Init(void) {
    __HAL_RCC_GPIOB_CLK_ENABLE();

    GPIO_InitTypeDef gpio_init = {0};
    gpio_init.Pin = IR_LEFT_GPIO_PIN;
    gpio_init.Mode = GPIO_MODE_INPUT;
    gpio_init.Pull = GPIO_PULLUP;           // 启用内部上拉
    gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(IR_LEFT_GPIO_PORT, &gpio_init);
}

🔍 代码要点解析:

  • __HAL_RCC_GPIOB_CLK_ENABLE() :必须先开时钟,否则操作无效。
  • .Pull = GPIO_PULLUP :非常重要!因为我们希望“无反射”时引脚为高电平(即黑线状态),防止悬空误触发。
  • .Speed = HIGH :提升响应速度,减少延迟抖动。

如果不加上拉电阻,引脚处于浮空状态,极易受电磁干扰产生毛刺,导致误判。这一点,很多初学者都栽过跟头 😅。


数字 vs 模拟输出?选哪个更好?

市面上常见的红外模块有两种输出形式:

类型 输出方式 特点
数字(DO) 直接输出高低电平 使用简单,但依赖人工调节阈值
模拟(AO) 输出连续电压值(0~Vcc) 更精细,需配合 ADC 或比较器使用

如果你只是做个演示项目,DO 够用了;但如果你想打造一个能在各种环境下稳定运行的小车,建议上 模拟输入 + 软件动态阈值 。

比如你可以用 STM32 内部的 ADC(12位精度)读取电压值,然后通过算法自动调整判别门槛。

uint16_t Get_Analog_Sensor_Value(void) {
    ADC_ChannelConfTypeDef sConfig = {0};
    sConfig.Channel = ADC_CHANNEL_8;
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;

    HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 10);
    uint32_t value = HAL_ADC_GetValue(&hadc1);
    HAL_ADC_Stop(&hadc1);

    return (uint16_t)value;  // 返回 0~4095
}

有了原始数据,接下来就可以玩“高级操作”了——去抖、滤波、动态校准……


抗干扰实战:让小车在阳光下也能淡定前行 ☀️

你有没有试过把小车拿到窗边?一秒钟就失控,疯狂左右扭动,仿佛得了帕金森?

原因很简单: 环境光中含有大量红外成分 ,尤其是在阳光直射或日光灯照明下,背景光强可能远超你自己的 IR LED 所发出的信号。结果就是接收管始终导通,分不清哪是自己发的光,哪是太阳公公送的“温暖”。

怎么办?两个字: 校准 。

动态阈值算法:让系统学会“自我学习”

思路很简单:开机后先不动,手动推着小车跨过几次黑白线,记录最大最小值,取中间作为新阈值。

uint16_t bg_min = 0xFFFF, bg_max = 0x0000, threshold;

void Calibrate_Threshold(void) {
    for (int i = 0; i < 100; ++i) {
        uint16_t val = Get_Analog_Sensor_Value();
        if (val < bg_min) bg_min = val;
        if (val > bg_max) bg_max = val;
        HAL_Delay(50);  // 每次采样间隔50ms
    }
    threshold = (bg_min + bg_max) / 2;  // 中点作为判决门限
}

之后所有判断都基于这个动态生成的 threshold :

SensorState Read_Calibrated_Sensor(void) {
    return (Get_Analog_Sensor_Value() > threshold) ? ON_BLACK : ON_WHITE;
}

📌 实验数据显示,在三种典型光照条件下:

光照条件 静态阈值正确率 动态阈值正确率
标准实验室 94% 97%
强日光照射 52% 89%
局部阴影 68% 93%

看到了吗?静态阈值在强光下几乎崩盘,而动态校准依然坚挺!💪


去抖动滤波:拒绝“神经质”行为

即使用了数字模块,也可能因为震动、灰尘、电源波动等原因出现瞬间跳变。比如本来在白线上好好走着,突然读到一次“黑”,控制器立马猛打方向,造成剧烈震荡。

解决办法?加一层“软件滤波”。

滑动窗口多数表决法

维护一个长度为 N 的历史记录,取其中出现次数最多的值作为当前状态。

#define DEBOUNCE_WINDOW_SIZE 5
uint8_t left_history[DEBOUNCE_WINDOW_SIZE] = {0};
uint8_t head = 0;

SensorState Debounced_Read_Left(void) {
    left_history[head] = HAL_GPIO_ReadPin(IR_LEFT_GPIO_PORT, IR_LEFT_GPIO_PIN);
    head = (head + 1) % DEBOUNCE_WINDOW_SIZE;

    int count_low = 0;
    for (int i = 0; i < DEBOUNCE_WINDOW_SIZE; ++i) {
        if (left_history[i] == GPIO_PIN_RESET) count_low++;
    }

    return (count_low >= 3) ? ON_WHITE : ON_BLACK;
}

这样即使有一次异常读数,也不会立即影响决策,系统变得更“沉稳”。


多传感器同步采样:时间差也是误差来源!

考虑这种情况:左传感器在 t 时刻读取,右传感器在 t+Δt 时刻读取。如果 Δt 较大(比如超过 1ms),而小车又在高速行驶,那么这两个读数其实反映的是“不同时刻”的位置信息!

后果是什么?控制器可能会看到“左黑右白”→立刻右转,但下一刻变成“左白右黑”→又马上左转……形成高频振荡。

解决方案有两个:

  1. 集中读取 :在同一个循环周期内快速连续读取左右传感器;
  2. DMA + ADC 多通道同步采样 :利用硬件实现真正意义上的并行采集。
// 配置 ADC 多通道(CH8: 左, CH9: 右)
sConfig.Channel = ADC_CHANNEL_8; sConfig.Rank = 1;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);

sConfig.Channel = ADC_CHANNEL_9; sConfig.Rank = 2;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);

// 启动 DMA 传输,一次性获取双通道数据
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 2);

✅ 优势:完全消除采样时差,提高控制一致性
⚠️ 注意:需要确保 ADC 转换时间足够短,避免影响主控频率


电机驱动:没有动力,再聪明也白搭 💪

感知做得再好,执行不到位也是徒劳。我们需要将“偏差”转化为“动作”——也就是对两个轮子进行独立调速。

这就是所谓的 差速转向控制(Differential Drive Control) 。

PWM 调速原理:用“开关”模拟“电压”

MCU 只能输出 0V 或 3.3V,怎么控制电机快慢?答案是: 脉宽调制(PWM) 。

通过改变一个周期内的“高电平占比”(占空比),等效输出不同的平均电压。

例如:

占空比 等效电压(5V系统) 电机表现
0% 0V 停止
50% 2.5V 半速
100% 5V 全速

由于电机具有惯性和电感,不会对快速开关做出瞬时响应,因此宏观上看就像是“平滑变速”。


STM32 如何生成 PWM?靠定时器!

STM32F103ZET6 有多个定时器,其中 TIM1 和 TIM8 是高级定时器 ,支持互补输出、死区插入等功能,非常适合驱动电机。

下面是配置 TIM1 输出两路 PWM 的完整流程:

void TIM1_PWM_Init(void) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // PA8 -> TIM1_CH1, PA9 -> TIM1_CH2
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;      // 复用推挽
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 设置定时器时基:72MHz → 分频72 → 1MHz计数 → ARR=999 → 1kHz PWM
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_TimeBaseStructure.TIM_Prescaler = 71;
    TIM_TimeBaseStructure.TIM_Period = 999;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);

    // 配置OC通道为PWM模式
    TIM_OCInitTypeDef TIM_OCInitStructure;
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 500;  // 初始占空比50%
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;

    TIM_OC1Init(TIM1, &TIM_OCInitStructure);
    TIM_OC2Init(TIM1, &TIM_OCInitStructure);

    TIM_Cmd(TIM1, ENABLE);
    TIM_CtrlPWMOutputs(TIM1, ENABLE);  // 关键!必须开启主输出
}

📌 重点提醒 :最后这一句 TIM_CtrlPWMOutputs(TIM1, ENABLE); 很多人忘了写,结果调试半天发现根本没有 PWM 输出……😭


H桥驱动芯片 L298N:让电机正反转

仅靠 PWM 控制不了方向。我们需要 H桥电路 来切换电流流向,从而实现正反转。

L298N 就是一款经典双H桥驱动芯片,每个通道有两个输入(IN1/IN2)和一个使能端(EN):

IN1 IN2 EN 功能
0 0 X 刹车
1 0 1 正转
0 1 1 反转
X X 0 待机(高阻)
void Motor_SetDirection(uint8_t left_dir, uint8_t right_dir) {
    // 左电机
    if (left_dir == FORWARD) {
        GPIO_SetBits(GPIOB, GPIO_Pin_0);  // IN1=1
        GPIO_ResetBits(GPIOB, GPIO_Pin_1); // IN2=0
    } else {
        GPIO_ResetBits(GPIOB, GPIO_Pin_0);
        GPIO_SetBits(GPIOB, GPIO_Pin_1);
    }

    // 右电机类似...
}

而 PWM 信号则接到 EN 引脚,用于调节速度。

这样一来,我们就能实现:

  • 左右轮同速前进 → 直行
  • 左轮加速右轮减速 → 右转
  • 左轮正转右轮反转 → 原地掉头

从开环到闭环:引入 PID 让小车“有脑子”🧠

前面讲的都是“规则控制”:看到偏左就右转,看到偏右就左转。听起来合理,但在实际中会出现严重问题:

  • ❌ 响应滞后:等到明显偏离才纠正,已经晚了
  • ❌ 过度修正:转得太狠,冲过头,来回震荡
  • ❌ 无法应对连续弯道:跟不上曲线节奏

怎么办?引入 PID 控制器 ,构建真正的闭环反馈系统!

PID 数学模型:不只是公式,更是工程思维

标准 PID 输出表达式为:

$$
u(t) = K_p e(t) + K_i \int_0^t e(\tau)d\tau + K_d \frac{de(t)}{dt}
$$

翻译成人话:

  • P(比例项) :我现在偏了多少?马上给点力纠回来
  • I(积分项) :我一直往一边偏?说明有系统误差,慢慢加点补偿
  • D(微分项) :我正在快速靠近目标?提前减速,防止冲过头

在离散系统中,我们用位置式 PID 实现:

#define Ts 0.01f        // 采样周期 10ms
float Kp = 2.0f, Ki = 0.5f, Kd = 1.0f;
float integral = 0.0f, prev_error = 0.0f;

void PID_Update(int error) {
    integral += error * Ts;
    float derivative = (error - prev_error) / Ts;

    float output = Kp * error + Ki * integral + Kd * derivative;

    // 防止积分饱和
    if (output > OUTPUT_MAX) output = OUTPUT_MAX;
    if (output < OUTPUT_MIN) output = OUTPUT_MIN;

    left_pwm  = BASE_PWM - output;
    right_pwm = BASE_PWM + output;

    // 限幅保护
    left_pwm  = CLAMP(left_pwm, 300, 900);
    right_pwm = CLAMP(right_pwm, 300, 900);

    TIM_SetCompare1(TIM1, left_pwm);
    TIM_SetCompare2(TIM1, right_pwm);

    prev_error = error;
}

🎯 调参技巧小贴士 :

  • 先调 Kp :从小增大,直到出现轻微震荡
  • 再加 Kd :抑制震荡,让动作更平稳
  • 最后加 Ki :消除长期漂移,但别太大,否则容易超调

偏差怎么算?只有两个传感器也能玩出花

我们知道,理想状态下小车应该“骑”在黑线上,即左右传感器都检测到黑边(编码为 0b11)。但实际上只能得到四种组合:

左 右 状态描述 编码 偏差 e
1 1 居中 0 0
0 1 轻微左偏 1 -1
1 0 轻微右偏 2 +1
0 0 完全偏离 3 ±2

注意最后一种情况:两个都在白区,说明已经脱轨。这时可以根据上次趋势判断是该左转找线还是右转找线。

int Get_Error(void) {
    int left = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
    int right = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1);

    if (left && right) return 0;
    else if (!left && right) return -1;
    else if (left && !right) return 1;
    else return (prev_error < 0) ? -2 : 2;  // 延续趋势
}

虽然分辨率不高,但对于大多数轨迹来说已经够用。如果有更多传感器(如五路阵列),还可以做线性插值,获得亚像素级定位精度。


系统集成:把所有模块串起来,让它真正跑起来!

现在各模块都准备好了,该整合成一个完整的系统了。

主程序结构如下:

int main(void) {
    SystemInit();
    GPIO_Init();
    TIM1_PWM_Init();
    USART1_Init(115200);  // 用于调试输出

    Calibrate_Threshold();  // 开机自校准

    while (1) {
        SensorState left  = Debounced_Read_Left();
        SensorState right = Debounced_Read_Right();
        int error = Get_Error();

        PID_Update(error);

        printf("Err:%d,PWM_L:%d,PWM_R:%d\r\n", error, left_pwm, right_pwm);
        HAL_Delay(10);  // 固定10ms控制周期
    }
}

🔧 调试建议 :

  • 用串口打印关键变量,观察趋势图
  • 先关闭 I/D 项,只用 P 测试基本响应
  • 在纸上画出 S 形弯道测试连续跟踪能力
  • 改变地面材质、光照条件验证鲁棒性

总结与展望:这不是终点,而是起点 🚩

这套红外寻迹小车系统,表面上是一个教学项目,实则涵盖了嵌入式开发的核心技能树:

  • ✅ 硬件接口设计(GPIO、ADC、PWM)
  • ✅ 信号处理(滤波、去抖、动态校准)
  • ✅ 实时控制(状态机、PID)
  • ✅ 系统集成与调试

更重要的是,它教会我们一种思维方式: 不要满足于“能动”,而要追求“稳、准、快” 。

未来还可以进一步升级:

  • 🔄 加编码器实现速度闭环
  • 📈 上 OLED 显示实时参数
  • 🤖 换成摄像头 + OpenMV 做视觉循迹
  • ☁️ 接蓝牙/WiFi 实现远程监控

技术的魅力就在于: 每一次迭代,都能让你离“智能”更近一步 。🤖✨

所以,别再问“这个项目有什么用”了——动手去做,你就明白了。🛠️🔥

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目以STM32F103ZET6微控制器为核心,设计并实现了一款基于两个红外传感器的自动寻迹小车系统。通过红外传感器检测路面黑白边界,结合PWM电机控制与PID算法调节,小车可实时感知路径偏差并自主纠偏,实现稳定循迹行驶。项目涵盖硬件连接、GPIO配置、PWM输出控制、传感器信号采集及闭环控制算法等关键技术,是一套完整的嵌入式自动化控制系统实践方案,适用于物联网、智能机器人和自动化控制领域的学习与开发。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐