文章目录
1.背景说明
SGP30可以检测空气中总有机气体含量(TVOC)和CO2含量。本文带你编写SGP30驱动程序,并使用ESP32进行测试验证。由于作者水平有限,软件设计架构难免有问题,欢迎批评指正。通用驱动代码托管到github上了(https://github.com/shengliwang/sgp30_driver),SGP30中英文手册也在此仓库中。
另外在树莓派上通过GPIO模拟I2C的驱动托管在https://github.com/shengliwang/VOC-dectect。本博客后续再对其说明,并移植到本仓库中。
2.代码实现
2.1定义跟开发平台相关的handle
由于驱动程序设计成可在各嵌入式芯片上移植,所以定义一个跟平台相关的数据结构。用户需要实现并指定I2C读写接口。
typedef int (*i2C_write_ptr)(uint8_t addr, const uint8_t *data, size_t data_len);
typedef int (*i2C_read_ptr)(uint8_t addr, uint8_t * data, size_t buf_len);
typedef void (*sleep_msec_ptr)(uint32_t mseconds);
typedef struct i2c_sgp30_t{
i2C_write_ptr i2c_write;
i2C_read_ptr i2c_read;
sleep_msec_ptr msleep;
uint8_t i2c_addr;
} *i2c_sgp30_handle_t;
在ESP32上具体实现时再说明。
2.2 初始化传感器
先定义两个宏用于提取uint16_t类型的数据的MSB和LSB。
#define MSB_OF_UINT16_T(d) ( (uint8_t)(d >> 8) )
#define LSB_OF_UINT16_T(d) ( (uint8_t)(d & 0x00ff) )
根据SGP30传感器手册,基本上所有的I2C命令都要先发送命令,然后读取数据。把这段代码抽象出来实现如下:
static int s_i2c_sgp30_send_cmd(i2c_sgp30_handle_t handle, uint16_t cmd, uint32_t duration_ms){
uint8_t arr_cmd[2];
arr_cmd[0] = MSB_OF_UINT16_T(cmd);
arr_cmd[1] = LSB_OF_UINT16_T(cmd);
if (0 != handle->i2c_write(handle->i2c_addr, arr_cmd, sizeof(arr_cmd))){
return SGP30_ERR;
}
handle->msleep(duration_ms);
return SGP30_OK;
}
int i2c_sgp30_init(i2c_sgp30_handle_t handle){
uint16_t cmd = CMD_Init_air_quality;
uint32_t duration = CMD_Init_air_quality_MAX_DURATION_TIME_MS;
if ( SGP30_OK != s_i2c_sgp30_send_cmd(handle, cmd, duration)){
return SGP30_ERR;
}
return SGP30_OK;
}
其中cmd参数支持的命令如下(具体可查看SGP30传感器手册)
// 命令字
#define CMD_Init_air_quality 0x2003
#define CMD_Measure_air_quality 0x2008
#define CMD_Get_baseline 0x2015
#define CMD_Set_baseline 0x201e
#define CMD_Set_humidity 0x2061
#define CMD_Measure_test 0x2032
#define CMD_Get_feature_set_version 0x202f
#define CMD_Measure_raw_signals 0x2050
//SGP30完成对应命令需要的时间
#define CMD_Init_air_quality_MAX_DURATION_TIME_MS (10*4)
#define CMD_Measure_air_quality_MAX_DURATION_TIME_MS (12*4)
#define CMD_Get_baseline_MAX_DURATION_TIME_MS (10*4)
#define CMD_Set_baseline_MAX_DURATION_TIME_MS (10*4)
#define CMD_Set_humidity_MAX_DURATION_TIME_MS (10*4)
#define CMD_Measure_test_MAX_DURATION_TIME_MS (220*3)
#define CMD_Get_feature_set_version_MAX_DURATION_TIME_MS (2*20)
#define CMD_Measure_raw_signals_MAX_DURATION_TIME_MS (25*4)
手册上命令字的说明:

2.2 获取TVOC和CO2eq数据
int i2c_sgp30_measure_air_quality(i2c_sgp30_handle_t handle, uint16_t * co2eq, uint16_t * tvoc){
uint16_t cmd = CMD_Measure_air_quality;
uint32_t duration = CMD_Measure_air_quality_MAX_DURATION_TIME_MS;
if ( SGP30_OK != s_i2c_sgp30_send_cmd(handle, cmd, duration)){
return SGP30_ERR;
}
uint8_t out[6] = {0};
handle->i2c_read(handle->i2c_addr, out, sizeof(out));
// crc check
uint8_t crc;
crc = s_sgp30_crc(&out[0], 2);
if (crc != out[2]){
return SGP30_CRC_ERR;
}
crc = s_sgp30_crc(&out[3], 2);
if (crc != out[5]){
return SGP30_CRC_ERR;
}
*co2eq = (out[0] << 8 | out[1] );
*tvoc = (out[3] << 8 | out[4] );
return SGP30_OK;
}
关于CRC的校验,SGP30指定如下:

本驱动的实现如下:
static uint8_t s_sgp30_crc(uint8_t *p, int len)
{
int i;
unsigned char crc = 0xff;
while(len--){
crc ^= *p++;
for (i = 0; i < 8; ++i){
if (crc & 0x80){
crc = (crc << 1) ^ 0x31;
}else{
crc <<= 1;
}
}
}
return crc;
}
2.3 其他功能实现
函数 i2c_sgp30_init 和 i2c_sgp30_measure_air_quality 分别用于初始化传感器和测量传感器数据。这两个功能已经能满足绝大多数应用了。另外,本驱动还提供了设置湿度的函数i2c_sgp30_set_humidity用于传感器进行湿度补偿。
其他函数的声明如下,具体实现参考github(https://github.com/shengliwang/sgp30_driver)
// 获取补偿算法的基线(init后的15s内为0)
int i2c_sgp30_get_baseline(i2c_sgp30_handle_t handle, uint16_t * baseline_co2eq, uint16_t * baseline_tvoc);
// 设置补偿算法的基线
int i2c_sgp30_set_baseline(i2c_sgp30_handle_t handle, uint16_t baseline_co2eq, uint16_t baseline_tvoc);
int i2c_sgp30_measure_test(i2c_sgp30_handle_t handle, uint16_t * data);
// 获取传感器特征集
int i2c_sgp30_get_feature_set_version(i2c_sgp30_handle_t handle, uint8_t * version);
// 获取传感器经动态补偿算法计算前的输入数据
int i2c_sgp30_measure_raw_signals(i2c_sgp30_handle_t handle, uint16_t * H2_signal , uint16_t * Ethanol_signal);
根据手册说明,在执行i2c_sgp30_init后的15s,i2c_sgp30_measure_air_quality函数测的结果固定为CO2eq: 400ppm, TVOC: 0ppb。动态补偿算法运行稳定后数值才会有变化。另外实测i2c_sgp30_get_baseline获取的两个基线在15s内也为0,动态补偿算法稳定后,才有变化。
最后,函数i2c_sgp30_measure_raw_signals用于获取动态补偿算法的输入数据。如下图:

H2和Ethanol这两个 原始信号经过动态补偿算法计算后得到CO2eq和TVOC数据(可使用i2c_sgp30_measure_air_quality函数获取)。
3.ESP32上的测试
esp32采用github最新的esp-idf。
esp开发环境搭建参考:https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/get-started/linux-macos-setup.html
esp32管脚说明参考:https://docs.espressif.com/projects/esp-hardware-design-guidelines/zh_CN/latest/esp32/schematic-checklist.html
本实例采用GPIO4和GPIO5分别作为I2C的SCL和SDA。
测试代码参考本github仓库:https://github.com/shengliwang/sgp30_driver
3.1 环境配置及代码实现
- esp开发环境搭建
- 创建项目文件并添加
CMakeLists.txt
mkdir -p esp32_example/main
项目CMakeLists.txt如下(创建到esp32_example目录下):
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp-sgp30)
esp32_example/main目录下的CMakeLists.txt如下:
idf_component_register(SRCS "sgp30_main.c" "../../sgp30_i2c_driver/i2c_sgp30.c" # 需要正确指定sgp30驱动在哪里!
INCLUDE_DIRS "" "../../sgp30_i2c_driver/")
创建esp32_example/main/Kconfig.projbuild 用于idf.py menuconfig设置I2C端口(此步也可以省略,省略后要在代码中手动指定I2C 端口),文件内容如下:
menu "sgp30 Configuration"
menu "I2C Master"
config I2C_MASTER_SCL
int "SCL GPIO Num"
default 4
help
GPIO number for I2C Master clock line.
config I2C_MASTER_SDA
int "SDA GPIO Num"
default 5
help
GPIO number for I2C Master data line.
config I2C_MASTER_FREQUENCY
int "Master Frequency"
default 400000
help
I2C Speed of Master device.
endmenu
endmenu
- 添加测试文件
esp32_example/main/sgp30_main.c文件如下:
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "driver/i2c_master.h"
#include "i2c_sgp30.h"
#define SCL_IO_PIN (CONFIG_I2C_MASTER_SCL) // default is 4(由idf.py menuconfig 命令根据Kconfig.projbuild生成)
#define SDA_IO_PIN (CONFIG_I2C_MASTER_SDA) // default is 5(如果没有Kconfig.projbuild文件需要手动指定)
#define PORT_NUMBER -1
static i2c_master_dev_handle_t g_i2c_dev = NULL;
static int s_esp_i2c_write(uint8_t addr, const uint8_t *data, size_t data_len){
(void)addr;
if (ESP_OK != i2c_master_transmit(g_i2c_dev, data, data_len, -1)){
return 1;
} // 无限等待?如果不是无限等待的话,data不能使用栈上的变量?
return 0;
}
static int s_esp_i2c_read(uint8_t addr, uint8_t * data, size_t buf_len){
(void)addr;
if (ESP_OK != i2c_master_receive(g_i2c_dev, data, buf_len, -1)){
return 1;
}
return 0;
}
static void s_esp_sleep_msec(uint32_t mseconds){
vTaskDelay(mseconds / portTICK_PERIOD_MS);
}
static esp_err_t s_app_i2c_bus_init(i2c_master_dev_handle_t * i2c_handle){
i2c_master_bus_config_t i2c_bus_config = {
.clk_source = I2C_CLK_SRC_DEFAULT,
.i2c_port = PORT_NUMBER,
.scl_io_num = SCL_IO_PIN,
.sda_io_num = SDA_IO_PIN,
.glitch_ignore_cnt = 7,
};
i2c_master_bus_handle_t bus_handle;
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &bus_handle));
i2c_device_config_t i2c_dev_conf = {
.scl_speed_hz = 100000, // or fast mode 400000,SGP30支持I2c fast mode模式
.device_address = 0x58,
};
i2c_master_dev_handle_t i2c_dev = NULL;
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &i2c_dev_conf, &i2c_dev));
*i2c_handle = i2c_dev;
return ESP_OK;
}
void s_app_i2c_bus_deinit(void){
i2c_master_bus_rm_device(g_i2c_dev);
}
void app_main(void)
{
vTaskDelay(2000 / portTICK_PERIOD_MS);
if (ESP_OK != s_app_i2c_bus_init(&g_i2c_dev)){
printf("i2c bus init failed\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
esp_restart();
}
// 注册平台相关的i2c操作函数到sgp30驱动中。
struct i2c_sgp30_t sgp30_config = {
.i2c_write = s_esp_i2c_write,
.i2c_read = s_esp_i2c_read,
.msleep = s_esp_sleep_msec,
.i2c_addr = 0x58,
};
i2c_sgp30_handle_t sgp30_handle = &sgp30_config;
if (SGP30_OK != i2c_sgp30_init(sgp30_handle)){
printf("sgp30 init failed\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
esp_restart();
}
uint8_t version[2];
if (SGP30_OK != i2c_sgp30_get_feature_set_version(sgp30_handle, version)){
printf("sgp30 get fearture set failed\n");
}
printf("sgp30 feature set: 0x%02x 0x%02x\n", version[0], version[1]);
// uint16_t test_data;
// i2c_sgp30_measure_test(sgp30_handle, &test_data);
// printf("test_data: 0x%x\n", test_data);
uint16_t base_co2eq, base_tvoc;
uint16_t h2_sig, eth_sig;
i2c_sgp30_get_baseline(sgp30_handle ,&base_co2eq, &base_tvoc); // should wait 20s to get after init air quality.
printf("tvoc_base: %d, co2_base: %dppm\n", base_tvoc, base_co2eq);
uint16_t co2eq, tvoc;
while(1){
i2c_sgp30_measure_air_quality(sgp30_handle, &co2eq, &tvoc);
printf("tvoc: %dppb, co2: %dppm\n", tvoc, co2eq);
i2c_sgp30_measure_raw_signals(sgp30_handle, &h2_sig, ð_sig);
printf("RAW data: H2_signal: %d, Ethanol_signal: %d\n", h2_sig, eth_sig);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
以上代码文件均托管到了github上,可以直接使用。
3.2 编译及测试
cd esp32_example
source $HOME/esp/esp-idf/export.sh
idf.py menuconfig # 生成sdkconfig 文件用于编译
idf.py build # 编译项目
idf.py flash monitor # 烧录到esp32中并开启串口 (ctrl+]退出monitor)
串口打印如下:

实物连接图:

3.3 甲醛比较多的密度板材封皮柜测试
最近家里装修了,床头柜发现为密度板封的皮,想想甲醛应该比较多。
柜台实物图如下:

传感器放在其中并等待10min测试结果如下:

结果稳定在170ppb左右。
1451

被折叠的 条评论
为什么被折叠?



