基于双红外传感器的STM32寻迹小车控制系统程序设计与实现
简介:本项目以STM32F103ZET6微控制器为核心,设计并实现了一款基于两个红外传感器的自动寻迹小车系统。通过红外传感器检测路面黑白边界,结合PWM电机控制与PID算法调节,小车可实时感知路径偏差并自主纠偏,实现稳定循迹行驶。项目涵盖硬件连接、GPIO配置、PWM输出控制、传感器信号采集及闭环控制算法等关键技术,是一套完整的嵌入式自动化控制系统实践方案,适用于物联网、智能机器人和自动化控制领域的学习与开发。
红外寻迹小车系统设计与工程实践:从感知到闭环控制的全链路优化
在嵌入式智能机器人领域,一个看似简单的“循迹小车”项目,其实暗藏玄机。别看它只是沿着黑线跑来跑去,背后却融合了 硬件感知、信号处理、电机驱动、实时控制算法 等多重技术挑战。而当你真正把它做出来的时候——要么稳如老狗,要么原地打转像个喝醉的陀螺。
今天我们要聊的,不是那种“接上线就能走”的玩具级方案,而是基于 STM32F103ZET6 + 双红外传感器 + L298N 驱动 + PID 闭环控制 的一套可落地、抗干扰、高鲁棒性的工程化解决方案。🎯
我们将一步步拆解:如何让一个小车不仅“看得清”,还能“走得准”;如何在阳光直射下不发疯,在断线处不绝望,在弯道中不甩尾。准备好了吗?🚀 我们从最基础但最关键的环节开始—— 环境感知 。
感知层:不只是“有光没光”,而是要“看懂世界”
很多人以为红外寻迹就是“亮了是白,暗了是黑”,但现实远比这复杂得多。💡
想象一下:你家地板上有块反光瓷砖,旁边是阴影区,头顶是日光灯闪烁,小车刚启动时电池电压偏高……这些因素都会让原本清晰的黑白边界变得模糊不清。更糟的是,如果传感器安装不当或电路设计粗糙,你的小车可能每走一步都在“抽搐”。
所以,真正的感知系统,必须做到三点:
- ✅ 能稳定采集物理信号(光强)
- ✅ 能准确转化为数字逻辑(黑白判断)
- ✅ 能自适应外部变化(光照波动)
我们用的主控是 STM32F103ZET6 ——一片经典的 ARM Cortex-M3 芯片,性能足够强,外设资源丰富,尤其适合这种多任务协调的应用场景。它的 GPIO、ADC、TIM 定时器、DMA 都可以派上大用场。
红外传感器怎么工作?别被“反射率”三个字唬住!
我们常用的红外避障模块其实是“反射式光电开关”。它由两部分组成:
- 红外发射管(IR LED) :发出波长为 850–940nm 的不可见光;
- 接收元件(通常是光电三极管) :检测是否有反射回来的光。
当光线照到不同颜色表面时,反射强度差异巨大:
| 表面材质 | 对 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),而小车又在高速行驶,那么这两个读数其实反映的是“不同时刻”的位置信息!
后果是什么?控制器可能会看到“左黑右白”→立刻右转,但下一刻变成“左白右黑”→又马上左转……形成高频振荡。
解决方案有两个:
- 集中读取 :在同一个循环周期内快速连续读取左右传感器;
- 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 实现远程监控
技术的魅力就在于: 每一次迭代,都能让你离“智能”更近一步 。🤖✨
所以,别再问“这个项目有什么用”了——动手去做,你就明白了。🛠️🔥
简介:本项目以STM32F103ZET6微控制器为核心,设计并实现了一款基于两个红外传感器的自动寻迹小车系统。通过红外传感器检测路面黑白边界,结合PWM电机控制与PID算法调节,小车可实时感知路径偏差并自主纠偏,实现稳定循迹行驶。项目涵盖硬件连接、GPIO配置、PWM输出控制、传感器信号采集及闭环控制算法等关键技术,是一套完整的嵌入式自动化控制系统实践方案,适用于物联网、智能机器人和自动化控制领域的学习与开发。
更多推荐



所有评论(0)