基于GD32VW553的MQ-3酒精传感器驱动移植与数据读取实战

最近在做一个环境监测的小项目,需要检测空气中的酒精浓度,正好手头有MQ-3传感器和GD32VW553开发板。MQ-3这个模块很常见,价格也便宜,但很多新手朋友在把它接到自己的开发板上时,总会遇到各种问题——引脚怎么接?ADC怎么配?数据怎么读?今天我就以GD32VW553这块RISC-V开发板为例,手把手带你走一遍完整的驱动移植流程,从硬件连接到代码编写,再到数据读取和转换,保证你看完就能用起来。

1. 认识MQ-3酒精传感器

MQ-3传感器核心是一层二氧化锡(SnO₂)气敏材料。这东西在干净空气里电阻很大(电导率低),可一旦周围有酒精蒸汽,它的电阻就会变小,而且酒精浓度越高,电阻变得越小。模块内部已经把这电阻变化转换成了电压信号,我们直接读取就行。

模块上有两个输出口:

  • AO(Analog Output):模拟电压输出,电压值会随酒精浓度变化,我们需要用ADC(模数转换器)来读取。
  • DO(Digital Output):数字开关量输出,模块内部用一个比较器(LM393)把AO电压和一个可调阈值比较,高于阈值就输出高电平,低于就输出低电平。这个阈值可以通过模块上的电位器调节,相当于一个灵敏度旋钮。

简单来说,AO给你的是“具体有多浓”的连续信息,DO给你的是“有没有超标”的开关信息。

模块的基本参数如下:

参数值
工作电压3.3V ~ 5V
工作电流约150mA
输出接口AO(模拟量)、DO(数字量)
读取方式ADC(读AO)、GPIO输入(读DO)
接口类型4 Pin 2.54mm排针

注意:MQ-3对酒精很敏感,但也会受汽油、烟雾等干扰,所以它适合做定性或半定量检测,比如酒驾预检、酒精泄露报警,不适合做精确的实验室级浓度分析。

2. 硬件连接与引脚规划

咱们先搞定接线。MQ-3模块就四根线,GD32VW553开发板上的引脚资源很丰富,这里我选择用PB0和PB2,原因很简单:PB0支持ADC功能,PB2是个普通GPIO,用起来方便。

具体的连接关系看下面这个表:

传感器模块引脚开发板引脚说明
VCC5V0接5V电源,模块内部有稳压,3.3V也能工作,但5V驱动能力更强。
GNDGND共地,必须接!
DOPB2数字输出,接GPIO输入引脚。
AOPB0模拟输出,必须接带ADC功能的引脚。

这里有个细节:PB0在GD32VW553上对应的ADC通道是 ADC_CHANNEL_8,这个信息等会儿写代码时会用到,先记一下。

接线时,建议用杜邦线直接连接,确保接触牢固。接好后,可以先给开发板上电,看看MQ-3模块上的电源指示灯是否亮起。

3. 新建驱动文件与工程配置

硬件连好了,接下来在代码里给它安个“家”。一个好的习惯是为每个外设模块创建独立的驱动文件,这样代码结构清晰,以后移植到别的项目也方便。

3.1 创建驱动文件

在你的工程目录下(比如 User 或 BSP 文件夹里),新建两个文件:

  • bsp_mq3.c
  • bsp_mq3.h

bsp 是“板级支持包”的缩写,这种命名方式很常见。

3.2 添加头文件路径

为了让编译器能找到我们新建的 bsp_mq3.h 文件,需要把它的所在目录添加到工程的包含路径(Include Paths)里。以Keil MDK为例:

  1. 点击魔术棒按钮(Options for Target)。
  2. 选择 C/C++ 选项卡。
  3. 在 Include Paths 一栏,点击添加按钮,然后找到并选中存放 bsp_mq3.h 的文件夹。

这一步很关键,不然编译时会报错“找不到头文件”。

4. 编写驱动代码(bsp_mq3.h)

头文件主要用来做宏定义和函数声明,相当于一个“使用说明书”。咱们打开 bsp_mq3.h,开始编写。

#ifndef BSP_CODE_BSP_MQ3_H_
#define BSP_CODE_BSP_MQ3_H_

#include "gd32vw55x.h"
#include "systick.h"

// 为了方便,统一延时函数接口
#ifndef delay_ms
#define delay_ms(x)    delay_1ms(x)
#endif
#ifndef delay_us
#define delay_us(x)    delay_1us(x)
#endif

// 一个宏搞定所有相关时钟的使能,后面初始化函数里直接调用就行
#define MODULE_BSP_RCU_ENABLE()   \
        rcu_periph_clock_enable(BSP_ADC_GPIO_RCU); \
        rcu_periph_clock_enable(BSP_DO_GPIO_RCU); \
        rcu_periph_clock_enable(BSP_ADC_RCU);

/* 引脚与ADC通道定义 */
// PB0 - ADC功能
#define BSP_ADC_GPIO_RCU     RCU_GPIOB
#define BSP_ADC_GPIO_PORT    GPIOB
#define BSP_ADC_GPIO_PIN     GPIO_PIN_0

#define BSP_ADC_RCU         RCU_ADC
#define BSP_ADC             ADC
#define BSP_ADC_CHANNEL     ADC_CHANNEL_8  // 重点!PB0对应通道8

// PB2 - 数字输入功能
#define BSP_DO_GPIO_RCU     RCU_GPIOB
#define BSP_DO_GPIO_PORT    GPIOB
#define BSP_DO_GPIO_PIN     GPIO_PIN_2

// 读取DO引脚电平的快捷方式
#define MQ_DO   gpio_input_bit_get(BSP_DO_GPIO_PORT, BSP_DO_GPIO_PIN)

// ADC采样次数,多次采样取平均可以滤除一些毛刺
#define SAMPLES   5

/* 函数声明 */
void Module_BSP_Init(void);               // 初始化函数
int Get_Adc_MQ3_Value(void);              // 获取ADC原始值
int Get_MQ3_Percentage_value(void);       // 获取浓度百分比
char Get_MQ3_DO(void);                    // 获取DO引脚状态

#endif /* BSP_CODE_BSP_MQ3_H_ */

头文件里把引脚、通道、时钟这些硬件相关的信息都用宏定义好了,以后万一要换引脚,只改这里就行,非常方便。ADC_CHANNEL_8 这个定义就是根据GD32VW553的数据手册来的,PB0固定对应通道8。

5. 编写驱动代码(bsp_mq3.c)

这是重头戏,包含了所有具体的操作逻辑。咱们分函数来看。

5.1 初始化函数 Module_BSP_Init

这个函数负责配置GPIO和ADC,让硬件准备好工作。

#include "bsp_mq3.h"
#include "stdio.h"

void Module_BSP_Init(void)
{
    /* 第一步:打开时钟开关 */
    MODULE_BSP_RCU_ENABLE(); // 这个宏一次使能了GPIOB和ADC的时钟

    /* 第二步:配置DO引脚(PB2)为输入模式 */
    // GPIO_MODE_INPUT是输入模式,GPIO_PUPD_NONE表示不上拉也不下拉
    gpio_mode_set(BSP_DO_GPIO_PORT, GPIO_MODE_INPUT, GPIO_PUPD_NONE, BSP_DO_GPIO_PIN);

    /* 第三步:配置ADC时钟和引脚 */
    // ADC时钟设为系统时钟(PCLK2)的4分频,保证ADC工作在合适频率
    adc_clock_config(ADC_ADCCK_PCLK2_DIV4);

    // 将PB0配置为模拟输入模式,这是ADC采集所必需的
    gpio_mode_set(BSP_ADC_GPIO_PORT, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, BSP_ADC_GPIO_PIN);

    /* 第四步:配置ADC工作参数 */
    adc_special_function_config(ADC_SCAN_MODE, ENABLE); // 使能扫描模式(虽然我们只用一个通道)
    adc_data_alignment_config(ADC_DATAALIGN_RIGHT);     // 数据右对齐,符合我们的阅读习惯
    adc_resolution_config(ADC_RESOLUTION_12B);          // 12位分辨率,ADC值范围0-4095

    // 规则组通道数设为1(因为我们只用了通道8)
    adc_channel_length_config(ADC_ROUTINE_CHANNEL, 1);
    // 禁用外部触发,使用软件触发启动转换
    adc_external_trigger_config(ADC_ROUTINE_CHANNEL, EXTERNAL_TRIGGER_DISABLE);
    adc_software_trigger_enable(ADC_ROUTINE_CHANNEL); // 使能软件触发

    /* 最后,使能ADC模块 */
    adc_enable();
}

初始化顺序很重要:先开时钟,再配引脚,最后设置ADC参数并开启。GPIO_MODE_ANALOG 这个模式专门用于ADC引脚,它会把内部的上拉、下拉电阻都断开,让引脚直接连接到ADC内部。

5.2 核心ADC读取函数 ADC_GET

这是一个静态函数(只在当前文件内有效),负责执行一次完整的ADC转换。

static int ADC_GET(void)
{
    int gAdcResult = 0;
    int timeOut = 1000; // 超时计数,防止卡死

    // 1. 指定要采样的通道(通道8)和采样时间
    adc_routine_channel_config(0, BSP_ADC_CHANNEL, ADC_SAMPLETIME_14POINT5);
    // 2. 软件触发,启动一次转换
    adc_software_trigger_enable(ADC_ROUTINE_CHANNEL);

    // 3. 等待转换完成标志位
    while(!adc_flag_get(ADC_FLAG_EOC) && timeOut--)
    {
        delay_us(1); // 稍作等待
    }

    // 4. 清除标志位,为下次转换做准备
    adc_flag_clear(ADC_FLAG_EOC);

    // 如果超时了,说明转换可能出了问题
    if(!timeOut)
    {
        printf("ADC_GET Failed!!!\r\n");
        return 0;
    }

    // 5. 读取转换结果
    gAdcResult = adc_routine_data_read();
    return gAdcResult;
}

这里用 ADC_SAMPLETIME_14POINT5 表示采样周期是14.5个ADC时钟周期,采样时间稍长一点,结果更稳定,适合MQ-3这种变化相对慢的信号。

5.3 获取平均ADC值 Get_Adc_MQ3_Value

单次ADC读数可能受干扰跳动,咱们采样5次取个平均值。

int Get_Adc_MQ3_Value(void)
{
    uint32_t Data = 0;
    int i;

    for(i = 0; i < SAMPLES; i++)
    {
        Data += ADC_GET(); // 累加5次采样值
        delay_ms(5);       // 每次采样间隔5ms,让传感器有稳定时间
    }

    Data = Data / SAMPLES; // 计算平均值
    return Data;
}

delay_ms(5) 这个间隔不是随便写的。MQ-3传感器的响应需要一点时间,连续快速采样读到的可能是同一个值,间隔几毫秒能让读数更真实地反映环境变化。

5.4 计算浓度百分比 Get_MQ3_Percentage_value

ADC读出来是0-4095的数字,咱们把它转换成0-100%的百分比,更直观。

int Get_MQ3_Percentage_value(void)
{
    int adc_max = 4095; // 12位ADC的最大值
    int adc_new = 0;
    int Percentage_value = 0;

    adc_new = Get_Adc_MQ3_Value(); // 获取平均ADC值

    // 核心转换公式:(当前值 / 最大值) * 100%
    Percentage_value = ((float)adc_new / (float)adc_max) * 100.f;
    return Percentage_value;
}

注意,这里用了强制类型转换 (float),是为了避免整数除法丢失小数精度。这个百分比是相对于ADC量程的,可以近似理解为“传感器输出电压占参考电压的比例”,数值越大,表示检测到的酒精浓度信号越强。

5.5 读取数字输出 Get_MQ3_DO

这个最简单,就是读取一个GPIO引脚的电平。

char Get_MQ3_DO(void)
{
    if( MQ_DO == 0 ) // MQ_DO就是前面定义的宏,直接读取PB2电平
    {
        return 0; // 低电平,表示未检测到超过阈值的酒精
    }
    else
    {
        return 1; // 高电平,表示检测到酒精浓度超过设定阈值
    }
}

DO输出是0还是1,完全取决于模块上那个蓝色电位器的位置。顺时针拧,阈值提高,变得更“迟钝”;逆时针拧,阈值降低,变得更“灵敏”。你可以根据实际需要调整。

6. 在主函数中调用与测试

驱动写好了,最后就是在主程序里用起来。打开你的 main.c 文件。

#include "gd32vw55x.h"
#include "systick.h"
#include <stdio.h>
#include "main.h"
#include "gd32vw553h_eval.h"
#include "bsp_mq3.h" // 包含我们自己的驱动头文件

int main(void)
{
    // ... 系统时钟、串口、LED等初始化代码(你的工程里应该已有)...
    // 这里假设你已经配置好了系统滴答定时器(systick)和串口(printf重定向)

    printf("\r\n=== MQ-3 Alcohol Sensor Test ===\r\n");

    // 初始化MQ-3传感器(配置GPIO和ADC)
    Module_BSP_Init();
    printf("MQ3 Sensor Initialization Complete\r\n");

    // 主循环,每隔500ms读取并打印一次数据
    while(1)
    {
        printf("\n"); // 换行,让每次输出更清晰
        printf("MQ3 ADC Value  = %d\r\n", Get_Adc_MQ3_Value());
        printf("MQ3 Percentage = [%d%%]\r\n", Get_MQ3_Percentage_value());
        printf("MQ3 DO Status  = %d\r\n", Get_MQ3_DO());

        delay_ms(500); // 延时500ms
    }
}

编译、下载程序到GD32VW553开发板。打开串口调试助手(比如XCOM、SecureCRT),设置好波特率(和你工程里配置的一致,比如115200),就能看到源源不断的数据输出了。

正常现象应该是这样的:

  • 在清洁空气中,ADC值和百分比会比较低(比如几十到几百,对应百分之几)。
  • 向传感器附近喷洒少量酒精(或用棉签沾酒精靠近),ADC值和百分比会迅速上升。
  • DO状态平时为0,当浓度超过你设定的阈值时,会跳变为1。

7. 调试与常见问题

如果你发现数据不对,别急,按下面几步排查:

  1. 没有数据输出?

    • 检查串口接线(TX、RX是否接反)、波特率是否匹配。
    • 确认 printf 已正确重定向到串口。
  2. ADC值始终为0或4095?

    • 为0:检查PB0(AO)接线是否松动或接错。用万用表测量AO引脚对地电压,在空气中应该有一个基础电压(比如1V左右),靠近酒精时会升高。如果电压为0,可能是传感器或供电问题。
    • 为4095(满量程):检查ADC参考电压。GD32VW553的ADC参考电压通常是VDDA(接3.3V)。如果AO引脚电压接近或超过3.3V,就会读到4095。MQ-3在5V供电下,AO输出电压可能超过3.3V,这时需要在AO和PB0之间串联一个分压电阻(比如1kΩ和2kΩ),将电压降到3.3V以内。
  3. DO状态不变化?

    • 调整模块上的蓝色电位器。逆时针旋转(通常)是降低阈值,让模块更灵敏,更容易输出高电平。
  4. 数据跳动厉害?

    • 尝试增加 SAMPLES 的数值(比如从5改到10),或增加 Get_Adc_MQ3_Value 函数中 delay_ms(5) 的延时。
    • 检查电源是否稳定。MQ-3工作电流有150mA,如果电源带载能力不足,电压会被拉低,导致读数不稳。确保开发板的5V输出足够。

这个驱动框架我已经在几个小项目里用过了,挺稳定的。关键在于理解ADC的配置流程和传感器数据的处理方式。掌握了MQ-3,其他类似的模拟输出传感器(比如MQ-2、MQ-135)的驱动也就大同小异了,无非是换换引脚和校准方式。希望这篇教程能帮你顺利搞定GD32VW553上的第一个传感器驱动。

更多推荐