简介:这份资源面向学习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 而不是一块 Arduino
2.1 主控与模块的职责划分
这套小车的典型架构是“一主多从”的传感器布局:STM32F103C8T6 作为主控,负责所有实时决策;电机驱动、传感器、遥控接收各自独立,主控只做采集和判断。为什么不用 Arduino?因为循迹和避障对时序有要求,STM32 的定时器资源多,PWM 输出稳定,超声波测距用输入捕获能拿到微秒级回波宽度,这些都是 Arduino 的pulseIn给不了的确定性。
整车的信号流大致是这样:红外循迹模块输出数字电平到 GPIO,超声波模块的 Trig 由定时器触发、Echo 接输入捕获,火焰传感器输出模拟量进 ADC,遥控信号走 USART 或外部中断,最后主控通过 TIM 的 PWM 通道控制 L298N 的两路使能,GPIO 控制四个方向引脚。这个划分的好处是每个模块都能单独调试,出问题容易定位。
| 模块 | 型号/类型 | 接口方式 | 主控资源占用 |
|---|---|---|---|
| 主控 | STM32F103C8T6 | — | — |
| 电机驱动 | L298N 双 H 桥 | PWM + GPIO | TIM2 CH1/CH2 |
| 循迹 | 四路红外对管 | GPIO 输入 | PB0-PB3 |
| 避障 | HC-SR04 超声波 | 定时器输入捕获 | TIM3 CH1 |
| 灭火 | 火焰传感器 + 小风扇 | ADC + GPIO | ADC1_IN0 |
| 遥控 | 蓝牙串口 / 红外接收 | USART1 / EXTI | PA9/PA10 |
2.2 电源与电机驱动的坑点
很多人第一次做这车,最容易翻车的地方不是代码,是供电。STM32 要 3.3V,L298N 的逻辑电平是 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 / (7199+1) = 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-SR04,Trig 给一个 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 基准速度。调车时只改这一个文件,不用满工程找魔法数字。我现在的习惯是,凡是现场要调的参数,一律提到头文件里加注释,标好单位和实测环境。
最后说个验证技巧:改完任何一条链路,先架空车轮测传感器输出,再落地低速跑,最后才全速。我早年图省事直接全速上电,结果小车一头撞墙上把超声波支架撞断,从那以后每次改完代码都强制走一遍“架空—低速—全速”的流程,再急也不跳步。希望这套拆解能帮你少走点弯路,把车先跑起来,再慢慢改成自己想要的样子。
本文还有配套的精品资源,点击获取