简介:本资源是一套面向嵌入式开发初学者与物联网项目实践者的STM32平台二氧化碳监测方案源码,聚焦SCD40传感器驱动与数据解析核心问题,适用于智能建筑环境监控、农业温室气体管理及工业安全检测等实际场景。压缩包共11个文件(6个.h头文件、5个.c源文件),总大小仅24KB,结构精简:包含Sensirion官方I2C底层驱动(sensirion_i2c.c/h、i2c_hal.c/h)、SCD40专用协议封装(scd4x_i2c.c/h)及通用配置模块(sensirion_common.c/h、sensirion_config.h),便于快速移植与调试。已有440人学习下载,代码严格遵循SCD40数据手册时序与CRC校验规范,内置初始化配置、周期测量触发、原始数据转换为ppm值及基础错误处理逻辑,无需额外依赖库即可在主流STM32 HAL/StdPeriph工程中直接集成。 老哥们,最近在折腾一个室内空气质量监测的小板子,主控用的STM32F103,传感器选了瑞士Sensirion家的SCD40二氧化碳传感器。这块芯片比同门的SCD30体积小了一大圈,功耗低到离谱,5秒一个采样周期的时候平均电流也就0.5mA左右,非常适合做电池供电的便携设备。这篇文章把我从零开始调通SCD40的完整过程整理出来,包括I2C时序怎么处理、CRC校验怎么算、测量数据怎么换算、ASC和FRC标定怎么用,再把我实际调试中踩过的坑一并交代清楚。
这套源程序的核心思路并不复杂:STM32通过I2C总线与SCD40通信,上电后发送启动周期测量命令,然后每隔5秒轮询一次数据就绪状态,就绪后读取9个字节的原始数据,经过CRC校验和公式换算,得到二氧化碳浓度、温度、湿度三个物理量。如果你正准备做新风系统联动、教室/办公室空气质量监测、智能家居CO2提醒这类项目,这篇文章可以直接当参考文档用。
1. 项目设计与方案选型
1.1 为什么选SCD40而不是SCD30或者其他传感器
先聊选型。市面上常见的CO2传感器方案其实不少,国产的MH-Z19系列用的比较多,价格便宜,串口输出,但体积大、功耗高,基本告别电池供电。Sensirion自家的SCD30是上一代主力,NDIR原理,精度不错,但同样存在体积和功耗偏大的问题。而SCD40用的是光声传感原理,不是传统的NDIR,所以能做到非常小的封装,尺寸只有8.5mm x 8.5mm x 5.8mm,比SCD30小了差不多5倍。
从参数上看,SCD40的CO2测量范围是0到40000ppm,精度为±(50ppm + 5%读数),这个精度级别用于室内空气质量监测完全够用。温度精度±0.8°C,相对湿度精度±6%,中规中矩,但胜在传感器直接把这几个量一起测了,省去了额外挂温湿度芯片的麻烦。SCD40还集成了自动自校准算法(ASC),在400ppm左右的基线环境下连续运行约一周,就能自动修正长期漂移,这一点对产品维护非常友好。
如果说缺点,主要有两个:一是I2C接口对时序有一定要求,不能随便乱读;二是传感器本身不含MCU功能,所有数据处理都要主机端自己搞定。这两个缺点其实也谈不上大问题,写入死数据手册里的示例代码就能解决,这也是我写这篇源程序记录的目的。
1.2 硬件连接与电路设计
SCD40的硬件连接非常标准化,总共只需要5个引脚:VDD、GND、SDA、SCL、SEL。我的板上用3.3V给传感器供电,SDA和SCL分别接到STM32F103的PB7和PB6上,这两个引脚复用为I2C1。SEL引脚是地址选择脚,接地时I2C地址是0x62(7位地址),如果接VDD则会变为0x63。我做的是单传感器方案,所以SEL直接接地。
这里有个容易被忽略的细节:SCD40虽然是光声式传感器,功耗比NDIR低很多,但它在测量周期内的瞬时电流峰值依然有几十毫安,如果供电端滤波不足,测量值会跳动得非常厉害。我实测在VDD和GND之间加了100nF陶瓷电容再加10uF钽电容之后,数据稳定性明显改善。I2C总线的上拉电阻我用的是4.7k,如果总线上挂了多个设备,比如OLED、Flash芯片之类,建议根据实际总线长度和速率调整,4.7k是常规起步值。
另外,SCD40的数据手册明确推荐使用短而直的总线布线,传感器尽量远离大功率电感、射频天线和发热元件,尤其是发热元件。光声式传感器的温湿度测量会被热源干扰,进而影响CO2浓度补偿后的数值。如果你的PCB板上有DC-DC电源或者WIFI模块,布局时一定要留出足够的隔离距离。
2. SCD40驱动层实现:I2C时序与CRC校验
2.1 SCD40的I2C协议基础
SCD40的I2C通信协议遵循标准I2C框架,但由于传感器内部状态机比较特殊,它有一些区别于普通I2C从设备的特性需要注意。首先是时钟拉伸,SCD40在接收到部分命令后,会拉低SCL时钟线以等待内部处理完成,这属于正常现象。STM32的硬件I2C外设在遇到时钟拉伸时,部分型号或库版本可能表现不稳定,这也是很多人在网上吐槽STM32硬件I2C“有bug”的根源。实际使用下来,如果选用HAL库的阻塞式传输并适当增加超时时间,通常不会有大问题,但实在不行就换软件模拟I2C。
第二点是命令帧结构。SCD40的命令有两类:一类是只有2字节命令码的无参数命令,比如启动周期测量、软复位;另一类是命令码后跟2字节参数再加1字节CRC校验的命令,比如强迫自校准、设置温度偏移等。所有返回数据也都按“2字节数据+1字节CRC”的格式组织,每两个字节的数据后跟一个CRC-8字节。
| 命令功能 | 命令码 |
|---|---|
| 启动周期测量 | 0x21B1 |
| 读取数据就绪状态 | 0xE4B8 |
| 读取测量结果 | 0xEC05 |
| 停止周期测量 | 0x3F86 |
| 软复位 | 0x3646 |
| 强迫自校准(FRC) | 0x362F |
| 设置温度偏移 | 0x241D |
这个命令码表建议收藏,写驱动的时候非常有用。以上命令码均以官方数据手册为准,实际项目中可以直接抄用。
2.2 CRC-8校验为什么必须自己写
SCD40的I2C总线本身不像SPI那样有CRC硬件支持,而环境电磁干扰、总线竞争、时序毛刺都可能导致传输数据出错。为了避免读到错误的CO2数值,Sensirion在协议层面加入了CRC-8校验,多项式为0x31,初始值为0xFF。这个多项式和SHT30、SHT40等Sensirion自家传感器完全一致,写一遍校验函数,以后所有型号都能复用。
CRC校验的具体规则是:每个数据字段拆分为2字节,紧接着的第三个字节就是这个2字节数据的CRC校验值。读取测量结果时,传感器返回9个字节,拆解为CO2原始值2字节+CRC、温度原始值2字节+CRC、湿度原始值2字节+CRC。我早期刚开始接触时想偷懒不校验,结果室内开着空气净化器的时候,数值偶尔会跳到几千甚至上万,加了校验之后才定位到是总线干扰导致的数据错误。所以这个校验一定不要省。
uint8_t scd40_crc8(const uint8_t *data, size_t len) { uint8_t crc = 0xFF; for (size_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t bit = 0; bit < 8; bit++) { if (crc & 0x80) { crc = (crc << 1) ^ 0x31; } else { crc <<= 1; } } } return crc; }注意这个函数的输入是2个字节,返回1个字节,调用方式是把每组数据的前两个字节传入,与返回的第三个字节对比。如果CRC不匹配,这一帧数据直接丢弃,等待下一个测量周期再读取。实际操作中,CRC校验失败的概率其实很低,最常出现的原因还是供电问题和I2C速率过快,后面我会展开说。
2.3 核心驱动函数实现
SCD40的上电初始化流程有一个固定顺序,顺序搞错了传感器可能一直无响应。我实际验证过的流程是:上电后先等待至少10ms,然后发送软复位命令0x3646,再等待100ms让传感器内部完成复位初始化,接着发送停止周期测量命令0x3F86(这一步是为了确保传感器处于已知状态),最后发送启动周期测量命令0x21B1。完成以上步骤后,传感器进入5秒周期的测量模式。
读取数据的流程同样有讲究,不能在启动测量后立刻读取,必须先轮询数据就绪状态寄存器。SCD40的测量周期是5秒,但内部实际完成一次准确测量需要的时间取决于环境条件,所以主机端必须通过0xE4B8命令读取就绪标志位,只有标志位置位后才能读取测量结果。如果强行在未就绪时读取,得到的数据可能是上一次周期的旧值,或者干脆是0xFFFF这样的异常值。
下面是HAL库下的核心读取函数,我注释写得比较详细,方便直接移植到自己的工程里:
#define SCD40_ADDR (0x62 << 1) #define SCD40_CMD_START (0x21B1) #define SCD40_CMD_READY (0xE4B8) #define SCD40_CMD_READ_DATA (0xEC05) #define SCD40_CMD_RESET (0x3646) typedef struct { uint16_t co2_ppm; float temp_c; float humidity_rh; } scd40_data_t; HAL_StatusTypeDef scd40_read_data_ready(uint8_t *ready) { uint8_t cmd[2] = { (SCD40_CMD_READY >> 8) & 0xFF, SCD40_CMD_READY & 0xFF }; uint8_t raw[2] = {0}; if (HAL_I2C_Master_Transmit(&hi2c1, SCD40_ADDR, cmd, 2, 100) != HAL_OK) return HAL_ERROR; if (HAL_I2C_Master_Receive(&hi2c1, SCD40_ADDR, raw, 2, 100) != HAL_OK) return HAL_ERROR; *ready = (raw[1] & 0x01); return HAL_OK; } HAL_StatusTypeDef scd40_read_measurement(scd40_data_t *data) { uint8_t cmd[2] = { (SCD40_CMD_READ_DATA >> 8) & 0xFF, SCD40_CMD_READ_DATA & 0xFF }; uint8_t buf[9] = {0}; if (scd40_read_data_ready(&(uint8_t){0}) == HAL_ERROR) // 先查就绪位 return HAL_ERROR; if (HAL_I2C_Master_Transmit(&hi2c1, SCD40_ADDR, cmd, 2, 100) != HAL_OK) return HAL_ERROR; if (HAL_I2C_Master_Receive(&hi2c1, SCD40_ADDR, buf, 9, 100) != HAL_OK) return HAL_ERROR; // 每组数据前2字节,第3字节是CRC for (int i = 0; i < 3; i++) { uint8_t crc = scd40_crc8(&buf[i * 3], 2); if (crc != buf[i * 3 + 2]) { return HAL_ERROR; } } uint16_t raw_co2 = (buf[0] << 8) | buf[1]; uint16_t raw_temp = (buf[3] << 8) | buf[4]; uint16_t raw_hum = (buf[6] << 8) | buf[7]; style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />