基于STM32与MQ-3的酒精检测系统:从ADC采样到报警显示完整实现
基于STM32的酒精检测系统是很多嵌入式学习者第一次把传感器、ADC、显示和报警串在一起的完整项目。它的价值不在于把酒精浓度测得多精确而在于它能帮你走通一条标准链路气体传感器产生模拟信号STM32 通过 ADC 采集再经过滤波、阈值判断最后驱动 OLED 和蜂鸣器给出结果。最近这类开源项目在 Gitee 和 GitHub 上都不少有的还带 PCB 工程和上位机适合想从点灯进阶到“数据采集人机交互”的人。最值得关注的不是传感器灵敏度而是代码里对信号的处理方式——是直接用单次 ADC 值还是做了稳定滤波和零点校准这决定项目能不能在实际环境里稳定运行。下面按我实际跑这类项目的顺序拆开讲。1. 先判断它到底是什么别被“检测精度”带偏1.1 这个系统实际上由四个链路组成网上搜“基于STM32的酒精检测系统”出来的方案大多长一个样STM32 开发板加一个 MQ-3 酒精传感器模块传感器检测到酒精后输出电压变化STM32 的 ADC 引脚读取这个电压再换算成酒精浓度或风险等级最后用 OLED 显示数值用蜂鸣器和 LED 报警。拆开看就是四段链路。第一段是传感器。MQ-3 属于半导体气敏传感器内部有一根加热丝和一个金属氧化物半导体层。酒精分子吸附在半导体表面后电导率会变化反映到模块输出就是电压变化。它便宜、响应快、电路简单是新手项目最常用的选型。第二段是信号采集。传感器模块一般有两个输出端子DO 是数字量输出可以调节模块上的电位器当浓度超过设定点时输出高低电平AO 是模拟量输出输出的电压随酒精浓度变化。想在屏幕上显示数值必须读 AO因为 DO 只能告诉你“超没超过”不能告诉你“现在是多少”。第三段是数据处理。STM32 读到的 ADC 原始值是 0 到 4095 的整数不代表酒精浓度。要把它变成电压值再和零点基准做差才能判断当前的信号变化是否明显。这里最忌讳直接拿一次 ADC 值去做判断因为传感器输出本身就在波动而且通电初期漂移尤其严重。第四段是人机交互和报警。OLED 显示当前状态蜂鸣器响、LED 亮告诉周围人超标了按键用来调整阈值或切换显示模式。这四段都不难但合在一起就是一个完整小项目。1.2 开源项目的核心价值是流程不是仪表级精度有一点必须先说清楚STM32 MQ-3 做出来的酒精检测系统可以做成功能完整的学习型项目但不要把它当成计量级仪器去用。MQ-3 对酒精有响应但对汽油、氢气、烟雾、甚至部分厨房油烟也可能有响应选择性有限。它的输出会受环境温度、湿度、传感器老化程度、供电稳定性影响。同一个传感器冬天和夏天测同一瓶酒精标准液ADC 值都可能差不少。所以开源项目里写的“当前酒精浓度xx ppm”更多是演示和参考不能当作执法级或医疗级数据。我见过不少人在评论区问“为什么我测出来和酒测仪差这么多”原因通常就在这里传感器类型决定了精度上限不是代码写得不好。如果你的目标是学习 ADC 采集、滤波、人机交互、板级调试这个项目非常合适如果你的目标是做一个能拿去对比酒精测试仪的仪器可能需要考虑电化学传感器成本和电路复杂度都会明显上升。2. 硬件选型与电路连接接线前先看三件事2.1 常用硬件清单这类项目最常见的一套硬件组合如下以 STM32F103C8T6 最小系统板为中心。模块常见型号作用备注主控板STM32F103C8T6 最小系统板数据采集和处理也叫 Blue Pill资料最多酒精传感器MQ-3 模块感知酒精浓度变化注意区分 DO 和 AO显示模块0.96 寸 OLEDSSD1306 驱动显示数值和状态用 I2C 接口接线少报警模块有源蜂鸣器 LED超阈值报警有源蜂鸣器直接给电平就会响按键轻触按键 2 到 3 个设置阈值、切换模式需要做软件消抖电源USB 5V / 独立 5V 电源给传感器加热丝和整板供电不能用 3.3V 给 MQ-3 加热除了这些还需要杜邦线、面包板或洞洞板、万用表。万用表在排查电压问题时非常有用不要省。2.2 最小接线参考下面这组接线是常见的参考接法具体引脚可以根据你自己手上的板子和代码调整。模块引脚STM32 引脚说明MQ-3 VCC5V传感器加热需要 5VMQ-3 GNDGND必须和 STM32 共地MQ-3 AOPA0模拟输出接 ADC 通道 0OLED VCC3.3VOLED 一般支持 3.3VOLED SCLPB6I2C 时钟线OLED SDAPB7I2C 数据线蜂鸣器正极PB0低电平或高电平触发看模块按键1PA2模式切换或阈值调整按键2PA3阈值加/减这里要特别说明MQ-3 模块的 VCC 建议接 5V因为加热丝需要足够功率。STM32 最小系统板的 3.3V 输出通常带不动加热丝。OLED 则接 3.3V 更稳妥SSD1306 这类屏幕在 5V 下虽然很多能用但容易发热接口电平也可能和 STM32 不匹配。2.3 最容易翻车的三个电气问题第一个问题是 AO 输出电压范围。MQ-3 模块的 AO 输出范围取决于厂家电路设计有些模块能达到接近 5V而 STM32 的 ADC 输入范围是 0 到 3.3V直接接上去轻则采样值不准确重则长期高电压损坏引脚。稳妥的做法是上电后用万用表测 AO 对地的最大电压。如果浓度升高时明显超过 3.3V就在 AO 和 STM32 之间加分压电阻或电位器。更简单的办法是换成已经做好信号调理的模块或者只用模块上的 DO 做报警功能AO 做趋势观察并加限压处理。第二个问题是共地。传感器、OLED、蜂鸣器和 STM32 必须共地也就是所有 GND 要连在一起。很多 ADC 值乱跳、报警误触发不是代码问题而是地线没接好信号参考点浮空了。第三个问题是供电。MQ-3 的加热丝启动电流比较大如果整个系统只靠电脑 USB 口供电可能在传感器预热阶段造成电压跌落导致 OLED 闪烁、蜂鸣器误响。建议用独立的 5V 电源适配器给系统供电或者至少用一个质量好一点的 USB 线。3. 环境准备与最小代码框架先让 ADC 稳定输出3.1 开发环境怎么选STM32F103 的开发环境通常有三种选择。第一种是 Keil MDK。最常见ST 官方资料、开源项目、毕业设计代码大多基于这个工程遇到问题容易搜到答案。缺点是界面老工程文件在不同 Keil 版本间偶尔会有兼容问题。第二种是 STM32CubeMX 生成 HAL 库工程再用 Keil、IAR 或 VSCode 编译。我用得最多的是这条路因为 CubeMX 可以把引脚、时钟、ADC、I2C 的外设配置生成好初学者不用手写寄存器也不容易漏掉时钟配置。第三种是 VSCode 加 EIDE 或 PlatformIO。如果你的 PC 环境已经装了 VSCode可以考虑但对新手来说前期配置成本稍微高一点。不管是哪种环境先把最基础的 LED 闪烁工程跑通再去做 ADC。如果连灯都不亮就急着接传感器后面出了问题很难判断是硬件还是软件。3.2 CubeMX 配置要点在 CubeMX 里新建工程芯片选择 STM32F103C8然后重点检查下面几项。SYS 里把 Debug 设置为 Serial Wire否则不小心把 SWD 引脚复用掉第二次烧录会失败。RCC 里把 HSE 设置为 Crystal/Ceramic Resonator前提是你的最小系统板上有 8MHz 晶振。如果没有晶振就不要把 HSE 打开直接使用内部时钟避免启动卡死。选择 ADC1使能 IN0 对应 PA0 引脚。采样时间尽量设置长一点比如 55.5 或 71.1 个周期。原因是酒精传感器的输出电压变化不像数字信号那么陡峭采样时间长一些读到的值更稳定。如果你要用硬件 I2C 驱动 OLED就在 I2C1 里打开标准模式引脚会被分配到 PB6 和 PB7。如果驱动代码用的是软件 I2C就不需要配置 I2C 外设只把 PB6、PB7 设为普通 GPIO 输出即可。这里最容易混乱一定要先确认代码里 OLED 驱动是硬件 I2C 还是软件模拟。时钟树部分一般可以按照 8MHz HSEPLL 倍频到 72MHz 来配置。如果用的是内部时钟频率上限可能到不了 72MHz建议先按低时钟跑起来。最后生成代码用 Keil 或者你选择的 IDE 编译烧录。3.3 最小可运行代码多次采样取平均先不要急着写 OLED 和报警第一步只做一件事读 ADC串口打印。这条链路通了后面所有显示和判断才有基础。下面是一个用 HAL 库写的多次采样取平均函数。uint16_t read_adc_avg(ADC_HandleTypeDef* hadc, uint8_t times) { uint32_t sum 0; for (uint8_t i 0; i times; i) { HAL_ADC_Start(hadc); if (HAL_ADC_PollForConversion(hadc, 10) HAL_OK) { sum HAL_ADC_GetValue(hadc); } HAL_ADC_Stop(hadc); HAL_Delay(1); } return (uint16_t)(sum / times); }主循环里可以这样用while (1) { uint16_t adc_value read_adc_avg(hadc1, 20); printf(ADC: %d\r\n, adc_value); HAL_Delay(500); }注意HAL_ADC_Start 和 HAL_ADC_Stop 这种写法对应单次转换模式。如果你在 CubeMX 里配置成连续转换模式就不需要每次启动和停止应该改成启动一次后持续读取。如果你不想用 printf 重定向也可以直接调用 HAL_UART_Transmit 把数值转成字符后通过串口发出去。关键不是用什么方式打印而是你能看到 ADC 值在变化。我在实测时一般会做这样一件事把传感器放在空气里观察 ADC 值然后靠近酒精瓶口观察 ADC 值是否明显跳升再移开观察数值是否回落。如果这个趋势清晰说明硬件链路基本正常可以进入下一步。如果数值纹丝不动或者乱跳先别改代码检查接线、供电和共地。4. 数据转换、阈值判断与报警逻辑核心在滤波与去抖4.1 从 ADC 原始值到电压值STM32F103 的 ADC 是 12 位满量程读取范围是 0 到 4095。如果参考电压是 3.3V那么电压值的计算就是float voltage (float)adc_value * 3.3f / 4095.0f;这一步有两个容易忽略的地方。第一不是所有开发板的参考电压都是准确的 3.3V。有的板子使用板载 LDO实际输出可能是 3.28V 或 3.32V。如果你要做精确换算用万用表量一下 3.3V 引脚再换成实际电压值。第二ADC 的值是相对参考电压的不是绝对值。也就是说如果供电电压偏低同样浓度的酒精产生的 ADC 值也会偏低可能影响判断。这也是为什么我不建议直接把“固定 ADC 阈值”烧死在代码里而是先测一个空气环境下的基准值再做相对判断。4.2 阈值和报警不要写死要做相对判断固定阈值的方式很简单但实际用起来很痛苦。因为传感器在洁净空气里的输出也会漂移可能早上是 800下午变成 900如果你写死 1000 作为阈值下午空气稍微波动就可能误报。我建议改成相对判断上电后先预热一段时间采集空气样本作为零点基线之后再根据当前 ADC 值和零点基线的差来判断是否超标。一个简化的参考代码如下#define BASELINE_SAMPLE_NUM 50 #define TRIGGER_OFFSET 80 uint16_t g_baseline 0; void calibrate_baseline(ADC_HandleTypeDef* hadc) { g_baseline read_adc_avg(hadc, BASELINE_SAMPLE_NUM); } uint8_t is_alcohol_exceeded(ADC_HandleTypeDef* hadc) { uint16_t cur read_adc_avg(hadc, 20); int32_t diff (int32_t)cur - (int32_t)g_baseline; if (diff TRIGGER_OFFSET) { return 1; } return 0; }这里 TRIGGER_OFFSET 是“当前值比空气基准值高多少就认为超标”的偏移量需要你实测后确定。不要照抄别人的值。如果你的环境里 ADC 波动在 30 以内偏移设成 80 就相对安全如果波动到 100偏移 80 必然误报。报警也不能只用一次判断。传感器信号是连续波动的如果有人只是从旁边走过去带起一阵风就可能让 ADC 值瞬时跳一下。更稳的做法是连续检测 N 次比如 5 次里超过阈值 3 次才真正进入报警状态。这就是工程上说的去抖。4.3 为什么开源代码里的“ppm”只能参考很多开源项目的 OLED 上会显示“酒精浓度xx ppm”看起来挺专业。但这个值是怎么来的一般是查 MQ-3 数据手册里的灵敏度特性曲线再结合厂家提供的经验公式算出来的。问题在于手册里的曲线是一条典型曲线不同批次、不同厂家的传感器差异很大再加上你的模块上有没有正确的负载电阻、供电电压是否一致都会让最终结果偏离“真实浓度”。而且 MQ-3 对所有还原性气体都有响应不是只针对酒精。所以那个“ppm”数字本质上是一个“换算出来的相对等级”用来演示和做趋势判断是可以的但不能当成精确定量结果。代码里通常会有一段类似float ppm 10.0 * pow(adc_ratio, -1.5)的拟合公式你可以保留显示但一定要在注释里或者在 README 里说明这是估测值。否则别人拿去用测出来和正规仪器不一样就会来质疑项目“不准确”。5. 显示、按键和对外接口把检测做成可操作设备5.1 OLED 显示策略OLED 项目里最常见的是 SSD1306 驱动的 0.96 寸屏I2C 接口只有两根线接起来非常方便。显示内容至少要包含当前 ADC 值或电压值、当前状态正常/超标、阈值偏移量、倒计时预热状态。如果一行放不下可以切换页面也可以只显示最重要的几个字段。刷新频率不要太高。OLED 全屏刷新一次需要不少时间如果你在 while 里一直调显示刷新CPU 大部分时间都在刷屏ADC 采样和按键响应都会变慢屏幕还会闪烁。我一般让 ADC 采样和状态判断跑在主循环前半段OLED 每隔 200 到 500 毫秒刷新一次数据区域而不是每毫秒都重画。如果遇到残影或局部数字重叠多半是刷新时没有先清掉原来的区域。SSD1306 驱动里可以只更新需要变化的区域比如把“ADC数值”所在的小矩形区域填充为黑色再画新值。5.2 按键设置阈值和参数保存按键看起来简单实际处理起来有个常见坑不去抖。机械按键按下和松开的一瞬间引脚电平会抖动几十毫秒如果不处理按一次可能触发好多次。最简单的去抖方式是检测到按键按下后延时 10 到 20 毫秒再读一次确认确实按下后再执行逻辑。高级一点可以写状态机但新手先把延时消抖跑通就够用。按键的功能可以这样设计按键功能按键1短按切换显示页面按键1长按进入阈值设置模式按键2在设置模式下增加阈值偏移按键3在设置模式下减少阈值偏移阈值调整好之后要保存。如果断电后恢复默认值每次都得重新校准非常麻烦。常用做法是把参数写入 STM32 内部 Flash 的一个空闲页每次启动时读取。也可以外接 AT24C02 EEPROM 芯片代码上就是 I2C 读写操作。开源项目里选择内部 Flash 更常见因为它不需要额外硬件但要小心不要写到程序代码区。正确做法是先查阅芯片 Flash 扇区划分一般的 F103C8 有多个 1KB 页选一个不用来存放固件的页来保存参数。5.3 串口、WiFi 等扩展方向很多开源项目在跑通本地显示后会想加一个 ESP8266 或 ESP32 把数据传到手机或服务器。思路没问题但不要一开始就做完整云端平台否则你会在网络协议和服务器部署上花大量时间反而忽略了检测系统本身。我建议先把串口通信打通。规定一个简单协议比如ALC:123 ADC:2046 STATE:NORMALSTM32 每隔 500 毫秒通过串口发送一帧数据电脑上用串口助手查看。这一步能稳定输出之后再让 ESP8266 接收串口数据转发到 MQTT 或 HTTP 服务。这样做的好处是每一层都能单独验证出问题时不至于前后端同时排查。如果你需要控制外部设备比如超标时切断某个阀门可以在 STM32 GPIO 上接继电器模块。但继电器线圈电流不小不要直接接在 STM32 引脚上要通过三极管或光耦驱动而且电路板电源要能承受继电器吸合时的电流波动。这个扩展方向适合做过基本硬件的人新手先不要急着上。6. 校准流程、开源项目落地和常见问题排查6.1 校准流程零点校准 跨度校准想让这类系统在固定环境里更可靠至少要做两步校准。第一步是零点校准。在一个洁净通风的环境里给系统上电让传感器预热 3 到 5 分钟然后采集 50 到 100 次 ADC 值求平均作为零点基线。这一步可以在每次开机时手动触发也可以做成上电自动校准。自动校准的缺点是如果环境本身已经有酒精或溶剂气体很可能把“有问题”的状态当成基线所以加一个“预热倒计时”和“校准确认按键”更合理。第二步是跨度校准。找一个已知浓度的酒精气体来源或者用一台已经校准的酒精测试仪做参照把传感器放在相同环境中记录不同浓度下的 ADC 值。然后在代码里做一阶线性拟合把 ADC 值映射到浓度等级。跨度校准能显著提高“相对测量”的可用性但不要追求完美拟合受传感器选择性和环境影响能做到“能区分低浓度、中浓度、高浓度”就已经达到这个项目的实用标准了。校准后的数据建议保存到 Flash 或 EEPROM。如果你只是把校准结果写在代码常量里每次改环境都要重新编译烧录很不方便。6.2 使用开源项目时怎么判断质量在 Gitee 或 GitHub 上搜“STM32 酒精检测”会看到很多项目。同名的也很多怎么判断能不能用先看项目文件结构。一个完整的项目应该包含固件源码、README、接线说明、原理图或 PCB 工程。如果只有一个 README 和几张效果图源码里没有实际工程文件那你想复现就要自己搭工程成本很高。再看源码里的关键部分。打开 ADC 读取函数看它有没有做多次采样和滤波打开报警逻辑看它是单次判断还是连续判断看有没有零点校准流程。如果这三点都没有项目很可能只适合“烧录后演示固定场景”不适合自己改装。还要看硬件细节。有的项目用的是 MQ-3有的用的是 MQ-303A有的用模拟输出有的只用数字输出 DO。这些都会影响代码逻辑。如果项目只用了 DO 做高电平报警那它其实没有浓度显示能力和你想要的“OLED 显示数值”不是一回事。最后看开源协议。有的项目允许学习和商用有的只允许学习。如果你只是学习影响不大如果要拿去毕设、比赛、产品原型或公司内部使用最好提前确认授权范围。6.3 常见问题和排查顺序跑通这类系统之后遇到的问题基本集中在下面几类。现象可能原因排查顺序串口没有输出波特率不对、引脚错、代码没重定向 printf先看串口助手再查 GPIO 配置ADC 值一直不变AO 接错、ADC 通道配置错、传感器供电异常用万用表测 AO 电压是否随浓度变化ADC 值跳动大未预热、采样次数少、供电不稳、地线接触不良延长预热提高滤波次数重新共地OLED 无显示I2C 地址不对、SDA/SCL 接反、未调通驱动先扫描 I2C 地址再确认引脚蜂鸣器频繁误响阈值太低、零点基线没校、电源波动引发干扰先看阈值偏移设置再检查基准值酒精浓度升高但读数下降传感器老化、供电电压不足、模块电位器调到极端先换电源测试再考虑换传感器排查顺序有一个通用原则先看现象再看输入最后改代码。很多问题不是代码逻辑而是硬件接线、供电、模块差异。我自己的排查习惯是先把 OLED 和蜂鸣器相关的代码暂时注释掉只保留“ADC 采样 串口打印”。因为这个最小链路能告诉我传感器是什么状态。如果串口打印的 ADC 值能随酒精靠近和远离而变化说明传感器和 STM32 的链路没问题再逐步把 OLED、按键、报警加回来。每次都只加一个模块出问题就能快速定位。如果屏幕显示值一直很高检查是不是模块电位器调得太敏感。MQ-3 模块上的蓝色电位器会改变数字输出 DO 的灵敏度同时也会影响一部分电路的工作点。把电位器逆时针调到中间位置再重新校准零点。如果传感器预热一晚后数值漂移很大先看供电。MQ-3 加热丝持续发热在面包板上长时间跑面包板插座氧化会导致接触电阻变化这也会让 ADC 值漂移。长期使用建议用洞洞板焊接或者用开发板加传感器模块的接插件不要长期依赖杜邦线。最后补一句这类项目真正做下来收获不是“我调好了 MQ-3”而是“我知道了模拟信号进入单片机之后有多容易受干扰也知道怎样用滤波、基线校准和连续判断让系统更可靠”。换一个传感器、换一块主控思路都一样先把输入读稳再谈显示、报警和通信。