基于STM32的智能循迹避障灭火小车:从硬件选型到状态机调度 简介这份资源面向学习STM32嵌入式开发的学生与电子爱好者提供一套完整的智能小车综合实践方案涵盖遥控、循迹、避障与灭火四类功能。项目以STM32为核心处理器串联红外循迹传感器、超声波测距、无线遥控模块与电机驱动并涉及PID控制、避障决策与电源管理等知识点适合作为课程设计、毕业设计或竞赛练手参考。压缩包共197个文件约6.9MB以c源文件与h头文件构成主要代码框架配合s启动文件、crf编译中间文件、o与d目标文件以及uvprojx、uvoptx工程配置和map映射文件完整保留了Keil工程结构便于直接打开编译与二次修改。目前已有449人学习下载。读者可从中获得从硬件模块协同到软件算法实现的整体思路对照工程文件理解外设配置、中断处理与调试流程快速搭建自己的智能小车原型并排查常见问题。1. 从一台“能自己找火、自己绕路”的小车说起去年帮一个做实训的朋友看代码他桌上摆着一台四轮小车STM32 最小系统板绑在亚克力底盘上前面一排红外探头顶上一个小风扇侧面还挂了个火焰传感器。他跟我说这玩意儿能循迹、能避障、还能自己找火苗吹灭。我当时第一反应是“这不就是个玩具”结果他把电源一开小车沿着黑线跑得稳稳的遇到矿泉水瓶自动拐弯看到打火机火苗就停下来对着吹——那一刻我承认这套基于 STM32 单片机的智能遥控循迹避障灭火小车是很多嵌入式新手从“点灯”跨到“做系统”最合适的一块跳板。它适合谁适合刚学完 GPIO、定时器、PWM想找一个能把多传感器、电机控制、遥控通信串起来的完整项目练手的人也适合带学生做课程设计、毕业设计的开发者因为它的模块边界清晰改一个功能不会牵一发动全身。整车的核心逻辑不复杂红外对管看黑线超声波加舵机扫障碍火焰传感器找火源L298N 驱动电机蓝牙或红外遥控接管人工操作。下面我按“这项目怎么搭、怎么跑、怎么改”的顺序把源码里真正值得抄的部分拆开讲。2. 整车架构与硬件选型为什么是 STM32 而不是一块 Arduino2.1 主控与模块的职责划分这套小车的典型架构是“一主多从”的传感器布局STM32F103C8T6 作为主控负责所有实时决策电机驱动、传感器、遥控接收各自独立主控只做采集和判断。为什么不用 Arduino因为循迹和避障对时序有要求STM32 的定时器资源多PWM 输出稳定超声波测距用输入捕获能拿到微秒级回波宽度这些都是 Arduino 的pulseIn给不了的确定性。整车的信号流大致是这样红外循迹模块输出数字电平到 GPIO超声波模块的 Trig 由定时器触发、Echo 接输入捕获火焰传感器输出模拟量进 ADC遥控信号走 USART 或外部中断最后主控通过 TIM 的 PWM 通道控制 L298N 的两路使能GPIO 控制四个方向引脚。这个划分的好处是每个模块都能单独调试出问题容易定位。模块型号/类型接口方式主控资源占用主控STM32F103C8T6——电机驱动L298N 双 H 桥PWM GPIOTIM2 CH1/CH2循迹四路红外对管GPIO 输入PB0-PB3避障HC-SR04 超声波定时器输入捕获TIM3 CH1灭火火焰传感器 小风扇ADC GPIOADC1_IN0遥控蓝牙串口 / 红外接收USART1 / EXTIPA9/PA102.2 电源与电机驱动的坑点很多人第一次做这车最容易翻车的地方不是代码是供电。STM32 要 3.3VL298N 的逻辑电平是 5V电机又要 7.4V 到 12V三套电压混在一起共地没做好就会出现“代码没问题但小车乱跑”。常见做法是电池经降压模块给主控和传感器供电电机电源单独走一路所有模块的 GND 必须接到同一点。L298N 的使能引脚记得接 PWM不要直接拉高否则小车只有全速和停两种状态循迹会抖得像筛糠。// 电机初始化TIM2 双通道 PWM频率 10kHz void Motor_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // PA0/PA1 作为 TIM2_CH1/CH2 的 PWM 输出 gpio.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); tim.TIM_Period 7199; // 72MHz / (71991) 10kHz tim.TIM_Prescaler 0; tim.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, tim); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; // 初始占空比 0 oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, oc); TIM_OC2Init(TIM2, oc); TIM_Cmd(TIM2, ENABLE); }这段代码的关键参数是TIM_Period和TIM_Prescaler。72MHz 主频下预分频设为 0、周期设为 7199得到的 PWM 频率是 10kHz这个频率下电机不会有明显啸叫L298N 也扛得住。TIM_Pulse就是占空比后面调速时改这个值即可范围 0 到 7199。注意两个通道要分别用TIM_SetCompare1和TIM_SetCompare2设置别搞混左右轮。3. 核心功能拆解循迹、避障、灭火三条链路怎么落地3.1 四路红外循迹的加权判断循迹的本质是“根据黑线偏离程度决定左右轮差速”。四路红外对管从左到右排开黑线压到哪个探头就说明车头偏向哪边。最朴素的做法是 if-else 堆叠但那样过弯会一顿一顿的。我一般用加权法给四个探头分配权重 -3、-1、1、3检测到黑线就累加权重最后根据总偏差调整左右轮 PWM。// 四路循迹返回偏差值负偏左正偏右 int Track_GetError(void) { int error 0; // 假设 PB0-PB3 对应从左到右四个探头低电平表示压黑线 if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) 0) error -3; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) 0) error -1; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2) 0) error 1; if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_3) 0) error 3; return error; // 全没压线返回 0视为直行 } // 根据偏差调整差速base 为基准速度 void Track_Follow(int base) { int error Track_GetError(); int left base - error * 80; // 80 是比例系数可调 int right base error * 80; left left 0 ? 0 : (left 7199 ? 7199 : left); right right 0 ? 0 : (right 7199 ? 7199 : right); Motor_SetSpeed(left, right); }这里的比例系数 80 是血泪经验值太小过弯迟钝太大直接画龙。实际调试时先把 base 设低一点比如 3000让车慢速跑观察过弯姿态再往上加。error为 0 时左右轮同速直行为负时左轮减速右轮加速车头自然向左修正。这套逻辑比 if-else 平滑得多而且改探头数量时只需要调权重。3.2 超声波避障的测距与决策超声波避障分两步先测距再决策。测距用 HC-SR04Trig 给一个 10 微秒以上的高脉冲Echo 返回高电平的持续时间就是声波往返时间除以 2 再乘声速340m/s得到距离。STM32 上用输入捕获最准因为delay轮询会被其他中断打断。// TIM3_CH1 输入捕获测超声波回波宽度 volatile uint16_t capture_val 0; volatile uint8_t capture_flag 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { if (capture_flag 0) { capture_val TIM_GetCapture1(TIM3); // 上升沿记录起点 TIM_SetCounter(TIM3, 0); capture_flag 1; } else { capture_val TIM_GetCapture1(TIM3); // 下降沿得到宽度 capture_flag 2; } TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); } } // 返回距离单位 cm超时返回 999 uint16_t Ultrasonic_GetDistance(void) { GPIO_SetBits(GPIOA, GPIO_Pin_5); // Trig 拉高 delay_us(15); GPIO_ResetBits(GPIOA, GPIO_Pin_5); // Trig 拉低 capture_flag 0; uint32_t timeout 0; while (capture_flag ! 2) { if (timeout 100000) return 999; // 约 50ms 超时 } // 定时器 1MHz 计数宽度单位 us距离 时间 * 0.017 return (uint16_t)(capture_val * 0.017f); }capture_val的单位是微秒乘 0.017 就是厘米340m/s ÷ 2 ÷ 10000。超时返回 999 是为了让上层逻辑知道“这次没测到”避免把无效值当近距离处理导致小车原地打转。避障决策我一般设两级阈值小于 20cm 停车并后退20 到 40cm 之间转向大于 40cm 正常行驶。舵机扫障碍时左右各扫 45 度哪边距离大往哪边转。3.3 火焰检测与灭火动作火焰传感器本质是一个红外接收管对火焰里的特定波长敏感输出模拟电压火越近电压越高。接 ADC 后设一个阈值超过就判定发现火源。灭火动作分两步先停车对准再开风扇。风扇用一个小 MOS 管或者继电器控制GPIO 拉高即可。// 火焰检测ADC1_IN0 采样返回 1 表示发现火源 uint8_t Flame_Detect(void) { ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); uint16_t val ADC_GetConversionValue(ADC1); // 阈值 2000 是实测值火苗 30cm 内基本能触发 return (val 2000) ? 1 : 0; } void Fire_Extinguish(void) { Motor_SetSpeed(0, 0); // 停车 GPIO_SetBits(GPIOC, GPIO_Pin_13); // 开风扇 delay_ms(3000); // 吹 3 秒 GPIO_ResetBits(GPIOC, GPIO_Pin_13); }阈值 2000 不是拍脑袋定的是在室内日光灯下测本底、再拿打火机在 30cm 处测峰值取中间值。换环境要重新标定这一步别省。风扇吹 3 秒是经验值火苗小的话 2 秒就灭大了要 5 秒可以做成可配置参数。4. 遥控接管与主循环调度状态机才是这车的骨架4.1 蓝牙/红外遥控的指令解析遥控模块的作用是让人工接管。蓝牙走 USART1收到一个字节就切换模式红外用外部中断解码 NEC 协议。两种方式最终都归一到同一个模式变量上主循环根据模式决定跑哪套逻辑。// 模式定义 typedef enum { MODE_TRACK 0, // 循迹 MODE_AVOID, // 避障 MODE_FIRE, // 灭火 MODE_MANUAL // 遥控 } CarMode; volatile CarMode g_mode MODE_TRACK; // USART1 接收中断收到 1 循迹2 避障3 灭火4 遥控 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t cmd USART_ReceiveData(USART1); switch (cmd) { case 1: g_mode MODE_TRACK; break; case 2: g_mode MODE_AVOID; break; case 3: g_mode MODE_FIRE; break; case 4: g_mode MODE_MANUAL; break; default: break; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }指令用单字符是为了调试方便串口助手直接敲就能切模式。实际产品里可以换成协议帧加校验但课程设计阶段单字符足够。注意中断里只改变量不做耗时操作这是嵌入式的铁律。4.2 主循环的状态机写法主循环不要写成一长串 if-else用 switch 按模式分发每个模式内部再调各自的函数。这样加新模式只需要加一个 case不会把原有逻辑搅乱。int main(void) { SystemInit(); Motor_Init(); Track_Init(); Ultrasonic_Init(); Flame_Init(); USART1_Init(9600); while (1) { switch (g_mode) { case MODE_TRACK: Track_Follow(3500); break; case MODE_AVOID: if (Ultrasonic_GetDistance() 20) { Motor_SetSpeed(0, 0); Motor_Backward(2000); Motor_TurnRight(2500); } else { Motor_SetSpeed(3500, 3500); } break; case MODE_FIRE: if (Flame_Detect()) { Fire_Extinguish(); } else { Motor_SetSpeed(2500, 2500); // 慢速搜索 } break; case MODE_MANUAL: // 遥控指令在中断里已解析这里只做执行 break; } delay_ms(10); // 主循环节拍避免空转占满 CPU } }delay_ms(10)这个节拍很重要既保证响应速度又不会让传感器读得太频繁导致数据抖动。避障模式里“后退再右转”是一个简单有效的脱困策略比原地转圈靠谱。灭火模式慢速搜索是为了给火焰传感器足够的采样时间跑太快容易错过火源。5. 避坑与排查这车最容易翻车的五个地方5.1 小车直线跑偏循迹像喝醉现象是明明四个探头都没压线小车却往一边偏。原因多半是左右电机特性不一致或者 L298N 两路使能的初始占空比没对齐。解决办法是先做电机校准把车架空给左右轮同样的 PWM 值看转速是否一致不一致就在代码里给慢的那边加一个补偿系数。我一般会在Motor_SetSpeed里留一个left_trim和right_trim全局变量调车时现场改。5.2 超声波读数跳变避障忽灵忽不灵现象是距离值在 5cm 和 200cm 之间乱跳。原因通常是 Trig 脉冲太短或者 Echo 被其他中断打断导致捕获丢失。解决方法是把 Trig 高电平保证在 15 微秒以上输入捕获中断优先级设高一点并且在测距函数里加超时保护。另外超声波探头之间会串扰两次测距之间至少隔 60ms别连续猛测。5.3 火焰传感器误触发日光灯下也报警现象是没火也开风扇。原因是火焰传感器的阈值设太低日光灯里的红外成分被误判。解决办法是现场标定先读环境本底值再读火源峰值阈值取两者中间偏上。如果环境光变化大可以加一个“连续 N 次超过阈值才确认”的滤波单次触发不算数。5.4 遥控切模式后小车不响应现象是串口发了指令模式变量也变了但小车动作没变。原因多半是主循环里某个模式卡在阻塞函数里出不来比如delay_ms(3000)灭火时这 3 秒内所有遥控指令都被吞掉。解决办法是把长延时拆成状态机或者用定时器做非阻塞延时保证主循环始终在转。5.5 电池一接电机就复位现象是小车一动主控就重启。原因是电机启动瞬间电流大把电池电压拉低主控欠压复位。解决办法是电机电源和主控电源分开走主控前面加一个大电容缓冲电池选放电倍率高一点的。这个坑我踩过不止一次后来每次做电机项目都先在电源上留余量。6. 二次开发把这台车改成你自己的项目底座这套代码的扩展性其实比看上去好因为三条功能链路是解耦的你完全可以只留一条把另外两条换成别的传感器。比如把火焰检测换成温湿度传感器做环境巡检车或者把循迹换成摄像头做视觉巡线主循环的状态机骨架不用动只换 case 里的实现。改造时我建议先做一件事把g_mode的切换逻辑和具体功能函数之间加一层函数指针表。这样加新模式不用改 switch注册一个函数就行。typedef void (*ModeFunc)(void); static void mode_track(void) { Track_Follow(3500); } static void mode_avoid(void) { /* 避障逻辑 */ } static void mode_fire(void) { /* 灭火逻辑 */ } ModeFunc mode_table[] { mode_track, mode_avoid, mode_fire }; // 主循环里改成 mode_table[g_mode]();这个改法的好处是模式数量和顺序解耦后面加蓝牙遥控的“手动挡”只需要往表里追加一项。验证方法也简单每加一个模式先用串口打印进入和退出日志确认状态切换正确再上电机实测。另一个值得做的改造是把所有阈值集中到一个config.h里包括循迹比例系数、避障距离阈值、火焰 ADC 阈值、PWM 基准速度。调车时只改这一个文件不用满工程找魔法数字。我现在的习惯是凡是现场要调的参数一律提到头文件里加注释标好单位和实测环境。最后说个验证技巧改完任何一条链路先架空车轮测传感器输出再落地低速跑最后才全速。我早年图省事直接全速上电结果小车一头撞墙上把超声波支架撞断从那以后每次改完代码都强制走一遍“架空—低速—全速”的流程再急也不跳步。希望这套拆解能帮你少走点弯路把车先跑起来再慢慢改成自己想要的样子。本文还有配套的精品资源点击获取