
简介本资源是一份面向嵌入式初学者与STM32实践者的完整硬件接口项目案例聚焦STM32微控制器与MQ2烟雾浓度传感器的软硬协同开发解决气体浓度采集、ADC数据转换及串口实时打印等典型嵌入式传感应用问题。压缩包共182个文件含31个C源文件如stm32f10x_adc.c、stm32f10x_usart.c、32个头文件h、32个目标文件o及配套链接脚本、映射文件map、可执行镜像axf/hex等完整覆盖Keil MDK工程结构包体大小为4.58MB。已有62人学习下载适用于课程设计、毕业实践或IoT传感模块快速验证。用户可直接编译运行获得从硬件连接说明、ADC初始化配置、模拟电压—浓度换算逻辑到串口数值输出的全流程代码支撑尤其包含针对STM32F10x系列的底层驱动适配与调试关键点注释显著降低传感器接入门槛。1. 项目概述从传感器到数据最近在整理工作室的杂物翻出来几个MQ-2烟雾传感器模块这玩意儿在智能家居、安防报警里算是常客了。正好手边有块STM32F103C8T6的核心板就想着干脆搭个简单的系统把传感器读到的模拟浓度值通过串口实时打印到电脑上看看。这个项目听起来简单不就是“读ADC - 串口打印”吗但真做起来从硬件连接到软件滤波再到数据标定每一步都有不少细节值得琢磨。它非常适合刚接触STM32和传感器的新手作为第一个完整的“感知-处理-输出”项目来练手也能让有经验的开发者回顾一下ADC配置、串口通信这些基础但至关重要的环节。整个过程我们将围绕STM32的ADC模数转换器功能展开学习如何采集MQ-2输出的模拟电压并将这个电压值转换为我们能理解的浓度相关数值最后通过串口助手清晰地展示出来。你会接触到硬件电路的连接、STM32CubeMX的图形化配置、HAL库函数的使用以及一些简单的数据处理技巧。无论你是想做一个烟雾报警器的原型还是单纯想理解传感器如何与微控制器“对话”这个项目都能给你一个直观的答案。2. 核心硬件解析与连接方案2.1 MQ-2传感器模块探秘MQ-2是一个广谱型半导体气敏传感器对液化气、丙烷、氢气、烟雾等可燃气体和烟雾都比较敏感。我们通常买到的是已经集成好的模块这大大降低了使用难度。模块的核心是MQ-2气敏元件和一个比较器电路比如LM393。模块通常有4个引脚VCC 供电引脚接5V或3.3V。注意虽然模块逻辑电平是3.3V兼容但传感器加热丝部分通常需要5V供电才能达到最佳工作状态。务必查看你的模块说明书很多模块的VCC接5VDO和AO输出仍是3.3V电平。GND 接地。DO 数字量输出。模块上通常有一个电位器可以调节检测阈值。当传感器感知的浓度超过这个阈值时DO引脚输出低电平反之高电平同时板载指示灯亮起。它适合做简单的开关报警。AO 模拟量输出。这是我们本次项目的重点。它会输出一个与气体浓度相关的模拟电压值通常是0-VCC之间。浓度越高输出电压一般也越高。模块上电后需要一段预热时间通常1-2分钟因为内部的陶瓷传感元件需要被加热丝加热到一定的工作温度后阻值才会稳定并对气体敏感。刚上电时的读数剧烈变化是正常现象。2.2 STM32的ADC接口与电路连接我们使用STM32的ADC来读取AO脚的模拟电压。以STM32F103C8T6为例它拥有2个12位的ADC精度为40962^12个等级。假设我们使用3.3V作为ADC的参考电压Vref那么ADC能分辨的最小电压变化是3.3V / 4096 ≈ 0.0008V (0.8mV)。连接电路图非常简单MQ-2模块 VCC- 开发板5V引脚或外部5V电源。MQ-2模块 GND- 开发板GND。MQ-2模块 AO- 开发板某个ADC输入引脚例如PA0对应ADC1的通道0。开发板的VDDMCU供电接3.3V。这里有一个关键细节MQ-2的AO输出范围是0到它的VCC假设5V。如果我们用3.3V参考电压的ADC去测量一个可能接近5V的电压会存在风险超过3.3V的输入可能会损坏STM32的ADC引脚。因此强烈建议在AO引脚和STM32的ADC引脚之间加入一个简单的电阻分压电路例如用两个电阻10kΩ和20kΩ将电压衰减到原来的1/35V - 1.67V在安全范围内。或者确认你的模块AO输出上限不超过3.3V。稳妥起见加个分压电路是万全之策。注意如果你使用的是3.3V给整个模块供电那么AO输出最大就是3.3V可以直接连接。但传感器灵敏度可能会因供电电压不同而略有差异。2.3 工具选型为什么是HAL库与CubeMX对于这个项目我选择使用STM32CubeMX进行初始化配置并基于HAL库硬件抽象层编写代码。原因如下快速原型开发CubeMX通过图形界面配置时钟、引脚、ADC参数、串口参数能自动生成初始化代码避免了繁琐的寄存器操作让我们能快速聚焦在应用逻辑上。可移植性HAL库的API在不同系列STM32芯片间有较好的一致性。今天在F103上写的ADC读取代码稍作修改就能移植到F407或G0系列上。维护与可读性对于初学者和中小型项目HAL库代码结构清晰更易于理解和维护。当然标准外设库SPL或直接寄存器操作效率更高但开发速度稍慢。本项目以功能实现和清晰理解为目标HAL库是更合适的选择。3. 软件环境搭建与CubeMX工程配置3.1 创建工程与核心外设配置首先打开STM32CubeMX新建一个工程选择你的具体芯片型号如STM32F103C8Tx。系统核心SYS 在“SYS”选项卡下将“Debug”设置为“Serial Wire”。这样可以使用ST-LINK进行下载和调试。时钟配置RCC 在“RCC”选项卡下将“High Speed Clock (HSE)”设置为“Crystal/Ceramic Resonator”。我们的开发板通常外部接有8MHz晶振。时钟树配置 进入“Clock Configuration”标签页。这是关键一步。我们需要为ADC和USART提供正确的时钟。将HSE输入频率设为8MHz。将PLL源选择为HSE。调整PLL倍频因子使系统时钟SYSCLK达到72MHz对于F103系列是常见最高频率。查看APB2总线时钟PCLK2ADC通常挂载在此总线下。确保其频率不超过72MHz通常设为72MHz或36MHz。ADC的时钟需要进一步分频后面会设置。ADC1配置在“Pinout Configuration”界面左侧找到“Analog”下的“ADC1”。启用一个通道例如“IN0”对应PA0引脚。PA0引脚在右侧的芯片图上会自动被标记为“ADC1_IN0”。在右侧的“Parameter Settings”选项卡中进行关键配置Resolution 设置为“12-bit”。这是我们需要的精度。Scan Conversion Mode 禁用Disable。我们只扫描一个通道。Continuous Conversion Mode 启用Enable。这样ADC会连续自动转换我们只需读取数据即可。Discontinuous Conversion Mode 禁用。DMA Continuous Requests 禁用。我们先用轮询方式简单直接。End Of Conversion Selection 保持默认。Clock Prescaler 选择分频系数确保ADC时钟ADCCLK不超过14MHzF103的极限。例如如果PCLK2是72MHz分频系数选“PCLK2 divided by 6”得到12MHz是安全的。Sampling Time 采样时间。对于MQ-2这样的慢速传感器不需要很高的采样速率。可以设置为“239.5 Cycles”以获得更稳定的采样。采样时间越长ADC内部电容充电越充分抗噪声能力越强但转换速度会变慢。这里我们追求稳定性。USART1配置用于串口打印找到“Connectivity”下的“USART1”。将模式设置为“Asynchronous”异步通信。引脚会自动分配为PA9TX和PA10RX。在参数设置中配置波特率为“115200”字长“8 Bits”停止位“1”无奇偶校验无硬件流控。生成工程代码转到“Project Manager”选项卡设置项目名称、存储路径选择IDE如MDK-ARM V5。在“Code Generator”部分选择“Copy only necessary library files”并勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”这会让代码结构更清晰。点击“GENERATE CODE”生成Keil工程文件。3.2 核心代码编写ADC读取与串口输出打开生成的Keil工程我们主要修改main.c文件。首先在/* USER CODE BEGIN PV */部分定义变量/* Private variables ---------------------------------------------------------*/ ADC_HandleTypeDef hadc1; UART_HandleTypeDef huart1; uint32_t adc_value 0; float voltage 0.0; float concentration_ratio 0.0; // 浓度比值 char msg[50]; // 用于存储格式化后的字符串在main()函数中系统初始化后进入主循环while (1)。第一步启动ADC并读取值。HAL库提供了轮询、中断和DMA三种方式。我们先使用简单的轮询。/* USER CODE BEGIN WHILE */ while (1) { // 启动ADC转换轮询模式 if (HAL_ADC_Start(hadc1) ! HAL_OK) { Error_Handler(); // 启动失败进入错误处理 } // 等待转换完成并获取值。HAL_MAX_DELAY表示一直等待直到完成。 if (HAL_ADC_PollForConversion(hadc1, HAL_MAX_DELAY) HAL_OK) { adc_value HAL_ADC_GetValue(hadc1); // 获取12位的ADC原始值 } HAL_ADC_Stop(hadc1); // 停止转换对于连续模式其实可以不用每次都停这里演示流程 /* USER CODE END WHILE */第二步将ADC原始值转换为电压值。// 计算实际电压值。假设Vref 3.3V ADC是12位0-4095 // 如果接了分压电路这里计算的是分压后的电压需要根据分压比反推传感器AO脚的实际电压。 voltage (float)adc_value * 3.3f / 4095.0f; // 示例如果分压电路是10k20k分压比为 R2/(R1R2) 20k/(10k20k)2/3 // 那么传感器AO脚的实际电压应为 voltage_actual voltage / (2.0f/3.0f); // 本例假设AO直接输出在0-3.3V范围内或已通过分压匹配。第三步将电压值转换为浓度相关值并格式化输出。MQ-2的灵敏度特性曲线并非线性且需要在实际洁净空气和已知浓度气体中校准才能得到精确的ppm值。对于一般演示我们常用“传感器电阻与空气中电阻的比值Rs/R0”来表征浓度变化。简化处理中我们可以直接用电压值或ADC值作为一个相对的“浓度指示”。更严谨的做法是计算Rs/R0。R0是传感器在洁净空气中的电阻Rs是在当前气体中的电阻。模块内部电路可以等效为传感器电阻Rs与一个负载电阻RL的分压。AO输出电压与Rs有关。公式推导略复杂但一个常见的近似公式是浓度比值 (传感器电压 - 零点电压) / 灵敏度系数。这里我们做一个极度简化的演示假设浓度与电压成正比仅用于演示// 简化处理将电压映射到一个0-100的“相对浓度”值仅演示用 // 假设3.3V对应1000V对应0。实际应用必须校准 concentration_ratio (voltage / 3.3f) * 100.0f; // 格式化字符串准备通过串口发送 // 使用snprintf安全格式化避免缓冲区溢出 snprintf(msg, sizeof(msg), ADC: %lu, Voltage: %.3f V, Ratio: %.2f%%\r\n, adc_value, voltage, concentration_ratio); // 通过串口发送字符串 HAL_UART_Transmit(huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY); // 延时一段时间避免串口数据刷屏太快 HAL_Delay(1000); // 每秒打印一次 /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */3.3 串口打印浮点数的关键点很多初学者会发现按照上面的代码写了但串口只打印出“ADC: 1234, Voltage:”后面的浮点数不见了。这是因为默认的Keil MDK-ARM编译器微库MicroLIB对浮点数格式化的支持不完整。解决方法有两种启用完整的标准C库推荐 在Keil的“Options for Target” - “Target”选项卡中取消勾选“Use MicroLIB”。然后勾选“Use Standard Library”。重新编译即可。这会增加代码体积但对于F103C8T664KB Flash来说这个项目完全装得下。如果必须使用MicroLIB 需要实现_sys_write等系统调用或者将浮点数转换为整数部分和小数部分分别打印例如int int_part (int)voltage; int dec_part (int)((voltage - int_part) * 1000); // 取三位小数 snprintf(msg, sizeof(msg), Voltage: %d.%03d V\r\n, int_part, dec_part);方法1是最简单直接的。4. 数据优化与滤波处理4.1 ADC采样稳定性提升技巧直接读取一次的ADC值往往噪声较大尤其是对于模拟传感器。常见的软件滤波方法有多次采样取平均 这是最简单有效的方法。在主循环读取ADC值的地方改为连续采样N次后取平均值。#define SAMPLE_TIMES 10 uint32_t adc_sum 0; for(int i0; iSAMPLE_TIMES; i) { HAL_ADC_Start(hadc1); if (HAL_ADC_PollForConversion(hadc1, HAL_MAX_DELAY) HAL_OK) { adc_sum HAL_ADC_GetValue(hadc1); } HAL_ADC_Stop(hadc1); // 可以加一个很小的延时如 HAL_Delay(1)避免ADC连续转换过热或干扰 } adc_value adc_sum / SAMPLE_TIMES;滑动平均滤波 维护一个固定长度的数组每次新采样值存入并覆盖最旧的值然后计算数组中所有值的平均值。这种方法能平滑数据且响应速度比简单平均快。#define FILTER_LEN 5 uint32_t adc_buf[FILTER_LEN] {0}; uint8_t buf_index 0; uint32_t adc_sum 0; // 在循环中 // ... 获取到单次adc_raw ... adc_sum - adc_buf[buf_index]; // 减去即将被覆盖的旧值 adc_buf[buf_index] adc_raw; // 存入新值 adc_sum adc_buf[buf_index]; // 加上新值 buf_index (buf_index 1) % FILTER_LEN; // 索引循环 adc_value adc_sum / FILTER_LEN; // 得到滤波后的值中位值平均滤波 采样N次去掉一个最大值和一个最小值然后对剩下的N-2个值取平均。这种方法能有效抑制脉冲干扰。4.2 传感器校准与数据标定思路要让显示的值有实际意义如ppm必须进行校准。这需要标准气体或一个相对稳定的环境。确定R0洁净空气中电阻 将传感器置于洁净空气中例如室外通风处预热足够时间20-30分钟使其稳定。读取此时的ADC值adc_air并计算对应的电压V_air。根据模块原理图通常RL已知比如10kΩ可以计算出R0。公式为Rs RL * (Vcc - V_out) / V_out其中V_out是AO脚电压。在洁净空气中Rs R0。记录下这个R0值可以固化在代码中。计算Rs/R0比值 在检测时实时计算当前的Rs方法同上然后计算比值ratio Rs / R0。查表或使用经验公式 MQ-2的数据手册会提供不同气体浓度下Rs/R0的比值曲线图。我们可以将曲线分段线性化或者使用近似的对数公式来估算浓度。例如对于某些气体浓度ppm与比值的关系近似为ppm A * (ratio)^B其中A和B是常数需要根据数据手册的曲线拟合得到。一个简化的、用于演示的代码片段可能如下// 假设已通过校准得到以下值 #define VCC 5.0f // 模块供电电压 #define RL 10.0f // 模块上的负载电阻单位千欧 #define R0_CLEAN_AIR 10.0f // 校准得到的洁净空气中传感器电阻单位千欧 float calculate_rs_from_adc(uint32_t adc_val) { float v_out (float)adc_val * 3.3f / 4095.0f; // ADC测得的电压 // 如果ADC前有分压需先反推回AO脚电压v_out_ao // float v_out_ao v_out * (R1R2)/R2; float v_out_ao v_out; // 假设直接连接 float rs RL * (VCC - v_out_ao) / v_out_ao; return rs; } float get_concentration_ratio(void) { uint32_t adc_filtered get_filtered_adc_value(); // 获取滤波后的ADC值 float rs calculate_rs_from_adc(adc_filtered); float ratio rs / R0_CLEAN_AIR; return ratio; // 这个比值越小通常表示浓度越高对于MQ-2 }在实际项目中你可以根据ratio值设定几个阈值来触发不同级别的报警比如ratio 0.6为轻度报警ratio 0.4为重度报警。5. 进阶优化与问题排查5.1 使用DMA实现后台连续采样与传输轮询方式效率低且HAL_Delay会阻塞整个程序。更高级的做法是使用DMA直接存储器访问让ADC连续自动转换并将结果源源不断地存入数组完全不需要CPU干预。主程序只需要定期去处理这个数组里的数据即可。在CubeMX中的配置在ADC1配置中启用“DMA Continuous Requests”。在“DMA Settings”标签页点击“Add”选择ADC1模式设为“Circular”循环模式数据宽度都为“Word”32位对应uint32_t。生成代码后在main.c中启动带DMA的ADC转换uint32_t adc_dma_buffer[BUFFER_SIZE]; // 定义一个缓冲区 HAL_ADC_Start_DMA(hadc1, adc_dma_buffer, BUFFER_SIZE);之后adc_dma_buffer数组会被自动填充。你可以开启一个定时器中断每隔一段时间如1秒去计算缓冲区中数据的平均值然后通过串口发送。这样主循环就完全自由了。5.2 常见问题与排查技巧串口无输出或乱码检查接线 TX接RXRX接TXGND接GND。检查波特率 确保代码中设置的波特率如115200与串口助手软件如XCOM、Putty的波特率完全一致。检查时钟配置 如果系统时钟配置错误会导致所有外设包括USART的时钟频率不对从而产生乱码。回头仔细检查CubeMX中的时钟树配置确保HSE、PLL、系统时钟、APB总线时钟设置正确。ADC读数不稳定跳动很大硬件滤波 在ADC输入引脚PA0到地之间并联一个0.1uF的瓷片电容可以滤除高频噪声。软件滤波 务必实施前面提到的多次采样平均或滑动平均滤波。检查电源 模拟部分对电源噪声敏感。确保给STM32和传感器模块的电源是干净的。可以在VCC和GND之间并联一个10uF电解电容和一个0.1uF瓷片电容。采样时间 适当增加ADC配置中的“Sampling Time”采样时间给ADC内部的采样保持电容更长的充电时间对高阻抗信号源更有利。ADC读数始终为0或40950 可能ADC引脚没有正确接收到电压检查连线确认AO脚有输出。也可能是ADC通道配置错误。4095满量程 输入电压超过了ADC的参考电压3.3V。检查MQ-2模块AO输出是否过高务必确认是否需要分压电路。代码下载后不运行启动模式 确认BOOT0和BOOT1引脚设置正确通常都接地从主Flash启动。复位电路 检查复位引脚是否正常尝试手动复位。调试器连接 如果使用ST-LINK检查驱动是否安装连接是否牢固。5.3 项目扩展思路这个基础项目可以很容易地扩展添加阈值报警 除了串口打印可以连接一个LED或蜂鸣器当浓度比值超过设定阈值时触发声光报警。接入OLED显示屏 使用I2C或SPI接口的OLED屏实时显示浓度值和曲线图脱离电脑。实现物联网上报 搭配ESP8266 Wi-Fi模块将浓度数据上传到云平台如阿里云、OneNET实现远程监控。多传感器融合 同时接入温湿度传感器如DHT11、其他气体传感器如MQ-135用于空气质量构建一个环境监测站。使用RTOS 引入FreeRTOS等实时操作系统将ADC采样、数据处理、串口发送、显示刷新等任务模块化提高代码的健壮性和可维护性。这个“STM32连接MQ-2”的项目就像一把钥匙打开了嵌入式系统与真实物理世界交互的大门。从电压的读取、数据的处理到最终信息的呈现每一个环节都蕴含着从模拟到数字、从硬件到软件的基本原理。动手做一遍遇到的问题和解决的思路远比只看文档要深刻得多。当你看到串口助手上那一行行随着烟雾靠近而变化的数字时那种把不可见的物理量变成可控数据的感觉正是嵌入式开发的乐趣所在。本文还有配套的精品资源点击获取