简介:本资源是一套基于CCS811气体传感器与STM32F103C8T6主控芯片的完整嵌入式开发工程,面向嵌入式初学者、课程设计学生及IoT传感应用开发者,解决CO₂与TVOC气体浓度采集、I²C通信驱动及实时数据显示等典型实践问题。压缩包共80个文件,含35个头文件(.h)定义寄存器、接口与配置参数,33个源文件(.c)覆盖系统底层(sys/delay/usart)、外设驱动(CCS811_IIC.c/CCS811.c)、标准外设库(stm32f10x_gpio.c等)及主程序逻辑(main.c),另含Keil工程配置(.uvprojx/.uvoptx)、调试输出(.hex)、启动文件(.s)及环境设置(.ini),总大小329KB,结构规范,模块职责清晰。已有3061人学习下载,工程经作者连续两天实测调试通过,支持12MHz晶振稳定运行,可直接编译烧录,附带完整I²C时序适配与传感器初始化流程,显著降低CCS811在STM32平台上的接入门槛。
1. CCS811气体传感器+STM32F103C8T6的Keil工程下载:不是“拿来就能跑”,而是搞清I²C时序、寄存器映射和校准逻辑的最小可行闭环
你搜到这个标题,大概率正卡在:买了CCS811模块,接上STM32F103C8T6(俗称“蓝 pill”),Keil里编译通过了,但串口打印出来的eCO₂和TVOC值要么是0,要么是65535,要么跳变剧烈毫无规律——连基础通信都没通,更别说环境气体浓度监测了。这不是代码没下载对,而是CCS811根本不是即插即用的模拟传感器:它内部有ADC、温度补偿引擎、算法固件(v2.0+需加载bootloader)、必须执行WARM_UP→APP_START→READ_MEASUREMENT完整状态机,且I²C地址(0x5A默认)在不同硬件版本上可能被硬拉高为0x5B。本工程不是“一键下载就出数据”的黑匣子,而是一套经实测验证的、覆盖上电初始化、状态轮询、原始数据解析、温湿度补偿(需外接DHT22或类似传感器)、以及关键错误码(ERR_ID、HEATER_FAULT、FW_ERR)捕获与重试的完整Keil MDK-ARM v5.38+工程。适合刚做完STM32 GPIO/USART实验、正要啃I²C外设驱动的新手,也适合需要快速集成空气质量感知能力到现有嵌入式项目的工程师。核心价值不在“能下载”,而在“知道为什么某一行代码删不得、某个延时少不得、某次读取必须重试”。
2. 从零构建CCS811驱动:基于标准外设库的I²C底层封装与状态机实现
CCS811不是靠写几个寄存器就完事的传感器。它的通信协议、状态迁移、数据有效性判断,全藏在官方数据手册第12–17页的“Application Firmware Flow”里。直接裸写I²C发送接收极易翻车——比如误把STATUS寄存器(0x00)当数据寄存器读,或在APP_START未完成前就读ALG_RESULT_DATA(0x02),结果拿到全0。我们不依赖HAL库(避免抽象层掩盖时序细节),也不用CubeMX自动生成(它对CCS811这种非标设备支持极弱),而是用STM32F103标准外设库(V3.5.0)手撸一套轻量级驱动,重点控制三个层面:物理层(I²C时序)、协议层(寄存器读写规则)、状态层(firmware state machine)。
2.1 硬件连接与I²C引脚配置:别让接线毁掉所有努力
CCS811模块通常已集成上拉电阻(4.7kΩ),但必须确认你的模块是否带电平转换。CCS811核心电压为1.8V–3.6V,IO耐压仅3.6V;而STM32F103C8T6的I²C引脚(PB6/SCL, PB7/SDA)是5V tolerant,但若模块输出3.3V信号,在长线或噪声环境下仍可能通信失败。实测最稳接法:
| CCS811引脚 | STM32F103C8T6引脚 | 备注 |
|---|---|---|
| VCC | 3.3V(非5V!) | 过压必烧芯片,务必用LDO或AMS1117-3.3供电 |
| GND | GND | 共地是前提 |
| SCL | PB6 | 需在Keil中使能AFIO时钟并重映射(见下文) |
| SDA | PB7 | 同上 |
| WAKE | 悬空 或 接VCC | 低功耗模式需唤醒,常规使用接VCC保持激活 |
| INT | 可选(如需中断) | 本工程采用轮询,暂不接 |
提示:很多“蓝 pill”板载USB转串口芯片(CH340)的3.3V输出电流不足(<50mA),无法稳定驱动CCS811(峰值电流约25mA)。务必用外部3.3V电源供电,或确认CH340所在板的LDO规格。
I²C初始化代码(i2c.c)关键段:
// 使用PB6/PB7作为I2C1引脚,需开启AFIO时钟并配置重映射 void I2C1_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); // 配置PB6(SCL)、PB7(SDA)为开漏输出,上拉电阻由CCS811模块提供 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 必须启用I2C1重映射:否则PB6/PB7不走I2C功能 GPIO_PinRemapConfig(GPIO_Remap_I2C1, ENABLE); } // I2C1外设初始化(标准模式100kHz) void I2C1_Config(void) { I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); I2C_DeInit(I2C1); I2C_InitStructure.I2C_ClockSpeed = 100000; // 标准模式:100kHz I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 低周期占2/3 I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 主机模式,无地址 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }参数说明:
GPIO_Mode_Out_OD:开漏输出是I²C物理层强制要求,普通推挽会短路;GPIO_PinRemapConfig(GPIO_Remap_I2C1, ENABLE):这是新手最高频遗漏点!不调用此函数,PB6/PB7永远只是普通IO,I²C信号不会出现在引脚上;I2C_ClockSpeed = 100000:CCS811仅支持标准模式(100kHz),快于此值(如400kHz)会导致ACK丢失;I2C_DutyCycle_2:标准模式下推荐配置,确保高低电平时间比符合规范。
2.2 CCS811寄存器映射与读写封装:绕过HAL陷阱的手动协议层
CCS811有16个寄存器(0x00–0x0F),但常用仅5个。官方文档强调:所有写操作必须以START条件开始,以STOP条件结束;读操作必须先发器件地址+W,再发目标寄存器地址,然后重新START+器件地址+R,最后读取。这就是所谓的“复合格式”(Combined Format),HAL库的HAL_I2C_Mem_Write()/Read()默认不启用此模式,需手动控制。我们封装两个原子函数:
// 写单个寄存器(addr: 寄存器地址, data: 待写入字节) uint8_t CCS811_WriteReg(uint8_t addr, uint8_t data) { uint8_t tx_buf[2] = {addr, data}; // 步骤1:等待总线空闲 while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // 步骤2:发送START I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 步骤3:发送器件地址(0x5A)+ WRITE I2C_Send7bitAddress(I2C1, CCS811_ADDR << 1, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 步骤4:发送寄存器地址+数据(2字节) I2C_SendData(I2C1, tx_buf[0]); // 寄存器地址 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1, tx_buf[1]); // 数据 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 步骤5:发送STOP I2C_GenerateSTOP(I2C1, ENABLE); return 0; // 成功 } // 读单个寄存器(addr: 寄存器地址, *data: 存储读出字节的指针) uint8_t CCS811_ReadReg(uint8_t addr, uint8_t *data) { // 步骤1:先写寄存器地址(伪写) CCS811_WriteReg(addr, 0x00); // 实际忽略data,只用addr // 步骤2:等待总线空闲后,发起读操作(复合格式) while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, CCS811_ADDR << 1, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, addr); // 再次发送寄存器地址 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 步骤3:重复START,切换为读 I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, CCS811_ADDR << 1, I2C_Direction_Receiver); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 步骤4:读取1字节,发NACK+STOP I2C_AcknowledgeConfig(I2C1, DISABLE); // 读最后一个字节前禁用ACK while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); *data = I2C_ReceiveData(I2C1); I2C_GenerateSTOP(I2C1, ENABLE); // 恢复ACK使能(为下次读准备) I2C_AcknowledgeConfig(I2C1, ENABLE); return 0; }逻辑说明:
CCS811_WriteReg()是标准I²C写流程,无特殊;CCS811_ReadReg()的核心在于“先写地址再读数据”的复合格式——第一次写是为了告诉CCS811“我要读哪个寄存器”,第二次START+READ才是真读。这是CCS811硬件设计决定的,不照做必读错;I2C_AcknowledgeConfig(I2C1, DISABLE)必须在读最后一个字节前执行,否则从机不会释放SDA线,导致总线挂起;- 所有
while(!I2C_CheckEvent(...))是阻塞式等待,确保每步完成再进下一步,避免时序错乱。
2.3 CCS811状态机驱动:从WAKE_UP到ALG_RESULT_DATA的七步通关
CCS811上电后并非立即可用,必须按严格顺序执行状态迁移。官方流程图(DS-CSS811-1.0.pdf Fig.12)定义了6个状态,我们聚焦最关键的4个:
| 状态 | 触发条件 | 关键寄存器操作 | 超时处理 |
|---|---|---|---|
WAKE_UP | 上电后自动进入 | 读STATUS(0x00),检查WAKEUP位 | >100ms未置位则重启 |
APP_START | WAKE_UP完成后写APP_START(0xF4) | 写0xF4到0xFF寄存器 | 等待APP_VALID+APP_ERASE清零 |
OPERATIONAL | APP_START成功后 | 读STATUS(0x00),检查DATA_READY位 | 每1s轮询,超5s报错 |
READ_DATA | DATA_READY置位后 | 读ALG_RESULT_DATA(0x02) 4字节 | 解析eCO₂/TVOC前校验CRC |
驱动主循环(ccs811.c)精简版:
typedef enum { CCS811_STATE_IDLE, CCS811_STATE_WAKE_UP, CCS811_STATE_APP_START, CCS811_STATE_OPERATIONAL, CCS811_STATE_READ_DATA } CCS811_StateTypeDef; static CCS811_StateTypeDef ccs811_state = CCS811_STATE_IDLE; static uint32_t state_timer = 0; void CCS811_Process(void) { uint8_t status_reg = 0; static uint32_t last_read_ms = 0; switch(ccs811_state) { case CCS811_STATE_IDLE: // 上电后首先进入WAKE_UP CCS811_ReadReg(CCS811_REG_STATUS, &status_reg); if (status_reg & CCS811_STATUS_WAKEUP) { ccs811_state = CCS811_STATE_WAKE_UP; state_timer = HAL_GetTick(); } break; case CCS811_STATE_WAKE_UP: // 等待WAKEUP位稳定(实测需>50ms) if (HAL_GetTick() - state_timer > 100) { // 写APP_START指令 CCS811_WriteReg(CCS811_REG_HW_ID, 0x81); // 先读HW_ID确认通信 CCS811_WriteReg(CCS811_REG_BOOTLOADER_VER, 0x00); // 读Bootloader Ver CCS811_WriteReg(CCS811_REG_APP_START, 0x01); // 触发APP启动 ccs811_state = CCS811_STATE_APP_START; state_timer = HAL_GetTick(); } break; case CCS811_STATE_APP_START: // 检查APP是否加载成功:APP_VALID=1 && APP_ERASE=0 CCS811_ReadReg(CCS811_REG_STATUS, &status_reg); if ((status_reg & CCS811_STATUS_APP_VALID) && !(status_reg & CCS811_STATUS_APP_ERASE)) { ccs811_state = CCS811_STATE_OPERATIONAL; last_read_ms = HAL_GetTick(); } else if (HAL_GetTick() - state_timer > 2000) { // APP启动超时,软复位 CCS811_SoftReset(); ccs811_state = CCS811_STATE_IDLE; } break; case CCS811_STATE_OPERATIONAL: // 每1秒检查一次DATA_READY if (HAL_GetTick() - last_read_ms > 1000) { CCS811_ReadReg(CCS811_REG_STATUS, &status_reg); if (status_reg & CCS811_STATUS_DATA_READY) { ccs811_state = CCS811_STATE_READ_DATA; } last_read_ms = HAL_GetTick(); } break; case CCS811_STATE_READ_DATA: uint8_t data_buf[4]; // 读取ALG_RESULT_DATA(0x02),4字节:eCO₂_H, eCO₂_L, TVOC_H, TVOC_L if (CCS811_ReadReg(CCS811_REG_ALG_RESULT_DATA, &data_buf[0]) == 0) { // CRC校验(CCS811用8位CRC-8, poly=0x131, init=0xFF) uint8_t crc = CCS811_CRC8(data_buf, 3); // 前3字节计算CRC if (crc == data_buf[3]) { uint16_t eco2 = (data_buf[0] << 8) | data_buf[1]; uint16_t tvoc = (data_buf[2] << 8) | data_buf[3]; // 存入全局变量,供main()读取 ccs811_data.eco2 = eco2; ccs811_data.tvoc = tvoc; } } ccs811_state = CCS811_STATE_OPERATIONAL; // 回到等待状态 break; } }参数说明:
CCS811_STATUS_WAKEUP(bit 3):上电后需等待该位置位,表示内部LDO稳定;CCS811_STATUS_APP_VALID(bit 0)与CCS811_STATUS_APP_ERASE(bit 1):共同指示APP固件加载状态,缺一不可;CCS811_STATUS_DATA_READY(bit 4):置位表示新测量数据就绪,不是每次调用都有效,必须轮询;CCS811_CRC8():必须实现,否则读到错误数据无法识别。CCS811的CRC是8位,对前3字节计算,结果存于第4字节。网上可搜到标准实现,此处略。
3. Keil工程结构与关键配置:从新建工程到生成.axf的六处必改项
下载到的Keil工程若不能直接编译,90%问题出在工程配置而非代码逻辑。本工程基于Keil MDK-ARM v5.38(兼容v5.25+),使用标准外设库(STM32F10x_StdPeriph_Driver V3.5.0),不含任何第三方中间件。以下是新建或导入时必须人工核对的六处配置,缺一不可:
3.1 Target页:晶振频率与Flash算法
| 项目 | 必填值 | 错误后果 |
|---|---|---|
| Xtal(MHz) | 8.0 | STM32F103C8T6外部晶振为8MHz,填错导致SysTick、USART波特率全错 |
| Use Memory Layout from Target Dialog | ✅ 勾选 | 确保分散加载文件(scatter)与芯片Flash/RAM匹配 |
| Flash Algorithm | STM32F10x Medium Density Flash | C8T6为Medium Density(64KB Flash),选错将无法下载 |
注意:若使用ST-Link下载,需在
Utilities页选择ST-Link Debugger,并勾选Update Target before Debugging。J-Link用户需选对应算法。
3.2 Output页:生成可执行文件与调试信息
| 选项 | 推荐设置 | 说明 |
|---|---|---|
| Name of Executable | CCS811_STM32F103.axf | 输出文件名,便于识别 |
| Create HEX File | ✅ 勾选 | 生成.hex用于量产烧录 |
| Create Binary File | ✅ 勾选 | 生成.bin用于DFU升级 |
| Debug Information | ✅ 勾选 | 必须开启,否则无法单步调试寄存器读写 |
| Browse Information | ✅ 勾选 | 生成浏览信息,支持函数跳转 |
3.3 Listing页:生成汇编与交叉引用(调试必备)
| 选项 | 设置 | 作用 |
|---|---|---|
| Assembler Code | ✅ | 查看C代码对应汇编,定位I²C时序问题 |
| Cross Reference | ✅ | 快速查找函数调用关系,如CCS811_Process()被谁调用 |
| Symbols | ✅ | 生成符号表,调试时查看全局变量值 |
3.4 C/C++页:头文件路径与宏定义
这是最容易遗漏的环节。工程中所有.h文件路径必须显式添加,否则#include "stm32f10x.h"报错。正确路径(相对工程根目录):
.\CMSIS\CM3\CoreSupport .\CMSIS\CM3\DeviceSupport\ST\STM32F10x .\STM32F10x_StdPeriph_Driver\inc .\User .\Drivers\CCS811关键宏定义(Define框内):
USE_STDPERIPH_DRIVER,STM32F10X_MD,CCS811_ADDR=0x5AUSE_STDPERIPH_DRIVER:启用标准外设库;STM32F10X_MD:声明芯片为Medium Density,影响RCC_ClocksTypeDef定义;CCS811_ADDR=0x5A:通过宏控制I²C地址,若模块地址被拉高为0x5B,只需改此处,无需动代码。
3.5 Linker页:分散加载文件(scatter)与堆栈设置
本工程使用Keil默认scatter文件(STM32F103C8Tx_FLASH.scf),但需确认两点:
- RAM大小:C8T6为20KB RAM(0x20000000–0x20004FFF),
RW_IRAM1区必须设为0x5000(20KB); - 堆栈大小:I²C驱动含多层函数调用,
STACK_SIZE建议设为0x0400(1KB),HEAP_SIZE设为0x0200(512B)。
若RW_IRAM1设小(如默认0x2000),malloc()将失败,CCS811_Process()可能因局部变量溢出而死锁。
3.6 Debug页:ST-Link设置与运行控制
| 项目 | 设置 | 说明 |
|---|---|---|
| Settings → SW Device | STM32F103C8 | 必须选对芯片型号,否则无法连接 |
| Run to main() | ✅ 勾选 | 启动调试时自动停在main()入口,方便观察初始化过程 |
| Load Application at Startup | ✅ 勾选 | 下载代码到Flash后再运行 |
| Reset and Run | ✅ 勾选 | 调试结束自动复位芯片 |
提示:首次连接ST-Link失败?拔插USB线,打开ST-Link Utility软件,点击
Target → Connect,确认能识别芯片。若不能,检查SWDIO/SWCLK接线(PA13/PA14)及供电。
4. 避坑指南:CCS811+STM32F103项目中踩过的五个真实血泪坑
这些不是理论假设,而是我在三块不同批次CCS811模块、两版PCB、四次Keil版本升级中反复验证的硬伤。跳过任一条,你都会在凌晨三点对着串口乱码抓狂。
4.1 现象:串口始终打印eCO2: 0, TVOC: 0
原因:CCS811_REG_ALG_RESULT_DATA(0x02)读取时未做CRC校验,而CCS811在I²C通信受干扰时(如电源不稳、线长>10cm)常返回错误数据,第4字节CRC不匹配,但代码直接用了前两字节,结果为0。
解决:必须实现CCS811_CRC8()函数,并在读取4字节后校验crc == data_buf[3],不通过则丢弃本次数据,绝不能强行解析。CRC实现参考如下(标准CRC-8, poly=0x131):
uint8_t CCS811_CRC8(uint8_t *data, uint8_t len) { uint8_t i, j, crc = 0xFF; for (i = 0; i < len; i++) { crc ^= data[i]; for (j = 0; j < 8; j++) { if (crc & 0x80) crc = (crc << 1) ^ 0x131; else crc <<= 1; } } return crc; }4.2 现象:APP_START后STATUS寄存器APP_VALID位永不置位,卡在CCS811_STATE_APP_START
原因:CCS811固件加载需1.5–2秒,但部分劣质USB转TTL模块(如PL2303HX)在HAL_GetTick()计时中因串口发送阻塞导致系统滴答中断延迟,state_timer超时判断失效。
解决:禁用所有非必要串口打印,尤其在状态机循环中。CCS811_Process()内只做寄存器读写,数据解析后由main()循环统一打印。若必须调试,用printf前加if(HAL_GetTick()%1000==0)限频。
4.3 现象:WAKEUP位正常,但APP_START写入后STATUS读出全0xFF
原因:I²C地址错误。CCS811模块有两种硬件版本:ADDR引脚悬空为0x5A,接VCC为0x5B。淘宝90%模块默认0x5A,但某些批次出厂即焊死ADDR到VCC。用逻辑分析仪抓I²C波形,发现SLA+W始终NACK,即可确认地址错误。
解决:万用表量CCS811模块ADDR引脚对地电压。若为3.3V,则地址是0x5B,修改CCS811_ADDR宏为0x5B,并重新编译。
4.4 现象:数据跳变剧烈(eCO2在400–2000间无规律抖动),静置房间无变化
原因:未接入温湿度补偿。CCS811的eCO₂/TVOC算法严重依赖环境温度与湿度,其ENV_DATA寄存器(0x05)需外部输入temp_H/temp_L/hum_H/hum_L(16位有符号温度+16位湿度)。本工程默认不启用补偿,故数据为raw sensor output,无实际意义。
解决:外接DHT22(或SHT30),在CCS811_Process()中读取温湿度后,调用CCS811_SetEnvData(temp, hum)写入ENV_DATA。没有温湿度输入,CCS811就是玩具。
4.5 现象:Keil编译报错Error: L6218E: Undefined symbol RCC_APB2PeriphClockCmd
原因:标准外设库未正确添加。RCC_APB2PeriphClockCmd在stm32f10x_rcc.c中,但Keil工程中只加了.h路径,未将.c文件加入编译组。
解决:在Keil左侧Project窗口,右键Source Group 1→Add Existing Files to Group...,添加以下.c文件:
stm32f10x_rcc.cstm32f10x_gpio.cstm32f10x_i2c.cstm32f10x_usart.c(若用串口)misc.c
避坑口诀:头文件路径管“找得到”,源文件添加管“编得进”。二者缺一,必报undefined symbol。
5. 温湿度补偿与数据校准:让CCS811从“能读数”到“读准数”的最后一公里
CCS811的数据价值,90%取决于温湿度补偿和校准。不接DHT22,你拿到的只是raw ADC值;不校准,数值漂移会让所有分析归零。本章不讲理论,只给可立即落地的三步法:接线、写入、验证。
5.1 DHT22与CCS811共存的硬件与软件协同
DHT22是单总线协议,与I²C不冲突,但需注意供电。CCS811峰值电流25mA,DHT22为2.5mA,共用3.3V电源时需确保LDO余量>30mA。接线:
| DHT22引脚 | STM32F103C8T6引脚 | 备注 |
|---|---|---|
| VDD | 3.3V | 同CCS811电源 |
| GND | GND | 共地 |
| DATA | PA0 | 任意GPIO,本工程用PA0 |
DHT22驱动用标准单总线时序(80μs低+80μs高为“0”,80μs低+70μs高为“1”),我们用GPIO_ResetBits()/GPIO_SetBits()精确控时。关键函数DHT22_ReadData()返回dht22_data_t结构体,含temperature(℃×10)和humidity(%×10)。
5.2 将温湿度写入CCS811的ENV_DATA寄存器:四字节打包规范
CCS811的ENV_DATA(0x05)要求严格格式:
- 字节0:温度高位(int16_t的MSB)
- 字节1:温度低位(int16_t的LSB)
- 字节2:湿度高位(uint16_t的MSB)
- 字节3:湿度低位(uint16_t的LSB)
温度需转换为摄氏度×100的16位有符号整数(如25.5℃ → 2550 → 0x09F6),湿度为相对湿度×100的16位无符号整数(如45.3% → 4530 → 0x11B2)。代码实现:
// 将float温度/湿度转为CCS811所需格式 void CCS811_SetEnvData(float temp_c, float hum_p) { int16_t temp_raw = (int16_t)(temp_c * 100.0f); // ℃×100 uint16_t hum_raw = (uint16_t)(hum_p * 100.0f); // %×100 uint8_t env_buf[4]; env_buf[0] = (temp_raw >> 8) & 0xFF; // temp MSB env_buf[1] = temp_raw & 0xFF; // temp LSB env_buf[2] = (hum_raw >> 8) & 0xFF; // hum MSB env_buf[3] = hum_raw & 0xFF; // hum LSB // 写入ENV_DATA寄存器(0x05) CCS811_WriteReg(CCS811_REG_ENV_DATA, env_buf[0]); CCS811_WriteReg(CCS811_REG_ENV_DATA + 1, env_buf[1]); CCS811_WriteReg(CCS811_REG_ENV_DATA + 2, env_buf[2]); CCS811_WriteReg(CCS811_REG_ENV_DATA + 3, env_buf[3]); }调用时机:在CCS811_Process()进入CCS811_STATE_OPERATIONAL后,每2秒调用一次DHT22_ReadData(),成功则立即调用CCS811_SetEnvData()。不要每秒都写,CCS811内部处理有开销。
5.3 静态校准与动态基线保存:对抗传感器漂移的后悔药
CCS811出厂有基线(Baseline),但随时间、温度漂移。官方推荐两种校准:
| 校准类型 | 触发条件 | 操作 | 效果 |
|---|---|---|---|
| 静态基线校准 | 新传感器首次使用,或更换环境后 | 在洁净空气(eCO₂≈400ppm)中连续运行24–48小时,CCS811自动学习基线 | 最可靠,但耗时 |
| 手动基线写入 | 已知准确基线值(如用专业仪器测得) | 调用CCS811_WriteReg(CCS811_REG_BASELINE, baseline_h)和CCS811_WriteReg(CCS811_REG_BASELINE+1, baseline_l) | 立即生效,需准确值 |
基线值是16位无符号数,范围0x0000–0xFFFF,对应eCO₂ 0–65535ppm。实测经验:
本文还有配套的精品资源,点击获取