发新帖本帖赏金 25.00元(功能说明)我要提问
返回列表
打印
[单片机芯片]

CH32V307使用GPIO模拟I2C驱动TVOC、CO2eq传感器SGP30

[复制链接]
790|7
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
dql2015|  楼主 | 2022-12-19 10:40 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 dql2015 于 2022-12-19 11:38 编辑

#申请原创#  
@21小跑堂
近年来,清洁的空气成为诸多重视健康生活人士的新需求。常见重要的空气质量指标有2种:
二氧化碳:
二氧化碳是一种微量气体。我们吸入氧气并呼出二氧化碳,因此在封闭空间中,二氧化碳浓度与人类活动和人口密度相关。高浓度的二氧化碳可能会导致头痛、困倦、精神萎靡和表现不佳。
VOC(挥发性有机化合物):
VOC 是在室温或更高温度下蒸发的含碳物质。短期接触会导致刺激、头晕或哮喘恶化;长期接触则可能会导致肺癌或损害肝脏或神经系统。
手头正好有一款seeed的空气质量传感器模块SGP30,SGP30是TVOC(总挥发性有机化合物)、CO2eq气体传感器,是一款数字多气体传感器,可轻松集成到空气净化器、指令控制通风和物联网应用中。 该传感器的的CMOSens®技术在单个芯片上提供完整的传感器系统,并带有I2C接口、温度控制的微型加热板和两个预处理的室内空气质量信号。
测量结果输出频率1Hz,TVOC输出范围0 ppb to 60000 ppb,CO2eq输出范围400 ppm to 60000 ppm,不通输出范围精度不同;


硬件接口

SGP30使用的是I2C接口:

供电、接口电平为1.8V,因此硬件设计上需要注意电平匹配:

硬件设计SCL、SDA上应增加上拉电阻(或者使用控制器管脚内部上拉电阻):

支持快速模式400KHz:

需要注意SDA valid time不超过0.9us:

设备地址
SGP30 I2C使用7bit从机地址0X58,最低位为读写控制位,0是读,1是写:

写地址为(0X58 << 1) = 0XB0
读为((0X58 << 1)) | 0X01 = 0XB1

控制命令
SGP30的命令都是双字节16bit的:

通信采用一问一答形式,主机发出命令,等待从机回复:

双字节命令先发高位MSB,后发低位LSB,以初始化SGP30命令(0X2003)为例,发送顺序为:0X20、0X03

测量命令
读取测量结果命令采用标准I2C形式,读写相关寄存器:
For the first 15s after the “Init_air_quality” command the sensor is in an initialization phase during which a “Measure_air_quality” command returns fixed values of
400 ppm CO2eq and 0 ppb TVOC.
需要特别注意这段话,SGP30上电后在发送初始化命令后,需要大约15秒完成初始化,在初始化过程读取的CO2浓度为400ppm,TVOC为0ppd且恒定不变。











软件复位
可以通过地址0x0006控制SGP30进行软复位:


读取序列号
SGP30内置48bit唯一序列号,通过0x3682地址读出:

CRC校验
数据校验采用8bit CRC和校验方式:

sgp30.h文件
#ifndef _SGP30_H_
#define _SGP30_H_

#include <stdio.h>
#include <stdint.h>


#define SGP30_ADDR          0x58
#define        SGP30_ADDR_WRITE        (SGP30_ADDR << 1)         //0xb0
#define        SGP30_ADDR_READ                ((SGP30_ADDR << 1) + 1)   //0xb1


/* 初始化空气质量测量 */
#define SGP30_CMD_INIT_AIR_QUALITY 0x2003

/* 开始空气质量测量 */
#define SGP30_CMD_MEASURE_AIR_QUALITY 0x2008

/* 获取串号 */
#define SGP30_CMD_GET_SERIAL_ID  0X3682

int sgp30_init(void);
int sgp30_read(uint16_t* CO2, uint16_t* TVOC);
int sgp30_get_serial_id(uint8_t id[6]);
int sgp30_soft_reset(void);

#endif /* _SGP30_H_ */

sgp30.c文件
#include "sgp30.h"
#include "debug.h"
#include "myiic.h"


#define RCC_CLK_PORT RCC_APB2Periph_GPIOA

#define SCL_PORT GPIOA
#define SCL_PIN GPIO_Pin_4

#define SDA_PORT GPIOA
#define SDA_PIN GPIO_Pin_5

#define IIC_SCL_H() GPIO_WriteBit(SCL_PORT,SCL_PIN,1)
#define IIC_SCL_L() GPIO_WriteBit(SCL_PORT,SCL_PIN,0)

#define IIC_SDA_H() GPIO_WriteBit(SDA_PORT,SDA_PIN,1)
#define IIC_SDA_L() GPIO_WriteBit(SDA_PORT,SDA_PIN,0)

#define READ_SDA() GPIO_ReadInputDataBit(SDA_PORT,SDA_PIN)


#define SGP30_SDA_Pin                      SDA_PIN
#define SGP30_SDA_GPIO_Port                SDA_PORT

#define SGP30_SCK_Pin                      SCL_PIN
#define SGP30_SCK_GPIO_Port                SCL_PORT


#define SGP30_SCK_L()    IIC_SCL_L()
#define SGP30_SCK_H()    IIC_SCL_H()
#define SGP30_SDA_L()    IIC_SDA_L()
#define SGP30_SDA_H()    IIC_SDA_H()
#define SGP30_READ_SDA() (READ_SDA() == Bit_SET ? 1 : 0)

/// 设置SDA引脚为输出模式
static void SDA_SET_OUT()
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    RCC_APB2PeriphClockCmd(RCC_CLK_PORT, ENABLE);

    GPIO_InitStructure.GPIO_Pin = SDA_PIN;//sda
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SDA_PORT, &GPIO_InitStructure);
    IIC_SDA_L();//必要的
}

/// 设置SDA引脚为输入模式
static void SDA_SET_IN()
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    RCC_APB2PeriphClockCmd(RCC_CLK_PORT, ENABLE);

    GPIO_InitStructure.GPIO_Pin = SDA_PIN;//sda
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SDA_PORT, &GPIO_InitStructure);
}

/// 微秒延时函数
static void sgp30_delay_us(uint32_t us)
{
    Delay_Us(us);
}

/// 毫秒延时函数
static void sgp30_delay_ms(uint32_t nms)
{
        Delay_Ms(nms);
}

/// 初始化SGP30的IIC引脚
static int sgp30_iic_init(void)
{
        GPIO_InitTypeDef GPIO_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_CLK_PORT, ENABLE);

    GPIO_InitStructure.GPIO_Pin = SCL_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//scl
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SCL_PORT, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = SDA_PIN;//sda
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(SDA_PORT, &GPIO_InitStructure);

    //必要的
        SGP30_SCK_H();  // 拉高时钟引脚
        SGP30_SDA_H();  // 拉高数据引脚
        return 0;
}

/////////////////////////////////////////////////////////////////////////////////////
///
///                               模拟IIC协议实现
///
/////////////////////////////////////////////////////////////////////////////////////

static void IIC_Start(void)
{
    SDA_SET_OUT();
    SGP30_SDA_H();
    SGP30_SCK_H();
    sgp30_delay_us(5);
    SGP30_SDA_L(); //START:when CLK is high,DATA change form high to low
    sgp30_delay_us(6);
    SGP30_SCK_L();
}

static void IIC_Stop(void)
{
    SDA_SET_OUT();
    SGP30_SCK_L();
    SGP30_SDA_L(); //STOP:when CLK is high DATA change form low to high
    SGP30_SCK_H();
    sgp30_delay_us(6);
    SGP30_SDA_H();
    sgp30_delay_us(6);
}

static uint8_t IIC_Wait_Ack(void)
{
    uint8_t tempTime = 0;
    SGP30_SDA_H();
    sgp30_delay_us(1);
    SDA_SET_IN();
    SGP30_SCK_H();
    sgp30_delay_us(1);
    while (SGP30_READ_SDA())
    {
        tempTime++;
        if (tempTime > 250)
        {
            IIC_Stop();
            return 1;
        }
    }
    SGP30_SCK_L();
    return 0;
}

static void IIC_Ack(void)
{
    SGP30_SCK_L();
    SDA_SET_OUT();
    SGP30_SDA_L();
    sgp30_delay_us(2);
    SGP30_SCK_H();
    sgp30_delay_us(5);
    SGP30_SCK_L();
}

static void IIC_NAck(void)
{
    SGP30_SCK_L();
    SDA_SET_OUT();
    SGP30_SDA_H();
    sgp30_delay_us(2);
    SGP30_SCK_H();
    sgp30_delay_us(5);
    SGP30_SCK_L();
}

static void IIC_Send_Byte(uint8_t txd)
{
    uint8_t t;
    SDA_SET_OUT();
    SGP30_SCK_L();
    for (t = 0; t < 8; t++)
    {
        if ((txd & 0x80) > 0) //0x80  1000 0000
            SGP30_SDA_H();
        else
            SGP30_SDA_L();
        txd <<= 1;
        sgp30_delay_us(2);
        SGP30_SCK_H();
        sgp30_delay_us(2);
        SGP30_SCK_L();
        sgp30_delay_us(2);
    }
}

static uint8_t IIC_Read_Byte(uint8_t ack)
{
    uint8_t i, receive = 0;
    SDA_SET_IN();
    for (i = 0; i < 8; i++)
    {
        SGP30_SCK_L();
        sgp30_delay_us(2);
        SGP30_SCK_H();
        receive <<= 1;
        if (SGP30_READ_SDA())
            receive++;
        sgp30_delay_us(1);
    }
    if (!ack)
        IIC_NAck();
    else
        IIC_Ack();
    return receive;
}

static int sgp30_iic_write(uint8_t addr, const uint8_t* buf, uint32_t len)
{
        int i;
    IIC_Start();
    IIC_Send_Byte(addr);
    IIC_Wait_Ack();
        for(i = 0; i < len; i++)
        {
                IIC_Send_Byte(buf[i]);
                IIC_Wait_Ack();
        }
    IIC_Stop();
        return 0;
}

static int sgp30_iic_read(uint8_t addr, uint8_t* buf, uint32_t len)
{
        int i;
    IIC_Start();
    IIC_Send_Byte(addr);
        IIC_Wait_Ack();
        for(i = 0; i < len - 1; i++)
        {
                buf[i] = IIC_Read_Byte(1);
        }
        buf[i] = IIC_Read_Byte(0);  // SGP30接收数据时候的最后一个字节不需要等待ACK
        IIC_Stop();
        return 0;
}

//////////////////////////////////////////////////////////////////////////////////////////
///
///       SGP30驱动代码
///
//////////////////////////////////////////////////////////////////////////////////////////

static uint8_t sgp30_checksum(const uint8_t* buf, uint32_t len);

int sgp30_get_serial_id(uint8_t id[6])
{
    uint8_t buf[32];
    uint8_t crc[3];

    buf[0] = (SGP30_CMD_GET_SERIAL_ID & 0XFF00) >> 8;
    buf[1] = (SGP30_CMD_GET_SERIAL_ID & 0X00FF);

    if (sgp30_iic_write(SGP30_ADDR_WRITE, buf, 2) < 0)
        return -1;

    if (sgp30_iic_read(SGP30_ADDR_READ, buf, 9) < 0)
        return -2;

    crc[0] = buf[2];
    crc[1] = buf[5];
    crc[2] = buf[8];

    id[0]  = buf[0];
    id[1]  = buf[1];
    id[2]  = buf[3];
    id[3]  = buf[4];
    id[4]  = buf[6];
    id[5]  = buf[7];

    if(
        sgp30_checksum(&id[0], 2) != crc[0] ||
        sgp30_checksum(&id[2], 2) != crc[1] ||
        sgp30_checksum(&id[4], 2) != crc[2]
    )
        return -3;

    return 0;
}

static uint8_t sgp30_checksum(const uint8_t* buf, uint32_t len)
{
        const uint8_t Polynomial = 0x31;
        uint8_t Initialization = 0XFF;
    uint8_t i = 0, k = 0;
        while(i < len)
        {
                Initialization ^= buf[i++];
                for(k = 0; k < 8; k++)
                {
                        if(Initialization & 0X80)
                                Initialization = (Initialization << 1) ^ Polynomial;
                        else
                                Initialization = (Initialization << 1);
                }
        }
        return Initialization;
}

int sgp30_soft_reset(void)
{
    uint8_t cmd = 0X06;
    return sgp30_iic_write(0X00, &cmd, 1);
}

int sgp30_init(void)
{
    uint8_t buf[2];

        // 初始化IIC
        if(sgp30_iic_init() < 0)
                return -1;

    // 软件复位
    if (sgp30_soft_reset() < 0)
        return -2;

    // 等待复位完成
    sgp30_delay_ms(50);

    buf[0] = (SGP30_CMD_INIT_AIR_QUALITY & 0XFF00) >> 8;
    buf[1] = (SGP30_CMD_INIT_AIR_QUALITY & 0X00FF);

        // 初始化控制测量参数
    if (sgp30_iic_write(SGP30_ADDR_WRITE, buf, 2) < 0)
        return -3;

    return 0;
}

int sgp30_read(uint16_t* CO2, uint16_t* TVOC)
{
    uint8_t buf[8] = {0};

    buf[0] = (SGP30_CMD_MEASURE_AIR_QUALITY & 0XFF00) >> 8;
    buf[1] = (SGP30_CMD_MEASURE_AIR_QUALITY & 0X00FF);

    // 启动空气质量测量
    if (sgp30_iic_write(SGP30_ADDR_WRITE, buf, 2) < 0)
        return -1;

    // 等待测量完成
    sgp30_delay_ms(10);

    // 读取收到的数据
    if (sgp30_iic_read(SGP30_ADDR_READ, buf, 6) < 0)
        return -2;

    // 校验CRC
    if (sgp30_checksum(&buf[3], 2) != buf[5])
        return -3;

    if (CO2 != NULL)    *CO2  = (buf[0] << 8) | buf[1];
    if (TVOC != NULL)   *TVOC = (buf[3] << 8) | buf[4];

    return 0;
}
main函数测试代码:
初始化未完成依据是TVOC == 0 && CO2 == 400
int main(void)
{
    Delay_Init();
    USART_Printf_Init(115200);

    while(sgp30_init() < 0)
    {
        printf(" sgp30 init fail\r\n");
        Delay_Ms(1000);
    }

    printf(" sgp30 init ok\r\n");

    if(sgp30_get_serial_id(ID) < 0)
    {
        printf(" sgp30 read serial id failed\r\n");
    }else
    {
        printf(" sgp30 read serial id is:");
        for(int i = 0; i < 6; i++)  printf("%02X", ID[i]);
        printf("\r\n");
    }

    printf(" sgp30 wait air for init");
    do
    {
        if(sgp30_read(&CO2, &TVOC) < 0)
        {
            printf("\r\n spg30 read failed");
        }else
        {
            printf(".");
        }
        Delay_Ms(500);
    }while(TVOC == 0 && CO2 == 400);

    printf("\r\n");

    while (1)
    {
        if(sgp30_read(&CO2, &TVOC) < 0)
        {
            printf(" sgp30 read fail\r\n");
        }
        else
        {
            printf("CO2:%5dppm TVOC:%5dppb\r\n", CO2, TVOC);
        }
        Delay_Ms(1000);
    }
}


实物接线:


串口调试助手打印结果:








使用特权

评论回复

打赏榜单

21ic小管家 打赏了 25.00 元 2022-12-20
理由:签约作者

沙发
claretttt| | 2023-1-5 12:43 | 只看该作者
这个TVOC都包括什么的?              

使用特权

评论回复
板凳
mattlincoln| | 2023-1-5 12:52 | 只看该作者
二氧化碳的数值测量怎么转换实际数值的?

使用特权

评论回复
地板
belindagraham| | 2023-1-5 13:39 | 只看该作者
这个是不是可以监测甲醛的数据呢              

使用特权

评论回复
5
mattlincoln| | 2023-1-6 12:49 | 只看该作者
这个是什么源代码              

使用特权

评论回复
6
maqianqu| | 2023-1-6 19:00 | 只看该作者
SGP30保持400是什么原因              

使用特权

评论回复
7
LOVEEVER| | 2023-2-8 09:41 | 只看该作者
belindagraham 发表于 2023-1-5 13:39
这个是不是可以监测甲醛的数据呢

说的有理,甲醛能测就更好了

使用特权

评论回复
8
Kear| | 2024-3-13 13:11 | 只看该作者
能发下源码码?谢谢

使用特权

评论回复
发新帖 本帖赏金 25.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

101

主题

372

帖子

7

粉丝