尧图精选

STM32F103驱动CCS811:实现eCO2与TVOC监测

🕒 发布时间:2026/9/1 14:35:56 📁 来源:尧图网络
简介面向STM32嵌入式开发者的CCS811环境传感器驱动工程包适用于智能家居、空气质量监测等场景的工程师与学习者。STM32F103作为Cortex-M3内核的经典微控制器通过I2C接口与ams公司的CCS811传感器通信实现CO2与TVOC浓度实时读取。工程基于Keil μVision搭建包含完整初始化、读写命令与数据解析代码并附带DHT11温湿度监测项目供对比学习。资源共418个文件以75个h头文件、71个c源文件、73个d与73个o编译中间文件为主另有hex固件、链接脚本、批处理工具及工程配置文件压缩包13.92MB目录结构涵盖用户代码、库文件、输出与文档等模块便于按需检索。已有1237人学习下载适合具备基础单片机知识、希望系统掌握I2C传感器驱动开发流程的读者参考。 如果你最近在做室内空气质量监测、智能家居联动这一类项目大概率绕不开CCS811这颗芯片。它个头不大却把气体传感器和算法处理浓缩到了一颗芯片里通过I2C总线就能直接读出等效二氧化碳eCO2和总挥发性有机物TVOC两组数据省去了自己跑标定算法的麻烦。这次我用手头最常见的STM32F103最小系统板把它点亮从硬件接线、寄存器时序到基线校准完整走了一遍。整个过程踩了几个比较典型的坑比如I2C地址选错、传感器一直输出默认值、环境补偿数据填反我把这些整理成文尽量让后来的人少走弯路。无论你用的是标准库、HAL库还是RTOSCCS811的驱动思路都一样底层I2C通信对上寄存器操作顺序对后面就只剩下读数据了。这篇文章适合刚接触气体传感器、想快速跑通第一版数据采集的开发者也适合已经在用CCS811但发现读数异常的人对照排查。1. CCS811这颗芯片到底解决了什么问题1.1 一颗传感器同时搞定eCO2和TVOC先说个比较反直觉的事实CCS811本质上并不是直接测量“二氧化碳浓度”或“有机物浓度”的传感器。它的核心是一个金属氧化物半导体MOS气体传感单元内部有一个微型加热器把传感器加热到工作温度气体分子吸附在敏感材料表面后引起电阻变化这些原始电信号再经过芯片内部的MCU和出厂固件算法换算最终输出我们习惯的ppm和ppb数值。所以CCS811做的事情是把过去需要“传感器加算法加标定”整个链条压缩到了单颗芯片里。你不需要关心内部电阻变化曲线也不需要自己去拟合温度和湿度对结果的影响只要通过I2C读寄存器就行。eCO2输出范围是400ppm到8192ppmTVOC输出范围是0ppb到1187ppb对于室内空气质量监测这个场景来说足够用了。1.2 为什么偏偏用STM32F103来带它选STM32F103的原因很实际它便宜、资料多、开发环境成熟是目前国内用得最多的Cortex-M3芯片之一。另一个关键点是CCS811的数字逻辑电平是3.3V而STM32F103的GPIO和I2C外设天生就是3.3V逻辑两者可以直接对接不需要加电平转换芯片。如果你拿5V的Arduino UNO去接CCS811那就必须加电平转换或者用开漏加外部上拉的方式处理否则长期运行存在超压损坏的风险。F103在这方面省了不少事这也是我最终选它而不是Arduino来做这个项目的原因之一。另外一个原因是F103资源够用跑一个传感器驱动再挂个OLED屏或串口打印都绰绰有余哪怕后面想加FreeRTOS做任务调度也完全带得动。2. 硬件连接从原理图到实物的几个关键决定2.1 引脚分配与供电CCS811常见封装下引脚比较多但实际项目里真正需要接的也就几根。我用的CCS811模块已经把上拉电阻、滤波电容、电源管理都做在板上了所以接线非常省事。下面是我这次使用的连接关系CCS811引脚功能说明接到STM32F103VDD主电源1.8V-3.6V3.3VGND地GNDSCLI2C时钟PB6SDAI2C数据PB7ADDRI2C地址选择GND地址0x5AWAK唤醒控制低有效GND固定低电平nRESET复位低有效3.3V或悬空nINT中断输出低有效不接或接PB8VDDIOIO接口电源3.3V这里有个容易被忽略的细节WAK引脚也标作nWAKE在数据手册里要求低电平才能进入正常工作状态如果不接或者悬空传感器可能一直停留在低功耗模式I2C有时能读到硬件ID但数据寄存器一直不更新。我一开始把WAK悬空结果就是驱动初始化正常但测量数据始终不跳变。后来把WAK直接接地问题立刻消失。供电方面VDD和VDDIO都必须接3.3V不能接5V。CCS811的VDD最高额定电压是3.6V接5V大概率当场烧毁或者寿命骤减。如果你是用电池供电还要注意CCS811内部有加热器工作电流峰值能到30mA左右电源纹波不能太大尽量在模块的VDD脚附近加一个10uF的钽电容或陶瓷电容。2.2 I2C上拉和电平匹配的细节I2C总线是漏极开路的SCL和SDA必须要有上拉电阻才能拉高电平。大多数成品模块已经板载了4.7kΩ或者10kΩ的上拉电阻不需要额外处理。但如果你买的是裸芯片或者自己画板子务必在SCL和SDA上各加一个4.7kΩ电阻到3.3V否则通信时好时坏表现是读寄存器有时返回正常、有时返回0xFF。使用STM32F103时GPIO要配置为复用开漏输出如果使用硬件I2C或者通用开漏输出如果使用软件模拟I2C。开漏输出配合外部上拉电阻是I2C标准推荐的接法。不少人图省事直接配成推挽输出也能通信但一旦总线上有多个设备、某个设备拉低总线时就可能出现电流倒灌的问题影响稳定性。2.3 “F103引脚到底能不能承受5V”这个老问题的答案在项目交流群里经常看到有人问“STM32F103的GPIO作为输入能不能承受5V”这个问题和接CCS811也有关系。F103的数据手册里确实标了一部分FT5V tolerant引脚比如PA0-PA15、PB0-PB15等部分引脚。但要注意CCS811是一个3.3V供电的器件它的SDA和SCL输出高电平是3.3VFT引脚能容忍5V输入不假但F103输出到CCS811的3.3V信号对CCS811来说也是正常的。真正需要担心的是反向情况如果你的主控板是5V供电的ArduinoGPIO输出高电平是5V这时直接接到CCS811的SDA/SCL就超出了器件的绝对最大额定值长期使用有风险。总而言之F103接CCS811不需要考虑5V问题但如果将来换5V主控记得加电平转换。3. 驱动程序编写模拟I2C与CCS811寄存器操作3.1 为什么我选择软件模拟I2C而不是硬件I2CSTM32F103的硬件I2C外设在标准外设库时代名声不算好BUSY标志卡死、错误标志清不掉是常见问题。HAL库发布之后硬件I2C的稳定性有所改善但配置过程依然比SPI和UART要繁琐。对于CCS811这种单从机、速率要求不高的场景软件模拟I2C反而是最稳的选择。软件模拟I2C的好处有三个一是代码逻辑完全可控出问题可以直接看波形二是方便移植以后换GD32、APM32或者其他M0内核芯片只需要改GPIO宏定义三是不依赖厂商库的I2C状态机避开了硬件外设的各个坑。缺点是会占用CPU但对于100kHz的I2C速率、几毫秒读一次数据来说CPU占用几乎可以忽略。所以我这次直接写了一套软件I2C的驱动后面如果项目要追求低功耗或者高吞吐再换成硬件I2C也不迟。3.2 I2C底层时序实现软件模拟I2C的核心就是按照I2C协议要求用GPIO翻转电平来模拟起始、停止、发送字节和接收字节。以下是我在STM32F103标准库环境下写的底层代码#include stm32f10x.h #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_PIN GPIO_Pin_7 #define I2C_GPIO_PORT GPIOB #define SCL_H() GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN) #define SCL_L() GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_PIN) #define SDA_H() GPIO_SetBits(I2C_GPIO_PORT, I2C_SDA_PIN) #define SDA_L() GPIO_ResetBits(I2C_GPIO_PORT, I2C_SDA_PIN) #define SDA_READ() (GPIO_ReadInputDataBit(I2C_GPIO_PORT, I2C_SDA_PIN)) static void i2c_delay(void) { // 系统时钟72MHz时大约产生1us左右延时 // 如果使用内部晶振建议把循环次数调大 for (volatile uint8_t i 0; i 40; i); } static void i2c_start(void) { SDA_H(); SCL_H(); i2c_delay(); SDA_L(); i2c_delay(); SCL_L(); i2c_delay(); } static void i2c_stop(void) { SDA_L(); SCL_H(); i2c_delay(); SDA_H(); i2c_delay(); } static uint8_t i2c_send_byte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x80) { SDA_H(); } else { SDA_L(); } dat 1; i2c_delay(); SCL_H(); i2c_delay(); SCL_L(); i2c_delay(); } // 释放SDA等待从机ACK SDA_H(); i2c_delay(); SCL_H(); i2c_delay(); uint8_t ack (SDA_READ() 0) ? 0 : 1; SCL_L(); i2c_delay(); return ack; } static uint8_t i2c_recv_byte(uint8_t ack) { uint8_t i, dat 0; SDA_H(); for (i 0; i 8; i) { dat 1; SCL_H(); i2c_delay(); if (SDA_READ()) { dat | 0x01; } SCL_L(); i2c_delay(); } // ack1表示主机发送ACKack0表示主机发送NACK if (ack) { SDA_L(); } else { SDA_H(); } i2c_delay(); SCL_H(); i2c_delay(); SCL_L(); i2c_delay(); SDA_H(); return dat; }这段代码里的i2c_delay是关键。如果延时太短SCL高电平时间不足从机跟不上时序通信会时好时坏。如果延时太长I2C速率降到十几kHz读一次CCS811要几十毫秒虽然功能正常但效率低。我实测在72MHz主频下循环40次产生的延时大约1us正好满足100kHz标准模式。如果你的板子用的是内部RC振荡器而不是外部晶振延时精度会差一些建议把循环次数提到60以上留足余量。3.3 寄存器读写封装与初始化流程有了底层I2C字节操作接下来就是按CCS811的寄存器地址来封装读写函数。注意一个细节读操作时写完寄存器地址之后要发一次repeated start然后再发从机地址加读位而不是先stop再start。否则部分从机可能会漏掉读命令。#define CCS811_ADDR 0x5A #define CCS811_WRITE_ADDR (CCS811_ADDR 1) #define CCS811_READ_ADDR ((CCS811_ADDR 1) | 0x01) uint8_t CCS811_WriteReg(uint8_t reg, uint8_t *buf, uint8_t len) { uint8_t i; i2c_start(); if (i2c_send_byte(CCS811_WRITE_ADDR)) { i2c_stop(); return 1; } if (i2c_send_byte(reg)) { i2c_stop(); return 2; } for (i 0; i len; i) { if (i2c_send_byte(buf[i])) { i2c_stop(); return 3; } } i2c_stop(); return 0; } uint8_t CCS811_ReadReg(uint8_t reg, uint8_t *buf, uint8_t len) { uint8_t i; i2c_start(); if (i2c_send_byte(CCS811_WRITE_ADDR)) { i2c_stop(); return 1; } if (i2c_send_byte(reg)) { i2c_stop(); return 2; } i2c_start(); if (i2c_send_byte(CCS811_READ_ADDR)) { i2c_stop(); return 3; } for (i 0; i len; i) { // 最后一字节发送NACK通知从机读结束 buf[i] i2c_recv_byte(i (len - 1) ? 0 : 1); } i2c_stop(); return 0; }初始化流程的顺序很重要。我见过很多人一上来就设置测量模式结果读数一直不动就是因为跳过了前面的应用启动步骤。正确顺序是先确认硬件ID再确认应用固件有效然后启动应用最后设置测量模式。#define CCS811_REG_STATUS 0x00 #define CCS811_REG_MEAS_MODE 0x01 #define CCS811_REG_ALG_RESULT 0x02 #define CCS811_REG_ENV_DATA 0x05 #define CCS811_REG_HW_ID 0x20 #define CCS811_REG_APP_START 0xF4 #define CCS811_REG_ERR_ID 0xE0 #define CCS811_REG_BASELINE 0x11 uint8_t CCS811_Init(void) { uint8_t buf[4]; // 上电后等待20ms让芯片内部稳压器稳定 delay_ms(20); // 读取硬件ID确认I2C通路正常 if (CCS811_ReadReg(CCS811_REG_HW_ID, buf, 1)) { return 1; } if (buf[0] ! 0x81) { return 2; } // 等待STATUS寄存器中APP_VALID位置位 for (uint8_t i 0; i 20; i) { CCS811_ReadReg(CCS811_REG_STATUS, buf, 1); if (buf[0] 0x10) { break; } delay_ms(50); } if (!(buf[0] 0x10)) { return 3; } // 启动固件应用让传感器从bootloader切换到测量模式 if (CCS811_WriteReg(CCS811_REG_APP_START, NULL, 0)) { return 4; } delay_ms(10); // 设置测量模式0x10对应每10秒测量一次 buf[0] 0x10; CCS811_WriteReg(CCS811_REG_MEAS_MODE, buf, 1); return 0; }为什么必须写0xF4这个启动命令因为CCS811出厂时可能处于bootloader模式此时虽然I2C能响应但不跑气体算法。只有写APP_START命令后固件应用才开始工作后续读取算法结果寄存器才有意义。这也是很多初学者驱动初始化“看起来成功”但拿不到数据的原因。3.4 读取eCO2/TVOC与状态判断数据读取分两步先看状态寄存器里的DATA_READY位再读6字节算法结果。为了稳妥我一次读8字节把STATUS和ERROR_ID也一并读出来方便排错。uint8_t CCS811_ReadResult(uint16_t *eco2, uint16_t *tvoc) { uint8_t buf[8]; uint8_t status; CCS811_ReadReg(CCS811_REG_STATUS, status, 1); if (!(status 0x08)) { return 1; // DATA_READY未置位数据未更新 } CCS811_ReadReg(CCS811_REG_ALG_RESULT, buf, 8); *eco2 (buf[0] 8) | buf[1]; *tvoc (buf[2] 8) | buf[3]; // buf[4]是状态buf[5]是错误ID if (buf[4] 0x01) { return 2; // ERROR置位需要读ERR_ID定位 } return 0; }主循环里可以每隔1秒调用一次CCS811_ReadResult如果返回1就继续等不做过多操作。如果返回2就读取ERR_ID寄存器0xE0看一下具体错误原因这个我在后面排查部分专门讲。4. 数据校准与保存从默认值到可信读数4.1 预热期和burn-in时间CCS811第一次上电使用读到的eCO2和TVOC很可能长时间停留在默认值400ppm和0ppb或者数值缓慢跳动。这是正常现象不是传感器坏了。芯片内部的加热器和算法需要时间进入稳定工作状态官方建议的burn-in时间是48小时至少要连续运行20分钟后再做数据校验。我实际测试下来新模块刚上电时数据完全不可信甚至会出现eCO2突跳到几千ppm的情况。跑了一个多小时后数值逐渐平稳跑一整天后数据才和同房间的参考仪器基本对齐。所以如果你只是临时测试一下读数发现数据怪怪的别急着怀疑硬件先让传感器连续运行一段时间再下结论。4.2 环境温湿度补偿CCS811的算法受环境温湿度影响明显在温度和湿度偏离正常室温环境时读数偏差会变大。为此CCS811提供了ENV_DATA寄存器0x05用来写入外部温湿度传感器读到的环境数据。void CCS811_SetEnvData(float temp_c, float rh) { uint8_t buf[4]; uint16_t t (uint16_t)((temp_c 25.0f) * 512.0f); uint16_t h (uint16_t)(rh * 512.0f); buf[0] (uint8_t)(t 8); buf[1] (uint8_t)(t 0xFF); buf[2] p a hrefhttps://download.csdn.net/download/leichaohahah/11076883 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
上一篇/下一篇内容由系统自动关联 返回资讯列表 →