STM32 进阶封神之路(二十):ADC 实战全攻略 —— 光敏 + 烟雾传感器采集 + 阈值报警(库函数 + 代码落地)

0 阅读15分钟

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 引脚连接说明
光敏电阻模块VCC3.3V传感器供电(避免 5V,防止 ADC 引脚过载)
光敏电阻模块GNDGND共地(采集稳定的前提)
光敏电阻模块AO(模拟输出)PA5(ADC1_CH5)模拟信号输入,需配置为模拟输入模式
MQ2 烟雾传感器VCC5VMQ2 需 5V 供电才能正常工作
MQ2 烟雾传感器GNDGND共地
MQ2 烟雾传感器AO(模拟输出)PC1(ADC1_CH11)模拟信号输入,需配置为模拟输入模式
(2)外设与 STM32 连接

表格

外设引脚STM32 引脚连接说明
LED正极(串 1KΩ 电阻)PB0推挽输出,低电平点亮
LED负极GND
蜂鸣器VCC3.3V有源蜂鸣器供电
蜂鸣器GNDPB1推挽输出,低电平触发报警
(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_GPIOMQ2_GPIOGPIO_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_5ADC_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 闪存芯片的数据存储,进一步拓展嵌入式系统的存储能力!