零知派——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]