STM32驱动SGP30(标准库+软件IIC)
0.简介
SGP30是由Sensirion公司推出的一款多功能空气质量传感器,采用CMOSens技术,通过I2C接口输出数据。具备高度集成、高灵敏度、低功耗等特点,适合用于家庭室内空气监测系统中。该模块可以同时测量空气中的总挥发性有机物(TVOC)和CO2当量浓度,相比MQ系列传感器,其数据更稳定、响应更迅速,适合精准的空气质量评估。
SGP30的核心原理是基于金属氧化物(MOX)气体感应技术。传感器表面涂覆了一层金属氧化物材料,这些材料在与空气中的气体分子接触时,会发生氧化还原反应,从而导致材料电导率的变化。这种变化会被内部电路转换为数字信号,SGP30采用多像素设计,每个像素对特定气体更敏感,传感器内部的算法会综合各像素的数据,输出稳定而精确的TVOC和CO2浓度值。SGP30计算CO2的值是基于检测到的H2浓度,因为室内环境中人类活动会产生H2,且与CO2水平有一定的相关性,因此CO2提供的是一个估算值,而非直接的CO2测量,测量范围见表。
检测量 | 测量范围 |
---|---|
TVOC | 0~60000ppb |
CO2 | 400~60000ppm |
1. 部分SGP30数据手册解读
通信地址
-
这部分就是说SGP30遵循IIC协议,从机地址是0X58,每个命令和返回数据都是十六位的数据,并且发送的命令的十六位包含三位CRC校验,接收的数据后面跟了八位的CRC校验。
-
由于地址只用到了7bit,最高位未使用,最低位为判断是读还是写,为0是读,为1是写
- 对于写SGP30,地址为(0X58 << 1) = 0XB0
- 对于读SGP30,地址为((0X58 << 1)) | 0X01 = 0XB1
读取数据
- 接收数据后要及时发送接收应答
- 我之前因为接收应答发送有问题,导致了收到的数据虽然不是65535,但是接收的数据一个是511,另一个是65535
-
SGP30获取的数据格式:2位CO2数据 + 1位CO2的CRC校验 + 2位TVOC数据 + 1位TVOC的CRC校验
-
模块上电需要15s左右初始化,在初始化阶段读取的CO2浓度为400ppm,TVOC为0ppd且恒定不变。
- 初始化SGP30命令:0x2003
- 获取空气质量值命令:0x2008
2. STM32软件IIC驱动SGP30
- 软件IIC的代码是在江科大的代码基础上稍微修改了一下
- 使用的时候直接在
sgp30.h
中修改引脚就行
sgp30.c
#include "sgp30.h"
/**
* 函 数:I2C写SCL引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
*/
void IIC_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(SGP30_GPIO_PORT, SGP30_SCL_GPIO_PIN, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C写SDA引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
*/
void IIC_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(SGP30_GPIO_PORT, SGP30_SDA_GPIO_PIN, (BitAction)BitValue); //根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C读SDA引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
*/
uint8_t IIC_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(SGP30_GPIO_PORT, SGP30_SDA_GPIO_PIN); //读取SDA电平
delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
/**
* 函 数:I2C初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
*/
void IIC_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(SGP30_GPIO_CLK, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = SGP30_SCL_GPIO_PIN | SGP30_SDA_GPIO_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SGP30_GPIO_PORT, &GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出
/*设置默认电平*/
GPIO_SetBits(SGP30_GPIO_PORT, SGP30_SCL_GPIO_PIN | SGP30_SDA_GPIO_PIN); //设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}
/*协议层*/
/**
* 函 数:I2C起始
* 参 数:无
* 返 回 值:无
*/
void IIC_Start(void)
{
IIC_W_SDA(1); //释放SDA,确保SDA为高电平
IIC_W_SCL(1); //释放SCL,确保SCL为高电平
IIC_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
IIC_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
/**
* 函 数:I2C终止
* 参 数:无
* 返 回 值:无
*/
void IIC_Stop(void)
{
IIC_W_SDA(0); //拉低SDA,确保SDA为低电平
IIC_W_SCL(1); //释放SCL,使SCL呈现高电平
IIC_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
/**
* 函 数:I2C发送一个字节
* 参 数:Byte 要发送的一个字节数据,范围:0x00~0xFF
* 返 回 值:无
*/
void IIC_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环8次,主机依次发送数据的每一位
{
IIC_W_SDA(Byte & (0x80 >> i)); //使用掩码的方式取出Byte的指定一位数据并写入到SDA线
IIC_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
IIC_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
}
/**
* 函 数:I2C接收一个字节
* 参 数:无
* 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
*/
uint8_t IIC_ReceiveByte(void)
{
uint8_t i, Byte = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
IIC_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
for (i = 0; i < 8; i ++) //循环8次,主机依次接收数据的每一位
{
IIC_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
if (IIC_R_SDA() == 1){Byte |= (0x80 >> i);} //读取SDA数据,并存储到Byte变量
//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
IIC_W_SCL(0); //拉低SCL,从机在SCL低电平期间写入SDA
}
return Byte; //返回接收到的一个字节数据
}
/**
* 函 数:I2C发送应答位
* 参 数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
* 返 回 值:无
*/
void IIC_SendAck(uint8_t AckBit)
{
IIC_W_SDA(AckBit); //主机把应答位数据放到SDA线
IIC_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
IIC_W_SCL(0); //拉低SCL,开始下一个时序模块
}
/**
* 函 数:I2C接收应答位
* 参 数:无
* 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
*/
uint8_t IIC_ReceiveAck(void)
{
uint8_t AckBit; //定义应答位变量
IIC_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
IIC_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
AckBit = IIC_R_SDA(); //将应答位存储到变量里
IIC_W_SCL(0); //拉低SCL,开始下一个时序模块
return AckBit; //返回定义应答位变量
}
//初始化IIC接口
void SGP30_Init(void)
{
IIC_Init();
SGP30_Write(0x20, 0x03);
// SGP30_ad_write(0x20,0x61);
// SGP30_ad_write(0x01,0x00);
}
void SGP30_Write(u8 a, u8 b)
{
IIC_Start();
IIC_SendByte(SGP30_write); //发送器件地址+写指令
IIC_ReceiveAck();
IIC_SendByte(a); //发送控制字节
IIC_ReceiveAck();
IIC_SendByte(b);
IIC_ReceiveAck();
IIC_Stop();
delay_ms(100);
}
u32 SGP30_Read(void)
{
u32 dat;
u8 crc;
IIC_Start();
IIC_SendByte(SGP30_read); //发送器件地址+读指令
IIC_ReceiveAck();
dat = IIC_ReceiveByte(); //CO2高位数据
IIC_SendAck(0);
dat <<= 8;
dat += IIC_ReceiveByte(); //CO2低位数据
IIC_SendAck(0);
crc = IIC_ReceiveByte(); //crc数据,舍去
IIC_SendAck(0);
crc = crc; //为了不让出现编译警告
dat <<= 8;
dat += IIC_ReceiveByte(); //TVOC高位数据
IIC_SendAck(0);
dat <<= 8;
dat += IIC_ReceiveByte();//TVOC低位数据
IIC_SendAck(0);
crc = IIC_ReceiveByte(); //crc数据,舍去
IIC_SendAck(1);
IIC_Stop();
return(dat);
}
void Get_CO2_TVOC(int n){
uint32_t sgp30_dat = 0,CO2_val = 0,TVOC_val = 0; // 初始化,避免随机的脏数据
u8 t;
for(t=0;t<n;t++)
{
SGP30_Write(0x20,0x08);
sgp30_dat = SGP30_Read();//读取SGP30的值
CO2_val += (sgp30_dat & 0xffff0000) >> 16;//取出CO2浓度值
TVOC_val += sgp30_dat & 0x0000ffff;
delay_ms(10);
}
CO2 = CO2_val / n;
TVOC = TVOC_val / n;
}
sgp30.h
#ifndef __SGP30_H
#define __SGP30_H
#include "stm32f10x.h"
#include "Delay.h"
// USART GPIO 引脚宏定义
#define SGP30_GPIO_CLK RCC_APB2Periph_GPIOA
#define SGP30_GPIO_PORT GPIOA
#define SGP30_SCL_GPIO_PIN GPIO_Pin_10
#define SGP30_SDA_GPIO_PIN GPIO_Pin_9
#define SGP30_read 0xb1 //SGP30的读地址
#define SGP30_write 0xb0 //SGP30的写地址
extern uint32_t TVOC,CO2;
void IIC_Init(void);
void IIC_Start(void);
void IIC_Stop(void);
void IIC_SendByte(uint8_t Byte);
uint8_t IIC_ReceiveByte(void);
void IIC_SendAck(uint8_t AckBit);
uint8_t IIC_ReceiveAck(void);
void SGP30_Init(void);
void SGP30_Write(u8 a, u8 b);
u32 SGP30_Read(void);
void Get_CO2_TVOC(int n);
#endif
Delay.c
#include "Delay.h"
#include "misc.h"
static u8 fac_us=0;//us延时倍乘数
static u16 fac_ms=0;//ms延时倍乘数
//初始化延迟函数
//SYSTICK的时钟固定为HCLK时钟的1/8
//SYSCLK:系统时钟
void delay_init(u8 SYSCLK)
{
// SysTick->CTRL&=0xfffffffb;//bit2清空,选择外部时钟 HCLK/8
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择外部时钟 HCLK/8
fac_us=SYSCLK/8;
fac_ms=(u16)fac_us*1000;
}
//延时nms
//注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms
//对72M条件下,nms<=1864
void delay_ms(u16 nms)
{
u32 temp;
SysTick->LOAD=(u32)nms*fac_ms;//时间加载(SysTick->LOAD为24bit)
SysTick->VAL =0x00; //清空计数器
SysTick->CTRL=0x01 ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL=0x00; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
//延时nus
//nus为要延时的us数.
void delay_us(u32 nus)
{
u32 temp;
SysTick->LOAD=nus*fac_us; //时间加载
SysTick->VAL=0x00; //清空计数器
SysTick->CTRL=0x01 ; //开始倒数
do
{
temp=SysTick->CTRL;
}
while(temp&0x01&&!(temp&(1<<16)));//等待时间到达
SysTick->CTRL=0x00; //关闭计数器
SysTick->VAL =0X00; //清空计数器
}
void delay_s(u16 ns)
{
for(int i=0;i<ns;i++)
delay_ms(1000);
}
Delay.h
#ifndef __DELAY_H
#define __DELAY_H
#include "stm32f10x.h"
void delay_init(u8 SYSCLK);
void delay_ms(u16 nms);
void delay_us(u32 nus);
void delay_s(u16 ns);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "usart.h"
#include "sgp30.h"
uint32_t TVOC = 0,CO2 = 0;
void sys_init(void);
int main(){
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init(72);
SGP30_Init(); //初始化SGP30
uart1_init(115200);
while(1){
Get_CO2_TVOC(5);
printf("CO2:%dppm------TVOC:%dppd\r\n",CO2,TVOC);
}
}