
简介本资源是一套面向嵌入式开发初学者与物联网项目实践者的STM32平台二氧化碳监测系统源程序聚焦SCD40传感器的驱动开发与数据解析解决环境监测类项目中气体浓度采集、通信适配与稳定读取等核心问题适用于智能楼宇、农业温室、教室空气质量监控等实际场景。压缩包共11个文件6个.h头文件定义接口与配置5个.c源文件实现I²C底层驱动、SCD4X协议封装、传感器初始化与CO₂浓度转换逻辑总大小仅24KB结构精简、模块清晰含sensirion_i2c_hal.c/h等标准HAL层适配代码及scd4x_i2c.c/h专用驱动便于移植与调试。已有440人学习下载开发者可直接复用完整I²C通信框架、校验处理机制、测量周期控制逻辑及原始数据到ppm值的精确换算函数显著降低SCD40在STM32上的集成门槛。 去年帮朋友做一套教室空气质量监测面板核心需求就两条测准CO2浓度、稳定上报到显示终端。传感器选了一圈最后定了STM32F103SCD40的组合。SCD40这颗芯片是Sensirion出的光声式CO2传感器I2C接口自带温湿度输出体积比指甲盖还小。这篇我把整个源程序从选型、协议细节、工程搭建到调试踩坑完整写一遍给正在搞STM32二氧化碳传感器项目的朋友一个可以直接抄作业的参考。这套方案能解决什么问题室内CO2浓度直接关联通风需求和人员舒适度教室、会议室、新风系统、智能家居面板都需要这个数据。SCD40适合对成本、体积、功耗敏感的产品搭配STM32做数据采集、OLED显示或者WiFi上报都很顺手。需要做电池供电的低功耗设备SCD40也有专门的低功耗测量模式。下面进入正题。1. 为什么要用SCD40选型逻辑与整体方案分析1.1 SCD40跟SCD30、MH-Z19这些传感器比优势在哪选CO2传感器的时候你的选项其实就三类老牌NDIR传感器MH-Z19系列、Sensirion家的SCD30/SCD40/SCD41、还有更贵的SenseAir S8。MH-Z19以UART/PWM输出为主模拟量处理麻烦而且体积不小。SCD30精度确实高测量范围能到10000ppm但功耗和尺寸都偏大做消费级产品有点杀鸡用牛刀。SCD40最打动我的三点第一光声传感原理带来的低功耗。传统NDIR需要一个红外光源加光路功耗很难降下来SCD40内部用脉冲红外光源配合MEMS麦克风检测光声信号周期测量模式下平均电流在毫安级别以下电池设备可以做。第二I2C接口直接出数据CO2、温度、湿度三合一。省掉了一个温湿度传感器对BOM成本和PCB面积都是实打实的帮助。第三封装小、标定好。SCD40是SMD贴片封装出厂前已经做了校准你拿到手只要供电、拉I2C、读数据不需要自己折腾标定流程。下面这个对比表可以帮你快速判断选型参数SCD40SCD30MH-Z19输出接口I2CI2C/UARTUART/PWMCO2测量范围400-2000ppm400-10000ppm400-5000ppm精度±(50ppm5%读数)±(30ppm3%读数)±(50ppm5%读数)温湿度输出有有无典型测量周期5秒2秒不定可配置功耗低较高中等封装形式SMD通孔/贴片通孔模块SCD40在400-2000ppm范围内精度标称是±(50ppm5%读数)教室、办公室这些场景CO2浓度一般在400-2500ppm之间完全够用。如果项目要求测量范围到5000ppm可以看SCD41代码几乎不用改命令完全兼容只是精度标称更好一点。1.2 硬件连接转接板、I2C上拉和电源注意点SCD40是DFN封装的原厂贴片器件手焊比较费劲建议直接买带转接板的模块一般会把I2C上拉电阻和LDO都做好。我自己用的时候直接在PCB上画封装参考电路就三根线VDD、SDA、SCL外加一个100nF去耦电容。接线表如下SCD40引脚STM32引脚说明VDD3.3V供电范围2.4V-5.5V建议稳定3.3VGNDGND共地SDAPB7I2C1需要上拉电阻到3.3VSCLPB6I2C1需要上拉电阻到3.3V上拉电阻的选择这里多说一句。SCD40支持100kHz标准模式、400kHz快速模式和最高1MHz模式但I2C上拉电阻和总线电容决定了信号边沿质量。我实测下来100kHz模式下用4.7k上拉跑杜邦线没啥问题如果你用400kHz建议把上拉换成2.2k并且线长尽量控制在10cm以内。STM32F103的I2C外设本身就比较挑时序很多网上说的SCL卡死无应答问题一半以上是上拉和线长造成的。供电方面SCD40对电源纹波不算特别敏感但不要让它和舵机、继电器共用一个电源。光声传感器内部有个微麦克风电源串扰会让原始信号波动直接影响CO2读数稳定性。我见过一个项目把SCD40和MOSFET驱动放一起读数能差出大几十ppm最后就是靠把供电分开解决的。2. 代码之前必须搞懂的核心SCD40的I2C协议与数据结构2.1 SCD40的I2C命令帧结构光看数据手册容易晕SCD40的I2C从机地址是0x627位写地址0xC4读地址0xC5。它的协议跟普通传感器不太一样不是简单的寄存器地址读写而是命令帧结构。每条命令固定2字节需要带参数的命令在后面追加2字节参数再追加1字节CRC校验。发送命令的格式是这样的START 0xC4 [2字节命令] [可选2字节参数 1字节CRC] STOP读取数据的格式则是先发读命令等数据就绪后主机再发起读事务START 0xC4 [0xEC 0x05] STOP 等待数据就绪 START 0xC5 [9字节数据] STOP第一次接触这个协议的人容易犯一个错误用HAL库的HAL_I2C_Mem_Write直接拿命令值当寄存器地址写。SCD40的命令是完整的两字节不是寄存器地址用Mem_Write很容易把第二个命令字节丢掉。更稳妥的做法是用HAL_I2C_Master_Transmit发送完整命令帧再用HAL_I2C_Master_Receive接收数据逻辑清楚也不容易错。常用的SCD40命令我整理一份功能命令参数说明启动周期测量0x21 0xB1无5秒一个数据点启动低功耗周期测量0x21 0x9D无30秒一个数据点停止测量0x3F 0x86无进入空闲模式读取数据就绪状态0xE4 0xB8返回2字节CRCbit111表示数据就绪读取测量数据0xEC 0x05返回9字节3个CRCCO2温度湿度强制校准0x36 0x2F2字节CRC校准到已知浓度启用/禁用自动校准0x24 0x24 / 0x24 0x232字节CRCASC功能设备重置0xD4 0x04无软复位这些命令在源程序里会逐个落地先记住一个原则跟SCD40打交道所有带参数的写入都要算CRC读取的每一段数据也都要校验CRC。2.2 CRC-8校验多项式0x31、初始值0xFF别直接用网上的错代码SCD40的I2C帧里每个字段后面都跟一个CRC-8校验字节保证传输过程数据没被破坏。它用的CRC-8参数是多项式0x31即x^8 x^5 x^4 1初始值0xFF输入输出都不取反。简单说就是标准CRC-8/MAXIM之外的一个变种直接拿网上某些通用的CRC-8实现去算基本都会挂。我给出一个按位计算的实现不依赖查表放到任何平台都能跑uint8_t scd40_crc8(const uint8_t *data, uint8_t len) { uint8_t crc 0xFF; for (uint8_t i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 0x80) crc (uint8_t)((crc 1) ^ 0x31); else crc 1; } } return crc; }算CRC时注意范围读取9字节数据帧时CO2字段是buf[0]和buf[1]校验字节是buf[2]温度字段是buf[3]和buf[4]校验字节是buf[5]湿度字段是buf[6]和buf[7]校验字节是buf[8]。三个字段各算各的不能把前6个字节合起来算一个CRC这是很多移植代码翻车的重灾区。我在调试时遇到过一种情况CRC算得对但每次读数据都报校验失败。最后定位到是I2C速率设成了400kHz、杜邦线又长波形边沿被拉得不够陡个别位被采错。把速率降回100kHz之后问题立刻消失。所以如果你的校验失败率很高先查时序再去怀疑CRC算法。2.3 测量模式与数据就绪机制决定你的主循环怎么写SCD40有三种测量模式周期测量、低功耗周期测量、单次测量。周期测量命令0x21B1传感器每5秒完成一次测量并更新内部数据低功耗周期测量0x219D每30秒一次适合电池供电但实时性要求不高的场景单次测量0x219E触发一次测一次测完回到待机状态适合按需唤醒的低功耗产品。不管是哪种模式读数据之前都要查数据就绪状态。SCD40内部维护一个状态字通过读0xE4B8命令获取返回2字节状态加上1字节CRC。状态字的bit11为1时代表新数据已经准备好这时候发0xEC05读9字节才有效。这个数据就绪机制很关键。如果你不管就绪位直接去读可能读到旧数据、全0、甚至半新半旧的数据。我见过有代码在发完周期测量命令后直接Delay 5秒再读大多数时候能工作但一旦传感器内部时序稍有抖动就会读到无效值。正确姿势是轮询状态字。单次测量模式下还有一个细节测量完成后数据保持在输出寄存器里读取一次后并不会自动清掉就绪位。也就是说你要自己管理这个周期已经读过了这个逻辑避免重复读取同一份数据。用周期测量模式会简单些我自己的大多数项目都是周期测量加轮询就绪位代码量最少。3. SCD40源程序实战工程搭建与完整代码3.1 用STM32CubeMX快速搭工程这些配置项别漏我基于STM32F103C8T6和HAL库来讲CubeMX版本用6.x即可。工程配置重点就四个时钟树、I2C1、USART1、SWD调试口。时钟树配置到72MHz主频这个没什么好说的。I2C1选择PB6/PB7速率直接选Standard Mode100kHz。前面说了SCD40支持快模式但STM32F103的I2C外设本身在Fast Mode下表现一般加上传感器对时序要求不低100kHz是最稳的。USART1配置成PA9/PA10115200-8-N-1用来printf调试。SWD调试口默认保留别闲着没事把PA13/PA14给禁用了调起来会非常痛苦。CubeMX里我补充一个容易漏的细节I2C1的时钟速度虽然选的是100000但别忘了把I2C外设时钟在时钟树里确认已经使能。F103的APB1外设时钟默认是36MHzI2C的时钟源要保证不是0不然I2C配置出来完全无法工作。配置项整理成一张表配置项值说明MCUSTM32F103C8T6蓝丸或其他板子都行主频72MHzHSEPLLI2C1PB6/PB7100kHz7位地址0x62USART1PA9/PA10115200-8-N-1调试口SWD保留PA13/PA14printf重定向用HAL库的标准做法重写fputcint fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 10); return ch; }注意工程要勾选Use MicroLIB否则Keil下printf的重定向会不正常。3.2 驱动层源程序SCD40读写、CRC校验、状态轮询与校准驱动文件命名为scd40.c和scd40.h不依赖具体业务方便以后移植到其他STM32型号。核心函数包括写命令、读数据、等待数据就绪、读取测量数据、自动校准、强制校准。头文件声明如下#ifndef __SCD40_H #define __SCD40_H #include stm32f1xx_hal.h #define SCD40_I2C_ADDR 0x62 uint8_t scd40_start_periodic_measurement(void); uint8_t scd40_start_low_power_periodic_measurement(void); uint8_t scd40_stop_measurement(void); uint8_t scd40_wait_data_ready(uint32_t timeout_ms); uint8_t scd40_read_measurement(uint16_t *co2, float *temp, float *humidity); uint8_t scd40_set_asc(uint8_t enable); uint8_t scd40_set_forced_recalibration(uint16_t target_ppm); #endif源文件实现注意我这里用的I2C句柄是全局的hi2c1如果你的项目里有多个I2C外设可以把句柄作为参数传进去我这里为了代码简洁直接用了默认句柄。#include scd40.h #include string.h #include stdio.h #define SCD40_CMD_START_PERIODIC 0x21B1 #define SCD40_CMD_START_LOW_POWER_PERIODIC 0x219D #define SCD40_CMD_STOP_PERIODIC 0x3F86 #define SCD40_CMD_READ_DATA_READY 0xE4B8 #define SCD40_CMD_READ_MEASUREMENT 0xEC05 #define SCD40_CMD_SET_ASC 0x2424 #define SCD40_CMD_CLEAR_ASC 0x2423 #define SCD40_CMD_FRC 0x362F #define SCD40_CMD_SOFT_RESET 0xD404 static uint8_t scd40_crc8(const uint8_t *data, uint8_t len) { uint8_t crc 0xFF; for (uint8_t i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 0x80) crc (uint8_t)((crc 1) ^ 0x31); else crc 1; } } return crc; } static uint8_t scd40_write_cmd(uint16_t cmd, const uint8_t *payload, uint8_t payload_len) { uint8_t buf[8]; uint8_t idx 0; buf[idx] (uint8_t)(cmd 8); buf[idx] (uint8_t)(cmd 0xFF); if (payload ! NULL payload_len 0) { memcpy(buf[idx], payload, payload_len); idx payload_len; buf[idx] scd40_crc8(payload, payload_len); } if (HAL_I2C_Master_Transmit(hi2c1, SCD40_I2C_ADDR 1, buf, idx, 100) ! HAL_OK) return 1; return 0; } static uint8_t scd40_read_data(uint16_t cmd, uint8_t *buf, uint8_t len) { uint8_t cmd_buf[2]; cmd_buf[0] (uint8_t)(cmd 8); cmd_buf[1] (uint8_t)(cmd 0xFF); if (HAL_I2C_Master_Transmit(hi2c1, SCD40_I2C_ADDR 1, cmd_buf, 2, 100) ! HAL_OK) return 1; if (HAL_I2C_Master_Receive(hi2c1, SCD40_I2C_ADDR 1, buf, len, 100) ! HAL_OK) return 2; return 0; } uint8_t scd40_start_periodic_measurement(void) { return scd40_write_cmd(SCD40_CMD_START_PERIODIC, NULL, 0); } uint8_t scd40_start_low_power_periodic_measurement(void) { return scd40_write_cmd(SCD40_CMD_START_LOW_POWER_PERIODIC, NULL, 0); } uint8_t scd40_stop_measurement(void) { return scd40_write_cmd(SCD40_CMD_STOP_PERIODIC, NULL, 0); } uint8_t scd40_wait_data_ready(uint32_t timeout_ms) { uint8_t buf[3]; uint32_t elapsed 0; while (elapsed timeout_ms) { if (scd40_read_data(SCD40_CMD_READ_DATA_READY, buf, 3) 0) { uint16_t status (uint16_t)((buf[0] 8) | buf[1]); if (status 0x0800) return 1; } HAL_Delay(100); elapsed 100; } return 0; } uint8_t scd40_read_measurement(uint16_t *co2, float *temp, float *humidity) { uint8_t buf[9]; memset(buf, 0, sizeof(buf)); if (scd40_read_data(SCD40_CMD_READ_MEASUREMENT, buf, 9) ! 0) return 1; if (scd40_crc8(buf[0], 2) ! buf[2]) return 2; if (scd40_crc8(buf[3], 2) ! buf[5]) return 3; p a hrefhttps://download.csdn.net/download/qq_35654286/90184692 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