ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32灭火机器人毕设源码解析:ADC火焰检测与PWM电机控制

2026/9/16 8:50:11 拓冰建站 浏览量
STM32灭火机器人毕设源码解析:ADC火焰检测与PWM电机控制 简介基于STM32的智能化灭火机器人项目源码专门为计算机、电子及相关专业的毕业设计、课程设计与期末大作业场景打造适合需要快速搭建完整嵌入式项目的学生和实战练习者。压缩包共102个文件核心由31个C源文件、48个H头文件及uvprojx工程文件构成集成STM32F10x系列常见外设驱动如ADC采样、CAN通信、DAC输出、PWM调速等覆盖传感器数据采集、电机控制与灭火执行等关键环节同时附带演示视频、辅助表格和说明文档包体仅9.66MB便于下载和本地调试。该项目为评审99分的高分设计代码结构清晰、可直接编译运行从火焰检测到路径规划再到灭火动作均有完整实现并考虑到初学者的复现体验配套资料能帮助理解整体工作流程。已有203人学习/下载可作为毕业设计蓝本或项目实战练习的完整参考。1. 为什么是 STM32灭火机器人毕设的结构与选题逻辑一个压缩包里躺着十几个stm32f10x_开头的.c文件这不是老师在凑代码量而是 STM32 标准外设库工程的基本面貌。灭火机器人这个题目在 stm32 毕业设计里属于综合度很高的那类它有传感器采集、有执行器控制、有状态切换还要考虑响应实时性一套下来把 ADC、定时器 PWM、GPIO 外部中断、串口都用上了比单纯做温湿度监测或者流水灯能体现的工程能力多一个量级。拿到这份 99 分源码之后很多人第一步就卡在MODEL.uvguix.Administrator上——双击它没有任何反应于是怀疑文件损坏。这个文件只是 Keil 的窗口布局缓存真正要打开的是.uvprojx工程文件。这篇就按一套完整可复现的路径先把灭火机器人的系统结构讲清楚再逐个模块拆代码最后落到烧录调试和现场标定上。2. 硬件信号链路与核心外设选型火焰检测、PWM 调速与转向判断2.1 从传感器引脚到执行器的信号链路灭火机器人要完成的行为闭环是感知火焰存在 → 判断火焰方位 → 调整车体姿态 → 接近火源 → 启动灭火装置。这条链路里传感器信号是模拟量进来控制量是 PWM 和 GPIO 电平出去。下面这张表是这套项目里最典型的信号映射拿到源码后建议先对照原理图确认每个外设挂在哪个引脚上。功能模块接口类型信号方向关键参数或说明火焰传感器红外/紫外模块ADC 模拟输入输入12 位采样参考电压 3.3V无火时接近满量程灰度循迹传感器2 路GPIO 数字输入输入黑白线电平相反低有效或高有效需实测直流减速电机左右各一TIM PWM GPIO 方向输出PWM 频率 20 kHz 左右IN1/IN2 控制方向灭火风机/水泵继电器GPIO 推挽输出输出高电平触发或通过 MOS 管驱动蜂鸣器TIM 输出比较或 GPIO输出用于报警与状态提示串口调试USART TX/RX双向常见 115200-8-N-1打印 ADC 值和状态从工程文件列表里可以看到stm32f10x_adc.c和stm32f10x_PWM.c这两个文件对应的就是火焰采样和电机调速。stm32f10x_can.c在这个项目里很可能是保留了 CAN 外设驱动但并未真正接入总线灭火机器人本体不需要 CAN 通信后面编译时可以把没有用到的外设文件从工程组里摘掉减少 Flash 占用和编译告警。2.2 火焰传感器为什么走 ADC 模拟量通道市面上常见的火焰传感器模块有两种输出数字量输出和模拟量输出。数字量模块内部自带一个比较器电位器调节灵敏度火焰达到一定强度后输出翻转电平。听起来更简单但有一个实际问题赛场或演示场地的环境光不一致上午和下午的红外背景值都不一样电位器调好的阈值换一个场地就可能失效。模拟量输出则保留了原始电压阈值判断放在单片机代码里改一个变量就能重新标定。这套项目走 ADC 通道采集模拟量就是为了把阈值决策权留在固件侧。STM32F103 的 ADC 是 12 位逐次逼近型参考电压一般接 3.3V所以单次采样的分辨率约为 0.8 mV。灭火检测并不需要这么高的精度反而需要抗抖动。我的做法是连续采样 8 次去掉最大值和最小值再取平均相当于一个轻量中值滤波。下面这段代码是标准外设库下最简的 ADC 单通道读取实现。void ADC1_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; // 使能 GPIOA 和 ADC1 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); // PA1 配置为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); // ADC1 独立模式单次转换右对齐软件触发 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); // 通道1采样周期尽量取大一些降低内阻影响 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); } uint16_t ADC_Get_Value(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); return ADC_GetConversionValue(ADC1); }ADC_RegularChannelConfig里的ADC_SampleTime_239Cycles5是采样周期参数数值越大采样时间越长对高内阻的信号源越友好。火焰传感器模块输出阻抗偏高用最短的 1.5 周期采样会引入明显噪声。软件触发模式下每次读取前要调用ADC_SoftwareStartConvCmd启动一次转换然后轮询EOC标志位。这样单次采集的耗时大约在几十微秒级别放在主循环里做 8 次滤波完全没有压力。2.3 电机调速与转向策略驱动两个直流减速电机最省事的方案是 L298N 或 TB6612FNG。L298N 便宜但压降大发热明显TB6612 内部是 MOS 管 H 桥12V 供电时效率高体积也小更适合装在灭火机器人这种小车平台上。无论用哪一款控制逻辑都一样PWM 引脚控制速度两个方向引脚控制正反转。比如左电机 IN1/IN2 分别接 PA2/PA3右电机 IN1/IN2 接 PB0/PB1PWM 分别来自 TIM2_CH1 和 TIM2_CH2。PWM 频率的选择有个实用原则低于 15 kHz 时电感容易发出人耳可闻的啸叫高于 25 kHz 时驱动芯片的开关损耗上升常见做法是取 20 kHz 左右。STM32F103 的 APB1 定时器时钟为 72 MHz设置预分频为 0、自动重载值为 3599输出频率就是 72 MHz / 3600 20 kHz。占空比通过TIM_SetCompare1调整数值范围 0~3599。void TIM2_PWM_Init(uint16_t arr, uint16_t psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // PA0 和 PA1 配置为复用推挽输出驱动 TIM2_CH1/CH2 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC2Init(TIM2, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); } void Motor_Speed(uint8_t left, uint8_t right) { TIM_SetCompare1(TIM2, (uint16_t)left * 36); // 0~100 映射到 0~3600 TIM_SetCompare2(TIM2, (uint16_t)right * 36); }TIM_OCMode_PWM1的含义是计数器的值小于比较寄存器时输出有效电平超过后翻转。这样TIM_SetCompare1写入 1800 时是 50% 占空比。TIM_Pulse初始化成 0 是为了保证上电瞬间电机不转避免刚通电就冲出去。方向控制引脚与 PWM 要分开配置方向引脚用普通推挽输出在转向逻辑里先改方向再给 PWM如果先给 PWM 再换方向电机轴会瞬间反向冲击一次长期运行会损伤减速箱齿轮。循迹转向的经典做法是两路灰度传感器差速判断。灰度传感器在白色地面输出高电平、在黑线上输出低电平左右两路组合成四种状态。直行时两路都压在白地上左偏时左轮压线右偏时右轮压线两路同时压线说明到了十字路口或者火源前方。控制代码直接写在主循环里简单但并不简陋。void Tracking_Control(void) { uint8_t left_sensor GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8); uint8_t right_sensor GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9); if (left_sensor 1 right_sensor 1) { // 两路都在白地直行 Motor_Speed(SPEED_STRAIGHT, SPEED_STRAIGHT); } else if (left_sensor 0 right_sensor 1) { // 左轮压线右轮加速向左修正 Motor_Speed(SPEED_TURN_LEFT_INNER, SPEED_TURN_LEFT_OUTER); } else if (left_sensor 1 right_sensor 0) { // 右轮压线左轮加速向右修正 Motor_Speed(SPEED_TURN_RIGHT_OUTER, SPEED_TURN_RIGHT_INNER); } else { // 两路同时压线可能是十字路口或火源标志线减速搜索 Motor_Speed(SPEED_SLOW, SPEED_SLOW); } }速度参数SPEED_STRAIGHT、SPEED_TURN_LEFT_INNER这类值不要拍脑袋定。常见的调法是先给一组中等占空比比如 35%放在地面实测直线偏差再把内轮速度降低 10 到 15 个百分点看转弯半径。转弯半径取决于轮距和速度差没有统一公式每台车的重心位置都会影响实际效果。3. 工程源码逐层拆解标准外设库、MODEL.uvguix 与主状态机3.1 先分清库文件与用户文件压缩包解开后文件列表第一眼容易让人发懵stm32f10x_adc.c、stm32f10x_can.c、stm32f10x_dac.c、stm32f10x_PWM.c……实际上这套工程文件可以分成三类。第一类是内核层core_cm3.c是 Cortex-M3 内核的访问函数包括中断优先级控制和 SysTick 配置system_stm32f10x.c负责时钟树初始化把芯片时钟从外部晶振倍频到 72 MHz。第二类是标准外设库驱动stm32f10x_adc.c、stm32f10x_can.c、stm32f10x_dac.c都在这一层它们只是外设驱动不包含任何业务逻辑。第三类是用户代码比如stm32f10x_scom.c这个文件标准外设库里没有叫scom的外设它是作者把串口调试封装命名成 Serial COMmunication属于用户应用文件在工程树里一般被放在 User 或 Application 分组里。拿到源码第一步不是点编译而是先在 Keil 工程树里看一遍每个分组里实际加入了哪些.c文件。文件在磁盘上存在和参与编译是两回事只有被加入工程组里的文件才会被编译链接。3.2 MODEL.uvguix.Administrator 是什么为什么不要双击它MODEL.uvguix.Administrator是最容易误导新手的文件。这个文件的扩展名uvguix是 Keil MDK 的 GUI 布局配置记录了窗口大小、断点位置、Watch 窗口里的变量列表。它不是源码不参与编译删掉也不影响工程打开。真正要打开的是MODEL.uvprojx这是 MDK5 的工程文件。如果文件资源管理器隐藏了扩展名两个文件看起来都叫MODEL区分方法是看图标工程文件图标带有芯片形状的标识uvguix文件图标更像一个空白的配置文件。顺带提醒一个项目备份习惯uvguix文件里记录的是本机用户名换一台电脑打开工程后 MDK 会生成一个新的uvguix.用户名文件老文件并不会删除。源码包发给别人之前可以把uvguix文件清掉避免别人打开时看到一堆残留的断点信息。压缩包里同时出现system_stm32f10x.c两次说明工程目录组织上可能重复加入了文件编译时 Keil 会给出重复定义的告警也需要在工程树里确认。3.3 主循环里的状态机灭火机器人不是简单的顺序执行而是要在多个行为之间切换沿黑线走、发现火焰、对准火源、启动灭火、返回起点。如果全用if嵌套逻辑会越写越乱调试时根本分不清当前处于哪个环节。正解是用一个状态机管理主循环。状态枚举定义在头文件里主循环只做三件事读传感器、更新状态、执行当前状态的动作。typedef enum { STATE_TRACKING, // 循迹行驶寻找火源 STATE_SEARCH_FIRE, // 原地旋转扫描定位火焰方位 STATE_APPROACH, // 对准火焰直线接近 STATE_EXTINGUISH, // 启动风机/水泵灭火 STATE_BACK // 灭火完成返回出发点 } RobotState_t; RobotState_t robot_state STATE_TRACKING; void Robot_Run(void) { switch (robot_state) { case STATE_TRACKING: Tracking_Control(); if (Fire_Detected()) { robot_state STATE_SEARCH_FIRE; } break; case STATE_SEARCH_FIRE: Rotate_Scan(); if (Fire_Aligned()) { robot_state STATE_APPROACH; } break; case STATE_APPROACH: Motor_Speed(SPEED_APPROACH, SPEED_APPROACH); if (Flame_Intensity() STRONG_THRESHOLD) { robot_state STATE_EXTINGUISH; } break; case STATE_EXTINGUISH: Motor_Speed(0, 0); Relay_On(); Delay_Ms(3000); Relay_Off(); robot_state STATE_BACK; break; case STATE_BACK: // 原路折返或按时间估算回程 Tracking_Control(); break; default: robot_state STATE_TRACKING; break; } }STATE_SEARCH_FIRE和STATE_APPROACH的区别是很多答辩老师喜欢问的点。搜索状态是原地旋转火焰传感器扫过 360 度记录哪一侧的采样值最大找到火焰方位后进入接近状态。接近状态不再旋转保持当前车头方向直行同时持续采样火焰强度强度超过强火阈值时说明已经到位停车灭火。这套流程避免了传感器只在中线附近采样的盲区也比单纯直线走过去对目标搜索要可靠。3.4 未使用外设文件要不要参与编译工程目录里有stm32f10x_cec.c这种文件CEC 是消费电子控制外设灭火机器人用不到。stm32f10x_can.c同理除非你的设计里用 CAN 总线挂了多个传感器节点否则不需要保留。在 Keil 工程树里右击分组选择 Manage Project Items把用不到的.c文件移除分组即可。移走之后 Flash 占用会小一点编译速度更快更重要的是避免某些外设库文件在编译时引用到目标芯片没有的寄存器定义从而产生undefined identifier这类奇怪报错。4. 从源码到烧录MDK 工程配置与常见坑4.1 芯片型号、启动文件与宏定义打开工程后第一件事是检查 Target 选项。Options for Target → Device 里选的芯片型号必须和目标板一致。从源码文件名推断这套项目最可能是 STM32F103 系列。不同容量的 F103 需要不同的启动文件中容量64~128 KB Flash用startup_stm32f10x_md.s高容量用hd.s启动文件选错往往表现为编译通过但下载后程序不跑因为向量表偏移和堆栈初始化都不对。还要确认 C/C 选项卡里的 Define 是否包含STM32F10X_MD。标准外设库里靠这个宏选择器件型号对应的寄存器定义。宏和启动文件、Device 里选的芯片型号三者必须一致这是 stm32 标准库新建工程最常见的翻车点。stm32f10x.h文件底部有一段条件编译如果STM32F10X_MD没定义很多外设结构体都不会被声明编译时会连篇报错。如果工程提示找不到stm32f10x.h需要在 Include Paths 里把Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x和Libraries\STM32F10x_StdPeriph_Driver\inc两个目录加进去注意 Keil 里路径用反斜杠相对路径要基于工程文件所在位置。4.2 烧录前检查清单下载器连上之后点击 Download 经常出现No Target Connected或者RDDI-DAP Error这类问题九成不在代码而在调试器和芯片配置。下面这张表是排查顺序建议按顺序检查。故障现象可能原因检查方法No Target Connected调试器驱动未安装或接线错误设备管理器里查看调试器是否识别SWDIO/SWCLK/GND 三根线是否接对Target DLL has been cancelledDebug 里选择的调试器与实际不符Options → Debug → 选择对应的 ST-Link 或 J-LinkFlash Download failed烧录算法未添加Utilities → Settings → Flash Download 里添加对应型号的 Flash 算法下载后程序不运行启动文件错误或 BOOT0 引脚拉高检查启动文件与芯片容量匹配确认 BOOT0 接地RDDI-DAP Error这个报错在 F103 上最常见的原因是 SWD 引脚被复用。如果代码里把 PA13/PA14 配置成了普通 GPIO调试器第二次就连接不上。解法是按住板子复位键在点击下载的瞬间松开复位让芯片停留在复位状态完成连接或者用串口 ISP 方式擦除 Flash 后重新用 SWD 下载。4.3 上电卡死时钟启动失败怎么定位程序烧录成功但板子没反应第一步要查时钟。system_stm32f10x.c里默认从外部高速晶振 HSE 启动代码会等待 HSE 就绪。如果板子上没有焊接 8 MHz 晶振或者晶振的负载电容不匹配导致起振失败SystemInit会一直卡在while (HSEStartUpStatus HSE_STARTUP_TIMEOUT)的逻辑里main 函数根本进不去。调试方法是全速运行后点击暂停看 PC 指针是否停在SystemInit的死循环里或者直接在main函数第一行打断点。如果确认晶振没问题还要检查RCC_ClocksInit后的系统时钟频率是否真的是 72 MHz。在调试器 Peripherals → Core Peripherals → RCC 里看当前 SYSCLK 值。很多开发板为了走线方便把晶振画成 8 M但也有部分板子用 12 M 甚至 25 M。SystemInit内部的倍频系数是按 8 M 外部晶振算的晶振不是 8 M 时需要用串口打印或者调整RCC_PLLConfig的倍频系数。项目源码里一般没有这个坑但换板子就会踩到所以拿到手先确认板子实际焊接的晶振频率再动代码。5. 串口日志、在线阈值标定与状态机裁剪5.1 让 printf 从串口吐出来调试这个项目串口是不可缺的观察窗口。源码里有stm32f10x_scom.c多半已经封装好了串口初始化和发送函数。最省事的做法是重定向fputc让printf直接走串口输出不用一个个调发送函数。重定向代码放在串口初始化之后注意 Keil 里要勾选 Use MicroLIB否则printf的完整 C 库会占用大量 Flash。int fputc(int ch, FILE *f) { // 等待发送数据寄存器为空 while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }主循环里加一行状态打印就可以在电脑上看到机器人到底跑在哪个状态printf(state%d adc%d th%d\r\n, robot_state, adc_value, fire_threshold);。%d对应整数变量这三个值足够定位大部分逻辑问题。建议打印频率控制在每秒 5 次以内串口波特率 115200 时这个频率不会阻塞主循环。5.2 火焰阈值在线标定火焰检测阈值不要写死在代码里反复烧录。把标定做成串口命令运行中直接校准。串口接收到字符c时把当前 ADC 背景值存入变量接收到f时把当前 ADC 值作为强火阈值。判定条件判断adc_value fire_threshold时认为有火。这样现场测试时先让机器人停在无火区域串口发一个c再用打火机靠近传感器发一个f阈值自动落在中间区域。void UART_Command_Parser(uint8_t ch) { if (ch c) { background_value ADC_Get_Filtered(); printf(background%d\r\n, background_value); } else if (ch f) { fire_value ADC_Get_Filtered(); fire_threshold (background_value fire_value) / 2; printf(threshold%d\r\n, fire_threshold); } else if (ch q) { printf(adc%d th%d\r\n, ADC_Get_Filtered(), fire_threshold); } }这段代码的关键是fire_threshold不落 Flash只存在 RAM 里掉电即失。调试完确认阈值可靠后再把数值写回#define FIRE_THRESHOLD_DEFAULT作为默认值这样正式运行时不需要外接串口也能正常工作。标定动作尽量在接近实际比赛光照的环境里做火焰探测对红外背景非常敏感台灯和白炽灯的频谱差异会影响最终阈值。5.3 按演示场景裁剪状态机毕业设计答辩现场和正式比赛规则不一样。比赛要求灭火完成后回到出发点才能得分答辩展示只需要让机器人成功灭火就行。用条件编译把回程状态裁掉能省下不少调试时间。在状态定义文件里加#define DEMO_MODE 1STATE_BACK分支直接改成STATE_TRACKING灭火完成后重新开始寻迹循环。裁剪时不删代码只让不用的分支不参与编译这样赛后想恢复完整逻辑也容易。最后把串口输出的state变量接到上位机界面或者数码管上答辩时让评委直接看到状态切换过程比口头解释有说服力得多。本文还有配套的精品资源点击获取