零知派——STM32+SCD41+旋转编码器:室内CO₂智能监测系统,三环可视化仪表盘 + 分级蜂鸣告警

电子说

1.4w人已加入

描述

目录

一、系统接线部分

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 具体接线图

监测系统

        编码器 CLK和 DT接到支持attachInterrupt外部中断的引脚D6和D12

1.4 连接实物图

监测系统

二、安装与使用部分

2.1 开源平台-输入"SCD41"并搜索-代码下载自动打开

监测系统

2.2 连接-验证-上传

监测系统

2.3 调试-串口监视器

监测系统

三、代码讲解部分

        本项目代码基于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 旋转编码器四态查表消抖算法

        本项目使用格雷码查表法,配合累积步数阈值,实现准确的方向判断

 

// 16个状态转移,每个值代表方向变化量(+1/-1/0)
const int8_t encoderTable[] = {
   0,-1, 1, 0,
   1, 0, 0,-1,
  -1, 0, 0, 1,
   0, 1,-1, 0
};

void updateEncoder() {
    uint8_t clk     = digitalRead(ENC_CLK);   // A相
    uint8_t dt      = digitalRead(ENC_DT);    // B相
    uint8_t encoded = (clk < < 1) | dt;        // 拼成2位格雷码
    
    // 用上一状态+当前状态组成4位索引,查表得方向
    int8_t dir = encoderTable[(lastEncoded < < 2) | encoded];
    
    if (dir != 0) {
        accSteps += dir;
        // 累积4步才触发翻页,过滤抖动产生的虚假脉冲
        if (accSteps >= 4) {
            accSteps = 0;
            if (millis() - lastEncTrigTime > ENC_COOLDOWN) {
                pageCW = true;              // 顺时针:下一页
                lastEncTrigTime = millis();
            }
        } else if (accSteps <= -4) {
            accSteps = 0;
            if (millis() - lastEncTrigTime > ENC_COOLDOWN) {
                pageCCW = true;             // 逆时针:上一页
                lastEncTrigTime = millis();
            }
        }
    }
    lastEncoded = encoded;
}

// 编码器通过 attachInterrupt 绑定到两个引脚
attachInterrupt(digitalPinToInterrupt(ENC_CLK), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENC_DT),  updateEncoder, CHANGE);

// 在 loop() 主循环中,中断设置的标志位被消费
if (pageCW)  
{ 
    pageCW = false;  
    currentPage = (currentPage+1)%PAGE_COUNT; 
    lastPage = -2; 
}
if (pageCCW) 
{ 
    pageCCW = false; 
    currentPage = (currentPage-1+PAGE_COUNT)%PAGE_COUNT; 
    lastPage = -2; 
}

 

lastPage=-2 是强制全刷新标志,翻页时重绘整个屏幕避免残影

3.3 圆环仪表盘绘制与局部刷新策略

        圆环绘制是本项目视觉效果的核心,也是性能优化的重点

 

// 基础弧段绘制:从 startDeg 到 endDeg(以正上方为0°,顺时针)
void drawArcSection(int16_t cx, int16_t cy, int16_t r,
                    int16_t startDeg, int16_t endDeg, uint16_t color) {
    for (int16_t deg = startDeg; deg <= endDeg; deg++) {
        // 坐标系旋转:-90° 使 0° 指向正上方
        float rad = (deg - 90) * PI / 180.0f;
        tft.drawPixel(cx + (int16_t)(r * cosf(rad)),
                      cy + (int16_t)(r * sinf(rad)), color);
    }
}

// 圆环(多层弧段叠加形成宽度)
void drawRingArc(int16_t cx, int16_t cy,
                 int16_t innerR, int16_t outerR,
                 int16_t startDeg, int16_t endDeg, uint16_t color) {
    for (int16_t r = innerR; r <= outerR; r++)
        drawArcSection(cx, cy, r, startDeg, endDeg, color);
}

 

圆环绘制

 

// 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 值控制占空比从而调节响度

 

void updateVolume() {
    int adc = analogRead(VOLUME_PIN);  // STM32 12位ADC:0~4095
    // 映射到 10%~90% 占空比,防止0%(无声)和100%(直流,损坏蜂鸣器)
    volumePercent = (uint8_t)map(adc, 0, 4095, 10, 90);
}

void beep(uint16_t freq, uint16_t dur) {
    if (freq == 0 || muteEnabled) return;
    uint32_t period   = 1000000UL / freq;          // 周期(微秒)
    uint32_t highTime = period * volumePercent / 100; // 高电平时间
    uint32_t lowTime  = period - highTime;            // 低电平时间
    uint32_t cycles   = (uint32_t)dur * 1000UL / period; // 总循环次数
    
    for (uint32_t i = 0; i < cycles; i++) {
        digitalWrite(BUZZER_PIN, HIGH);
        delayMicroseconds(highTime);
        digitalWrite(BUZZER_PIN, LOW);
        delayMicroseconds(lowTime);
    }
}

 

三档告警频率对应表:

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(); }
}

 

系统流程图

监测系统

3.5 SCD4x库API使用

API 调用时机 内部原理
mySensor.begin(Wire, true, true, false, true) setup() 一次 stopPeriodic→CRC序列号验证→ASC设置→startPeriodic
mySensor.readMeasurement() loop() 轮询 内部先调 getDataReadyStatus(),bit[10:0]≠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[x]; // 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_t  tempHumidity;
  tempHumidity.unsigned16 = 0;
  scd4x_unsigned16Bytes_t  tempTemperature;
  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[2];
    for (byte x = 0; x < 9; x++)
    {
      byte incoming = _i2cPort- >read();

      switch (x)
      {
      case 0:
      case 1:
        tempCO2.bytes[x == 0 ? 1 : 0] = incoming; // Store the two CO2 bytes in little-endian format
        bytesToCrc[x] = incoming; // Calculate the CRC on the two CO2 bytes in the order they arrive
        break;
      case 3:
      case 4:
        tempTemperature.bytes[x == 3 ? 1 : 0] = incoming; // Store the two T bytes in little-endian format
        bytesToCrc[x % 3] = incoming; // Calculate the CRC on the two T bytes in the order they arrive
        break;
      case 6:
      case 7:
        tempHumidity.bytes[x == 6 ? 1 : 0] = incoming; // Store the two RH bytes in little-endian format
        bytesToCrc[x % 3] = 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[10:0] 全为 0 表示数据未就绪,任意一位为 1 表示可读

四、项目结果演示

4.1 操作流程

初始化上电

监测系统

        TFT 显示启动画面(CO₂ MONITOR + Lingzhi Lab 2026),约 1 秒后发出两声短鸣提示初始化成功

显示 "SCD41 Running..." 动态圆点,约 5 秒后第一帧数据到达,主界面自动切换为实时数据显示

主界面(Page 0)

        上方 CO₂ 大圆环 + 下方温度/湿度小圆环同时展示,圆环颜色随数值实时变化,右侧等级标签跟随更新

监测系统

旋转编码器右转:翻到下一页(CO₂详情 → 温湿度详情 → 系统信息 → 循环回主界面)

旋转编码器左转:翻到上一页

SW 按键按下

        底栏 LIVE 切换为 MUTE(橙色),蜂鸣器静音;再次按下恢复 LIVE(绿色)

监测系统

旋转电位器

        可以调节蜂鸣器告警音量(最小 10% 占空比细声,最大 90% 占空比响亮)

监测系统

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 的红外光有强烈的选择性吸收

监测系统

传感器内部:

红外光源周期性发射宽谱红外光

光穿过含有待测气体的腔体

特定波长的光被 CO₂分子吸收,剩余光被探测器接收

通过 Beer-Lambert 定律计算 CO₂浓度

        A = ε × c × L
        A: 吸光度    ε: 摩尔消光系数(CO₂固有属性)    c: 浓度(即我们要测的值)    L: 光程长度(腔体固定)

5.1 SCD41 通信时序

        SCD41 的 I2C 地址固定为 0x62,不可更改。通信采用标准 I2C 协议,命令格式为 16 位命令字(MSB 先发)

①写命令时序

监测系统

②基础命令

监测系统

start_periodic_measurement(0x21B1)发出后必须等待 5 秒才能读取,发送其他命令(如 stop_periodic_measurement 0x3F86)后需等待 500ms 才能继续操作

③读数据时序

监测系统

④一次完整测量读取的数据帧格式

        总计 9 字节,每两字节数据附一字节 CRC-8

监测系统

温度换算

监测系统

湿度换算

监测系统

5.2 自动自校准机制

        SCD41 内置 ASC 算法,假设传感器在使用期间至少每周会暴露一次室外新鲜空气(约 400ppm)。ASC 会统计历史测量中的最低 CO₂值,并以此为基准自动漂移校准

监测系统

5.3 旋转编码器原理

        EC11 是一种机械式增量旋转编码器,内部有两组相位差 90° 的触点(A相/CLK 和 B相/DT)。旋转时两相产生交替的高低电平变化,通过判断两相的相位超前/滞后关系确定旋转方向

①光电编码原理

监测系统

编码器内部有一个带有栅格的光码盘,红外发射管和接收管分别位于码盘两侧、旋转时,栅格交替遮挡光线,产生脉冲信号

②A/B相信号产生

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

监测系统

正转: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的顺序循环

监测系统

  
逆时针旋转时,B 引脚先于 A 引脚接地,格雷码按00→10→11→01→00的顺序循环

监测系统

        每旋转一格产生 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

审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分