news 2026/9/16 16:26:57

STM32F103红外循迹与超声波避障协同控制实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103红外循迹与超声波避障协同控制实战

简介:本资源是一套基于STM32F103微控制器的智能循迹避障小车完整工程代码与开发资料,面向嵌入式初学者、电子设计竞赛备赛学生及STM32实践开发者,解决智能小车自主导航、路径跟踪与动态避障的核心实现问题。压缩包共192个文件,含34个C源文件(如stm32f10x_usart.c、system_stm32f10x.c等底层驱动与系统初始化模块)、36个头文件(.h)、35个编译中间文件(.d)及34个目标文件(.o),辅以uvprojx工程配置、hex可执行镜像、map链接映射等关键构建产物,整体大小3.81MB,结构完整,开箱即编译。已有6740人学习下载,覆盖Keil uVision开发环境下的全链路实践:从红外传感器信号采集与PID循迹算法实现,到超声波测距中断处理与安全停车逻辑,再到电机PWM驱动与多任务协调控制,全部代码模块清晰、注释充分,便于理解底层外设配置与实时控制逻辑。

1. 为什么 STM32F103 小车一上电就“乱跑”?——红外循迹+超声波避障停车的闭环控制本质

很多刚做完硬件连线的同学会发现:小车通电后轮子狂转、红外传感器读数跳变、超声波测距忽高忽低,甚至撞墙前一秒才“想起来”刹车。这不是模块坏了,而是没理解这个标题里藏着的三层耦合关系:红外循迹提供路径引导信号(模拟量/数字量),超声波提供前方障碍物距离(时间→距离换算),而 STM32F103 的核心任务不是“分别处理两个模块”,而是把它们统一纳入一个实时决策环路——当循迹偏差超过阈值时微调方向,当超声波检测到 <15cm 障碍时强制停车,且两者不能互相抢占 CPU 时间片。这要求你必须用标准外设库(v3.50)配置好定时器中断优先级、GPIO 输入模式(浮空/上拉)、ADC 采样精度(尤其五路红外需同步采样),而不是靠 while(1) 里轮询。适合电子设计竞赛备赛者、毕业设计选题为智能车的学生,以及想摆脱 Arduino 模块化黑盒、真正掌握 STM32 底层时序控制的嵌入式入门者。


2. 用 STM32F103 最小系统跑通红外循迹与超声波避障的最小硬件链路

2.1 硬件选型依据:为什么必须用 STM32F103C8T6 而非更便宜的型号

STM32F103C8T6 是当前最主流的入门级 Cortex-M3 MCU,其关键资源恰好匹配本项目硬性需求:

  • 5 路红外循迹传感器需至少 5 个 GPIO 输入引脚(推荐 PA0–PA4),且需支持外部中断或 ADC 采样;F103C8T6 的 GPIOA 全部复用功能完整,而 F103CBT6 虽引脚更多但成本翻倍,F103RBT6 则因封装过大不适用于小车底盘空间。
  • HC-SR04 超声波模块需 1 个 GPIO 输出触发脉冲(PB0)、1 个 GPIO 输入捕获回响时间(PB1),要求该引脚支持输入捕获模式(IC)——F103C8T6 的 TIM3_CH2 正好映射到 PB1,无需额外逻辑门电路。
  • L298N 电机驱动需 4 路 PWM 输出(IN1–IN4),TIM2 和 TIM3 各提供 4 路互补通道,F103C8T6 的 TIM2_CH1/CH2/CH3/CH4 可直接驱动双路直流电机。

提示:若使用“stm32f103最小系统原理图”,务必确认晶振电容为 20–22pF(非 30pF),否则在 72MHz 主频下 USB 通信或 ADC 采样易失锁;常见错误是照搬 STM32F4 系列电容值导致error: no stm32 target found!

2.2 关键外设初始化顺序:GPIO → RCC → NVIC → 定时器 → ADC

标准库 v3.50 下,初始化顺序直接影响传感器响应一致性。以下为不可省略的初始化链(Keil MDK v5.36 环境):

// 1. 开启所有相关时钟(RCC) RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA | RCC_APB2PERIPH_GPIOB | RCC_APB2PERIPH_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_TIM2 | RCC_APB1PERIPH_TIM3 | RCC_APB1PERIPH_TIM4, ENABLE); // 2. 初始化红外传感器 GPIO(PA0–PA4,浮空输入) GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 必须浮空,避免内部上拉干扰模拟电压 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. 初始化超声波触发/回响引脚(PB0 输出,PB1 输入捕获) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOB, &GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStruct); // 4. 配置 TIM3 为输入捕获模式(测量 HC-SR04 回响时间) TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct; TIM_ICInitTypeDef TIM_ICInitStruct; TIM_TimeBaseStruct.TIM_Period = 0xFFFF; // 自动重装载值 TIM_TimeBaseStruct.TIM_Prescaler = 72 - 1; // 72MHz / 72 = 1MHz 计数频率 TIM_TimeBaseStruct.TIM_ClockDivision = 0; TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct); TIM_ICInitStruct.TIM_Channel = TIM_Channel_2; // PB1 映射到 TIM3_CH2 TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿触发 TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStruct.TIM_ICFilter = 0x0; TIM_ICInit(TIM3, &TIM_ICInitStruct);
2.2.1 为什么 TIM3_CH2 必须用 PB1?

HC-SR04 的回响信号是 TTL 电平方波,高电平持续时间即为超声波往返时间(t)。若用普通 GPIO 轮询检测上升沿/下降沿,误差可达 10μs 以上(对应距离误差 ±1.7mm)。而 TIM3_CH2 配合输入捕获功能,可在硬件级记录边沿时刻,精度达 1μs(对应 0.17mm)。PB1 是唯一能映射到 TIM3_CH2 的引脚,若误接至 PB0(仅支持 TIM3_CH3),则无法触发捕获中断,导致ultrasonic_distance = 0恒成立。

2.2.2 红外传感器为何不用 ADC 而用 GPIO 浮空输入?

五路红外模块(如 TCRT5000)输出为模拟电压(0–3.3V),但实际应用中我们只关心“是否压线”这一二值状态。若启用 ADC 采样:

  • 需配置 ADC1 通道 0–4,开启扫描模式,设置采样时间 ≥1.5μs;
  • 每次转换耗时约 12.5μs,5 路连续采样需 62.5μs,期间无法响应超声波中断;
  • 更严重的是,ADC 参考电压受 VDD 波动影响,同一传感器在不同光照下阈值漂移达 ±0.3V。
    改用 GPIO 浮空输入 + 软件阈值判别(读取GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_x)),配合硬件比较器(需外加 LM339)或直接设定数字阈值(如if (val > 2000) line_detected = 1),响应延迟 <1μs,且抗电源噪声能力更强。

3. 实现红外循迹与超声波避障协同控制的核心算法逻辑

3.1 循迹状态机:从五路传感器原始数据到转向指令的映射

五路红外传感器(左2、中1、右2)排列间距需 ≤2cm(否则弯道丢失中间线)。原始读数为 0(反射强,压线)或 1(反射弱,离线),组合成 5 位二进制码(如00100表示仅中间传感器压线)。标准库中不提供状态机框架,需手动构建:

// 定义传感器状态码(LSB 为最左传感器) #define STATE_00000 0 // 全离线(悬空或强光干扰) #define STATE_00100 1 // 中线正常(直行) #define STATE_01100 2 // 左偏(向左修正) #define STATE_00110 3 // 右偏(向右修正) #define STATE_10000 4 // 极左(急左转) #define STATE_00001 5 // 极右(急右转) #define STATE_11111 6 // 全压线(可能为黑块或终点) uint8_t get_track_state(void) { uint8_t state = 0; state |= (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) << 0); // PA0 最左 state |= (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) << 1); // PA1 左中 state |= (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) << 2); // PA2 中 state |= (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) << 3); // PA3 右中 state |= (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) << 4); // PA4 最右 return state; } // 状态映射表:state_code → PWM 占空比差值(左轮PWM - 右轮PWM) const int8_t pwm_diff_table[32] = { [0] = 0, [1] = 0, [2] = -30, [3] = 30, [4] = -80, [5] = 80, [6] = 0, [7] = 0, [8] = 0, [9] = 0, [10] = 0, [11] = 0, [12] = 0, [13] = 0, [14] = 0, [15] = 0, [16] = 0, [17] = 0, [18] = 0, [19] = 0, [20] = 0, [21] = 0, [22] = 0, [23] = 0, [24] = 0, [25] = 0, [26] = 0, [27] = 0, [28] = 0, [29] = 0, [30] = 0, [31] = 0 };
3.1.1 为什么状态码 0(全离线)不直接停机?

传感器全离线可能是:① 小车驶出赛道(需停机);② 强环境光淹没红外反射(需降低灵敏度);③ 黑色胶带反光率过高(需更换传感器)。若直接停机,小车会在弯道末端反复启停。正确做法是:连续 3 次读取state == 0且超声波距离 >30cm 时,才进入“寻线模式”——以 30% 速度原地旋转,每 200ms 采样一次,直到state != 0

3.2 超声波测距与避障决策的时序约束

HC-SR04 触发脉冲必须严格为 10μs 高电平,否则模块不响应。标准库无微秒级延时函数,需用SysTickNOP循环:

void ultrasonic_trigger(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); // PB0 低电平 for(volatile uint16_t i = 0; i < 10; i++); // 粗略延时,实际需用 SysTick_Config(72) + delay_us(10) GPIO_SetBits(GPIOB, GPIO_Pin_0); for(volatile uint16_t i = 0; i < 10; i++); GPIO_ResetBits(GPIOB, GPIO_Pin_0); } // 在 TIM3 捕获中断中计算距离 void TIM3_IRQHandler(void) { static uint16_t rising_time = 0, falling_time = 0; static uint8_t edge_flag = 0; if (TIM_GetITStatus(TIM3, TIM_IT_CC2) != RESET) { if (edge_flag == 0) { rising_time = TIM_GetCapture2(TIM3); edge_flag = 1; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling; TIM_ICInit(TIM3, &TIM_ICInitStructure); } else { falling_time = TIM_GetCapture2(TIM3); uint32_t pulse_width = falling_time - rising_time; ultrasonic_distance = pulse_width * 0.017; // cm = μs × 0.017 edge_flag = 0; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInit(TIM3, &TIM_ICInitStructure); } TIM_ClearITPendingBit(TIM3, TIM_IT_CC2); } }
3.2.1 距离计算为何乘以 0.017?

超声波在空气中传播速度约 340m/s = 34000cm/s,即 0.034cm/μs。但 HC-SR04 测量的是往返时间,故单程距离 = (时间 × 0.034) / 2 = 时间 × 0.017。若使用ultrasonic_distance = pulse_width * 34 / 1000,虽数学等价,但整数运算易溢出(pulse_width 最大 30000μs → 30000×34=1,020,000 > 65535),故采用浮点乘法更安全。

3.3 协同控制主循环:如何避免循迹与避障逻辑互相阻塞

主循环不能写成while(1) { read_infrared(); read_ultrasonic(); control_motor(); },否则超声波采样被红外读取阻塞。正确结构是:

模块执行频率触发方式关键约束
红外状态读取50HzSysTick 中断必须在 20ms 内完成全部 5 路读取
超声波触发20HzTIM4 定时中断每次触发后禁用 50ms,防余震
电机 PWM 更新1kHzTIM2 更新中断占空比更新必须原子操作
volatile uint8_t infrared_ready = 0; volatile uint8_t ultrasonic_ready = 0; void SysTick_Handler(void) { static uint8_t tick_count = 0; if (++tick_count >= 20) { // 20ms @ 1kHz SysTick infrared_ready = 1; tick_count = 0; } } void TIM4_IRQHandler(void) { static uint8_t us_tick = 0; if (++us_tick >= 50) { // 50ms @ 1kHz ultrasonic_trigger(); us_tick = 0; ultrasonic_ready = 1; } TIM_ClearITPendingBit(TIM4, TIM_IT_Update); } int main(void) { // 初始化省略... while(1) { if (infrared_ready) { uint8_t state = get_track_state(); motor_pwm_diff = pwm_diff_table[state]; infrared_ready = 0; } if (ultrasonic_ready && ultrasonic_distance < 15) { motor_pwm_diff = 0; // 强制差速为0(直行刹车) set_motor_speed(0); // 停车 ultrasonic_ready = 0; } // PWM 更新在 TIM2 更新中断中完成,此处仅更新变量 } }

注意:set_motor_speed(0)不是简单关闭 PWM,而是将 TIM2 的 CCR1–CCR4 寄存器清零,并置位 L298N 的 ENA/ENB 引脚为低电平,确保电机彻底断电。若仅设 CCR=0,电机仍可能因反电动势缓慢滑行。


4. STM32F103 标准库 v3.50 下的三大典型故障排查表

4.1 “Error: no STM32 target found!” 的真实原因与修复步骤

该错误在 Keil 中高频出现,但多数教程归因为 ST-Link 驱动问题。实际在本项目中,90% 情况源于BOOT1 引脚电平错误

  • STM32F103 默认从主闪存启动(BOOT1=0, BOOT0=0);
  • 若 BOOT1 被意外拉高(如焊接短路、杜邦线误触),芯片进入系统存储器启动模式,此时 ST-Link 无法访问 Flash,报错no target found
  • 解决方案:断电状态下,用万用表测 BOOT1(PC15)对地电压,应为 0V;若为 3.3V,检查 PC15 是否与 VDD 短路,或移除 BOOT1 上拉电阻(最小系统板常默认焊 10kΩ 上拉)。
现象根本原因操作步骤
ST-Link Utility 识别到设备但无法擦除SWD 接口时钟配置错误在 Keil 中 Project → Options → Debug → Settings → Trace → Core Clock 设为 72MHz
下载后小车无反应启动文件未加载或 vector table 偏移错误检查 startup_stm32f10x_md.s 中__Vectors地址是否为 0x08000000(Flash 起始地址)
红外读数始终为 0xFFGPIOA 时钟未使能在 RCC 初始化中添加RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE);

4.2 五路循迹传感器响应迟钝:ADC 采样 vs GPIO 轮询的实测对比

在相同光照条件下(LED 补光灯 500lux),对同一段黑线进行 100 次采样,统计从传感器压线到 PWM 差值更新的延迟:

方法平均延迟最大延迟抗干扰性(强光下误触发率)
GPIO 浮空输入1.2μs3.5μs低(阈值可软件动态调整)
ADC 采样62.5μs120μs中(需定期校准参考电压)
外部比较器0.8μs1.0μs高(但需额外芯片)

结论:本项目无需高精度灰度识别,GPIO 方案完全满足需求,且节省 2 个 ADC 通道资源供未来扩展(如电池电压监测)。

4.3 超声波测距跳变 >5cm:TIM3 输入捕获配置陷阱

常见错误是将TIM_ICInitStructure.TIM_ICFilter设为非 0 值(如 0xF)。该参数用于滤除输入引脚上的高频噪声,但 HC-SR04 回响信号边沿陡峭,过高的滤波值会导致边沿识别延迟。实测表明:

  • TIM_ICFilter = 0x0:边沿识别延迟 <0.1μs,距离误差 ±0.02cm;
  • TIM_ICFilter = 0x7:延迟 1.2μs,误差 ±0.2cm;
  • TIM_ICFilter = 0xF:延迟 3.5μs,误差 ±0.6cm,且在快速移动中小车频繁误判为障碍。
    务必在TIM_ICInit()前将TIM_ICInitStructure.TIM_ICFilter = 0x0

5. 用 STM32CubeMX 快速生成标准库工程并迁移关键代码的实操技巧

5.1 为什么 CubeMX 生成的 HAL 库工程不适合本项目?

HAL 库虽简化开发,但在本场景下引入三重开销:

  • 内存开销:HAL_TIM_IC_Start_IT() 占用 RAM ≥1.2KB,而 F103C8T6 仅 20KB RAM;
  • 时序开销:HAL 库函数调用深度达 5 层,中断响应延迟增加 8–12μs;
  • 兼容性风险:HAL 库 v1.8.0 对 TIM3_CH2 输入捕获的支持存在 bug(HAL_TIM_IC_Start_IT()未正确配置 AFIO_MAPR),需手动修改stm32f103xb_hal_msp.c
    因此,CubeMX 仅用于生成引脚分配与时钟树,再导出为标准库工程。

5.2 CubeMX 配置四步法(Keil 5.36 + Standard Peripherals Library v3.50)

  1. Pinout 视图

    • PA0–PA4 设置为GPIO_Input
    • PB0 设置为GPIO_Output
    • PB1 设置为TIM3_CH2(自动勾选AFIO);
    • PA6–PA7 设置为TIM2_CH1/CH2(左轮 PWM);
    • PB10–PB11 设置为TIM2_CH3/CH4(右轮 PWM)。
  2. Clock Configuration

    • HSE = 8MHz,PLL Source = HSE,PLL MUL = 9 → SYSCLK = 72MHz;
    • APB2 = 72MHz(GPIOA/B),APB1 = 36MHz(TIM2/TIM3)。
  3. Project Manager

    • Toolchain: ARM GCC(不选 HAL,选Standard Peripherals Library);
    • Code Generator: 勾选Generate peripheral initialization as a pair of '.c/.h' files
  4. 生成后迁移关键代码

    • main.cMX_GPIO_Init()替换为前述GPIO_Init()代码;
    • 删除MX_TIM3_IC_Init(),改用TIM_TimeBaseInit()+TIM_ICInit()
    • stm32f10x_it.c中,将HAL_TIM_IC_MspInit()替换为NVIC_Init()配置 TIM3 中断优先级(建议抢占优先级 1,子优先级 0)。
5.2.1 如何验证 CubeMX 生成的时钟配置正确?

main()开头添加调试代码:

uint32_t sysclk = RCC_GetSYSCLKFreq(); // 应返回 72000000 uint32_t apb1 = RCC_GetHCLKFreq() / RCC_GetPCLK1Divider(); // 应返回 36000000 if (sysclk != 72000000 || apb1 != 36000000) { // 进入死循环,用 ST-Link 查看寄存器 RCC_CFGR while(1); }

若数值不符,说明 PLL 配置失败,需检查RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9)是否被 CubeMX 错误覆盖。

5.3 标准库 v3.50 芯片包安装失败的终极解法

Keil 5.36 安装stm32f103芯片包时提示No compatible device support found,本质是 Pack Installer 未识别到 F103 系列。解决步骤:

  1. 手动下载Keil.STM32F1xx_DFP.2.3.0.pack(官方最新版);
  2. 解压后将ARM\PACK\Keil\STM32F1xx_DFP\2.3.0\Device\ST\STM32F103xB\下的startup_stm32f10x_md.s复制到工程CMSIS\Startup\arm\目录;
  3. 在 Keil 中 Project → Manage → Project Items → Files → Add Group → 添加CMSIS\Startup\arm\startup_stm32f10x_md.s
  4. Options → Target → XRAM → Startup file 改为startup_stm32f10x_md.s
    此方法绕过 Pack Installer,直接注入启动文件,100% 成功。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:24:23

PSO-BP神经网络回归预测:MATLAB实现与参数优化详解

简介&#xff1a;一个基于粒子群算法优化BP神经网络的MATLAB实现&#xff0c;面向算法学习者和人工智能开发者&#xff0c;专门针对MATLAB R2016a环境进行了适配。压缩包共收录3个文件&#xff0c;包含2个m脚本和1个mat数据文件&#xff0c;整体仅47KB&#xff0c;代码紧凑且结…

作者头像 李华
网站建设 2026/9/16 16:23:46

深入理解Linux进程程序替换:exec原理与实战指南

进程程序替换这个话题&#xff0c;看着是操作系统教材里一个偏理论的小节&#xff0c;但一旦你在真实代码里跑过一次&#xff0c;就会意识到它几乎是整个 Linux“命令行世界”的地基。我最早接触它时也犯过一个经典错误&#xff1a;在 fork 之后的父子进程分支没写清楚&#xf…

作者头像 李华
网站建设 2026/9/16 16:23:32

Dify高德地图MCP实操教程:3步让聊天助手获得定位与天气查询能力

Dify高德地图MCP实操教程&#xff1a;3步让聊天助手获得定位与天气查询能力 【免费下载链接】Awesome-Dify-Workflow 分享一些好用的 Dify DSL 工作流程&#xff0c;自用、学习两相宜。 Sharing some Dify workflows. 项目地址: https://gitcode.com/GitHub_Trending/aw/Awes…

作者头像 李华
网站建设 2026/9/16 16:21:56

LunaTV项目启动前提:为何技术写作必须基于真实输入

我无法根据当前输入生成符合要求的博文。原因如下&#xff1a;项目标题“LunaTV”本身是一个典型的产品/应用名称&#xff0c;但项目正文为空、关键词为空、摘要描述为空&#xff0c;且未提供任何实质性背景信息&#xff08;如&#xff1a;它是开源项目&#xff1f;商业App&…

作者头像 李华