ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32驱动CCS811空气质量传感器:双串口架构与I2C寄存器操作详解

STM32驱动CCS811空气质量传感器:双串口架构与I2C寄存器操作详解 简介本资源是一套基于 STM32 平台的 CCS811 空气质量传感器示例工程面向嵌入式开发者和环境监测爱好者解决 CO2 与 TVOC 浓度采集、串口数据收发及传感器初始化配置等常见问题。工程采用双串口设计一个串口用于读取传感器输出的气体浓度数据另一个用于下发控制与采集指令便于快速搭建实时监测节点。资源包共 174 个文件包含 STM32 标准外设库的 .h/.c 源码、Keil 工程文件.uvprojx/.uvoptx、编译产物.axf/.hex及调试配置文件整体约 4.36MB适合直接打开工程查看或二次开发。目前已有 776 人学习下载。开发者可从中获得 CCS811 驱动流程、USART 串口通信配置、定时采样与数据处理思路以及完整的 Keil 工程目录结构尤其适合初次接触气体传感器或嵌入式串口应用的入门及进阶用户。1. 打开一个 CCS811 示例程序前先搞清楚它到底在调什么拿到这份标题为“ccs811示例程序”的压缩包里面是.uvguix、.axf、_sct.Bak这些 Keil MDK 工程残留再加上stm32f10x_tim.c、stm32f10x_i2c.c、stm32f10x_usart.c等标准外设库文件第一反应通常是这又是一个基于 STM32F103 的传感器 demo。但真正开始读代码时你会发现它解决的问题不是“如何让 CCS811 能读到数据”而是“如何在双串口架构下让传感器数据采集和指令控制互不干扰”。这个定位很关键——CCS811 本身只是 I2C 接口的空气质量传感器而这份示例里用了两个串口串口一负责把 CO₂ 和 TVOC 浓度持续吐给上位机串口二用来接收校准、改采样周期这样的控制命令。这样做的好处是采集链路和控制链路物理隔离数据推送不会因为指令交互而产生时序抖动。适合谁读准备在 STM32 上接入 CCS811 做室内空气监测、新风系统或空气质量采集器的嵌入式开发工程师。新手可以照着工程结构理解一套完整的数据通路熟手也能从它的双串口设计和 I2C 寄存器操作时序里找到值得参考的边界处理方式。2. CCS811 的 I2C 通信与寄存器操作基础2.1 传感器内部结构与测量原理CCS811 是 ams 公司推出的金属氧化物MOX基气体传感器内部集成微控制器和模拟前端能够输出等效 CO₂ 浓度eCO₂单位 ppm和 TVOC 浓度单位 ppb。与传统电化学传感器不同它不需要外接大体积的电解池而是通过加热 MOX 薄膜改变其阻值再经 ADC 采样和内部算法换算成气体浓度。这个“内部算法”是 CCS811 的关键它必须在出厂前完成烧录并且在上电后要对传感器进行“应用启动”操作否则数据寄存器读出来一直是无效值。通信接口方面CCS811 是标准 I2C 从机7 位地址为0x5A地址引脚 ADDR 拉低或0x5B拉高。示例程序里通常用0x5A。I2C 时钟最高支持 400 kHz也就是 Fast Mode。对 STM32F10x 来说配置 I2C 外设时钟标准库里有I2C_InitStructure.I2C_ClockSpeed 400000但要注意 STM32F103 的硬件 I2C 存在著名的“死锁”问题所以很多示例会改用 GPIO 模拟 I2C。这份工程里既然引用了stm32f10x_i2c.c大概率用的是硬件 I2C但你可以在ccs811.c里看到是否有I2C_GenerateSTART这类调用以此判断是硬件还是模拟。2.2 I2C 总线上的关键寄存器从地址、状态字与数据寄存器操作 CCS811 前必须把这几个寄存器焊在脑子里寄存器名地址功能HW_ID0x20硬件 IDCCS811 返回0x81STATUS0x00状态字Bit0 表示数据是否就绪Bit3 表示错误APP_START0xF4启动应用固件写入任意字节MEAS_MODE0x01驱动模式与测量周期ALG_RESULT_DATA0x02读取 5 字节eCO₂2 字节、TVOC2 字节、状态1 字节BASELINE0x11基线校准值读/写 2 字节ENV_DATA0x05写入温湿度补偿值读写这些寄存器时I2C 时序上有一点容易忽略读取ALG_RESULT_DATA时需要先发送寄存器地址然后在同一次总线事务内连续读取 5 个字节。如果你分两次读传感器内部的数据指针会复位导致读到的是同一份数据。示例代码里一般写成uint8_t ccs811_read_alg_data(uint16_t *eco2, uint16_t *tvoc) { uint8_t buf[5]; // 指定寄存器地址 0x02 buf[0] CCS811_ALG_RESULT_DATA; // 先写寄存器地址 i2c_write(CCS811_ADDR, buf, 1); // 再连续读5字节 i2c_read(CCS811_ADDR, buf, 5); *eco2 ((uint16_t)buf[0] 8) | buf[1]; *tvoc ((uint16_t)buf[2] 8) | buf[3]; return buf[4]; // 返回状态字节 }这段代码的关键在于i2c_write和i2c_read之间必须保持总线占用不能有 Stop 条件。很多初学者在这里习惯写一个通用的“写寄存器地址 读数据”函数但内部如果拆成了两个独立的 Start-Stop 周期读到的数据就是未定义值。参数方面CCS811_ADDR使用的 7 位地址是0x5A在标准库的宏定义里要注意左移一位变成 8 位写地址0xB4、读地址0xB5这是 I2C 驱动里最常见的错误来源。2.3 初始化序列的 C 代码实现CCS811 从上电到能读数必须经过一步“复位 应用启动”。示例程序里初始化函数的结构大致如下void ccs811_init(I2C_TypeDef *i2c_port) { uint8_t hw_id 0; uint8_t tmp; // 1. 读取硬件ID确认传感器在线 ccs811_read_reg(CCS811_HW_ID, hw_id, 1); if (hw_id ! 0x81) { // 硬件异常挂起或打印错误 return; } // 2. 软件复位可选但正式代码建议做 tmp 0x11; ccs811_write_reg(CCS811_SW_RESET, tmp, 1); // 0xFF 寄存器 delay_ms(10); // 3. 启动应用固件 tmp 0x10; ccs811_write_reg(CCS811_APP_START, tmp, 1); delay_ms(100); // 4. 读取STATUS确认固件已启动 ccs811_read_reg(CCS811_STATUS, tmp, 1); if (!(tmp 0x10)) { // Bit4: APP_VALID // 应用未有效说明bootloader未正确跳转 return; } // 5. 设置驱动模式 tmp 0x10; // 模式2每秒测量一次ALE1INT1 ccs811_write_reg(CCS811_MEAS_MODE, tmp, 1); }这里的CCS811_SW_RESET是0xFF写入序列0x11 0xE5 0x72 0x8A才有效。示例里如果简化成写一个字节0x11其实触发不了复位这也是一个容易踩的坑。更好的做法是完整写入四字节复位序列。APP_START寄存器写入后必须等待至少 50 ms让内部固件完成加载否则后续读取数据会超时。初始化完成后MEAS_MODE 的低四位决定了测量周期0000空闲、0001每秒一次、0010每 10 秒一次、0011每 60 秒一次。示例程序里看到的是0x10其中 Bit4 置 1 表示数据就绪时把 INT 引脚拉低如果你不需要中断可以改成0x00或者0x08。3. 读懂这份 STM32 示例程序的工程骨架3.1 从 Keil 工程文件到外设库的映射解压后看到的.uvguix.11650是 Keil uVision5 的 GUI 布局文件记录窗口位置不影响编译.axf是 ARM 可执行文件也就是上次编译的带符号输出可以直接加载到调试器里看程序跑到哪_sct.Bak是分散加载文件的备份链接脚本的来源。真正的核心逻辑在ccs811.c、uart_driver.c这类源码文件里。对应的 STM32 标准外设库文件功能可以这样归类文件作用在本项目中的角色stm32f10x_i2c.c硬件 I2C 外设的寄存器操作初始化 I2C1连接 CCS811stm32f10x_usart.cUSART 外设驱动初始化 USART1数据输出和 USART2指令输入stm32f10x_rcc.c时钟控制开启 GPIO、I2C、USART、DMA 的时钟stm32f10x_adc.cADC 外设驱动可能用于读取传感器供电电压或板载温湿度模拟量stm32f10x_tim.c定时器提供毫秒级延时和测量周期调度stm32f10x_flash.c内部 Flash 操作保存用户校准参数比如 CCS811 的基线值stm32f10x_can.cCAN 控制器这个示例里大概率没用但工程模板里带了把can.c编译进来并不是问题标准库编译时会根据USE_STDPERIPH_DRIVER宏选择性地链接。关键是看stm32f10x_conf.h里是否注释掉了对应模块。如果你的工程编译时报stm32f10x_can.c缺少CAN相关外设定义检查是否没有把#define STM32F10X_HD加到全局宏里。3.2 串口一与串口二的分工设计这份示例采用双串口的架构不是随便拍脑袋决定的。串口一USART1通常连接 PC 或串口屏数据格式设计成适合人读的 CSV 或 JSON 行串口二USART2则连接调试终端或遥控面板只接收 ASCII 指令比如CAL\r\n代表校准基线MODE2\r\n代表切换测量模式。这种设计在代码上体现为两个独立的环形缓冲区。串口一的中断服务函数只做一件事把接收到的数据放入uart1_rxbuf主循环从缓冲区里取数据解析串口二同理但新增了一个“指令完成”标志位。这样可以避免在半双工总线上同时收发导致数据错乱。void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ch (uint8_t)USART_ReceiveData(USART1); ringbuf_write(uart1_rxbuf, ch); } } void USART2_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { ch (uint8_t)USART_ReceiveData(USART2); if (ch \n) { uart2_cmd_ready 1; } else { ringbuf_write(uart2_rxbuf, ch); } } }中断里最关键的是把耗时操作全部放到主循环里中断只做数据搬移。你会发现uart2_cmd_ready这个标志位不在中断里清零而是在主循环处理完指令后清零。这种设计能防止你在解析指令的中途又插入新的中断导致指令被截断。串口参数方面USART1 设为 115200 8N1USART2 设为 9600 8N1两者波特率不同说明串口一偏向高速数据流串口二偏向低速控制指令这样即使指令串口收到电磁干扰也不会影响数据上报。3.3 主循环中的数据采集与指令处理主循环一般会写成状态机而不是阻塞式延时轮询。下面是一段典型的调度逻辑int main(void) { uint16_t eco2, tvoc; uint8_t status; uint32_t last_measure 0; char line[64]; ccs811_init(I2C1); uart1_init(115200); uart2_init(9600); while (1) { // 采集任务每1000ms读一次 if (get_tick() - last_measure 1000) { status ccs811_read_alg_data(eco2, tvoc); if ((status 0x01) 0) { // ERROR bit 未置位 snprintf(line, sizeof(line), eco2%u,tvoc%u\r\n, eco2, tvoc); uart1_send_line(line); } last_measure get_tick(); } // 指令任务有完整行就解析 if (uart2_cmd_ready) { uint8_t cmd[16]; int len ringbuf_read_all(uart2_rxbuf, cmd, sizeof(cmd)-1); cmd[len] \0; if (strncmp((char *)cmd, CAL, 3) 0) { ccs811_read_baseline(baseline); flash_write_baseline(baseline); uart2_send_str(OK\r\n); } else if (strncmp((char *)cmd, MODE, 5) 0) { uint8_t mode atoi((char *)cmd 5); ccs811_set_measure_mode(mode); uart2_send_str(OK\r\n); } else { uart2_send_str(ERR\r\n); } uart2_cmd_ready 0; } } }主循环里两个任务的时间片分别是 1 秒和“串口二来命令时”两者互不阻塞。如果你在ccs811_read_alg_data里读到的状态字节最低位为 1说明传感器内部算法报错此时应该读取0xE0寄存器里的具体错误码而不是直接打印无效浓度值。示例程序如果没有处理这个分支你需要自己补上。这个主循环还有一个隐藏问题snprintf在每次数据更新时都会调用如果波特率输出速度跟不上生成速度缓冲区会堆积。这里 115200 波特率大约每秒传 11520 字节而一行数据最多 30 个字节每秒 1 次的频率完全没问题。如果你把测量周期改成 10 Hz就必须减少行长度或者改用二进制协议否则上位机解析会滞后。4. 参数调整与实际部署中的坑4.1 驱动模式选择与测量周期CCS811 的 MEAS_MODE 低四位直接关系到功耗和数据新鲜度。示例程序里写的是0x10这是“模式 2”每秒测量一次。但要注意模式 1 到模式 3 的数据更新率分别对应 1s、10s、60s而传感器内部加热周期是固定的每次测量启动后需要等待约 20 ms 才能从 ALG_RESULT_DATA 里读到有效数据。如果你在初始化后立刻读取状态寄存器会一直显示数据未就绪。实际室外空气监测、新风系统这类应用我一般建议用模式 210 秒一次因为 CO₂ 浓度变化本身是慢变量每秒读数意义不大而且每 10 秒一次的功耗大约只有每秒一次模式的六分之一。如果你用电池供电甚至可以考虑模式 360 秒一次配合外部 RTC 唤醒。void ccs811_set_measure_mode(uint8_t mode) { uint8_t reg mode 0x07; // 保留最高两位的INT设置清除低四位 uint8_t old; ccs811_read_reg(CCS811_MEAS_MODE, old, 1); old (old 0xF0) | reg; ccs811_write_reg(CCS811_MEAS_MODE, old, 1); }这里的old 0xF0是为了保留 Bit4 和 Bit5 的中断使能设置。如果你直接写入mode会把之前配置好的中断引脚功能冲掉。很多示例程序忽略了这个保留位操作导致在切换模式之后传感器不再产生中断信号。4.2 基线校准写出 BASELINE 和读取基线CCS811 的浓度算法会随时间漂移环境本底不同也会导致同一颗传感器在不同地方测出偏差。官方给出的做法是在稳定环境下比如室内恒温恒湿持续 12 小时以上读取 BASELINE 寄存器数值保存到 Flash下次上电时再写回。这个校准过程不是一次性的建议每运行 1000 小时后重新校准一次否则累积误差可能超过 10%。读取和写入基线的代码uint16_t ccs811_read_baseline(void) { uint8_t buf[2]; ccs811_read_reg(CCS811_BASELINE, buf, 2); return ((uint16_t)buf[0] 8) | buf[1]; } void ccs811_write_baseline(uint16_t baseline) { uint8_t buf[2]; buf[0] (baseline 8) 0xFF; buf[1] baseline 0xFF; ccs811_write_reg(CCS811_BASELINE, buf, 2); }写基线时有一个前置条件必须先让传感器处于空闲模式MEAS_MODE 0然后写入基线再重新设置测量模式。如果你在测量过程中直接写寄存器写入会被忽略。示例程序里的CAL指令如果只读基线而不切换到空闲模式校准结果是无效的。正确的校准流程是收到CAL指令后先写MEAS_MODE 0再读 BASELINE保存到 Flash最后恢复原来的测量模式。这里要注意恢复测量模式后传感器需要至少 10 分钟才能重新稳定到新的基线因此不建议频繁校准。4.3 你可能遇到的时序错误与解决硬件 I2C 在 STM32F103 上有一个常见问题总线忙标志BUSY清除不及时。表现为第一次冷启动时 I2C 通信正常热复位后卡在I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY)。原因是 I2C 外设在复位瞬间 SDA 被拉低外设认为总线被占用。解决方法是复位 I2C 外设后再初始化void i2c_bus_recover(void) { // 切换为GPIO模式手动翻转9个时钟 GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; // 假设I2C1 gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_Mode GPIO_Mode_Out_OD; GPIO_Init(GPIOB, gpio); for (int i 0; i 9; i) { GPIO_SetBits(GPIOB, GPIO_Pin_6); delay_us(5); GPIO_ResetBits(GPIOB, GPIO_Pin_6); delay_us(5); } // 恢复I2C外设 I2C_DeInit(I2C1); }如果你不想处理这些硬件 I2C 的边角问题最简单的办法是把stm32f10x_i2c.c从工程中移除改用两个 GPIO 模拟 I2C。对于 CCS811 这种低速传感器模拟 I2C 在 100 kHz 下完全够用而且能彻底避开死锁。示例程序里保留了硬件 I2C 文件你可能需要阅读它的ccs811_read_byte函数看每次传输前是否有while (I2C_GetFlagStatus(...BUSY));这样的等待没有的话建议加上超时退出。5. 验证与调试技巧从串口数据到波形验证5.1 用两个串口会话验证双通道独立性把 USART1 接 USB 转 TTL 工具打开串口助手USART2 接调试串口。先给 USART1 发送eco2420,tvoc8这样的数据流同时通过 USART2 发送MODE2\r\n观察 USART1 的输出是否出现卡顿。如果输出中断超过 200ms说明你的指令处理和串口发送共用缓冲区主循环里在发送时被指令处理阻塞了。正确的做法是把 UART1 的uart1_send_line改成中断发送或 DMA 发送不要让 CPU 在发送函数里空转等待。5.2 用逻辑分析仪抓 I2C 时序确认应答当遇到初始化失败时逻辑分析仪比调试器更直观。把探头夹在 I2C 的 SDA、SCL 上设置触发模式为 Start 条件抓取一次APP_START写入时隙。正常波形应该显示Start → 从机地址0xB4写 → 寄存器地址0xF4→ 数据字节 → Stop。如果看到的是 ACK 位超时第 9 个时钟没有拉低说明传感器没有上电或地址错误。检查 ADDR 引脚电平时注意 STM32 的 GPIO 输出高电平能力如果 ADDR 悬空默认是低电平地址为0x5A如果被拉高但用的是开源极输出且没有上拉电阻地址会随机。5.3 校准基线验证以及一个容易算错的换算关系用打火机气体短距离吹传感器不要让火焰接触观察数据是否飙升然后放在清净空气中 10 分钟看是否回落。如果回落后的值与标称值偏移超过 50ppm执行CAL指令然后读回基线值。基线值是一个无符号 16 位整数通常落在 3800~4500 之间。若你得到 0xFFFF说明读到了未初始化区域写入 Flash 的函数有问题。换算方面ALG_RESULT_DATA 的原始值已经是 ppm/ppb不需要再乘系数。但如果你从寄存器读到的值是 0x6400那表示 eCO₂ 0x6400 25600不对要还原为字节序buf[0]0x64, buf[1]0x00则 eCO₂ 0x6400 25600 ppm这在人附近是不可能的真实场景下读到0x01 0xA4才是 420 ppm。所以检查数据时先确认字节序是否正确很多示例程序在上位机软件里写反了高地位导致读数凭空放大 256 倍。这个例子是把双串口采集工程迁移到新板卡时最容易忽略的第一个排错点。本文还有配套的精品资源点击获取
返回列表