今日份
关闭门窗,开启空调新风低档位,睡一个晚上,可以看出,除了甲醛和TVOC值得关心,二氧化碳浓度也挺重要的咧。
关闭门窗,是为了隔绝噪音和光线。 新风低档位,是因为中高档噪音有点大。
实测过,连新风都不开,二氧化碳数值大概在1800+ppm,接近2000。 实测过,最有效降低二氧化碳,是开启卫身间的排气扇,就是挺吵的。
代码Go
过程中,不会有太详细的理论,毕竟小白我也不知道理解的准不准确哈,理论可以参考附近的大神文章系列。
目录结构
- arduino-air-monitor 项目目录
- arduino-air-monitor.ino 程序入口
- src
- docs 一些传感器文档
- sgp_40 TVOC 模块 sgp40 的官方算法
- m_HCHO.h 甲醛 模块逻辑
- m_TVOC.h TVOC 模块逻辑
- m_CO2.h 二氧化碳 模块逻辑
- m_Temperature_Tumidity.h 温湿度 模块逻辑
- m_UV.h UV 紫外钱 模块逻辑
- DFRobot_SGP40.h TVOC 模块 sgp40 库 改版
- DFRobot_SGP40.cpp TVOC 模块 sgp40 库 改版
- clock.h 时钟逻辑
- display.h OLED 显示器逻辑
- buttons.h 按钮逻辑
根据 Arduino IDE 官方说明和特点,根目录文件会直接显示在 IDE 的 Tabs 中:
而且,其它代码需放在 src 目录下,才可以方便通过 #include "src/xxx/xxx.h" 引入。
程序入口
以下是基本结构:
#include "display.h"
#include "clock.h"
#include "buttons.h"
#include "m_Temperature_Tumidity.h"
#include "m_UV.h"
#include "m_TVOC.h"
#include "m_CO2.h"
#include "m_HCHO.h"
// ...略
// Last process time
long lastProcessSecond = 0;
void setup() {
Serial.begin(115200);
Buttons::Btn_1.init();
Display::OLED.init();
Clock::RTC.init();
Module::TVOC.init();
Module::HCHO.init();
Module::CO2.init();
}
void process(bool display) {
// ...略
Module::HCHO.read();
// 打印信息
char nowStr[20] = "";
now.tostr(nowStr);
Serial.println(nowStr);
Serial.print("Temperature:");
Serial.print(Module::Temperature.getValue());
Serial.println(Module::Temperature.unit);
Serial.print("Humidity:");
Serial.print(Module::Humidity.getValue());
Serial.println(Module::Humidity.unit);
Serial.print("UV:");
Serial.print(Module::UV.getValue());
Serial.println(Module::UV.unit);
Serial.print("CO2:");
Serial.print(Module::CO2.getValue());
Serial.println(Module::CO2.unit);
Serial.print("TVOC:");
Serial.print(Module::TVOC.getValue());
Serial.println(Module::TVOC.unit);
Serial.print("HCHO:");
Serial.print(Module::HCHO.getValue());
Serial.println(Module::HCHO.unit);
if (display) {
// OLED 显示逻辑
// ...略
}
}
bool oledDisplay = true;
void loop() {
Buttons::Btn_1.loop();
bool clicked = Buttons::Btn_1.getValue();
if (clicked) {
oledDisplay = !oledDisplay;
}
if (oledDisplay) {
// OLED 显示
// ...略
} else {
// OLED 不显示
// ...略
}
// ...略
// process each second
if (millis() / 1000 - lastProcessSecond >= 1) {
Serial.println();
Serial.print("loop:");
Serial.print(millis() / 1000);
Serial.println("s");
process(oledDisplay);
lastProcessSecond = millis() / 1000;
}
}
// 后面定义了一些 OLED 显示逻辑,可以理解成 UI 层的事情
// ...略
这里特别说一说: 由于 loop(),是使了劲的不停快速连续调用。 但是,不能使了劲的不停请求传感器,它们扛不住。 可是,显示时间又希望每次 loop 更新一次。 这里就不适合直接用 delay了。 所以这里定义了一个 lastProcessSecond,配合 millis() 获得启动时长(秒),计算每秒间隔,每秒才执行一次 process 方法,更新一次各传感器的值。
模块定义
设计
上面 “m_” 开头的代码问题,我定义为各种传感器的入口。 因为它们有类似的行为和步骤,抽离出来后,程序入口可以清爽许多,程序入口只需要关心传感器的初始化、更新、读取值即可。 基本结构如下:
// 必要依赖
#include <Arduino.h>
// 其它依赖
#include <XXX.h>
#include "src/YYY.h"
// 宏定义
// - 引脚定义
#define _Pin_AAA 1
#define _Pin_BBB A2
// - I2C 地址定义
#define _Address_CCC 0Xff
// 其它定义
// ...
// “模块”
namespace Module {
// 初始化一些库的实例
// SoftI2C SoftWire = //...
// DFRobot_SGP40 _m_sgp_40(&SoftWire);
// ...
struct _指标名 {
// 初始化逻辑(放在程序入口 setup 中)
void init() {
// ...
}
// 读取(放在程序入口 loop 中)
void read() {
// ...
}
// 读取值
int getValue() {
// ...
}
// 其它方法逻辑
// ...
// 单位名
char unit[6] = "ddd";
} 指标名;
}
试过用类/对象的方式进行定义抽离,但发现依赖的一些库,它需要 setup 之外顶层进行实例化,再次用类/对象包装意义不大,后来感觉用命名空间和结构体简单包装一下也挺好。
使用
以下是基本使用思路:
// ...略
#include "m_指标名.h"
// ...略
void setup() {
Serial.begin(115200);
// ...略
Module::指标名.init();
// ...略
}
void loop() {
// ...略
Module::指标名.read();
// ...略
// 实际上,读取传感器放在每秒执行的 process 中(上面“程序入口”提到)
Serial.print("指标名:");
Serial.print(Module::指标名.getValue());
Serial.println(Module::指标名.unit);
// ...略
}
逐个分析
m_HCHO.h
#include <Arduino.h>
#include <SoftwareSerial.h>
// 基于WZ Library(由于该库使用了Stream.h,出现无法解释的异常,因此简单重写之)
// 甲醛传感器
// 型号:WZ-S
// 接口:5V->VCC(5V)、G->GND、R->D9(RX)、T->D8(TX)
// 协议:UART
#define _Pin_HCHO_RX 8
#define _Pin_HCHO_TX 9
#define _Frame_Start_HCHO 0Xff // 帧 起始符
#define _Frame_Len_HCHO 9 // 帧长度
namespace Module {
SoftwareSerial _serial_hcho(_Pin_HCHO_RX, _Pin_HCHO_TX);
namespace WZ_S {
// 帧数据
byte buffer[_Frame_Len_HCHO] = {};
// 帧数据组装完成
bool buffer_done = false;
// 上一次的值
int lastData = 0;
// 值
int data = 0;
void passiveMode() {
// 0xFF, 0x01, 0x78, 0x41, 0x00, 0x00, 0x00, 0x00, 0x46
_serial_hcho.write((int)0xFF);
_serial_hcho.write((int)0x01);
_serial_hcho.write((int)0x78);
_serial_hcho.write((int)0x41);
_serial_hcho.write((int)0x00);
_serial_hcho.write((int)0x00);
_serial_hcho.write((int)0x00);
_serial_hcho.write((int)0x00);
_serial_hcho.write((int)0x46);
}
void requestRead() {
// 0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79
_serial_hcho.write((int)0xFF);
_serial_hcho.write((int)0x01);
_serial_hcho.write((int)0x86);
_serial_hcho.write((int)0x00);
_serial_hcho.write((int)0x00);
_serial_hcho.write((int)0x00);
_serial_hcho.write((int)0x00);
_serial_hcho.write((int)0x00);
_serial_hcho.write((int)0x79);
}
void Calc() {
unsigned int timeout = 0;
// 位序号
int count = 0;
// 起始
bool flag_start = false;
// 结束
bool flag_end = false;
// 当前位
byte byte_val = 0x00;
// 容错10次
while (flag_end == false && timeout < _Frame_Len_HCHO * 10) {
timeout++;
// 串口可用
if (_serial_hcho.available() > 0) {
// 按位读数
char int_val = (char)_serial_hcho.read();
byte_val = (byte)int_val;
// 帧数据组装
buffer[count] = byte_val;
// 起始符
if (byte_val == _Frame_Start_HCHO) {
// 更新 位序号
count = 0;
// 起始
flag_start = true;
}
// 下一位
count++;
// 起始
if (flag_start) {
// 读取长度
if (count >= _Frame_Len_HCHO) {
// 重置
count = 0;
// 成功/完成
buffer_done = true;
flag_end = true;
}
}
}
}
}
bool Check() {
uint8_t sum = 0;
// 1~7位求和
for (unsigned int i = 1; i < _Frame_Len_HCHO - 1; i++) {
sum += buffer[i];
}
sum = (~sum) + 1;
return sum == buffer[_Frame_Len_HCHO - 1];
}
void Read() {
_serial_hcho.listen();
// 串口可用
if (_serial_hcho.isListening()) {
unsigned int timeout = 0;
// 容错10次
while (buffer_done == false && timeout < 10) //等待数据接收一帧完整
{
timeout++;
// 计算
Calc();
}
// 重置
buffer_done = false;
if (Check()) {
// 更新数值
data = buffer[2] * 256 + buffer[3]; // 数据定义:高八位+低八位
// 容错:超过量程
if (data < 0 || data > 10000) {
data = lastData;
}
lastData = data;
}
}
// _serial_hcho.end();
}
}
struct _HCHO {
void init() {
_serial_hcho.begin(9600);
WZ_S::passiveMode();
}
void read() {
WZ_S::requestRead();
WZ_S::Read();
}
int getValue() {
return WZ_S::data;
}
char unit[6] = "ug\/m";
} HCHO;
}
通讯方式是 UART
WZ Library 使用更高级的 Stream 库,这里改成自己实现读写数据帧的方式,命令和读写参数是参考 WZ Library 和 文档所说的。
文档关键内容:
【数据帧信息】
停止位:可以理解成下一个数据帧的起始符,代码中的算法是当作起始符查找的。 数据帧长度是 1+8 = 9 位。
#define _Frame_Len_HCHO 9
【被动模式】
这里选择使用它的被动模式,也是文档称呼的“问答式”。 启动默认是主动模式,需要发送下面的指令切换。
void passiveMode() {
// ...
}
【请求读取】
被动模式下,需要发送下面的命令,要求它返回数据。
void Read() {
// ...
}
【读取解读】
这里选择单位是 ug/m3,所以应该提取2、3位的数据(高位、低位)。
void Calc() {
// ...
}
【校验】
根据文档算法进行校验。
void Check() {
// ...
}
小白思路: 发送、接收命令都是以 N个字节的数据帧通讯的(这里是 9 个)。 读取数值的基本步骤是:
- 通过串口 read() 方法,不停读取设备发送的字节数据。
- 找到起始符,开始组装数据帧。
- 组装完成后,进行校验。
- 校验成功,根据数据帧的2、3位按文档公式计算数值。
未完待续。。。
多多支持其它文章 juejin.cn/user/155656…
又或者请我喝杯奶茶😍 vue3-zoom-drag