SGP30气体传感器驱动移植实战:基于CW32F030C8T6的TVOC与eCO2甲醛检测

最近在做一个室内空气质量监测的小项目,需要检测甲醛和总挥发性有机物(TVOC),选来选去,最终锁定了SGP30这款传感器。它集成度高,输出信号已经校准好,用起来比较省心。不过,网上关于CW32单片机驱动SGP30的资料不多,移植过程踩了几个坑。今天我就把整个驱动移植到CW32F030C8T6开发板上的过程,手把手分享给大家,希望能帮你快速上手。

这篇文章适合正在使用CW32、GD32、STM32等Cortex-M0/M0+内核MCU,并想接入SGP30传感器进行环境监测的朋友。我会从传感器原理、硬件连接到代码移植、数据解析,一步步讲清楚。

1. 认识SGP30:它是什么,能测什么?

在写代码之前,咱们先得搞清楚手里的传感器到底是个啥。SGP30是一款数字输出的气体传感器,核心是金属氧化物(MOx)气敏材料。简单来说,它的“鼻子”就是一层特殊的纳米颗粒薄膜,当目标气体(比如甲醛、酒精等挥发性有机物)接触到这层薄膜时,会与薄膜表面的氧气发生反应,改变薄膜的电阻。传感器内部的专用芯片(ASIC)负责测量这个电阻变化,经过复杂的处理和校准,最终输出我们需要的数字信号。

它的最大特点就是“多合一”和“开箱即用”:

  • 多合一:一颗芯片里集成了4个传感元件,可以综合判断空气质量。
  • 开箱即用:出厂时已经完成了校准,我们拿到手直接通过I2C读取数据就行,不需要自己再做复杂的标定,这对初学者和快速开发非常友好。

SGP30主要输出两个关键指标:

  1. eCO2(等效二氧化碳):范围是400~60000 ppm。注意,它并不是直接测量二氧化碳,而是通过检测挥发性有机物(VOC)来等效计算出的二氧化碳浓度值。在室内空气质量(IAQ)监测中,这个值很有参考意义。
  2. TVOC(总挥发性有机物):范围是0~60000 ppb。这才是直接反映甲醛、苯、酒精等多种有害有机物总量的指标,是判断室内污染的关键。

注意:很多朋友关心它能不能直接测甲醛。严格来说,SGP30对多种VOC都有响应,其中包含甲醛。它输出的TVOC值是一个综合反映,可以用于评估包含甲醛在内的挥发性有机物污染水平。对于需要精准甲醛浓度的场合,可能需要更专业的传感器。

模块硬件很简单:

  • 工作电压:3.3V(和CW32开发板完美匹配)
  • 通信接口:I2C(只需要两根线)
  • 引脚:通常就4个(VCC, GND, SDA, SCL)

2. 硬件连接与I2C地址解析

硬件连接是第一步,千万不能接错。CW32F030C8T6开发板(以立创开发板为例)和SGP30模块的连接如下表所示:

CW32F030C8T6引脚SGP30模块引脚说明
3.3VVCC电源正极
GNDGND电源地
PB8 (或其他GPIO)SDAI2C数据线
PB9 (或其他GPIO)SCLI2C时钟线

连接时,建议在SDA和SCL线上各加一个4.7kΩ的上拉电阻到3.3V,这是I2C总线标准的要求,能保证信号稳定。有些模块可能已经内置了,如果通信不稳定,首先检查这里。

接下来是通信的“暗号”——I2C设备地址。根据数据手册,SGP30的7位I2C从机地址是 0x58。

这里有个细节要注意:在I2C协议中,实际发送的8位地址是由7位设备地址和1位读写位组成的。所以:

  • 写操作时:发送 (0x58 << 1) = 0xB0 (读写位为0,表示写)
  • 读操作时:发送 (0x58 << 1) \| 0x01 = 0xB1 (读写位为1,表示读)

这个0xB0和0xB1就是我们待会儿写代码时要用的地址。

3. 核心驱动代码移植与讲解

原始资料提供了完整的代码,但直接复制粘贴可能不知其所以然。我结合自己的理解,把关键部分拆开揉碎了讲。

3.1 引脚与宏定义 (bsp_sgp30.h)

头文件里主要完成硬件相关的定义,方便我们移植和修改。

#ifndef _BSP_SGP30_H_
#define _BSP_SGP30_H_

#include "board.h" // 包含CW32的库头文件

// 1. 时钟与端口定义
#define RCC_SGP30_ENABLE()      __RCC_GPIOB_CLK_ENABLE() // 使能GPIOB时钟
#define PORT_SGP30              CW_GPIOB                 // 使用GPIOB端口
#define GPIO_SDA                GPIO_PIN_8               // SDA接在PB8
#define GPIO_SCL                GPIO_PIN_9               // SCL接在PB9

// 2. SDA引脚方向控制宏(软件模拟I2C的关键)
// 设置SDA为开漏输出模式(用于发送数据)
#define SDA_OUT()   { \
    GPIO_InitTypeDef GPIO_InitStruct; \
    GPIO_InitStruct.Pins = GPIO_SDA; \
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; \ // 开漏输出
    GPIO_InitStruct.Speed = GPIO_SPEED_HIGH; \
    GPIO_Init(PORT_SGP30, &GPIO_InitStruct); \
}

// 设置SDA为输入模式(用于接收数据)
#define SDA_IN()    { \
    GPIO_InitTypeDef GPIO_InitStruct; \
    GPIO_InitStruct.Pins = GPIO_SDA; \
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT; \
    GPIO_InitStruct.Speed = GPIO_SPEED_HIGH; \
    GPIO_Init(PORT_SGP30, &GPIO_InitStruct); \
}

// 3. 引脚读写宏
#define SDA_GET()       GPIO_ReadPin(PORT_SGP30, GPIO_SDA) // 读取SDA电平
#define SDA(x)          GPIO_WritePin(PORT_SGP30, GPIO_SDA, (x?GPIO_Pin_SET:GPIO_Pin_RESET) ) // 设置SDA电平
#define SCL(x)          GPIO_WritePin(PORT_SGP30, GPIO_SCL, (x?GPIO_Pin_SET:GPIO_Pin_RESET) ) // 设置SCL电平

// 函数声明
void SGP30_Init(void);
uint32_t SGP30_Read(void);
void SGP30_Write_cmd(uint8_t a, uint8_t b);

#endif

关键点说明:

  • GPIO_MODE_OUTPUT_OD(开漏输出):这是I2C总线标准的推荐模式。开漏输出可以方便地实现“线与”,当多个设备挂在总线上时不会冲突。简单理解就是:MCU只能把线拉低(输出0),释放时靠上拉电阻拉高(输出1)。
  • 宏定义SDA_OUT()和SDA_IN():因为我们在用GPIO软件模拟I2C,需要在发送和接收时动态切换SDA引脚的方向,这两个宏就是干这个的。

3.2 软件模拟I2C底层时序 (bsp_sgp30.c)

CW32F030虽然有硬件I2C,但有时软件模拟更灵活、调试更方便。这里实现了最基础的I2C时序函数。

// IIC起始信号:SCL高电平时,SDA产生一个下降沿
void IIC_Start(void)
{
    SDA_OUT();
    SCL(0); // 确保SCL为低,总线空闲
    delay_us(1);
    SDA(1); // SDA拉高
    SCL(1); // SCL拉高
    delay_us(5); // 保持一段时间,形成起始条件
    SDA(0); // SDA拉低,产生下降沿
    delay_us(5);
    SCL(0); // 拉低SCL,准备传输数据
    delay_us(5);
}

// IIC停止信号:SCL高电平时,SDA产生一个上升沿
void IIC_Stop(void)
{
    SDA_OUT();
    SCL(0);
    SDA(0); // 先确保SDA为低
    delay_us(1);
    SCL(1); // SCL拉高
    delay_us(5);
    SDA(1); // SDA拉高,产生上升沿
    delay_us(5);
}

// 发送一个字节(数据从高位开始)
void Send_Byte(uint8_t dat)
{
    int i = 0;
    SDA_OUT();
    SCL(0); // 拉低时钟开始数据传输
    for(i = 0; i < 8; i++)
    {
        // 取出最高位(bit7)发送
        SDA((dat & 0x80) >> 7);
        delay_us(1);
        SCL(1); // 拉高时钟,数据被采样
        delay_us(5);
        SCL(0); // 拉低时钟,为下一位做准备
        delay_us(5);
        dat <<= 1; // 左移,处理下一位
    }
}

// 读取一个字节(数据从高位开始)
unsigned char Read_Byte(void)
{
    unsigned char i, receive = 0;
    SDA_IN(); // SDA设置为输入,准备读取从机数据
    for(i = 0; i < 8; i++)
    {
        SCL(0);
        delay_us(5);
        SCL(1); // 拉高时钟,从机放置数据到SDA线
        delay_us(5);
        receive <<= 1; // 左移,为接收新位腾出空间
        if(SDA_GET()) // 读取SDA线电平
        {
            receive |= 1; // 如果为高,则该位为1
        }
        delay_us(5);
    }
    SCL(0);
    return receive;
}

提示:delay_us()函数需要你自己根据系统时钟频率实现,通常用简单的循环来实现微秒级延时。时序中的延时参数(如delay_us(5))是经验值,如果通信不稳定,可以适当调整。

3.3 SGP30命令发送与数据读取

这是驱动传感器的核心,需要严格按照SGP30的指令格式来。

SGP30常用命令:

  • 0x2003:初始化命令(IAQ Init)
  • 0x2008:读取空气质量测量值命令(Measure IAQ)
// 向SGP30发送命令(命令为2字节)
void SGP30_Write_cmd(uint8_t regaddr_H, uint8_t regaddr_L)
{
    IIC_Start();
    Send_Byte(0xB0); // 发送写地址 (0x58 << 1)
    I2C_WaitAck();   // 等待传感器应答
    Send_Byte(regaddr_H); // 发送命令高字节
    I2C_WaitAck();
    Send_Byte(regaddr_L); // 发送命令低字节
    I2C_WaitAck();
    IIC_Stop();
    delay_ms(100); // 命令后需要等待一段时间
}

// 从SGP30读取测量数据
uint32_t SGP30_Read(void)
{
    uint32_t dat = 0;
    uint8_t crc;

    IIC_Start();
    Send_Byte(0xB1); // 发送读地址 (0x58 << 1) | 0x01
    I2C_WaitAck();

    // 读取CO2数据高8位
    dat = Read_Byte();
    IIC_Send_Ack(0); // 发送应答(ACK)
    dat <<= 8;

    // 读取CO2数据低8位
    dat += Read_Byte();
    IIC_Send_Ack(0);

    // 读取CO2的CRC校验字节(此处示例代码未使用,实际产品中建议校验)
    crc = Read_Byte();
    IIC_Send_Ack(0);

    // 读取TVOC数据高8位
    dat <<= 8;
    dat += Read_Byte();
    IIC_Send_Ack(0);

    // 读取TVOC数据低8位
    dat <<= 8;
    dat += Read_Byte();
    IIC_Send_Ack(1); // 最后发送非应答(NACK),停止读取

    IIC_Stop();
    return dat; // 返回的32位数据中,高16位是CO2,低16位是TVOC
}

数据格式解析(非常重要!) SGP30一次读取返回6个字节: [CO2_MSB, CO2_LSB, CO2_CRC, TVOC_MSB, TVOC_LSB, TVOC_CRC] 我们的函数将这6个字节组合成一个32位的uint32_t返回,其结构如下:

bit31~bit16: CO2 测量值
bit15~bit0:  TVOC 测量值

所以在主函数里,我们需要用移位和掩码操作把它们分开。

3.4 初始化函数

初始化非常简单,就是配置好GPIO,然后发送初始化命令。

void SGP30_Init(void)
{
    SGP30_GPIO_Init(); // 初始化GPIO引脚(配置为开漏输出)
    SGP30_Write_cmd(0x20, 0x03); // 发送初始化命令 0x2003
}

4. 在主函数中应用与数据解析

驱动写好了,最后就是在主循环里调用它,并解析出我们需要的CO2和TVOC值。

#include "board.h"
#include "stdio.h"
#include "bsp_uart.h" // 用于串口打印
#include "bsp_sgp30.h"

int32_t main(void)
{
    board_init(); // 开发板基础初始化(系统时钟等)
    uart1_init(115200); // 初始化串口1,波特率115200,用于打印数据
    SGP30_Init(); // 初始化SGP30传感器
    delay_ms(100); // 稍作延时

    while (1)
    {
        uint32_t CO2Data, TVOCData;
        uint32_t sgp30_dat;

        // 1. 发送读取测量值命令
        SGP30_Write_cmd(0x20, 0x08);

        // 2. 读取原始数据(32位)
        sgp30_dat = SGP30_Read();

        // 3. 数据解析:从32位数据中分离出CO2和TVOC
        CO2Data = (sgp30_dat & 0xffff0000) >> 16; // 取高16位
        TVOCData = sgp30_dat & 0x0000ffff;        // 取低16位

        // 4. 通过串口打印结果
        printf("CO2: %d ppm, TVOC: %d ppb\r\n", CO2Data, TVOCData);

        delay_ms(1000); // 每秒读取一次
    }
}

5. 实际使用中的注意事项与踩坑记录

代码跑起来不难,但要让数据准确可靠,有几个坑点我必须提醒你:

  1. 漫长的上电初始化:SGP30模块上电后需要大约15秒进行初始化。在这期间,你读到的CO2值会固定为400ppm,TVOC值为0ppb。只有等到TVOC不为0且CO2不为400时,才说明初始化完成,数据开始有效。 所以程序里最好加个判断,等初始化完成再开始记录数据。

  2. 数据波动与滤波:气体传感器非常敏感,容易受环境气流、温度影响,刚开始读数波动很大是正常现象。通常需要运行几分钟(甚至更久)才会逐渐稳定。在实际项目中,一定要对读取到的数据进行软件滤波,比如滑动平均滤波、中值滤波等,否则数据会跳得你怀疑人生。

  3. CRC校验:示例代码为了简洁,跳过了CRC校验。CRC是循环冗余校验,用于检查数据传输过程中是否出错。在产品开发中,强烈建议加上CRC校验,确保数据的可靠性。SGP30数据手册提供了CRC8的计算方法。

  4. I2C上拉电阻:如果通信失败,第一个要检查的就是SDA和SCL线上有没有接4.7kΩ的上拉电阻到3.3V。没有上拉,电平无法被正确拉高,通信必然失败。

  5. 供电要干净:传感器对电源噪声比较敏感,尽量确保3.3V电源稳定。如果开发板是USB供电,且连接了其他大电流设备,可能会引入噪声。

按照上面的步骤,你应该就能在CW32F030C8T6上成功驱动SGP30,并读取到室内的TVOC和eCO2数据了。把这个模块和温湿度传感器(如DHT11)结合起来,就能搭建一个功能完整的室内空气质量监测终端。如果遇到问题,多检查时序和硬件连接,大部分问题都出在这两方面。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐