eefocus_4087784 发表于 2026-5-29 15:47:56

零知派——STM32+SCD41+旋转编码器:室内CO₂智能监测系统

    ✔零知派(零知开源)是一个专为电子初学者/电子兴趣爱好者设计的开源软硬件平台,在硬件上提供超高性价比STM32系列开发板、物联网控制板。取消了Bootloader程序烧录,让开发重心从 “配置环境” 转移到 “创意实现”,极大降低了技术门槛。零知开源编程软件,内置上千个覆盖多场景的示例代码,支持项目源码一键下载,项目文章在线浏览。零知派(零知开源)平台通过软硬件协同创新,让你的创意快速转化为实物,来动手试试吧!
✔访问零知实验室,获取更多实战项目和教程资源吧!
www.lingzhilab.com


目录
一、系统接线部分
1.1 硬件清单1.2 接线方案表1.3 具体接线图1.4 连接实物图二、安装与使用部分
三、代码讲解部分
3.1 Softwire 正确初始化序列3.2 旋转编码器四态查表消抖算法3.3 圆环仪表盘绘制与局部刷新策略3.4 音量控制及多频率告警3.5 SCD4x库API使用① CRC-8 校验原理② 数据就绪轮询机制四、项目结果演示
4.1 操作流程4.2 视频演示五、工作原理讲解
5.1 SCD41 通信时序5.2 自动自校准机制5.3 旋转编码器原理六、常见问题解答(FAQ)
Q1:码器旋转方向反了,或者非常灵敏转一格跳很多页?Q2:蜂鸣器一直响或完全不响?Q3:为什么CO₂数据长时间显示"--"?
项目概述
本项目基于零知派标准板(主控 STM32F103RBT6)驱动 Sensirion SCD41 NDIR CO₂传感器,配合 ST7789 240×240 TFT 显示屏、旋转编码器和无源蜂鸣器,实现了一套完整的室内空气质量实时监测系统。系统采用浅色简约主题,以圆环仪表盘的形式同屏展示 CO₂浓度、温度、湿度三路数据,并通过旋转编码器在四个功能页面间自由切换,电位器实时调节告警音量,SW 按键一键切换静音


项目难点
问题描述:SoftWire 时序在多外设共存场景下崩溃,导致 SCD41 无法初始化

解决方案: 启动画面不调用 beep()、 TFT init 后加 100ms 稳定延时、使用全局 Wire 对象

一、系统接线部分1.1 硬件清单

序号 模块 规格 数量
1 零知派标准板 STM32F103RBT6,Arduino 兼容 1
2 CO₂传感器 Sensirion SCD41,I2C,3.3V 1
3 TFT 显示屏 ST7789,240×240,SPI 1
4 旋转编码器 EC11,带按键 SW 1
5 无源蜂鸣器 3.3V,支持 PWM 驱动 1
6 滑动变阻器 10kΩ,线性 1
7 杜邦线 母对母 若干


1.2 接线方案表严格按照代码中的宏定义进行接线,不得随意更改,否则编码器中断和 ADC 会失效

①SCD41 CO₂传感器

SCD41 引脚 零知派标准板 代码定义 说明
SDA A4 SoftWire SDA 软件 I2C 数据
SCL A5 SoftWire SCL 软件 I2C 时钟
VDD 5V — 供电,注意噪声
GND GND — 接地
旋转编码器 EC11
编码器引脚 零知派标准板 代码定义 说明
CLK(A相) D6 #define ENC_CLK 6 接外部中断
DT(B相) D12 #define ENC_DT 12 接外部中断
SW(按键) D14 #define ENC_SW 14 INPUT_PULLUP
VCC 5V — —
GND GND — —
蜂鸣器 & 电位器
器件 零知派标准板 代码定义 说明
蜂鸣器 + D3 #define BUZZER_PIN 3 数字输出,PWM
蜂鸣器 − GND — —
电位器中间脚 A0 #define VOLUME_PIN A0 模拟输入,12位ADC
电位器两端 3.3V / GND — 左侧接3.3V / 右侧接GND
请注意:ST7789显示屏直插零知派标准板TFT引脚,无需单独接线


1.3 具体接线图
https://file1.elecfans.com/web3/M00/62/34/wKgZO2oY6m-Aa_PUABDjR7ksHbo355.png编码器 CLK和 DT接到支持attachInterrupt外部中断的引脚D6和D12

1.4 连接实物图
https://file1.elecfans.com/web3/M00/62/34/wKgZPGoY6muAH9zNAB3cbgeJsEY536.png

二、安装与使用部分
2.1 开源平台-输入"SCD41"并搜索-代码下载自动打开
https://file1.elecfans.com/web3/M00/62/34/wKgZPGoY6riARkruAAewdEwT76c342.png
2.2 连接-验证-上传
https://file1.elecfans.com/web3/M00/62/34/wKgZO2oY6r2APNKeAAtgWxZKLcQ544.png
2.3 调试-串口监视器
https://file1.elecfans.com/web3/M00/62/34/wKgZPGoY6sKAN6B9AAuytJ6rdvY769.png

三、代码讲解部分      本项目代码基于SparkFun_SCD4x_Arduino_Library(底层使用SoftWire)和Adafruit_ST7789驱动

3.1 Softwire 正确初始化序列
// setup() 中的传感器初始化序列

// Step 1:TFT 初始化(SPI外设配置)
tft.init(240, 240);
tft.setRotation(3);
tft.fillScreen(COL_BG);
showSplash();// ← 纯显示,绝对不调用 beep()

// Step 2:等待 SPI 外设稳定,给 I2C 上拉恢复时间
delay(100);   // ← 这 100ms 是解决 err=268 的关键

// Step 3:启动 SoftWire 总线
Wire.begin(); // 使用 SoftWire.cpp 末尾定义的全局 Wire 对象
delay(50);    // I2C 总线电平建立稳定时间

// Step 4:SparkFun 库初始化
// begin(wirePort, measBegin, autoCalibrate, skipStop, pollDevType)
bool ok = mySensor.begin(Wire, true, true, false, true);参数说明:

参数 值 含义
wirePort Wire 使用全局 SoftWire 对象
measBegin true 初始化后立即启动周期测量
autoCalibrate true 启用 SCD41 自动校准(ASC)
skipStop false 先执行 stopPeriodicMeasurement 确保干净状态
pollDevType true 读取 feature set 版本,自动识别 SCD40/41
SoftWire 的 NOP-loop 时序在 STM32 多外设环境下不够稳定,导致该命令的 CRC 校验失败返回 268。SparkFun 的 begin() 通过 getSerialNumber() 的 CRC 校验能验证通信正常
3.2 旋转编码器四态查表消抖算法本项目使用格雷码查表法,配合累积步数阈值,实现准确的方向判断

<blockquote><p>// 16个状态转移,每个值代表方向变化量(+1/-1/0)</p>lastPage=-2 是强制全刷新标志,翻页时重绘整个屏幕避免残影

3.3 圆环仪表盘绘制与局部刷新策略圆环绘制是本项目视觉效果的核心,也是性能优化的重点

<blockquote><p>// 基础弧段绘制:从 startDeg 到 endDeg(以正上方为0°,顺时针)</p>圆环绘制
// Step1:擦除整个圆环区域(用背景色填充外圆)
tft.fillCircle(cx, cy, outerR+2, COL_BG);

// Step2:绘制底层灰色轨道(300°满量程)
drawRingArc(cx, cy, innerR, outerR, 0, 300, COL_RING_BG);

// Step3:绘制有色填充弧(根据数据值映射到0~300°)
int16_t arc = (int16_t)map(constrain(co2, 400, 2500), 400, 2500, 0, 300);
if (arc > 0) drawRingArc(cx, cy, innerR, outerR, 0, arc, co2Color(co2));

// Step4:填充圆心区域(遮住innerR以内的像素,恢复白底)
tft.fillCircle(cx, cy, innerR-1, COL_BG);局部刷新策略
// 只有首次进入页面或强制刷新时(full=true)才重绘静态背景
void drawPageCO2(bool full) {
    if (full) {
      tft.fillScreen(COL_BG);    // 仅此处全屏清空
      drawTopBar("CO2 Detail");
      drawBottomBar("TURN to switch");
    }
    // 以下每次循环都执行:只刷新圆环和数值区域
    // 数值文字前先精确擦除其占用矩形
    tft.fillRect(cx-48, labelY-12, 88, 16, COL_BG);// 擦除等级标签
    // 再重绘
    tft.print(co2Label(co2));
}      bool full = (currentPage != lastPage);
      lastPage= currentPage;
full 为 true 只在翻页瞬间,之后每帧都是 false,只刷新有数据变化的区域,大幅减少 SPI 传输量

3.4 音量控制及多频率告警      蜂鸣器通过软件 PWM(手动控制高低电平时间)实现不同频率,由电位器 ADC 值控制占空比从而调节响度
<blockquote><p>void updateVolume() {</p>三档告警频率对应表:

CO₂ 范围 等级 圆环颜色 告警行为
0~400 ppm WAIT 灰色(数据未就绪) 无
400~800 ppm GOOD 绿色 0x07C0 无告警
800~1000 ppm FAIR 青绿 0x0454 单次 440Hz 短鸣
1000~1500 ppm POOR 琥珀黄 0xFEA0 双次 440Hz 鸣叫
1500~2000 ppm BAD 橙色 0xFC40 三次 280Hz 告警
>2000 ppm CRIT 红色 0xF800 三次 280Hz 急促告警
void handleBuzzer() {
    if (!dataValid) return;
    // 节流:最快每 4 秒告警一次,避免持续噪音
    if (millis() - lastBeepTime < BEEP_INTERVAL) return;
   
    if      (co2 > CO2_BAD){ beepPattern(BEEP_CRIT, 3, 200, 150); lastBeepTime=millis(); }
    else if (co2 > CO2_POOR) { beepPattern(BEEP_WARN, 2, 150, 120); lastBeepTime=millis(); }
    else if (co2 > CO2_FAIR) { beepPattern(BEEP_WARN, 1, 100,   0); lastBeepTime=millis(); }
}系统流程图
https://file1.elecfans.com/web3/M00/62/35/wKgZPGoY6vuAX7VWAATe74KmrIg816.png
3.5 SCD4x库API使用

API 调用时机 内部原理
mySensor.begin(Wire, true, true, false, true) setup() 一次 stopPeriodic→CRC序列号验证→ASC设置→startPeriodic
readMeasurement loop() 轮询 内部先调 getDataReadyStatus(),bit≠0才读;一次读取 9 字节含3组 CRC
mySensor.getCO2() 数据就绪后 返回 uint16_t,单位 ppm,范围 400~5000
mySensor.getTemperature() 数据就绪后 返回 float,公式:-45 + 175×rawT/65535
mySensor.getHumidity() 数据就绪后 返回 float,公式:100×rawH/65535

① CRC-8 校验原理//x^8+x^5+x^4+1 = 0x31
uint8_t SCD4x::computeCRC8(uint8_t data[], uint8_t len)
{
uint8_t crc = 0xFF; //Init with 0xFF

for (uint8_t x = 0; x < len; x++)
{
    crc ^= data; // XOR-in the next input byte

    for (uint8_t i = 0; i < 8; i++)
    {
      if ((crc & 0x80) != 0)
      crc = (uint8_t)((crc < < 1) ^ 0x31);
      else
      crc < <= 1;
    }
}

return crc; //No output reflection
}SCD41 每两字节数据后附一字节 CRC,多项式为 x⁸+x⁵+x⁴+1 = 0x31,初始值 0xFF。SparkFun 库在每次 I2C 读取后自动验证,校验失败则 readMeasurement() 返回 false


② 数据就绪轮询机制bool SCD4x::readMeasurement(void)
{
//Verify we have data from the sensor
if (getDataReadyStatus() == false)
    return (false);

scd4x_unsigned16Bytes_t tempCO2;
tempCO2.unsigned16 = 0;
scd4x_unsigned16Bytes_ttempHumidity;
tempHumidity.unsigned16 = 0;
scd4x_unsigned16Bytes_ttempTemperature;
tempTemperature.unsigned16 = 0;

_i2cPort- >beginTransmission(SCD4x_ADDRESS);
_i2cPort- >write(SCD4x_COMMAND_READ_MEASUREMENT > > 8);   //MSB
_i2cPort- >write(SCD4x_COMMAND_READ_MEASUREMENT & 0xFF); //LSB
if (_i2cPort- >endTransmission() != 0)
    return (false); //Sensor did not ACK

delay(1); //Datasheet specifies this

#if SCD4x_ENABLE_DEBUGLOG
uint8_t receivedBytes = (uint8_t)
#endif // if SCD4x_ENABLE_DEBUGLOG
_i2cPort- >requestFrom((uint8_t)SCD4x_ADDRESS, (uint8_t)9);
bool error = false;
if (_i2cPort- >available())
{
    byte bytesToCrc;
    for (byte x = 0; x < 9; x++)
    {
      byte incoming = _i2cPort- >read();

      switch (x)
      {
      case 0:
      case 1:
      tempCO2.bytes = incoming; // Store the two CO2 bytes in little-endian format
      bytesToCrc = incoming; // Calculate the CRC on the two CO2 bytes in the order they arrive
      break;
      case 3:
      case 4:
      tempTemperature.bytes = incoming; // Store the two T bytes in little-endian format
      bytesToCrc = incoming; // Calculate the CRC on the two T bytes in the order they arrive
      break;
      case 6:
      case 7:
      tempHumidity.bytes = incoming; // Store the two RH bytes in little-endian format
      bytesToCrc = incoming; // Calculate the CRC on the two RH bytes in the order they arrive
      break;
      default: // x == 2, 5, 8
      //Validate CRC
      uint8_t foundCrc = computeCRC8(bytesToCrc, 2); // Calculate what the CRC should be for these two bytes
      if (foundCrc != incoming) // Does this match the CRC byte from the sensor?
      {
          #if SCD4x_ENABLE_DEBUGLOG
          if (_printDebug == true)
          {
            _debugPort- >print(F("SCD4x::readMeasurement: found CRC in byte "));
            _debugPort- >print(x);
            _debugPort- >print(F(", expected 0x"));
            _debugPort- >print(foundCrc, HEX);
            _debugPort- >print(F(", got 0x"));
            _debugPort- >println(incoming, HEX);
          }
          #endif // if SCD4x_ENABLE_DEBUGLOG
          error = true;
      }
      break;
      }
    }
}
else
{
    #if SCD4x_ENABLE_DEBUGLOG
    if (_printDebug == true)
    {
      _debugPort- >print(F("SCD4x::readMeasurement: no SCD4x data found from I2C, I2C claims we should receive "));
      _debugPort- >print(receivedBytes);
      _debugPort- >println(F(" bytes"));
    }
    #endif // if SCD4x_ENABLE_DEBUGLOG
    return (false);
}

if (error)
{
    #if SCD4x_ENABLE_DEBUGLOG
    if (_printDebug == true)
      _debugPort- >println(F("SCD4x::readMeasurement: encountered error reading SCD4x data."));
    #endif // if SCD4x_ENABLE_DEBUGLOG
    return (false);
}
//Now copy the int16s into their associated floats
co2 = (float)tempCO2.unsigned16;
temperature = -45 + (((float)tempTemperature.unsigned16) * 175 / 65536);
humidity = ((float)tempHumidity.unsigned16) * 100 / 65536;

//Mark our global variables as fresh
co2HasBeenReported = false;
humidityHasBeenReported = false;
temperatureHasBeenReported = false;

return (true); //Success! New data available in globals.
}寄存器 bit 全为 0 表示数据未就绪,任意一位为 1 表示可读
四、项目结果演示4.1 操作流程初始化上电
https://file1.elecfans.com/web3/M00/62/35/wKgZPGoY7AiAJnNfAB6DmLcWIU8108.pngTFT 显示启动画面(CO₂ MONITOR + Lingzhi Lab 2026),约 1 秒后发出两声短鸣提示初始化成功


显示 "SCD41 Running..." 动态圆点,约 5 秒后第一帧数据到达,主界面自动切换为实时数据显示
主界面(Page 0)
上方 CO₂ 大圆环 + 下方温度/湿度小圆环同时展示,圆环颜色随数值实时变化,右侧等级标签跟随更新
https://file1.elecfans.com/web3/M00/62/35/wKgZO2oY7A2ABlQdACDMF4kPIsU801.png

[*]旋转编码器右转:翻到下一页(CO₂详情 → 温湿度详情 → 系统信息 → 循环回主界面)
[*]旋转编码器左转:翻到上一页
SW 按键按下
底栏 LIVE 切换为 MUTE(橙色),蜂鸣器静音;再次按下恢复 LIVE(绿色)
https://file1.elecfans.com/web3/M00/62/35/wKgZPGoY7AaANbMJAAWfWYN0uPg608.png
旋转电位器
可以调节蜂鸣器告警音量(最小 10% 占空比细声,最大 90% 占空比响亮)
https://file1.elecfans.com/web3/M00/62/35/wKgZPGoY7A6AMxeuAB2EWruGOxw639.png
CO₂溶度超过 1000ppm蜂鸣器单次告警;超过 1500ppm 双次;超过 2000ppm 三次急促告警,圆环变红

4.2 视频演示
https://live.csdn.net/v/528726?spm=1001.2014.3001.5501
本视频展示基于零知派标准板驱动 Sensirion SCD41 CO₂传感器的完整室内空气质量监测系统。演示内容包括:系统上电初始化流程、ST7789 TFT 浅色主题三环仪表盘实时刷新效果、旋转编码器左右翻页切换四个显示界面(主界面/CO₂详情/温湿度露点/系统信息)、电位器实时调节蜂鸣器音量、SW 按键静音切换、以及模拟高CO₂环境时的分级告警蜂鸣效果

五、工作原理讲解SCD41 采用光声光谱(PAS)技术,是一种基于 NDIR 的 CO₂测量方案。其工作核心是CO₂分子对波长约 4.26 μm 的红外光有强烈的选择性吸收
https://file1.elecfans.com/web3/M00/62/35/wKgZPGoY7BGAOdJBAAVj_eV4rRI427.png
传感器内部:

[*]红外光源周期性发射宽谱红外光
[*]光穿过含有待测气体的腔体
[*]特定波长的光被 CO₂分子吸收,剩余光被探测器接收
[*]通过 Beer-Lambert 定律计算 CO₂浓度
      A = ε × c × L
A: 吸光度    ε: 摩尔消光系数(CO₂固有属性)    c: 浓度(即我们要测的值)    L: 光程长度(腔体固定)

5.1 SCD41 通信时序      SCD41 的 I2C 地址固定为 0x62,不可更改。通信采用标准 I2C 协议,命令格式为 16 位命令字(MSB 先发)
①写命令时序
https://file1.elecfans.com/web3/M00/62/35/wKgZPGoY7RKAfiYDAABDjMLWTlE693.png
②基础命令
https://file1.elecfans.com/web3/M00/62/35/wKgZO2oY7S6ADuLzAACdn0U_1bY735.png
start_periodic_measurement(0x21B1)发出后必须等待 5 秒才能读取,发送其他命令(如 stop_periodic_measurement 0x3F86)后需等待 500ms 才能继续操作
③读数据时序
https://file1.elecfans.com/web3/M00/62/35/wKgZO2oY7w-AeCZ5AABObsq_NPU491.png
④一次完整测量读取的数据帧格式
      总计 9 字节,每两字节数据附一字节 CRC-8
https://file1.elecfans.com/web3/M00/62/36/wKgZPGoY7yCANX9XAALw6jkkgFQ237.png
温度换算
https://file1.elecfans.com/web3/M00/62/36/wKgZPGoY7y6ALUo-AAASs_PePnE533.png
湿度换算
https://file1.elecfans.com/web3/M00/62/35/wKgZO2oY7zSAfKiPAAASQDm82IE602.png
5.2 自动自校准机制      SCD41 内置 ASC 算法,假设传感器在使用期间至少每周会暴露一次室外新鲜空气(约 400ppm)。ASC 会统计历史测量中的最低 CO₂值,并以此为基准自动漂移校准
https://file1.elecfans.com/web3/M00/62/36/wKgZPGoY7zyAYJcXAADHute8pKU933.png
5.3 旋转编码器原理      EC11 是一种机械式增量旋转编码器,内部有两组相位差 90° 的触点(A相/CLK 和 B相/DT)。旋转时两相产生交替的高低电平变化,通过判断两相的相位超前/滞后关系确定旋转方向

①光电编码原理
https://file1.elecfans.com/web3/M00/62/35/wKgZO2oY70qAQvrhAAAndoD-H60889.png
编码器内部有一个带有栅格的光码盘,红外发射管和接收管分别位于码盘两侧、旋转时,栅格交替遮挡光线,产生脉冲信号

②A/B相信号产生
      正交编码: 两个光电传感器安装位置相差1/4个栅格间距,产生相位差90°的A相和B相信号

https://file1.elecfans.com/web2/M00/A9/A1/wKgZomUnpXiAPhdvAAX24toIEmk322.gif正转:A 相脉冲的上升沿 / 下降沿超前B 相 90°, A 相先变化,B 相后变化
反转:B 相脉冲的上升沿 / 下降沿超前A 相 90°, B 相先变化,A 相后变化

③格雷码编码
将 A、B 两相的当前值拼成 2 位二进制(encoded = CLK<<1 | DT),与上一次状态一起组成 4 位索引((last<<2)|current),查 16 格表直接得到方向值 {+1/-1/0}

顺时针旋转时,A 引脚先于 B 引脚接地,格雷码按00→01→11→10→00的顺序循环
https://file1.elecfans.com/web3/M00/62/35/wKgZO2oY73eAAXILAABOIIG6hCA641.png
逆时针旋转时,B 引脚先于 A 引脚接地,格雷码按00→10→11→01→00的顺序循环
https://file1.elecfans.com/web3/M00/62/36/wKgZO2oY74GAFtMVAABPA5ezPAY554.png
      每旋转一格产生 4 个有效边沿,累积满 ±4 才确认一次有效旋转


六、常见问题解答(FAQ)Q1:码器旋转方向反了,或者非常灵敏转一格跳很多页?      A:方向反:交换 CLK 和 DT 的接线,或将 +1/-1 查表结果对换。跳页过多:检查 accSteps 阈值是否为 4,以及 ENC_COOLDOWN 是否设为 400ms

Q2:蜂鸣器一直响或完全不响?
      A:确认使用的是无源蜂鸣器,有源蜂鸣器只能响单一固定频率,无法响应 PWM。完全不响检查 muteEnabled 是否为 true(底栏显示 MUTE),以及 D3 引脚接线

Q3:为什么CO₂数据长时间显示"--"?
      A:SCD41 startPeriodicMeasurement 后首帧数据需要 5 秒。若超过 15 秒仍无数据,检查 A4/A5 接线和 供电。可临时开启 SparkFun 库的 Debug 输出:mySensor.enableDebugging(Serial)


项目资源整合
SCD41 数据手册:      SCD4x Data Sheet
SCD4x 库文件:            sparkfun/SparkFun_SCD4x_Arduino_Library













页: [1]
查看完整版本: 零知派——STM32+SCD41+旋转编码器:室内CO₂智能监测系统