SGP30
这是SGP30官方文档里开头的介绍,简单来说就是SGP30是一个数字多像素气体传感器,然后具有长期稳定性和低漂移。
这些我们都不用管,我们只需要知道SGP30是通过I2C来通信的,并且可以采集的数据有CO2和TVOC的含量。TVOC是“Total Volatile Organic Compounds”,意思是总挥发性有机化合物。
可以来看一下它的参数。
TVOC的输出范围是0~60000ppb,而CO2的范围是400~60000ppm。一开始没注意范围,我看CO2一直都在400以上还以为出了啥问题。
对了,在SGP30上电初始化之后会有一段时间输出的CO2固定是400,而TVOC固定是0,是正常现象,等一会就可以正常采集数据了,后面会再说
接下来是电气规格,这边要注意电压不能超过1.98V!!!跟我们平时常用的3.3V和5V不一样。直接使用SGP30需要进行电平转换。
不过我用的是模块,已经把电平转换芯片加上去了,供电3.3V~5V都是可以的。
除了电气规则,还有一个就是物理环境,但是大家一般都不会处于这么极端的环境吧。
工作温度在-40℃~85℃之间,湿度在10%95%之间。
然后是通信时间,可以看到SGP30支持的I2C最大速率是400K。
了解完上面的内容之后就可以开始研究如何和SGP30用I2C通信了。
SGP30的七位从机地址是0x58,因此从机地址+读的地址就是0xB1,从机地址+写的地址就是0xB0,我们也可以写成 ( 0x58<<1 | 0x01) 和 ( 0x58<<1 | 0x00)
通信的时序就是I2C的标准,不一样的是SGP30的指令分两个字节发送,也就是说我们发送一个命令需要发送两次。
并且SGP30发来的数据,每俩字节就跟一个CRC校验位。
CRC校验多项式是0x31,我们可以直接搜索CRC在线校验计算器帮我们计算。当然,我们也可以忽略掉,但是还是要接收。
接下来看看指令。
虽然不多,但是我们用到的更少,我们基本上只用俩命令,第一个是0x2003初始化,第二个是0x2008获取采集数据。
我们在一开始的发送0x2003初始化一下,等待12ms(文档里说的,但我们最好多延时一会儿)
然后我们就可以发送0x2008采集数据了,等待10ms(我们最好多等一会),会返回给我们6个byte的数据,其中前俩字节是CO2的数据,第三个是CO2数据的CRC校验码,第四五个字节是TVOC的数据,最后一个字节是TVOC的CRC校验码。
根据文档里说的,在初始化(0x2003)的15s内,我们获取采集数据(0x2008)收到的结果会是400和0。并且我们需要每秒发送一次0x2008就可以保证SGP30内部的补偿算法生效,采集的数据会更精确。
至此我们就知道应该如何使用SGP了,接下来我会贴出示例代码,结合代码和注释以及上文,相信大家就都可以将SGP30这个模块移植到各自的板子上了。
软件I2C使用SGP30(以GD32为例)
虽然我这边演示的是GD32(因为最近在用的板子是GD32),但是其他板子也是一样的流程,不一样的只是操作GPIO的方式不一样,自己修改一下即可。
另外有个小坑需要注意一下,我们STM32模拟I2C的时候GPIO配置的是推挽输出模式(没试过开漏),然后在弄GD32的时候一开始我也配置的是推挽,结果连ACK都没收到,排查了好久才发现需要将GD32的GPIO配置为开漏才可以进行软件模拟I2C。
#include "board.h"
#include <stdio.h>
#include "Z_UART.h"
#define SCL_Pin GPIO_PIN_8
#define SDA_Pin GPIO_PIN_9
#define IIC_PORT GPIOB
//下面Z_I2C开头的是软件I2C,不是使用SGP30的重点
void Z_I2C_Init(void){
rcu_periph_clock_enable(RCU_GPIOB);
gpio_mode_set(IIC_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP,SCL_Pin|SDA_Pin);
gpio_output_options_set(IIC_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ,SCL_Pin|SDA_Pin);
gpio_bit_write(IIC_PORT,SCL_Pin,1);
gpio_bit_write(IIC_PORT,SDA_Pin,1);
}
void Z_I2C_SetSCL(uint8_t signal){
if(signal==1) gpio_bit_write(IIC_PORT,SCL_Pin,1);
else gpio_bit_write(IIC_PORT,SCL_Pin,0);
delay_us(5);
}
void Z_I2C_SetSDA(uint8_t signal){
if(signal==1) gpio_bit_write(IIC_PORT,SDA_Pin,1);
else gpio_bit_write(IIC_PORT,SDA_Pin,0);
delay_us(5);
}
uint8_t Z_I2C_GetSDA(void){
return gpio_input_bit_get(IIC_PORT,SDA_Pin);
}
void Z_I2C_Start(void){
Z_I2C_SetSDA(1);
Z_I2C_SetSCL(1);
Z_I2C_SetSDA(0);
Z_I2C_SetSCL(0);
}
void Z_I2C_End(){
Z_I2C_SetSDA(0);
Z_I2C_SetSCL(1);
Z_I2C_SetSDA(1);
}
void Z_I2C_SendByte(uint8_t byte){
Z_I2C_SetSCL(0);
for(int i=0;i<8;++i){
if((byte&0x80)==0) Z_I2C_SetSDA(0);
else Z_I2C_SetSDA(1);
byte<<=1;
Z_I2C_SetSCL(1);
Z_I2C_SetSCL(0);
}
}
uint8_t Z_I2C_ReveiceByte(){
uint8_t data=0x00;
Z_I2C_SetSDA(1);
for(int i=0;i<8;++i){
Z_I2C_SetSCL(1);
if(Z_I2C_GetSDA()==1) data|=(0x80>>i);
Z_I2C_SetSCL(0);
}
return data;
}
void Z_I2C_SendACK(uint8_t ack){
if(ack==0) Z_I2C_SetSDA(0);
else Z_I2C_SetSDA(1);
Z_I2C_SetSCL(1);
Z_I2C_SetSCL(0);
}
uint8_t Z_I2C_ReveiceACK(){
Z_I2C_SetSDA(1);
Z_I2C_SetSCL(1);
uint8_t ack=Z_I2C_GetSDA();
Z_I2C_SetSCL(0);
return ack;
}
//获取SGP30的数据并打印
void printfSGP30(void){
Z_I2C_Start(); //I2C起始时序
Z_I2C_SendByte(0x58<<1|0x00); //发送从机地址+写(0xB0)
if(0 != Z_I2C_ReveiceACK()) printf("ACK error\r\n"); //接收ACK
Z_I2C_SendByte(0x20); //发送采集命令0x2008,需要分两次发送
if(0 != Z_I2C_ReveiceACK()) printf("ACK error\r\n");
Z_I2C_SendByte(0x08);
if(0 != Z_I2C_ReveiceACK()) printf("ACK error\r\n");
Z_I2C_End(); //结束I2C
delay_ms(100); //需要等待10ms,保险起见延时久一点
uint16_t data[4] = {0};
Z_I2C_Start();
Z_I2C_SendByte(0x58<<1|0x01); //发送从机地址+读(0xB0)
if(0 != Z_I2C_ReveiceACK()) printf("ACK error\r\n"); //接收ACK
data[0] = Z_I2C_ReveiceByte(); //接收CO2的高8位
Z_I2C_SendACK(0); //发送ACK
data[1] = Z_I2C_ReveiceByte(); //接收CO2的低八位
Z_I2C_SendACK(0);
printf("CRC is 0x%X\r\n",Z_I2C_ReveiceByte()); //获取并打印CO2的CRC,可以不处理,但是一定要读取
Z_I2C_SendACK(0);
data[2] = Z_I2C_ReveiceByte(); //接收TVOC(16bit)
Z_I2C_SendACK(0);
data[3] = Z_I2C_ReveiceByte();
Z_I2C_End(); //TVOC的CRC不接收了,直接结束I2C通信
printf("%X\t%X\t%X\t%X\r\n",data[0],data[1],data[2],data[3]); //打印一下原始数据
printf("CO2 is %d,TVOC is %d\r\n",data[0]<<8|data[1],data[2]<<8|data[3]); //打印一下CO2和TVOC
}
int main(void){
board_init();
//初始化串口,为了将结果打印到串口助手上,不懂怎么操作的小伙伴可以看看之前关于串口的文章
Z_UART_Init();
Z_I2C_Init(); //初始化软件I2C相关引脚
Z_I2C_Start(); //I2C起始时序
Z_I2C_SendByte(0xB0); //发送从机地址+写
if(0 != Z_I2C_ReveiceACK()) printf("ACK error\r\n");
Z_I2C_SendByte(0x20); //发送初始化命令0x2003
if(0 != Z_I2C_ReveiceACK()) printf("ACK error\r\n");
Z_I2C_SendByte(0x03);
if(0 != Z_I2C_ReveiceACK()) printf("ACK error\r\n");
Z_I2C_End(); //结束I2C
delay_ms(100); //初始化需要10ms,但我们还是延时久一点
while (1){
printfSGP30();
delay_ms(1000);
}
}
可以收到数据,并且前十几秒的数据固定是400和0是正常现象。
GD32硬件I2C
之前的文章讲了GD32的引脚IIC,那么我们也加上GD32引脚IIC的例子吧。没看过且感兴趣的小伙伴可以再回顾一下。
【GD32】08 - IIC(以SHT20为例)-CSDN博客文章浏览阅读550次,点赞26次,收藏14次。接下来是设置IIC通信的模式与地址,模式我们自然是选择I2C模式的,而地址可以选择7位或者是10位的(10位的参数截图没截上,因为卡在手册的下一页了),这个根据我们通信的模块的从机地址而定。今天来了解一下GD32中的硬件IIC,其实我个人是觉得软件IIC比较方便的,不过之前文章里用的都是软件IIC,今天就算是走出自己的舒适圈,我们来了解了解GD32中的硬件IIC。关于IIC以及本文中演示的SHT20,在之前的文章里都有,并且也不是本文的重点,因此这里就不介绍了,不了解且感兴趣的小伙伴可以去看看之前的文章。https://blog.csdn.net/m0_63235356/article/details/140020224?spm=1001.2014.3001.5501
#include "board.h"
#include <stdio.h>
#include "Z_UART.h"
//获取SGP30的数据并打印
void printfSGP30(void){
i2c_start_on_bus(I2C0); //I2C起始时序
while(!i2c_flag_get(I2C0,I2C_FLAG_SBSEND) );
i2c_master_addressing(I2C0,0xB0,I2C_TRANSMITTER); //发送从机地址+写
while(!i2c_flag_get(I2C0,I2C_FLAG_ADDSEND) ); //等待从机发送完毕之后得到回应(即从机地址正确)
i2c_flag_clear(I2C0,I2C_FLAG_ADDSEND);
i2c_data_transmit(I2C0,0x20);
while(!i2c_flag_get(I2C0,I2C_FLAG_BTC) );
i2c_data_transmit(I2C0,0x08);
while(!i2c_flag_get(I2C0,I2C_FLAG_BTC) );
i2c_stop_on_bus(I2C0); //结束I2C
delay_ms(100); //需要等待10ms,保险起见延时久一点
i2c_start_on_bus(I2C0); //I2C起始时序
while(!i2c_flag_get(I2C0,I2C_FLAG_SBSEND) );
i2c_master_addressing(I2C0,0xB0,I2C_RECEIVER); //发送从机地址+读
while(!i2c_flag_get(I2C0,I2C_FLAG_ADDSEND) ); //等待从机发送完毕之后得到回应(即从机地址正确)
i2c_flag_clear(I2C0,I2C_FLAG_ADDSEND);
i2c_ack_config(I2C0, I2C_ACK_ENABLE);
uint16_t data[4] = {0};
while(!i2c_flag_get(I2C0,I2C_FLAG_RBNE) ); //等待接收缓冲区不为空
data[0] = i2c_data_receive (I2C0);
while(!i2c_flag_get(I2C0,I2C_FLAG_RBNE) ); //等待接收缓冲区不为空
data[1] = i2c_data_receive (I2C0);
while(!i2c_flag_get(I2C0,I2C_FLAG_RBNE) ); //等待接收缓冲区不为空
uint8_t crc = i2c_data_receive (I2C0);
printf("CRC is %X\r\n",crc);
while(!i2c_flag_get(I2C0,I2C_FLAG_RBNE) ); //等待接收缓冲区不为空
data[2] = i2c_data_receive (I2C0);
i2c_ack_config(I2C0, I2C_ACK_DISABLE);
while(!i2c_flag_get(I2C0,I2C_FLAG_RBNE) ); //等待接收缓冲区不为空
data[3] = i2c_data_receive (I2C0);
i2c_stop_on_bus(I2C0);
printf("%X\t%X\t%X\t%X\r\n",data[0],data[1],data[2],data[3]); //打印一下原始数据
printf("CO2 is %d,TVOC is %d\r\n",data[0]<<8|data[1],data[2]<<8|data[3]); //打印一下CO2和TVOC
}
int main(void){
board_init();
//初始化串口,为了将结果打印到串口助手上,不懂怎么操作的小伙伴可以看看之前关于串口的文章
Z_UART_Init();
//开启时钟
rcu_periph_clock_enable(RCU_I2C0);
rcu_periph_clock_enable(RCU_GPIOB);
//初始化硬件IIC的引脚
gpio_af_set(GPIOB, GPIO_AF_4,GPIO_PIN_8|GPIO_PIN_9);
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_8|GPIO_PIN_9);
gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ,GPIO_PIN_8|GPIO_PIN_9);
i2c_deinit(I2C0); //复位IIC0
i2c_clock_config(I2C0, 100000, I2C_DTCY_2); //设置IIC速率为100k
i2c_ack_config(I2C0, I2C_ACK_ENABLE); //使能应答
i2c_enable(I2C0); //使能IIC
i2c_start_on_bus(I2C0); //I2C起始时序
while(!i2c_flag_get(I2C0,I2C_FLAG_SBSEND) );
i2c_master_addressing(I2C0,0xB0,I2C_TRANSMITTER); //发送从机地址+写
while(!i2c_flag_get(I2C0,I2C_FLAG_ADDSEND)); //等待从机发送完毕之后得到回应(即从机地址正确)
i2c_flag_clear(I2C0,I2C_FLAG_ADDSEND);
i2c_data_transmit(I2C0,0x20);
while(!i2c_flag_get(I2C0,I2C_FLAG_BTC) );
i2c_data_transmit(I2C0,0x03);
while(!i2c_flag_get(I2C0,I2C_FLAG_BTC) );
i2c_stop_on_bus(I2C0); //结束I2C
delay_ms(100); //初始化需要10ms,但我们还是延时久一点
while (1){
printfSGP30();
delay_ms(1000);
}
}
ESP32硬件I2C
这边也提供一下ESP32硬件I2C的代码吧,ESP32的硬件I2C方便好多,一下子就调通了。
顺便一提,因为没找到实习,于是决定暑假开始录制ESP32(ESP-IDF)的视频了,可以的话可以把今年服务外包的项目(省赛都没进)的硬件部分当个练手项目分享出来(基于ESP32),还有时间的话可以再录制一下GD32的视频。
ESP32已经在录了,相信没过多久就可以和大家见面了。可以关注一下b站的同名账号,微信视频号也会一起发。
#include <stdio.h>
#include "driver/i2c.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// 获取SGP30的数据并打印
void printfSGP30(void) {
i2c_cmd_handle_t cmd_handle = i2c_cmd_link_create();
i2c_master_start(cmd_handle);
i2c_master_write_byte(cmd_handle, 0xB0, true);
i2c_master_write_byte(cmd_handle, 0x20, true);
i2c_master_write_byte(cmd_handle, 0x08, true);
i2c_master_stop(cmd_handle);
i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, 100 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd_handle);
vTaskDelay(100 / portTICK_PERIOD_MS);
uint8_t data[6] = {0};
cmd_handle = i2c_cmd_link_create();
i2c_master_start(cmd_handle);
i2c_master_write_byte(cmd_handle, 0xB1, true);
i2c_master_read_byte(cmd_handle, &data[0], I2C_MASTER_ACK);
i2c_master_read_byte(cmd_handle, &data[1], I2C_MASTER_ACK);
i2c_master_read_byte(cmd_handle, &data[2], I2C_MASTER_ACK);
i2c_master_read_byte(cmd_handle, &data[3], I2C_MASTER_ACK);
i2c_master_read_byte(cmd_handle, &data[4], I2C_MASTER_ACK);
i2c_master_read_byte(cmd_handle, &data[5], I2C_MASTER_ACK);
i2c_master_stop(cmd_handle);
i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, 100 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd_handle);
printf("CO2 is %d,TVOC is %d\r\n", (uint16_t)data[0] << 8 | data[1],
(uint16_t)data[3] << 8 | data[4]); // 打印一下CO2和TVOC
}
void app_main(void) {
printf("hello world\r\n");
i2c_config_t i2c_initer = {
.clk_flags = 0, // 选择默认时钟源
.master.clk_speed = 100000, // 指定速率为100Kbit,最大可以为400Kbit
.mode = I2C_MODE_MASTER, // 主机模式
.scl_io_num = 7, // 指定SCL的GPIO口
.scl_pullup_en = true, // SCL接上拉电阻
.sda_io_num = 8, // 指定SDA的GPIO口
.sda_pullup_en = true, // SDA接上拉电阻
};
if (i2c_param_config(I2C_NUM_0, &i2c_initer) == ESP_OK)
printf("i2c parm config success\r\n");
else
printf("config fail\r\n");
if (i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0) == ESP_OK)
printf("i2c driver install success\r\n");
else
printf("driver fail\r\n");
i2c_cmd_handle_t cmd_handle = i2c_cmd_link_create();
i2c_master_start(cmd_handle);
i2c_master_write_byte(cmd_handle, 0xB0, true);
i2c_master_write_byte(cmd_handle, 0x20, true);
i2c_master_write_byte(cmd_handle, 0x03, true);
i2c_master_stop(cmd_handle);
i2c_master_cmd_begin(I2C_NUM_0, cmd_handle, 100 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd_handle);
while (1) {
printfSGP30();
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
也可以正常打印出结果。
文档原文包括译文,以及卖家发的资料我都打包好了,大家可以关注我的同名公众号“折途想要敲代码”,回复关键词“SGP30”即可免费下载啦。