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"); //接收ACKZ_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(); //结束I2Cdelay_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"); //接收ACKdata[0] = Z_I2C_ReveiceByte(); //接收CO2的高8位Z_I2C_SendACK(0); //发送ACKdata[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); //发送初始化命令0x2003if(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(); //结束I2Cdelay_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); //结束I2Cdelay_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); //复位IIC0i2c_clock_config(I2C0, 100000, I2C_DTCY_2); //设置IIC速率为100ki2c_ack_config(I2C0, I2C_ACK_ENABLE); //使能应答i2c_enable(I2C0); //使能IICi2c_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); //结束I2Cdelay_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");elseprintf("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");elseprintf("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”即可免费下载啦。