STM32 进阶封神之路(二十):ADC 实战全攻略 —— 光敏 + 烟雾传感器采集 + 阈值报警(库函数 + 代码落地)
上一篇我们吃透了 ADC 的底层原理、核心参数和初始化流程,这一篇聚焦实战落地 —— 基于 STM32F103,结合光敏电阻(光照检测)和 MQ2 烟雾传感器(气体浓度检测),从硬件连接、代码解析、多通道采集,到中断优化、阈值报警,手把手带你实现 “模拟信号采集→数字转换→外设联动” 的完整闭环,所有代码基于提供的adc.h扩展,可直接编译运行!
本文覆盖 “单通道采集→多通道切换→中断非阻塞采集→阈值报警” 全场景,同时解析实战中的关键细节和避坑要点,让你不仅 “会配置 ADC”,更能 “灵活应用 ADC” 到传感器数据采集项目中!
一、实战准备:硬件环境与核心需求
1. 硬件清单
- 主控:STM32F103C8T6 最小系统板;
- 传感器:光敏电阻模块(电压型)、MQ2 烟雾传感器模块(电压型);
- 外设:LED(指示光照状态)、蜂鸣器(烟雾报警);
- 辅助硬件:10KΩ 下拉电阻(光敏电阻分压)、USB-TTL 模块(串口打印)、杜邦线、面包板。
2. 核心实战需求
- 单通道采集:通过 ADC1_CH5(PA5)采集光敏电阻电压,转换为光照强度;
- 多通道采集:切换至 ADC1_CH11(PC1)采集 MQ2 烟雾传感器电压,转换为烟雾浓度;
- 阈值联动:光照过暗时 LED 点亮,烟雾浓度超标时蜂鸣器报警;
- 数据输出:串口打印采集的 ADC 值和对应电压值,实时监控状态;
- 优化拓展:实现 ADC 中断非阻塞采集,避免占用主循环资源。
3. 关键硬件连接(核心!接线错误导致采集失败)
(1)传感器与 STM32 连接
表格
| 模块 | 传感器引脚 | STM32 引脚 | 连接说明 |
|---|---|---|---|
| 光敏电阻模块 | VCC | 3.3V | 传感器供电(避免 5V,防止 ADC 引脚过载) |
| 光敏电阻模块 | GND | GND | 共地(采集稳定的前提) |
| 光敏电阻模块 | AO(模拟输出) | PA5(ADC1_CH5) | 模拟信号输入,需配置为模拟输入模式 |
| MQ2 烟雾传感器 | VCC | 5V | MQ2 需 5V 供电才能正常工作 |
| MQ2 烟雾传感器 | GND | GND | 共地 |
| MQ2 烟雾传感器 | AO(模拟输出) | PC1(ADC1_CH11) | 模拟信号输入,需配置为模拟输入模式 |
(2)外设与 STM32 连接
表格
| 外设 | 引脚 | STM32 引脚 | 连接说明 |
|---|---|---|---|
| LED | 正极(串 1KΩ 电阻) | PB0 | 推挽输出,低电平点亮 |
| LED | 负极 | GND | |
| 蜂鸣器 | VCC | 3.3V | 有源蜂鸣器供电 |
| 蜂鸣器 | GND | PB1 | 推挽输出,低电平触发报警 |
(3)分压电路说明(光敏电阻)
光敏电阻的阻值随光照强度变化(光照越强,阻值越小),需通过分压电路将阻值变化转换为电压变化,才能被 ADC 采集:
- 电路结构:3.3V → 光敏电阻 → PA5 → 10KΩ 下拉电阻 → GND;
- 电压变化:光照越强→光敏电阻阻值越小→PA5 引脚电压越高→ADC 采样值越大;
- 关键注意:必须串联下拉电阻,否则 PA5 引脚悬空,无法采集稳定电压。
二、核心代码解析:从配置到采集全流程
基于提供的adc.h代码,补充外设初始化、中断配置、主函数逻辑,形成完整可运行代码:
1. 头文件与全局变量定义
c
运行
#include "adc.h"
#include "stm32f10x.h"
#include <stdio.h>
// ADC采样值全局变量
uint16_t ADC_LIGHT_VALUE = 0; // 光敏电阻采样值
uint16_t ADC_MQ2_VALUE = 0; // MQ2烟雾传感器采样值
// 阈值定义(可根据实际硬件调整)
#define Light_threshold 2000 // 光照阈值(采样值≤2000→光照过暗)
#define MQ2_threshold 3000 // 烟雾阈值(采样值≥3000→浓度超标)
// 串口发送函数(printf重定向用)
void USART1_SendByte(uint8_t data) {
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, data);
}
// printf重定向
int fputc(int ch, FILE *f) {
USART1_SendByte((uint8_t)ch);
return ch;
}
// 串口初始化(115200bps 8N1,用于打印采集数据)
void USART1_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
// PA9(TX)复用推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10(RX)浮空输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStruct);
USART_Cmd(USART1, ENABLE);
}
2. 外设初始化(LED + 蜂鸣器)
c
运行
// LED初始化(PB0,推挽输出)
void LED_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_SetBits(GPIOB, GPIO_Pin_0); // 初始熄灭
}
// 蜂鸣器初始化(PB1,推挽输出)
void Beep_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
GPIO_SetBits(GPIOB, GPIO_Pin_1); // 初始关闭(高电平)
}
// LED控制函数(提供给Light_Handle调用)
void LED4_On(void) {
GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 低电平点亮
}
void LED4_Off(void) {
GPIO_SetBits(GPIOB, GPIO_Pin_0); // 高电平熄灭
}
// 蜂鸣器控制函数(提供给MQ2_Handle调用)
void Beep_ON(void) {
GPIO_ResetBits(GPIOB, GPIO_Pin_1); // 低电平触发
}
void Beep_OFF(void) {
GPIO_SetBits(GPIOB, GPIO_Pin_1); // 高电平关闭
}
3. ADC 核心配置函数解析(ADC_Config)
提供的ADC_Config函数已完成 ADC 初始化核心流程,重点解析关键细节:
c
运行
void ADC_Config(void) {
Light_GPIO(); // 初始化光敏电阻GPIO(PA5模拟输入)
MQ2_GPIO(); // 初始化MQ2 GPIO(PC1模拟输入)
// 1. 开启ADC1时钟(APB2总线)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
// 2. ADC时钟分频(APB2=72MHz→ADC时钟=12MHz,≤14MHz上限)
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 3. 定义ADC初始化结构体
ADC_InitTypeDef ADC_InitStruct={0};
// 4. 结构体参数配置(关键!决定ADC工作模式)
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; // 单次转换模式(启动一次,转换一次)
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐(12位数据存储在bit0~bit11)
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发(手动启动转换)
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; // ADC1独立工作模式
ADC_InitStruct.ADC_NbrOfChannel = 1; // 单次转换1个通道
ADC_InitStruct.ADC_ScanConvMode = DISABLE; // 非扫描模式(单通道无需扫描)
// 5. 写入配置参数到ADC1
ADC_Init(ADC1,&ADC_InitStruct);
// 6. 使能ADC1
ADC_Cmd(ADC1,ENABLE);
// 7. ADC校准(提高采集精度,不可省略)
ADC_ResetCalibration(ADC1); // 复位校准寄存器
while(ADC_GetResetCalibrationStatus(ADC1)); // 等待复位完成
ADC_StartCalibration(ADC1); // 启动校准
while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成
}
4. 单通道采集函数解析
(1)光敏电阻采集(ADC_Read_Light)
c
运行
void ADC_Read_Light(void) {
// 1. 配置采集通道:ADC1_CH5(PA5),转换顺序1,采样时间28.5个时钟周期
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_28Cycles5);
// - 采样时间选择:28.5个周期兼顾精度和速度,避免过短导致采样不稳定
// 2. 软件启动ADC转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
// 3. 等待转换完成(查询EOC标志位)
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)==RESET);
// 4. 读取转换结果(12位采样值,范围0~4095)
ADC_LIGHT_VALUE=ADC_GetConversionValue(ADC1);
// 5. 清除转换完成标志位(读取ADC_DR后自动清除,此处手动清除更稳妥)
ADC_ClearFlag(ADC1, ADC_FLAG_EOC);
// 6. 串口打印采样值和对应电压值(转换公式:电压=采样值×3.3V/4096)
float light_voltage = (ADC_LIGHT_VALUE / 4096.0) * 3.3;
printf("光照强度 - ADC值:%d,电压:%.2fV\r\n", ADC_LIGHT_VALUE, light_voltage);
// 7. 光照阈值判断,控制LED
Light_Handle();
}
(2)MQ2 烟雾传感器采集(ADC_Read_MQ2)
c
运行
void ADC_Read_MQ2(void) {
// 1. 切换通道:ADC1_CH11(PC1),其他参数与光敏电阻一致
ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 1, ADC_SampleTime_28Cycles5);
// 2. 启动转换
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
// 3. 等待转换完成
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)==RESET);
// 4. 读取采样值
ADC_MQ2_VALUE=ADC_GetConversionValue(ADC1);
// 5. 清除标志位
ADC_ClearFlag(ADC1, ADC_FLAG_EOC);
// 6. 串口打印
float mq2_voltage = (ADC_MQ2_VALUE / 4096.0) * 3.3;
printf("烟雾浓度 - ADC值:%d,电压:%.2fV\r\n", ADC_MQ2_VALUE, mq2_voltage);
// 7. 烟雾阈值判断,控制蜂鸣器
MQ2_Handle();
}
5. 阈值处理函数(Light_Handle+MQ2_Handle)
c
运行
// 光照阈值处理:ADC值≤2000→光照过暗→LED点亮
void Light_Handle(void) {
if(ADC_LIGHT_VALUE <= Light_threshold) {
LED4_On();
printf("光照过暗,LED已点亮\r\n");
} else {
LED4_Off();
printf("光照充足,LED已熄灭\r\n");
}
}
// 烟雾阈值处理:ADC值≥3000→浓度超标→蜂鸣器报警
void MQ2_Handle(void) {
if(ADC_MQ2_VALUE >= MQ2_threshold) {
Beep_ON();
printf("烟雾浓度超标!蜂鸣器报警\r\n");
} else {
Beep_OFF();
printf("烟雾浓度正常,蜂鸣器关闭\r\n");
}
}
6. 主函数:实现循环采集与多通道切换
c
运行
int main(void) {
// 初始化外设:串口(打印)→ LED→ 蜂鸣器→ ADC
USART1_Init();
LED_Init();
Beep_Init();
ADC_Config();
printf("ADC传感器采集系统初始化成功!\r\n");
printf("阈值配置:光照过暗≤%d(ADC值),烟雾超标≥%d(ADC值)\r\n", Light_threshold, MQ2_threshold);
printf("=======================================\r\n\r\n");
while(1) {
// 采集光敏电阻数据(间隔1秒)
ADC_Read_Light();
delay_ms(1000);
// 采集MQ2烟雾传感器数据(间隔1秒)
ADC_Read_MQ2();
delay_ms(1000);
printf("=======================================\r\n\r\n");
}
}
// 简单延时函数(控制采集间隔)
void delay_ms(uint32_t ms) {
uint32_t i, j;
for(i = 0; i < ms; i++) {
for(j = 0; j < 1000; j++);
}
}
三、进阶优化:ADC 中断非阻塞采集(避免主循环阻塞)
上述代码采用 “查询式采集”,采集过程中主循环会阻塞等待转换完成。优化为 “中断式采集” 后,ADC 转换完成后触发中断,主循环可并行执行其他任务,效率更高:
1. 中断配置扩展(添加到ADC_Config函数末尾)
c
运行
// ADC中断配置(转换完成触发中断)
void ADC_Interrupt_Config(void) {
NVIC_InitTypeDef NVIC_InitStruct;
// 使能ADC1转换完成中断
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE);
// 配置NVIC中断优先级
NVIC_InitStruct.NVIC_IRQChannel = ADC1_2_IRQn; // ADC1和ADC2共享中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
2. 中断服务函数(处理转换结果)
c
运行
// 定义当前采集通道标志
typedef enum {
ADC_CHANNEL_LIGHT = 0,
ADC_CHANNEL_MQ2
} ADC_Channel_TypeDef;
ADC_Channel_TypeDef current_channel = ADC_CHANNEL_LIGHT;
// ADC1/2中断服务函数
void ADC1_2_IRQHandler(void) {
float voltage;
// 检查ADC1转换完成中断标志
if(ADC_GetITStatus(ADC1, ADC_IT_EOC) != RESET) {
if(current_channel == ADC_CHANNEL_LIGHT) {
// 读取光敏电阻采样值
ADC_LIGHT_VALUE = ADC_GetConversionValue(ADC1);
voltage = (ADC_LIGHT_VALUE / 4096.0) * 3.3;
printf("光照强度(中断)- ADC值:%d,电压:%.2fV\r\n", ADC_LIGHT_VALUE, voltage);
Light_Handle();
// 切换到MQ2通道,下一次中断采集烟雾数据
ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 1, ADC_SampleTime_28Cycles5);
current_channel = ADC_CHANNEL_MQ2;
} else {
// 读取MQ2采样值
ADC_MQ2_VALUE = ADC_GetConversionValue(ADC1);
voltage = (ADC_MQ2_VALUE / 4096.0) * 3.3;
printf("烟雾浓度(中断)- ADC值:%d,电压:%.2fV\r\n", ADC_MQ2_VALUE, voltage);
MQ2_Handle();
// 切换回光敏电阻通道
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_28Cycles5);
current_channel = ADC_CHANNEL_LIGHT;
}
// 清除中断标志位
ADC_ClearITPendingBit(ADC1, ADC_IT_EOC);
// 启动下一次转换(连续采集)
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
}
3. 中断模式主函数
c
运行
int main(void) {
USART1_Init();
LED_Init();
Beep_Init();
ADC_Config();
ADC_Interrupt_Config(); // 初始化ADC中断
printf("ADC中断采集系统初始化成功!\r\n");
printf("=======================================\r\n\r\n");
// 启动第一次转换(光敏电阻通道)
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_28Cycles5);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(1) {
// 主循环可并行执行其他任务(如LED流水灯、串口通信)
// 此处仅做示例,无其他任务
}
}
四、实验现象与数据验证
1. 正常环境下的串口输出(查询式采集)
plaintext
ADC传感器采集系统初始化成功!
阈值配置:光照过暗≤2000(ADC值),烟雾超标≥3000(ADC值)
=======================================
光照强度 - ADC值:3500,电压:2.88V
光照充足,LED已熄灭
烟雾浓度 - ADC值:1500,电压:1.23V
烟雾浓度正常,蜂鸣器关闭
=======================================
光照强度 - ADC值:3480,电压:2.86V
光照充足,LED已熄灭
烟雾浓度 - ADC值:1520,电压:1.25V
烟雾浓度正常,蜂鸣器关闭
=======================================
2. 特殊场景验证
(1)光照过暗场景
用手遮挡光敏电阻,串口输出:
plaintext
光照强度 - ADC值:1800,电压:1.48V
光照过暗,LED已点亮
- 现象:LED 点亮,符合阈值判断逻辑。
(2)烟雾超标场景
向 MQ2 传感器吹气(模拟烟雾),串口输出:
plaintext
烟雾浓度 - ADC值:3200,电压:2.62V
烟雾浓度超标!蜂鸣器报警
- 现象:蜂鸣器持续报警,移开传感器后恢复正常。
3. 中断模式输出特点
- 无需手动调用采集函数,中断自动触发,主循环无阻塞;
- 采集速度更快,数据更新更及时,适合高频采集场景。
五、ADC 实战避坑指南(10 + 高频错误)
1. 采集值为 0 或 4095(满量程),无变化
- 原因 1:GPIO 模式配置错误(未设为模拟输入模式);解决:确保
Light_GPIO和MQ2_GPIO中GPIO_Mode = GPIO_Mode_AIN,而非普通输入模式; - 原因 2:传感器接线错误(VCC/GND 接反或未接);解决:重新检查传感器供电和共地,确保 VCC 接 3.3V(光敏)/5V(MQ2),GND 共地;
- 原因 3:分压电路缺失(光敏电阻未串联下拉电阻);解决:在光敏电阻和 GND 之间串联 10KΩ 下拉电阻,否则 PA5 引脚悬空,采集值固定为 0 或 4095。
2. 采样值波动过大(跳变超过 100)
- 原因 1:采样时间过短(未给电容充足充电时间);解决:将采样时间从
ADC_SampleTime_1Cycles5改为ADC_SampleTime_28Cycles5或更长; - 原因 2:电源纹波过大(传感器供电不稳定);解决:在传感器 VCC 引脚旁并联 0.1μF 陶瓷电容,滤除高频纹波;
- 原因 3:环境干扰(传感器靠近电源模块或高频信号线);解决:重新布局面包板,传感器远离 5V 电源和 USB 线。
3. 多通道采集时数据混淆(光照值与烟雾值颠倒)
- 原因:通道配置错误(
ADC_Channel_5与ADC_Channel_11混淆);解决:确保ADC_Read_Light配置ADC_Channel_5(PA5),ADC_Read_MQ2配置ADC_Channel_11(PC1),对应传感器接线正确。
4. 中断采集时中断不响应
- 原因 1:未使能 ADC 中断(
ADC_ITConfig未调用);解决:在ADC_Interrupt_Config中调用ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE); - 原因 2:NVIC 未配置 ADC 中断通道;解决:配置
NVIC_IRQChannel = ADC1_2_IRQn(ADC1 和 ADC2 共享中断通道); - 原因 3:未清除中断标志位;解决:在中断服务函数中调用
ADC_ClearITPendingBit(ADC1, ADC_IT_EOC)。
5. 采样值与实际电压不符(误差超过 0.2V)
- 原因 1:未进行 ADC 校准;解决:确保
ADC_Config中校准步骤完整(复位校准→启动校准→等待完成); - 原因 2:参考电压不稳定(VDDA 未滤波);解决:在 STM32 VDDA 引脚(若有)旁并联 1μF 电解电容,提高参考电压稳定性;
- 原因 3:数据对齐方式错误(左对齐未修正);解决:若改为左对齐,需将采样值右移 4 位(
ADC_GetConversionValue(ADC1) >> 4)再计算电压。
六、总结:ADC 实战核心要点与进阶方向
1. 核心要点回顾
- ADC 实战核心流程:GPIO 模拟输入配置→ADC 时钟分频→模式配置→校准→通道配置→启动转换→读取值→数据处理;
- 关键函数:
ADC_RegularChannelConfig(通道配置)、ADC_SoftwareStartConvCmd(启动转换)、ADC_GetConversionValue(读取采样值); - 多通道采集:通过切换
ADC_Channel_x实现,无需重新初始化 ADC; - 避坑核心:GPIO 模式正确、采样时间充足、校准不可省略、共地良好、分压电路完整。
2. 进阶学习方向
- 数据滤波:通过滑动平均算法(连续采集 5 次取平均值)减小采样波动;
- 多通道 DMA 采集:使用 DMA 直接传输 ADC 采样值到内存,彻底解放 CPU;
- 内部通道采集:采集 STM32 内置温度传感器(ADC1_CH16)和参考电压(ADC1_CH17);
- 低功耗采集:结合 ADC 休眠模式,定时唤醒采集,降低系统功耗;
- 高精度采集:外接基准电压芯片(如 LM4040),替代 VDDA 作为参考电压,提高采集精度。
掌握 ADC 实战后,你已具备嵌入式系统 “模拟信号采集” 的核心能力,可应用于温湿度传感器(模拟型)、压力传感器、音频采集等场景。下一篇我们将学习 SPI 通信,实现 STM32 与 W25Q64 闪存芯片的数据存储,进一步拓展嵌入式系统的存储能力!