当前位置: 首页 > news >正文

基于STM32的AGS10 MEMS TVOC传感器I2C驱动移植与室内空气质量监测实战

基于STM32的AGS10 MEMS TVOC传感器I2C驱动移植与室内空气质量监测实战

最近在做一个室内环境监测的小项目,需要检测空气中的TVOC(总挥发性有机物)浓度,选用了AGS10这款数字输出的MEMS传感器。它用起来挺方便的,直接通过I2C接口输出数据,不用像模拟传感器那样还得接ADC。但在STM32上把它调通,还是遇到了一些小坑,特别是I2C时序和CRC校验部分。今天我就把整个移植过程,从硬件连接到软件驱动,再到数据读取和验证,手把手地分享给大家,希望能帮你快速上手。

这篇文章适合正在学习STM32(特别是F1系列)并想接入数字传感器的朋友。咱们会用到一块立创的开发板,但原理是通用的,其他STM32开发板也完全可以参考。读完本文,你将能独立完成AGS10传感器的驱动编写、数据读取和校验,并最终在串口上看到实时的TVOC浓度值。

1. 认识AGS10:你的空气“嗅探器”

在动手写代码之前,咱们先花几分钟了解一下要驱动的“主角”——AGS10传感器。知道它的脾气,后面调试起来才顺手。

AGS10是一款采用MEMS(微机电系统)技术的TVOC气体传感器。简单来说,它内部有个对气体敏感的小结构,当空气中的有机挥发物(比如甲醛、苯、酒精等)浓度变化时,这个小结构的电学特性会改变,传感器内部的专用数字模块就把这个变化直接转换成数字信号,通过I2C接口送出来。

这样做的好处很明显:驱动电路简单(就两根信号线),抗干扰能力强(数字信号比模拟信号稳定),而且精度和稳定性都很好。它特别适合用在智能家居、新风系统或者便携式空气质量检测仪里。

它的几个关键参数,咱们做硬件连接和软件配置时得心里有数:

参数项规格说明对编程的影响
工作电压3.0V - 6.0V直接用STM32的3.3V供电就行,电平匹配。
通信接口I2C从机模式需要用STM32的GPIO模拟或硬件I2C去通信。
接口速率≤ 15 kHz特别注意:速度很慢,软件模拟I2C时延时要给足。
采样周期≥ 2秒两次读取数据之间至少要间隔2秒。
预热时间≥ 120秒上电后要等待至少2分钟,读数才稳定准确。
输出单位ppb (十亿分之一)读回来的数据直接就是ppb单位的浓度值。
测量范围0 ~ 99999 ppb数据用32位变量存储绰绰有余。

注意:I2C速率最高只有15kHz,这是很多朋友容易忽略的地方。如果你用软件模拟I2C,delay函数的时间要算对,否则通信会失败。硬件I2C也要把时钟频率配置到15kHz以下。

2. 硬件连接:把传感器“插”到开发板上

硬件连接很简单,但有几个细节决定了通信的成败。我用的主控是STM32F103,传感器模块是淘宝上常见的AGS10 breakout board。

所需材料:

  • STM32开发板(这里以立创开发板为例)
  • AGS10传感器模块
  • 杜邦线若干
  • 两个4.7kΩ的上拉电阻(非常重要!)

接线步骤:

  1. 供电:将AGS10模块的VCCGND分别连接到开发板的3.3VGND
  2. 信号线:将模块的SDASCL引脚连接到STM32的任意两个GPIO口。在示例代码中,我们用的是PB8PB9
  3. 关键一步——上拉电阻:I2C总线是开漏输出,必须加上拉电阻才能输出高电平。在SDASCL线上,各接一个4.7kΩ的电阻到3.3V。这个电阻可以放在开发板端,也可以放在传感器模块端。

连接好之后,硬件部分就搞定了。实物连接可以参考下图(想象一下):STM32的PB8接模块SDA,PB9接模块SCL,两根线都通过4.7k电阻拉到3.3V。

3. 软件驱动移植:手把手编写I2C底层代码

原始资料里提供了完整的驱动代码,但直接复制粘贴可能不知道所以然。咱们一起来拆解一下,看看每一部分是怎么工作的。整个驱动代码主要分为两个文件:bsp_ags10.c(功能实现)和bsp_ags10.h(引脚和宏定义)。

3.1 第一步:配置头文件,定义你的引脚

首先看bsp_ags10.h。这个文件的作用是硬件抽象,把具体的引脚定义放在这里,以后换用其他引脚(比如换成PC10PC11)只需要改这个文件,非常方便。

#ifndef _BSP_AGS10_H_ #define _BSP_AGS10_H_ #include "stm32f10x.h" // 1. 端口和时钟定义:我们使用GPIOB,对应APB2总线时钟 #define RCC_AGS10 RCC_APB2Periph_GPIOB #define PORT_AGS10 GPIOB // 2. 具体引脚定义:使用PB8作为SDA,PB9作为SCL #define GPIO_SDA GPIO_Pin_8 #define GPIO_SCL GPIO_Pin_9 // 3. 关键宏定义:快速切换SDA引脚的方向 // 设置SDA为开漏输出模式(用于发送数据) #define AGS10_SDA_OUT() {\ GPIO_InitTypeDef GPIO_InitStructure;\ GPIO_InitStructure.GPIO_Pin = GPIO_SDA;\ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;\ GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;\ GPIO_Init(PORT_AGS10, &GPIO_InitStructure);\ } // 设置SDA为上拉输入模式(用于接收数据) #define AGS10_SDA_IN() {\ GPIO_InitTypeDef GPIO_InitStructure;\ GPIO_InitStructure.GPIO_Pin = GPIO_SDA;\ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;\ GPIO_Init(PORT_AGS10, &GPIO_InitStructure);\ } // 4. 读写引脚电平的宏 #define AGS10_GETSDA() GPIO_ReadInputDataBit(PORT_AGS10, GPIO_SDA) // 读取SDA电平 #define AGS10_SDA(x) GPIO_WriteBit(PORT_AGS10, GPIO_SDA, (x?Bit_SET:Bit_RESET) ) // 设置SDA电平 #define AGS10_SCL(x) GPIO_WriteBit(PORT_AGS10, GPIO_SCL, (x?Bit_SET:Bit_RESET) ) // 设置SCL电平 // 函数声明 void ags10_gpio_init(void); uint32_t ags10_read(void); #endif

提示:GPIO_Mode_Out_OD是开漏输出模式,这是I2C通信的标准要求,必须这样设置。GPIO_Mode_IPU是内部上拉输入模式,在读取数据时,利用单片机内部的上拉电阻辅助总线保持高电平。

3.2 第二步:初始化GPIO引脚

bsp_ags10.c中,第一个要写的函数就是引脚初始化。这个函数会在main函数最开始调用一次。

void ags10_gpio_init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 1. 打开GPIOB的时钟开关 RCC_APB2PeriphClockCmd(RCC_AGS10, ENABLE); // 2. 配置PB8和PB9为开漏输出,并初始化为高电平 GPIO_InitStructure.GPIO_Pin = GPIO_SDA | GPIO_SCL; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度可以选50MHz,实际由延时控制 GPIO_Init(PORT_AGS10, &GPIO_InitStructure); // 3. 将SDA和SCL线拉高,总线处于空闲状态 GPIO_SetBits(PORT_AGS10, GPIO_SDA | GPIO_SCL); }

这个函数执行后,两个GPIO口就准备好了,并且总线处于空闲(SDA和SCL均为高电平)状态。

3.3 第三步:实现最基础的I2C时序函数

AGS10的I2C速率很慢,所以我们用GPIO模拟(“软件I2C”)更方便,也更容易理解时序。我们需要实现几个最基础的原子操作:起始信号、停止信号、发送一个字节、接收一个字节、等待应答。

起始信号 (Start Condition):SCL为高电平时,SDA出现一个下降沿。

void AGS10_IIC_Start(void) { AGS10_SDA_OUT(); // 确保SDA为输出模式 AGS10_SDA(1); // SDA高 AGS10_SCL(1); // SCL高 delay_1us(5); // 保持一段时间 AGS10_SDA(0); // SDA拉低,产生下降沿 delay_1us(5); AGS10_SCL(0); // SCL拉低,准备后续数据传输 delay_1us(5); }

停止信号 (Stop Condition):SCL为高电平时,SDA出现一个上升沿。

void AGS10_IIC_Stop(void) { AGS10_SDA_OUT(); AGS10_SCL(0); // 先确保SCL为低 AGS10_SDA(0); // SDA为低 AGS10_SCL(1); // SCL拉高 delay_1us(5); AGS10_SDA(1); // SDA拉高,产生上升沿 delay_1us(5); }

发送一个字节:数据在SCL为低电平时变化,在SCL为高电平时保持稳定。数据从最高位(MSB)开始发送。

void AGS10_IIC_Send_Byte(uint8_t dat) { int i = 0; AGS10_SDA_OUT(); AGS10_SCL(0); // 起始时SCL为低 for(i = 0; i < 8; i++) { // 取出最高位,右移7位后,结果不是1就是0 AGS10_SDA( (dat & 0x80) >> 7 ); delay_1us(1); // 数据稳定时间 AGS10_SCL(1); // 拉高SCL,从机在此时采样数据位 delay_1us(5); // 保持高电平,满足传感器时序要求 AGS10_SCL(0); // 拉低SCL,准备发送下一位 delay_1us(5); dat <<= 1; // 数据左移,准备发送下一位 } }

接收一个字节:主机控制SCL产生时钟,并在SCL高电平时读取SDA线上的数据。

unsigned char AGS10_IIC_Read_Byte(void) { unsigned char i, receive = 0; AGS10_SDA_IN(); // 重要!将SDA设置为输入模式,读取外部电平 for(i = 0; i < 8; i++) { AGS10_SCL(0); delay_1us(5); AGS10_SCL(1); // 拉高SCL,让从机放置数据 delay_1us(5); receive <<= 1; // 左移,为接收新数据位腾出空间 if( AGS10_GETSDA() ) // 读取SDA引脚电平 { receive |= 1; // 如果为高电平,该位置1 } } AGS10_SCL(0); // 读完8位后,将SCL拉低 return receive; }

等待应答 (Wait Acknowledge):主机发送完一个字节(8位数据)后,会释放SDA线(设置为输入),并在第9个时钟脉冲的高电平期间检测SDA是否为低电平(低电平表示从机应答)。

unsigned char AGS10_I2C_WaitAck(void) { char ack = 0; unsigned char ack_flag = 10; // 超时计数 AGS10_SCL(0); AGS10_SDA(1); // 主机先释放SDA线 AGS10_SDA_IN(); // 设置为输入,准备检测 delay_1us(5); AGS10_SCL(1); // 产生第9个时钟脉冲 delay_1us(5); // 循环检测SDA是否被从机拉低 while( (AGS10_GETSDA() == 1) && ( ack_flag ) ) { ack_flag--; delay_1us(5); } if( ack_flag <= 0 ) // 超时,未收到应答 { AGS10_IIC_Stop(); // 发送停止信号 return 1; // 返回1表示非应答/失败 } else // 收到应答 { AGS10_SCL(0); AGS10_SDA_OUT(); // 检测完毕,重新设置为输出模式 } return ack; // 返回0表示应答成功 }

3.4 第四步:实现CRC8校验函数

AGS10传感器为了保证数据传输的可靠性,使用了CRC8校验。主机在收到数据后,需要自己计算一遍CRC值,并与传感器发来的CRC字节进行比较,一致才说明数据正确。

CRC可以理解为一个复杂的“指纹”算法,任何一位数据出错,算出来的“指纹”都会变。AGS10使用的CRC8参数是:初始值0xFF,多项式0x31

uint8_t Calc_CRC8(uint8_t *dat, uint8_t Num) { uint8_t i, byte, crc = 0xFF; // 初值0xFF for(byte = 0; byte < Num; byte++) { crc ^= (dat[byte]); // 先与数据异或 for(i = 0; i < 8; i++) // 对每一位进行处理 { if(crc & 0x80) crc = (crc << 1) ^ 0x31; // 多项式0x31 else crc = (crc << 1); } } return crc; }

这个函数接收一个数据数组dat和它的长度Num,返回计算出的CRC8值。在读取AGS10数据时,我们会用前4个字节的数据来计算CRC,然后与传感器发来的第5个字节(CRC字节)对比。

3.5 第五步:编写核心数据读取函数

这是驱动层最顶层的函数ags10_read()。它按照AGS10的通信协议,完成一次完整的数据读取和校验流程。

AGS10的I2C地址是固定的0x34(写)0x35(读)。读取TVOC浓度的流程是:

  1. 主机发送写地址0x34,并发送命令字节0x00(启动测量)。
  2. 主机发送读地址0x35,连续读取5个字节数据。
  3. 用前4个字节计算CRC8,与第5个字节比较。
uint32_t ags10_read(void) { uint8_t timeout = 0; uint8_t data[5] = {0}; // 存储读回的5个字节 uint32_t TVOC_data = 0; // 1. 发送启动测量命令 AGS10_IIC_Start(); AGS10_IIC_Send_Byte(0X34); // 发送写地址 if(AGS10_I2C_WaitAck() == 1) return 1; // 通信失败 AGS10_IIC_Send_Byte(0X00); // 发送命令字节0x00 if(AGS10_I2C_WaitAck() == 1) return 2; // 发送失败 AGS10_IIC_Stop(); // 发送停止信号 // 2. 等待传感器准备数据(防止它忙) do { delay_1ms(1); timeout++; AGS10_IIC_Start(); AGS10_IIC_Send_Byte(0X35); // 发送读地址 } while((AGS10_I2C_WaitAck() == 1) && (timeout < 50)); // 等待应答,最多50ms if(timeout >= 50) return 3; // 等待超时 // 3. 连续读取5个字节数据 data[0] = AGS10_IIC_Read_Byte(); // 字节0 AGS10_IIC_Send_Ack(); // 发送应答 data[1] = AGS10_IIC_Read_Byte(); // 字节1 AGS10_IIC_Send_Ack(); data[2] = AGS10_IIC_Read_Byte(); // 字节2 AGS10_IIC_Send_Ack(); data[3] = AGS10_IIC_Read_Byte(); // 字节3 AGS10_IIC_Send_Ack(); data[4] = AGS10_IIC_Read_Byte(); // 字节4 (CRC字节) AGS10_IIC_Send_Nack(); // 最后一个字节发送非应答 AGS10_IIC_Stop(); // 发送停止信号 // 4. CRC校验 if(Calc_CRC8(data, 4) != data[4]) // 用前4个字节计算CRC,与第5字节比较 { return 4; // 校验失败 } // 5. 组合有效数据 (字节1、2、3组成24位TVOC数据) TVOC_data = (data[1] << 16) | (data[2] << 8) | data[3]; return TVOC_data; // 返回TVOC浓度值,单位ppb }

这个函数的返回值需要特别注意:

  • 如果返回1, 2, 3, 4,表示通信过程中出现了错误。
  • 如果返回一个0~99999之间的数,那就是读取成功的TVOC浓度值(单位ppb)。

4. 上机验证:在main函数中读取并打印数据

驱动写好了,最后一步就是在主函数里调用它,并通过串口把数据打印出来看看。这里假设你已经配置好了串口1(USART1)用于打印。

#include "stm32f10x.h" #include "board.h" #include "bsp_uart.h" #include "stdio.h" #include "bsp_ags10.h" int main(void) { uint32_t tvoc_value = 0; uint8_t retry_count = 0; // 系统初始化 board_init(); // 串口初始化,用于打印数据 uart1_init(115200); // AGS10传感器GPIO初始化 ags10_gpio_init(); printf("AGS10 TVOC Sensor Test Start...\r\n"); printf("Please wait for sensor preheating (about 120s)...\r\n"); // 传感器预热等待 delay_1ms(120000); // 等待120秒预热 printf("Preheating done. Start reading data.\r\n"); while(1) { tvoc_value = ags10_read(); // 根据返回值判断读取状态 switch(tvoc_value) { case 1: printf("Error: I2C communication failed.\r\n"); break; case 2: printf("Error: Send command failed.\r\n"); break; case 3: printf("Error: Sensor response timeout.\r\n"); break; case 4: printf("Error: CRC check failed.\r\n"); break; default: // 读取成功,打印TVOC浓度 printf("TVOC Concentration = %ld ppb\r\n", tvoc_value); break; } // 两次读取之间至少间隔2秒(传感器要求) delay_1ms(2000); } }

把代码编译下载到开发板,打开串口助手(波特率115200),你就能看到TVOC浓度的输出了。对着传感器哈一口气,或者喷一点酒精在附近(注意别直接喷到传感器上!),数值应该会有明显的上升,这说明你的驱动工作正常了。

5. 调试心得与常见问题排查

最后,分享几个我在调试过程中踩过的坑和解决办法:

  1. 通信完全失败,一直返回错误1或2

    • 检查硬件:首先用万用表量一下SDA和SCL线在不通信时是不是高电平(约3.3V)。如果不是,检查上拉电阻是否接好、阻值是否合适(4.7k-10k)。
    • 检查接线:确认SDA、SCL、VCC、GND没有接错或虚焊。
    • 检查地址:确认发送的I2C地址是0x34(写)和0x35(读)。
  2. 能通信但CRC校验总是失败(返回错误4)

    • 时序问题:这是最常见的原因。AGS10的I2C速率很慢(≤15kHz),检查delay_1us(5)这样的延时是否足够。可以尝试把延时再加大一点,比如改成delay_1us(10)
    • 电源噪声:传感器对电源质量比较敏感。尝试在传感器的VCC和GND之间并联一个10uF的电解电容和一个0.1uF的瓷片电容,进行滤波。
  3. 读数不稳定或为0

    • 预热不足:AGS10需要至少120秒的预热时间才能输出稳定准确的数据。刚上电就读数是不准的,务必等待足够时间。
    • 采样间隔太短:两次读取操作之间必须间隔至少2秒,否则传感器可能来不及完成一次完整的测量。
  4. 关于delay_1usdelay_1ms函数

    • 示例代码中的delay_1usdelay_1ms需要你自己实现,通常可以用SysTick定时器或者简单的循环延时来实现。确保它们的精度,特别是微秒级延时,是软件I2C稳定的关键。

好了,关于AGS10传感器在STM32上的驱动移植和实战应用就讲到这里。整个过程从硬件连接到软件调试,虽然步骤不少,但一步步跟着做下来,你会发现并没有想象中那么难。最重要的是理解了I2C的通信流程和AGS10这个特定传感器的协议。把这个驱动调通后,你就可以轻松地把它集成到你的环境监测项目里去了。

http://www.jsqmd.com/news/496181/

相关文章:

  • SOC芯片设计中的DFT实战:OCC时钟管理与ATPG测试架构全解析
  • 影刀 RPA 实战进阶:从官方教程到企业级应用开发心法
  • LC滤波器设计避坑指南:为什么你的FPGA实现和仿真结果总对不上?
  • 零代码黑苹果配置:OpCore Simplify自动化工具如何让72小时调试变成15分钟流程
  • StructBERT文本相似度WebUI快速上手:无需代码,打开网页就能用的AI工具
  • DAMOYOLO-S企业应用:制造业缺陷检测中替代传统OpenCV方案实测
  • 安卓系统日志全解析:从内核到应用层的dmesg与logcat使用指南
  • 如何高效回收沃尔玛购物卡?方法超简单 - 团团收购物卡回收
  • Verilog文件管理实战:如何用-y和libext简化大型设计的filelist维护
  • ccmusic-database/music_genre一文详解:Gradio状态管理与异步推理优化
  • 2026年国网在线监测系统TOP品牌盘点:技术实力与市场口碑深度解析 - 品牌推荐大师1
  • Flowise消息通知:邮件/Webhook事件推送配置
  • 讲讲BWT倍世净水器,技术先进吗,北京地区哪家口碑好 - 工业推荐榜
  • 5分钟搞定:用C++手搓一个Brainfuck解释器(附完整代码)
  • 告别自动提交:在DBeaver中配置事务手动提交模式
  • TechWiz LCD 3D应用:FFS仿真
  • Dice Loss与Focal Loss在医学图像分割中的实战对比
  • 值得推荐的超声波流量计供应商排名,南京欧卡排第几? - 工业品牌热点
  • PID智能小车调参实战(一)
  • VirtualLab:泰伯效应的建模
  • 2026年四川地区环保装配式墙板性价比排名,价格多少钱 - myqiye
  • Excel VBA宏实战:动态列图片链接批量转嵌入图片
  • FoxPro(VFP) 进阶指南:深入解析Visual FoxPro SYS函数的实战应用
  • AIGlasses OS Pro效果实测:复杂光照与天气条件下的鲁棒性表现
  • GLM-OCR模型压缩与加速:在边缘设备部署的可行性探索
  • 2026寻上海小红书代运营?老牌公司服务更靠谱,小红书代运营推荐优选实力品牌 - 品牌推荐师
  • X射线成像中的泰伯效应
  • 重构黑苹果配置体验:OpCore Simplify如何用智能技术终结EFI调试噩梦
  • 实用教程:雪女-斗罗大陆模型在星图平台的部署与调用详解
  • OCAD应用:光学系统热环境分析