SGP30气体传感器在天空星STM32F407上的I2C驱动移植与TVOC/eCO2测量实战

📅 发布时间:2026/7/14 22:27:41 👁️ 浏览次数:
SGP30气体传感器在天空星STM32F407上的I2C驱动移植与TVOC/eCO2测量实战
SGP30气体传感器在天空星STM32F407上的I2C驱动移植与TVOC/eCO2测量实战最近在做一个室内空气质量监测的小项目需要用到气体传感器。对比了几款之后发现SGP30这款传感器挺有意思它不仅能测等效二氧化碳eCO2还能测总挥发性有机物TVOC而且价格不贵接口也简单就是I2C。正好手头有块天空星的STM32F407开发板就决定用它来驱动SGP30。今天我就把整个移植和调试的过程包括踩过的坑详细地分享给大家希望能帮到正在做类似项目的朋友。SGP30是一款数字气体传感器内部集成了多个金属氧化物传感元件出厂就校准好了我们直接通过I2C读取数据就行不用自己再做复杂的标定。它主要输出两个值TVOC范围0~60000 ppb和eCO2范围400~60000 ppm。对于智能家居、环境监测这类应用来说数据完全够用。咱们这个教程的目标很明确在天空星STM32F407开发板上通过软件模拟I2C的方式成功驱动SGP30传感器并稳定地读取到TVOC和eCO2数据。我会从最基础的原理讲起然后手把手带你写驱动代码最后完成测试。即使你之前没怎么接触过I2C或者气体传感器跟着步骤走也能搞定。1. 准备工作了解SGP30与硬件连接在动手写代码之前咱们得先把传感器和芯片怎么“对话”搞清楚再把线接对。1.1 SGP30传感器关键信息根据数据手册有几个关键点必须记住通信接口标准的I2C接口。设备地址7位地址是0x58。这里有个细节我们实际发送的8位地址需要根据读写操作来组合写操作地址(0x58 1) 0xB0读操作地址(0x58 1) | 0x01 0xB1核心命令SGP30的命令都是两个字节16位。我们最常用两个初始化命令0x2003。发送这个命令启动传感器。测量命令0x2008。发送这个命令来获取TVOC和eCO2数据。数据格式读取一次数据会收到6个字节。顺序是eCO2高字节eCO2低字节eCO2数据的CRC校验字节TVOC高字节TVOC低字节TVOC数据的CRC校验字节上电特性模块上电后需要大约15秒进行初始化。在这期间你读到的eCO2值会固定是400ppmTVOC值是0ppb。所以程序里需要判断只有当读到的值脱离了这个“初始状态”才说明传感器真正准备好了。1.2 硬件连接与引脚选择我用的天空星STM32F407开发板选择用PB8和PB9这两个引脚来软件模拟I2C。当然你也可以换成其他任意GPIO记得在代码里改一下就行。接线非常简单只有四根线SGP30模块引脚STM32F407引脚说明VCC3.3V电源正极务必接3.3VGNDGND电源地SCLPB9I2C时钟线SDAPB8I2C数据线注意一定要确保电源是3.3V接5V可能会损坏传感器。另外SGP30工作电流约40mA确保你的电源能提供足够的电流。2. 驱动代码移植与详解接下来就是核心部分了——写驱动代码。我会把代码分成几个部分并解释每一块是干什么的为什么要这么写。2.1 头文件 (bsp_sgp30.h) 的配置头文件主要完成宏定义和函数声明把硬件相关的引脚配置都放在这里以后换引脚改起来方便。#ifndef _BSP_SGP30_H_ #define _BSP_SGP30_H_ #include stm32f4xx.h // 1. 端口与引脚定义 // 使用GPIOB的8、9脚模拟I2C #define RCC_SGP30 RCC_AHB1Periph_GPIOB #define PORT_SGP30 GPIOB #define GPIO_SDA GPIO_Pin_8 #define GPIO_SCL GPIO_Pin_9 // 2. 关键宏定义快速切换SDA引脚方向 // 软件模拟I2C时SDA线一会儿要输出发数据一会儿要输入读数据所以方向要能快速切换。 #define SDA_OUT() do{ \ GPIO_InitTypeDef GPIO_InitStructure; \ GPIO_InitStructure.GPIO_Pin GPIO_SDA; \ GPIO_InitStructure.GPIO_Mode GPIO_Mode_OUT; \ GPIO_InitStructure.GPIO_OType GPIO_OType_OD; // 开漏输出方便实现“线与” \ GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; \ GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; // 内部上拉保证空闲时为高电平 \ GPIO_Init(PORT_SGP30, GPIO_InitStructure); \ }while(0) #define SDA_IN() do{ \ GPIO_InitTypeDef GPIO_InitStructure; \ GPIO_InitStructure.GPIO_Pin GPIO_SDA; \ GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN; \ GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; \ GPIO_Init(PORT_SGP30, GPIO_InitStructure); \ }while(0) // 3. 引脚电平操作宏 #define SDA_GET() GPIO_ReadInputDataBit(PORT_SGP30, GPIO_SDA) // 读取SDA输入电平 #define SDA(x) GPIO_WriteBit(PORT_SGP30, GPIO_SDA, (x?Bit_SET:Bit_RESET)) // 设置SDA输出电平 #define SCL(x) GPIO_WriteBit(PORT_SGP30, GPIO_SCL, (x?Bit_SET:Bit_RESET)) // 设置SCL输出电平 // 4. 函数声明 void SGP30_Init(void); uint32_t SGP30_Read(void); void SGP30_Write_cmd(uint8_t regaddr_H, uint8_t regaddr_L); #endif2.2 源文件 (bsp_sgp30.c) 之基础I2C时序首先我们需要用软件模拟出I2C通信最基本的几个时序起始、停止、发送应答、等待应答、发送一个字节、接收一个字节。这是驱动任何I2C设备的基础。#include bsp_sgp30.h #include stdio.h #include board.h // 包含了delay_ms等函数 // 引脚初始化配置PB8和PB9为开漏输出并上拉 void SGP30_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_SGP30, ENABLE); // 使能GPIOB时钟 GPIO_InitStructure.GPIO_Pin GPIO_SDA | GPIO_SCL; GPIO_InitStructure.GPIO_Mode GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType GPIO_OType_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; // 内部上拉 GPIO_Init(PORT_SGP30, GPIO_InitStructure); GPIO_SetBits(PORT_SGP30, GPIO_SDA | GPIO_SCL); // 初始置高 } // I2C起始信号SCL高电平时SDA产生一个下降沿 void IIC_Start(void) { SDA_OUT(); SCL(0); // 确保SCL为低 delay_us(1); SDA(1); // SDA拉高 SCL(1); // SCL拉高 delay_us(5); SDA(0); // SDA拉低产生下降沿 delay_us(5); SCL(0); // 拉低SCL准备传输数据 delay_us(5); } // I2C停止信号SCL高电平时SDA产生一个上升沿 void IIC_Stop(void) { SDA_OUT(); SCL(0); SDA(0); delay_us(1); SCL(1); delay_us(5); SDA(1); // SDA拉高产生上升沿 delay_us(5); } // 主机发送应答(ACK)或非应答(NACK)信号 // ack0: 发送应答(ACK)拉低SDA // ack1: 发送非应答(NACK)保持SDA高 void IIC_Send_Ack(unsigned char ack) { SDA_OUT(); SCL(0); if(!ack) SDA(0); // 发送ACK else SDA(1); // 发送NACK delay_us(5); SCL(1); // 产生一个时钟脉冲 delay_us(5); SCL(0); SDA(1); // 释放SDA线 } // 主机等待从机(SGP30)的应答信号 // 返回0: 收到应答返回1: 超时未收到应答通信失败 unsigned char I2C_WaitAck(void) { unsigned char ack_flag 10; // 超时计数 SCL(0); SDA(1); // 主机先释放SDA线 SDA_IN(); // 切换SDA为输入准备读取从机应答 delay_us(5); SCL(1); // 产生时钟脉冲从机应在此期间拉低SDA delay_us(5); // 循环检测SDA是否被拉低并计数防死循环 while( (SDA_GET()1) (ack_flag) ) { ack_flag--; delay_us(5); } if( ack_flag 0 ) // 超时未收到应答 { IIC_Stop(); // 发送停止信号 return 1; } else // 收到应答 { SCL(0); SDA_OUT(); // 切换回输出模式为后续发送数据做准备 } return 0; } // 向I2C总线发送一个字节数据 void Send_Byte(uint8_t dat) { int i 0; SDA_OUT(); SCL(0); // 拉低时钟准备开始 for( i 0; i 8; i ) { // 从最高位(bit7)开始发送 SDA( (dat 0x80) 7 ); delay_us(1); SCL(1); // 拉高时钟数据被采样 delay_us(5); SCL(0); // 拉低时钟为下一位数据做准备 delay_us(5); dat 1; // 左移准备发送下一位 } } // 从I2C总线读取一个字节数据 unsigned char Read_Byte(void) { unsigned char i, receive 0; SDA_IN(); // SDA设置为输入模式 for(i 0; i 8; i ) { SCL(0); delay_us(5); SCL(1); // 拉高时钟此时从机应更新SDA数据 delay_us(5); receive 1; // 左移为接收新数据位腾出空间 if( SDA_GET() ) // 读取SDA线上的电平 { receive | 1; // 如果为高则对应位置1 } delay_us(5); } SCL(0); return receive; }2.3 源文件 (bsp_sgp30.c) 之SGP30专用函数有了基础的I2C时序函数我们就可以封装针对SGP30的读写函数了。// 向SGP30发送一个命令16位 void SGP30_Write_cmd(uint8_t regaddr_H, uint8_t regaddr_L) { IIC_Start(); Send_Byte(0xB0); // 发送写地址 (0x58 1) I2C_WaitAck(); Send_Byte(regaddr_H); // 发送命令高字节 I2C_WaitAck(); Send_Byte(regaddr_L); // 发送命令低字节 I2C_WaitAck(); IIC_Stop(); delay_ms(100); // 命令发送后需要等待一段时间 } // 从SGP30读取一次测量数据6个字节 // 返回值是一个32位数高16位是eCO2低16位是TVOCCRC字节被丢弃 uint32_t SGP30_Read(void) { uint32_t dat 0; uint8_t crc; IIC_Start(); Send_Byte(0xB1); // 发送读地址 (0x58 1) | 0x01 I2C_WaitAck(); // 读取eCO2高字节并发送ACK dat Read_Byte(); IIC_Send_Ack(0); dat 8; // 读取eCO2低字节并发送ACK dat Read_Byte(); IIC_Send_Ack(0); // 读取eCO2的CRC校验字节并发送ACK此处代码未实际校验CRC crc Read_Byte(); IIC_Send_Ack(0); dat 8; // 读取TVOC高字节并发送ACK dat Read_Byte(); IIC_Send_Ack(0); dat 8; // 读取TVOC低字节并发送NACK表示读取结束 dat Read_Byte(); IIC_Send_Ack(1); // 发送NACK IIC_Stop(); return dat; // 返回的数据格式0x[CO2_H][CO2_L][TVOC_H][TVOC_L] } // SGP30初始化函数 void SGP30_Init(void) { SGP30_GPIO_Init(); // 初始化GPIO SGP30_Write_cmd(0x20, 0x03); // 发送初始化命令 0x2003 }提示上面的SGP30_Read函数为了简化跳过了CRC校验。在实际要求高的场合建议加上CRC校验计算确保数据可靠性。SGP30使用的CRC8多项式是0x31 (x^8 x^5 x^4 1)。3. 在主程序中测试与数据读取驱动函数都写好了最后就是在主函数里把它们用起来。这里有个关键点处理传感器的上电初始化时间。#include board.h #include bsp_uart.h #include stdio.h #include bsp_sgp30.h int main(void) { uint32_t CO2Data, TVOCData; // 定义eCO2和TVOC变量 uint32_t sgp30_dat; // 存放从传感器读回的原始数据 board_init(); // 开发板基础初始化时钟、系统滴答定时器等 uart1_init(115200U); // 初始化串口1用于打印数据到电脑 SGP30_Init(); // 初始化SGP30传感器发送0x2003命令 delay_ms(100); printf(SGP30 Sensor Test Start...\r\n); // 等待传感器初始化完成约15秒 // 在此期间读到的eCO2固定为400TVOC固定为0 while (1) { SGP30_Write_cmd(0x20, 0x08); // 发送测量命令 0x2008 sgp30_dat SGP30_Read(); // 读取数据 CO2Data (sgp30_dat 0xffff0000) 16; // 提取高16位作为eCO2 TVOCData sgp30_dat 0x0000ffff; // 提取低16位作为TVOC // 判断是否脱离初始化状态 if(CO2Data ! 400 || TVOCData ! 0) { printf(Sensor Initialized!\r\n); break; } printf(Initializing... CO2:%d, TVOC:%d\r\n, CO2Data, TVOCData); delay_ms(1000); // 每秒检测一次 } // 主循环持续读取并打印数据 while (1) { SGP30_Write_cmd(0x20, 0x08); sgp30_dat SGP30_Read(); CO2Data (sgp30_dat 0xffff0000) 16; TVOCData sgp30_dat 0x0000ffff; printf(eCO2: %d ppm, TVOC: %d ppb\r\n, CO2Data, TVOCData); delay_ms(2000); // 每2秒读取一次 } }4. 实际调试经验与注意事项代码烧录进去串口助手打开你可能会遇到以下情况别慌这都是正常的上电后前15秒左右数据不变这是传感器在自检和预热eCO2显示400TVOC显示0。耐心等待即可。初始化完成后数据跳动较大气体传感器非常敏感周围空气的微小流动、人呼出的气体都会导致读数波动。这是正常现象不是程序bug。如何让数据更稳定硬件上尽量让传感器远离风口、热源保持通风但不要有剧烈气流。软件上可以对连续读取的多个值做滑动平均滤波。比如连续读10次去掉最大最小值再求平均能有效平滑数据。I2C通信失败如果一直卡在初始化等待循环或者读回全是0请检查接线是否正确、牢固VCC, GND, SDA, SCL。电源是否是稳定的3.3V。上拉电阻是否接好如果模块上没有需要在SDA和SCL线上各接一个4.7kΩ电阻到3.3V。我们代码里使用了内部上拉对于短距离通信通常够用但外接上拉会更可靠。用逻辑分析仪或示波器抓一下I2C波形看起始、地址、数据、停止信号是否正常。按照上面的步骤做完你的天空星STM32F407应该就能稳定地读取SGP30传感器的数据了。这个驱动框架是通用的你完全可以把它用到其他STM32型号或者其他I2C设备上只需要修改一下引脚定义和初始化部分就行。希望这篇教程能帮你顺利搞定SGP30祝你项目成功