Датчики углекислого газа CO2 – виды, преимущества и недостатки

Датчики углекислого газа CO2 – виды, преимущества и недостатки Анемометр

Далее следует…

На этом история не закончилась, а все только начиналось, потому что когда прибор перешел на автономное питание ему резко поплохело и история его лечения заслуживает отдельного топика, вот осциллограмма из истории болезни:

UPD:Схема подключения типичная для указанных датчиков, более детально о распиновке в скетче.

Рабочий скетч для Arduino UNO

Скетч собирался (копипастился) с разных кусков, так что сильно не ругайте, надеюсь разобраться в нем можно

#include <SFE_BMP180.h>
#include <Wire.h>
#include "DHT.h"
#define PIN_SCE 7 // LCD CS .... Pin 2
#define PIN_RESET 6 // LCD RST .... Pin 1
#define PIN_DC 5 // LCD Dat/Com. Pin 3
#define PIN_SDIN 4 // LCD SPIDat . Pin 4
#define PIN_SCLK 3 // LCD SPIClk . Pin 5 // LCD Gnd .... Pin 8 // LCD Vcc .... Pin 6 // LCD Vlcd ... Pin 7
#define LCD_C LOW
#define LCD_D HIGH
#define LCD_X 84
#define LCD_Y 48
#define LCD_CMD 0
SFE_BMP180 pressure;
#define DHTPIN 10
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
#define ALTITUDE 113.0 // Altitude of SparkFun's HQ in Boulder, CO. in meters
int a = 0;
int sensorPin = A0; // вход подключается к Aout модуля с датчиком СО2
int vBattPin = A1; // вход используется для мониторинга за состояние батареи
int sensorValue = 0; // variable to store the value coming from the sensor
float adc_step=5/1024.0f; //шаг измерения АЦП
float a_k=5.0894E-7; //магическая константа модели датчика СО2
float b_k=6.7303E-4;//еще одна магическая константа модели датчика СО2
float v_0=1.09; //напряжение на выходе датчика на свежем воздухе
int ledPin=13;
static const byte ASCII[][5] =
{ {0x00, 0x00, 0x00, 0x00, 0x00} // 20 ,{0xff, 0xff, 0xff, 0xff, 0xff} // 21 !
//,{0x00, 0x00, 0x5f, 0x00, 0x00} // 21 !
,{0x00, 0x07, 0x00, 0x07, 0x00} // 22 "
,{0x14, 0x7f, 0x14, 0x7f, 0x14} // 23 #
,{0x24, 0x2a, 0x7f, 0x2a, 0x12} // 24 $
,{0x23, 0x13, 0x08, 0x64, 0x62} // 25 %
,{0x36, 0x49, 0x55, 0x22, 0x50} // 26 &
,{0x00, 0x05, 0x03, 0x00, 0x00} // 27 '
,{0x00, 0x1c, 0x22, 0x41, 0x00} // 28 (
,{0x00, 0x41, 0x22, 0x1c, 0x00} // 29 )
,{0x14, 0x08, 0x3e, 0x08, 0x14} // 2a *
,{0x08, 0x08, 0x3e, 0x08, 0x08} // 2b
,{0x00, 0x50, 0x30, 0x00, 0x00} // 2c ,
,{0x08, 0x08, 0x08, 0x08, 0x08} // 2d -
,{0x00, 0x60, 0x60, 0x00, 0x00} // 2e .
,{0x20, 0x10, 0x08, 0x04, 0x02} // 2f /
,{0x3e, 0x51, 0x49, 0x45, 0x3e} // 30 0
,{0x00, 0x42, 0x7f, 0x40, 0x00} // 31 1
,{0x42, 0x61, 0x51, 0x49, 0x46} // 32 2
,{0x21, 0x41, 0x45, 0x4b, 0x31} // 33 3
,{0x18, 0x14, 0x12, 0x7f, 0x10} // 34 4
,{0x27, 0x45, 0x45, 0x45, 0x39} // 35 5
,{0x3c, 0x4a, 0x49, 0x49, 0x30} // 36 6
,{0x01, 0x71, 0x09, 0x05, 0x03} // 37 7
,{0x36, 0x49, 0x49, 0x49, 0x36} // 38 8
,{0x06, 0x49, 0x49, 0x29, 0x1e} // 39 9
,{0x00, 0x36, 0x36, 0x00, 0x00} // 3a :
,{0x00, 0x56, 0x36, 0x00, 0x00} // 3b ;
,{0x08, 0x14, 0x22, 0x41, 0x00} // 3c <
,{0x14, 0x14, 0x14, 0x14, 0x14} // 3d =
,{0x00, 0x41, 0x22, 0x14, 0x08} // 3e >
,{0x02, 0x01, 0x51, 0x09, 0x06} // 3f ?
,{0x32, 0x49, 0x79, 0x41, 0x3e} // 40 @
,{0x7e, 0x11, 0x11, 0x11, 0x7e} // 41 A
,{0x7f, 0x49, 0x49, 0x49, 0x36} // 42 B
,{0x3e, 0x41, 0x41, 0x41, 0x22} // 43 C
,{0x7f, 0x41, 0x41, 0x22, 0x1c} // 44 D
,{0x7f, 0x49, 0x49, 0x49, 0x41} // 45 E
,{0x7f, 0x09, 0x09, 0x09, 0x01} // 46 F
,{0x3e, 0x41, 0x49, 0x49, 0x7a} // 47 G
,{0x7f, 0x08, 0x08, 0x08, 0x7f} // 48 H
,{0x00, 0x41, 0x7f, 0x41, 0x00} // 49 I
,{0x20, 0x40, 0x41, 0x3f, 0x01} // 4a J
,{0x7f, 0x08, 0x14, 0x22, 0x41} // 4b K
,{0x7f, 0x40, 0x40, 0x40, 0x40} // 4c L
,{0x7f, 0x02, 0x0c, 0x02, 0x7f} // 4d M
,{0x7f, 0x04, 0x08, 0x10, 0x7f} // 4e N
,{0x3e, 0x41, 0x41, 0x41, 0x3e} // 4f O
,{0x7f, 0x09, 0x09, 0x09, 0x06} // 50 P
,{0x3e, 0x41, 0x51, 0x21, 0x5e} // 51 Q
,{0x7f, 0x09, 0x19, 0x29, 0x46} // 52 R
,{0x46, 0x49, 0x49, 0x49, 0x31} // 53 S
,{0x01, 0x01, 0x7f, 0x01, 0x01} // 54 T
,{0x3f, 0x40, 0x40, 0x40, 0x3f} // 55 U
,{0x1f, 0x20, 0x40, 0x20, 0x1f} // 56 V
,{0x3f, 0x40, 0x38, 0x40, 0x3f} // 57 W
,{0x63, 0x14, 0x08, 0x14, 0x63} // 58 X
,{0x07, 0x08, 0x70, 0x08, 0x07} // 59 Y
,{0x61, 0x51, 0x49, 0x45, 0x43} // 5a Z
,{0x00, 0x7f, 0x41, 0x41, 0x00} // 5b [
,{0x02, 0x04, 0x08, 0x10, 0x20} // 5c ¥
,{0x00, 0x41, 0x41, 0x7f, 0x00} // 5d ]
,{0x04, 0x02, 0x01, 0x02, 0x04} // 5e ^
,{0x40, 0x40, 0x40, 0x40, 0x40} // 5f _
,{0x00, 0x01, 0x02, 0x04, 0x00} // 60 `
,{0x20, 0x54, 0x54, 0x54, 0x78} // 61 a
,{0x7f, 0x48, 0x44, 0x44, 0x38} // 62 b
,{0x38, 0x44, 0x44, 0x44, 0x20} // 63 c
,{0x38, 0x44, 0x44, 0x48, 0x7f} // 64 d
,{0x38, 0x54, 0x54, 0x54, 0x18} // 65 e
,{0x08, 0x7e, 0x09, 0x01, 0x02} // 66 f
,{0x0c, 0x52, 0x52, 0x52, 0x3e} // 67 g
,{0x7f, 0x08, 0x04, 0x04, 0x78} // 68 h
,{0x00, 0x44, 0x7d, 0x40, 0x00} // 69 i
,{0x20, 0x40, 0x44, 0x3d, 0x00} // 6a j
,{0x7f, 0x10, 0x28, 0x44, 0x00} // 6b k
,{0x00, 0x41, 0x7f, 0x40, 0x00} // 6c l
,{0x7c, 0x04, 0x18, 0x04, 0x78} // 6d m
,{0x7c, 0x08, 0x04, 0x04, 0x78} // 6e n
,{0x38, 0x44, 0x44, 0x44, 0x38} // 6f o
,{0x7c, 0x14, 0x14, 0x14, 0x08} // 70 p
,{0x08, 0x14, 0x14, 0x18, 0x7c} // 71 q
,{0x7c, 0x08, 0x04, 0x04, 0x08} // 72 r
,{0x48, 0x54, 0x54, 0x54, 0x20} // 73 s
,{0x04, 0x3f, 0x44, 0x40, 0x20} // 74 t
,{0x3c, 0x40, 0x40, 0x20, 0x7c} // 75 u
,{0x1c, 0x20, 0x40, 0x20, 0x1c} // 76 v
,{0x3c, 0x40, 0x30, 0x40, 0x3c} // 77 w
,{0x44, 0x28, 0x10, 0x28, 0x44} // 78 x
,{0x0c, 0x50, 0x50, 0x50, 0x3c} // 79 y
,{0x44, 0x64, 0x54, 0x4c, 0x44} // 7a z
,{0x00, 0x08, 0x36, 0x41, 0x00} // 7b {
,{0x00, 0x00, 0x7f, 0x00, 0x00} // 7c |
,{0x00, 0x41, 0x36, 0x08, 0x00} // 7d }
,{0x10, 0x08, 0x08, 0x10, 0x08} // 7e ←
,{0x00, 0x06, 0x09, 0x09, 0x06} // 7f →
};
void LcdCharacter(char character)
{ LcdWrite(LCD_D, 0x00); for (int index = 0; index < 5; index ) { LcdWrite(LCD_D, ASCII[character - 0x20][index]); } LcdWrite(LCD_D, 0x00);
}
void LcdClear(void)
{ for (int index = 0; index < LCD_X * LCD_Y / 8; index ) { LcdWrite(LCD_D, 0x00); }
}
void LcdInitialise(void)
{ pinMode(PIN_SCE, OUTPUT); pinMode(PIN_RESET, OUTPUT); pinMode(PIN_DC, OUTPUT); pinMode(PIN_SDIN, OUTPUT); pinMode(PIN_SCLK, OUTPUT); digitalWrite(PIN_RESET, LOW); // delay(1); digitalWrite(PIN_RESET, HIGH); LcdWrite( LCD_CMD, 0x21 ); // LCD Extended Commands. LcdWrite( LCD_CMD, 0xBf ); // Set LCD Vop (Contrast). //B1 LcdWrite( LCD_CMD, 0x04 ); // Set Temp coefficent. //0x04 LcdWrite( LCD_CMD, 0x14 ); // LCD bias mode 1:48. //0x13 LcdWrite( LCD_CMD, 0x0C ); // LCD in normal mode. 0x0d for inverse LcdWrite(LCD_C, 0x20); LcdWrite(LCD_C, 0x0C);
}
void LcdString(char *characters)
{ while (*characters) { LcdCharacter(*characters ); }
}
void LcdWrite(byte dc, byte data)
{ digitalWrite(PIN_DC, dc); digitalWrite(PIN_SCE, LOW); shiftOut(PIN_SDIN, PIN_SCLK, MSBFIRST, data); digitalWrite(PIN_SCE, HIGH);
}
// gotoXY routine to position cursor
// x - range: 0 to 84
// y - range: 0 to 5
void gotoXY(int x, int y)
{ LcdWrite( 0, 0x80 | x); // Column. LcdWrite( 0, 0x40 | y); // Row.
}
char * floatToString(char * outstr, double val, byte precision, byte widthp){ char temp[16]; byte i; // compute the rounding factor and fractional multiplier double roundingFactor = 0.5; unsigned long mult = 1; for (i = 0; i < precision; i ) { roundingFactor /= 10.0; mult *= 10; } temp[0]=''; outstr[0]=''; if(val < 0.0){ strcpy(outstr,"-"); val = -val; } val = roundingFactor; strcat(outstr, itoa(int(val),temp,10)); //prints the int part if( precision > 0) { strcat(outstr, "."); // print the decimal point unsigned long frac; unsigned long mult = 1; byte padding = precision -1; while(precision--) mult *=10; if(val >= 0) frac = (val - int(val)) * mult; else frac = (int(val)- val ) * mult; unsigned long frac1 = frac; while(frac1 /= 10) padding--; while(padding--) strcat(outstr,"0"); strcat(outstr,itoa(frac,temp,10)); } // generate space padding if ((widthp != 0)&&(widthp >= strlen(outstr))){ byte J=0; J = widthp - strlen(outstr); for (i=0; i< J; i ) { temp[i] = ' '; } temp[i ] = ''; strcat(temp,outstr); strcpy(outstr,temp); } return outstr;
}
void drawLine(void)
{ unsigned char j; for(j=0; j<84; j ) // top	{ gotoXY (j,0); LcdWrite (1,0x01); } for(j=0; j<84; j ) //Bottom	{ gotoXY (j,5); LcdWrite (1,0x80); } for(j=0; j<6; j ) // Right	{ gotoXY (83,j); LcdWrite (1,0xff); }	for(j=0; j<6; j ) // Left	{ gotoXY (0,j); LcdWrite (1,0xff); }
}
float VoltageToPPM(float voltage)
{ return pow(b_k, voltage)/a_k;
}
void setup(void)
{
//analogReference(INTERNAL); LcdInitialise(); LcdClear(); gotoXY(0,0); if (pressure.begin()) LcdString("BMP180 init success"); else { // Oops, something went wrong, this is usually a connection problem, // see the comments at the top of this sketch for the proper connections. LcdString("BMP180 init failnn"); while(1); // Pause forever. } dht.begin();
}
void loop(void)
{ sensorValue = analogRead(sensorPin); delay(50); sensorValue = analogRead(sensorPin); delay(50); char buf[20]; float value=sensorValue*adc_step; gotoXY(0,0); LcdString("CO2:"); LcdString(floatToString(buf, VoltageToPPM(value),1,5)); LcdString("ppm"); gotoXY(0,1); LcdString("CO2 V:"); LcdString(floatToString(buf, value,4,5)); gotoXY(0,2); delay(50); sensorValue = analogRead(vBattPin); delay(50); sensorValue = analogRead(vBattPin); value=sensorValue*adc_step; LcdString("V batt:"); LcdString(floatToString(buf, value,3,4));
// static bool led_on_off=true;
// digitalWrite(ledPin, led_on_off);
// led_on_off=!led_on_off; char status; double T,P,p0,a; // Loop here getting pressure readings every 10 seconds. // If you want sea-level-compensated pressure, as used in weather reports, // you will need to know the altitude at which your measurements are taken. // We're using a constant called ALTITUDE in this sketch: // If you want to measure altitude, and not pressure, you will instead need // to provide a known baseline pressure. This is shown at the end of the sketch. // You must first get a temperature measurement to perform a pressure reading. // Start a temperature measurement: // If request is successful, the number of ms to wait is returned. // If request is unsuccessful, 0 is returned. status = pressure.startTemperature(); if (status != 0) { // Wait for the measurement to complete: delay(status); // Retrieve the completed temperature measurement: // Note that the measurement is stored in the variable T. // Function returns 1 if successful, 0 if failure. status = pressure.getTemperature(T); if (status != 0) { // Print out the measurement: //gotoXY(0,3); // LcdString("temp: "); //LcdString(floatToString(buf, T,1,4)); // LcdString(" C"); // Start a pressure measurement: // The parameter is the oversampling setting, from 0 to 3 (highest res, longest wait). // If request is successful, the number of ms to wait is returned. // If request is unsuccessful, 0 is returned. status = pressure.startPressure(2); if (status != 0) { // Wait for the measurement to complete: delay(status); // Retrieve the completed pressure measurement: // Note that the measurement is stored in the variable P. // Note also that the function requires the previous temperature measurement (T). // (If temperature is stable, you can do one temperature measurement for a number of pressure measurements.) // Function returns 1 if successful, 0 if failure. status = pressure.getPressure(P,T); if (status != 0) { // Print out the measurement: gotoXY(0,5); //lcd.print("ap "); LcdString(floatToString(buf, P*0.7501,1,4)); //lcd.print(" mb"); // The pressure sensor returns abolute pressure, which varies with altitude. // To remove the effects of altitude, use the sealevel function and your current altitude. // This number is commonly used in weather reports. // Parameters: P = absolute pressure in mb, ALTITUDE = current altitude in m. // Result: p0 = sea-level compensated pressure in mb p0 = pressure.sealevel(P,ALTITUDE); // we're at 1655 meters (Boulder, CO) LcdString("-"); LcdString(floatToString(buf, p0*0.7501,1,4)); //Serial.print(" mb, "); //Serial.print(p0*0.0295333727,2); //Serial.println(" inHg"); // On the other hand, if you want to determine your altitude from the pressure reading, // use the altitude function along with a baseline pressure (sea-level or other). // Parameters: P = absolute pressure in mb, p0 = baseline pressure in mb. // Result: a = altitude in m. a = pressure.altitude(P,p0); } else LcdString("error retrieving pressure measurementn"); } else LcdString("error starting pressure measurementn"); } else LcdString("error retrieving temperature measurementn"); } else LcdString("error starting temperature measurementn"); float h = dht.readHumidity(); float t = dht.readTemperature(); gotoXY(0,4); if (isnan(t) || isnan(h)) { LcdString("Failed to read from DHT"); } else{ LcdString(floatToString(buf, h,1,4)); LcdString("%"); } gotoXY(0,3); LcdString("temp: "); LcdString(floatToString(buf, t,1,4)); LcdString(" C"); delay(1000);
}

Про анемометры:  Датчики давления

Четыре датчика co2 в одном устройстве: сверяем показания.

1. MH-Z14A — самый заслуженный, но вполне рабочий датчик. Отличается крупным размером.

2. MH-Z19 — «народный» датчик, бюджетный вариант.

3. MH-Z19В — Это исправленная и дополненная версия MH-Z19.

4. SenseAir S8 — самый дорогой и самый авторитетный.

Я уже делал обзор трех из четырёх сегодняшних датчиков. Там можно найти их технические характеристики. Все они более-менее одинаковы. Так что сегодня остановимся на невошедшим в тот отбор MH-Z19B.

Поставляется я антистатическом пакете:
Датчики углекислого газа CO2 – виды, преимущества и недостатки

Внутри сам датчик, ножки уже припаяны:
Датчики углекислого газа CO2 – виды, преимущества и недостатки

Корпус датчика такой же, как и у MH-Z19, но платы немного отличаются:
Датчики углекислого газа CO2 – виды, преимущества и недостатки

Наклейка сбоку позволяет их не перепутать:

Датчики углекислого газа CO2 – виды, преимущества и недостатки

Мое устройство выглядит как новогодняя елка:

Датчики углекислого газа CO2 – виды, преимущества и недостатки

В прошлом обзоре его уже довольно обсмеяли, так что я не буду на нем останавливаться подробно.Там просто ардуина, экран, часы, четыре датчика, логгер, записывающий на флешку и блютус адаптер.

Если кому интересен код — вот код:

при нажатии выпадет несколько экранов

/*
© Tykhon, 2022.
MH-Z19
TX: 3<->5 A0
RX: 3<->5 A1
Gnd: Gnd
Vin: 5v
MH-Z19b
TX: 3<->5 4
RX: 3<->5 A1
Gnd: Gnd
Vin: 5v
MH-Z14A
1_ 5v: 5v
2_Gnd: Gnd
11_RX: 3<->5 7
10_TX: 3<->5 6
S8:
G : 5v
G0: Gnd
Rx: 3<->5 A3
Tx: 3<->5 A2
DS3231
SCL: A5
SDA: A4
Gnd: Gnd
VDD: 5v
TFT
Led: -> 150 Ohm -> 5v
SCK: -> 1 KOhm -> 13
SDA: -> 1 KOhm -> 11
A0: -> 1 KOhm -> 8
Reset: -> 1 KOhm -> 9
CS: -> 1 KOhm -> 10
Gnd: Gnd
Vcc: 5v
*/
#include "Adafruit_GFX.h" // for LCD Core graphics library
#include <Adafruit_ST7735.h> // for LCD Hardware-specific library
#include <SoftwareSerial.h>
#include "Wire.h"
/////////////////////// for LCD //////////////////////////////
#define TFT_CS 10
#define TFT_RST 9 // you can also connect this to the Arduino reset in which case, set this #define pin to -1!
#define TFT_DC 8
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST); //for LCD
#define DS1307_ADDR 0x68 // RTC address
int s8_co2;
int z14_co2;
int z19_co2;
int z19b_co2;
int z19_t;
int z19_ss;
int z19b_t;
int z19b_ss;
int s8_co2_last;
int z14_co2_last;
int z19_co2_last;
int z19b_co2_last;
int s8_co2_mean_last;
int z14_co2_mean_last;
int z19_co2_mean_last;
int z19b_co2_mean_last;
int s8_co2_mean;
int z14_co2_mean;
int z19_co2_mean;
int z19b_co2_mean;
int s8_co2_mean2;
int z14_co2_mean2;
int z19_co2_mean2;
int z19b_co2_mean2;
SoftwareSerial mySerial_z19(A0, A1);
SoftwareSerial mySerial_z19b(4, 5);
SoftwareSerial mySerial_s8(A2, A3);
SoftwareSerial mySerial_z14(7, 6);
byte cmd_z14[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
unsigned char response_z14[9];
String ppmString = " ";
byte cmd_z19[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
byte cmd_s8[7] = {0xFE,0x44,0x00,0x08,0x02,0x9F,0x25};
unsigned char response_z19[9];
unsigned char response_z19b[9];
byte response_s8[] = {0,0,0,0,0,0,0};
unsigned int current_minute, lastminute;
int second_delay = 30000; // delay between measurements, ms
int work_period = 2; // period for recording data, min
float smoothing_factor = 0.5; // coefficient for Kalman filter
float smoothing_factor2 = 0.15; // coefficient for Kalman filter
//////////////////////////////////////// functions //////////////////////////////////////////////
byte bcdToDec(byte val) { return ( (val/16*10) (val) );
}
String getdate(){ Wire.beginTransmission(DS1307_ADDR); // Reset the register pointer byte zero = 0x00; Wire.write(zero); Wire.endTransmission(); Wire.requestFrom(DS1307_ADDR, 7); int secondint = bcdToDec(Wire.read()); int minuteint = bcdToDec(Wire.read()); current_minute = minuteint; int hour = bcdToDec(Wire.read() & 0b111111); //24 hour time int weekDay = bcdToDec(Wire.read()); //0-6 -> sunday - Saturday int monthDay = bcdToDec(Wire.read()); int month = bcdToDec(Wire.read()); int year = bcdToDec(Wire.read()); String second = String(secondint); if (secondint < 10) {second ="0" second;}; String minute = String(minuteint); if (minuteint < 10) {minute ="0" minute;}; return String(hour) ":" minute ":" second " " String(monthDay) "/" String(month) "/" String(year);
}
void sendRequest_s8(byte packet[]){ mySerial_s8.begin(9600); while(!mySerial_s8.available()){ //keep sending request until we start to get a response mySerial_s8.write(cmd_s8,7); delay(50); } int timeout=0; //set a timeout counter while(mySerial_s8.available() < 7 ) { //Wait to get a 7 byte response timeout ; if(timeout > 10) { //if it takes to long there was probably an error while(mySerial_s8.available()) //flush whatever we have mySerial_s8.read(); break; //exit and try again } delay(50); } for (int i=0; i < 7; i ) { response_s8[i] = mySerial_s8.read(); } mySerial_s8.end();
}
unsigned long getValue_s8(byte packet[])
{ int high = packet[3]; //high byte for value is 4th byte in packet in the packet int low = packet[4]; //low byte for value is 5th byte in the packet unsigned long val = high*256 low; //Combine high byte and low byte with this formula to get value return val;
}
void sendRequest_z14(){ mySerial_z14.begin(9600); mySerial_z14.write(cmd_z14, 9); memset(response_z14, 0, 9); mySerial_z14.readBytes(response_z14, 9); int i; byte crc = 0; for (i = 1; i < 8; i ) crc =response_z14[i]; crc = 255 - crc; crc ; if ( !(response_z14[0] == 0xFF && response_z14[1] == 0x86 && response_z14[8] == crc) ) { Serial.println("CRC error z14: " String(crc) " / " String(response_z14[8])); } else { unsigned int responseHigh = (unsigned int) response_z14[2]; unsigned int responseLow = (unsigned int) response_z14[3]; z14_co2 = (256*responseHigh) responseLow; }; mySerial_z14.end();
}
void sendRequest_z19(){ mySerial_z19.begin(9600); mySerial_z19.write(cmd_z19, 9); memset(response_z19, 0, 9); mySerial_z19.readBytes(response_z19, 9); int i; byte crc = 0; for (i = 1; i < 8; i ) crc =response_z19[i]; crc = 255 - crc; crc ; if (response_z19[0] != 0xFF) {Serial.println("CRC error z19: response_z19[0] != 0xFF: " String(response_z19[0]));}; if (response_z19[1] != 0x86) {Serial.println("CRC error z19: response_z19[1] != 0x86: " String(response_z19[1]));}; if (response_z19[8] != crc) {Serial.println("CRC error z19: response_z19[8] != crc: " String(response_z19[8]) " / " String(crc));}; if ( !(response_z19[0] == 0xFF && response_z19[1] == 0x86 && response_z19[8] == crc) ) { Serial.println("CRC error z19: " String(crc) " / " String(response_z19[8])); } else { unsigned int responseHigh = (unsigned int) response_z19[2]; unsigned int responseLow = (unsigned int) response_z19[3]; unsigned int responseTT = (unsigned int) response_z19[4]; unsigned int responseSS = (unsigned int) response_z19[5]; z19_t = responseTT-40; z19_ss = responseSS; z19_co2 = (256*responseHigh) responseLow; }; mySerial_z19.end();
}
void sendRequest_z19b(){ mySerial_z19b.begin(9600); mySerial_z19b.write(cmd_z19, 9); memset(response_z19b, 0, 9); mySerial_z19b.readBytes(response_z19b, 9); int i; byte crc = 0; for (i = 1; i < 8; i ) crc =response_z19b[i]; crc = 255 - crc; crc ; if (response_z19b[0] != 0xFF) {Serial.println("CRC error z19b: response_z19b[0] != 0xFF: " String(response_z19b[0]));}; if (response_z19b[1] != 0x86) {Serial.println("CRC error z19b: response_z19b[1] != 0x86: " String(response_z19b[1]));}; if (response_z19b[8] != crc) {Serial.println("CRC error z19b: response_z19b[8] != crc: " String(response_z19b[8]) " / " String(crc));}; if ( !(response_z19b[0] == 0xFF && response_z19b[1] == 0x86 && response_z19b[8] == crc) ) { Serial.println("CRC error z19b: " String(crc) " / " String(response_z19b[8])); } else { unsigned int responseHigh = (unsigned int) response_z19b[2]; unsigned int responseLow = (unsigned int) response_z19b[3]; unsigned int responseTT = (unsigned int) response_z19b[4]; unsigned int responseSS = (unsigned int) response_z19b[5]; z19b_t = responseTT-40; z19b_ss = responseSS; z19b_co2 = (256*responseHigh) responseLow; }; mySerial_z19b.end();
}
void tft_form(){ tft.setTextSize(2); tft.setTextColor(0x177F); tft.setCursor(2, 4); tft.print("z14"); tft.setCursor(2, 33); tft.print("z19"); tft.setCursor(2, 62); tft.print("z19b"); tft.setCursor(2, 91); tft.print("s8");
}
void tft_output(){ tft.setTextSize(2); tft.setCursor(56, 4); tft.setTextColor(ST7735_BLACK); tft.print(String(z14_co2_mean_last)); tft.setCursor(56, 4); tft.setTextColor(ST7735_WHITE); tft.print(String(z14_co2_mean)); z14_co2_mean_last = z14_co2_mean; tft.setCursor(56, 33); tft.setTextColor(ST7735_BLACK); tft.print(String(z19_co2_mean_last)); tft.setCursor(56, 33); tft.setTextColor(ST7735_WHITE); tft.print(String(z19_co2_mean)); z19_co2_mean_last = z19_co2_mean; tft.setCursor(56, 62); tft.setTextColor(ST7735_BLACK); tft.print(String(z19b_co2_mean_last)); tft.setCursor(56, 62); tft.setTextColor(ST7735_WHITE); tft.print(String(z19b_co2_mean)); z19b_co2_mean_last = z19b_co2_mean; tft.setCursor(56, 91); tft.setTextColor(ST7735_BLACK); tft.print(String(s8_co2_mean_last)); tft.setCursor(56, 91); tft.setTextColor(ST7735_WHITE); tft.print(String(s8_co2_mean)); s8_co2_mean_last = s8_co2_mean; tft.setTextSize(1); tft.setCursor(110, 4); tft.setTextColor(ST7735_BLACK); tft.print(String(z14_co2_last)); tft.setCursor(110, 4); tft.setTextColor(ST7735_WHITE); tft.print(String(z14_co2)); z14_co2_last = z14_co2; tft.setCursor(110, 33); tft.setTextColor(ST7735_BLACK); tft.print(String(z19_co2_last)); tft.setCursor(110, 33); tft.setTextColor(ST7735_WHITE); tft.print(String(z19_co2)); z19_co2_last = z19_co2; tft.setCursor(110, 62); tft.setTextColor(ST7735_BLACK); tft.print(String(z19b_co2_last)); tft.setCursor(110, 62); tft.setTextColor(ST7735_WHITE); tft.print(String(z19b_co2)); z19b_co2_last = z19b_co2; tft.setCursor(110, 91); tft.setTextColor(ST7735_BLACK); tft.print(String(s8_co2_last)); tft.setCursor(110, 91); tft.setTextColor(ST7735_WHITE); tft.print(String(s8_co2)); s8_co2_last = s8_co2; tft.setTextSize(1); tft.fillRect(0, 117, 160, 120, ST7735_BLACK); tft.setCursor(0, 117); tft.print(String(getdate()));
}
void z19setup() {
/* 2 3 6 7 {0x86,0x00,0x00,0x00}, // mhz_cmnd_read_ppm {0x79,0xA0,0x00,0x00}, // mhz_cmnd_abc_enable {0x79,0x00,0x00,0x00}, // mhz_cmnd_abc_disable {0x87,0x00,0x00,0x00}, // mhz_cmnd_zeropoint {0x8D,0x00,0x00,0x00}, // mhz_cmnd_reset {0x99,0x00,0x03,0xE8}, // mhz_cmnd_set_range_1000 {0x99,0x00,0x07,0xD0}, // mhz_cmnd_set_range_2000 {0x99,0x00,0x0B,0xB8}, // mhz_cmnd_set_range_3000 {0x99,0x00,0x13,0x88}}; // mhz_cmnd_set_range_5000
*/
// byte setrangeA_cmd[9] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x07, 0xD0, 0x8F}; // set range 0 - 2000ppm byte setrangeA_cmd[9] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88, 0xCB}; // set range 0 - 5000ppm
// byte setrangeA_cmd[9] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86}, // mhz_cmnd_abc_disable
// byte setrangeA_cmd[9] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6}; // mhz_cmnd_abc_enable
// byte setrangeA_cmd[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}; // data request unsigned char setrangeA_response[9]; mySerial_z19.begin(9600); mySerial_z19.write(setrangeA_cmd,9); mySerial_z19.readBytes(setrangeA_response, 9); int setrangeA_i; byte setrangeA_crc = 0; for (setrangeA_i = 1; setrangeA_i < 8; setrangeA_i ) setrangeA_crc =setrangeA_response[setrangeA_i]; setrangeA_crc = 255 - setrangeA_crc; setrangeA_crc = 1; if ( !(setrangeA_response[0] == 0xFF && setrangeA_response[1] == 0x99 && setrangeA_response[8] == setrangeA_crc) ) { Serial.println("Range CRC error: " String(setrangeA_crc) " / " String(setrangeA_response[8]) " (bytes 6 and 7)"); } else { Serial.println("Range was set! (bytes 6 and 7)"); } delay(1000); mySerial_z19.end();
}
/////////////////////////////////////// setup /////////////////////////////////////////
void setup(void) { Serial.begin(38400); Wire.begin();
// z19setup(); tft.initR(INITR_BLACKTAB); // initialize a ST7735S chip, black tab Serial.println("init"); tft.setTextWrap(false); // Allow text to run off right edge tft.setRotation(3); tft.fillScreen(ST7735_BLACK); tft_form();
}
//////////////////////////////////////////////// loop /////////////////////////////////////////////////////
void loop() { sendRequest_z14(); sendRequest_z19(); sendRequest_z19b(); sendRequest_s8(cmd_s8); s8_co2 = getValue_s8(response_s8); if (!s8_co2_mean) s8_co2_mean = s8_co2; if (!z14_co2_mean) z14_co2_mean = z14_co2; if (!z19_co2_mean) z19_co2_mean = z19_co2; if (!z19b_co2_mean) z19b_co2_mean = z19b_co2; s8_co2_mean = s8_co2_mean - smoothing_factor*(s8_co2_mean - s8_co2); z14_co2_mean = z14_co2_mean - smoothing_factor*(z14_co2_mean - z14_co2); z19_co2_mean = z19_co2_mean - smoothing_factor*(z19_co2_mean - z19_co2); z19b_co2_mean = z19b_co2_mean - smoothing_factor*(z19b_co2_mean - z19b_co2); if (!s8_co2_mean2) s8_co2_mean2 = s8_co2; if (!z14_co2_mean2) z14_co2_mean2 = z14_co2; if (!z19_co2_mean2) z19_co2_mean2 = z19_co2; if (!z19b_co2_mean2) z19b_co2_mean2 = z19b_co2; s8_co2_mean2 = s8_co2_mean2 - smoothing_factor2*(s8_co2_mean2 - s8_co2); z14_co2_mean2 = z14_co2_mean2 - smoothing_factor2*(z14_co2_mean2 - z14_co2); z19_co2_mean2 = z19_co2_mean2 - smoothing_factor2*(z19_co2_mean2 - z19_co2); z19b_co2_mean2 = z19b_co2_mean2 - smoothing_factor2*(z19b_co2_mean2 - z19b_co2); getdate(); tft_output();
// if (((current_minute)%work_period == 0)&&(lastminute != current_minute)) {
String dataString = getdate() " s8~~: " String(s8_co2_mean2) " s8~: " String(s8_co2_mean) " s8: " String(s8_co2) " z14~~: " String(z14_co2_mean2) " z14~: " String(z14_co2_mean) " z14: " String(z14_co2) " z19~~: " String(z19_co2_mean2) " z19~: " String(z19_co2_mean) " z19: " String(z19_co2) " z19b~~: " String(z19b_co2_mean2) " z19b~: " String(z19b_co2_mean) " z19b: " String(z19b_co2);
// " z19_t: " String(z19_t) " z19b_t: " String(z19b_t); dataString.replace(".",","); dataString.replace(" ","t"); Serial.println(dataString); lastminute = current_minute;
// }; delay(second_delay);
}

Что же я увидел как только подключил датчик и показания прибора устаканились? Датчик завышал уровень CO2 примерно на 200-250 ppm. Это много, этого нельзя не заметить. Я выставил свою елку на улицу и она там простояла ночь.

Датчики углекислого газа CO2 – виды, преимущества и недостатки

На улице должно быть порядка 400 ppm, а никак не 650.

И я решил откалибровать датчик. В этом одно из главных преимуществ этой модели датчика относительно Z19 без «B»: она калибруется не по 0ppm, а по 400ppm. Т.е. вместо азотной камеры этот датчик для калибровки достаточно просто вынести на улицу.

Вообще, в датчике предусмотрена автоматическая калибровка. Время от времени датчик смотрит на свои показания за несколько прошедших дней и находит в них минимальное значение. Исходя из презумпции, что периодически помещение с датчиком проветривается и там оказывается 400 ppm CO2, датчик меняет свои внутренние коэффициенты с тем, чтобы отснятый минимум соответствовал как раз этому уровню. Так что самый простой вариант — подождать, пока калибровка произойдет автоматически.

Второй вариант — использовать специальную команду. Подать ее в порт и все произойдет.
Третий вариант — замкнуть один из выводов датчика на землю и продержать 7 секунд. Именно такой способ я и выбрал.

Показания сразу стали равны 400 ppm и вскоре я внес датчик домой.

Датчики углекислого газа CO2 – виды, преимущества и недостатки

Как говорится, «взяли вилку». Был перелет, теперь недолет. На те же 250 ppm теперь меньше. Ну что ж, придется ждать, пока сработает автокорректировка.
А пока посмотрим на совпадение показаний других датчиков. MH-Z14A все больше завышает. MH-Z19 путается в показаниях и график выглядит как кардиограмма инфарктника. Но она все-таки больше совпадает с S8, нежели график MH-Z14A. В прошлом тесте все было наоборот. Так что некоторый дрейф присутствует у всех.
Для сглаживания локальных всплесков и провалов снятых значений я применил фильтр Кальмана. Это простой и эффективный способ отбросить лишнее и сконцентрироваться на важном в показаниях приборов. Для настройки фильтра требуется определить его коэффициент, для чего я добавил в выводимую программой таблицу несколько колонок, а потом построил по ним графики.

Датчики углекислого газа CO2 – виды, преимущества и недостатки

Датчики углекислого газа CO2 – виды, преимущества и недостатки

Как по мне, 0,15 — отличный выбор.Если прибор нужен в основном для принятия решений уровня «открывать форточку или нет» — то такая точность вполне достаточна, а визуально такой график воспринимается легче.

Еще я захотел сравнить показания встроенных в датчики термометров.

Датчики углекислого газа CO2 – виды, преимущества и недостатки

Нельзя однозначно сказать, что рассматриваемый датчик завышает температуру. Термометр — недокументированная фича, и используется он только для выстраивания внутренних коэффициентов датчика. Так что он измеряет свою температуру, а не окружающую.

Пожалуй, надо проверить работу ШИМ- выхода датчика. В некоторых случаях неудобно опрашивать датчик, посылая байты в компорт. Тогда есть альтернатива: на pwm выходе присутствует прямоугольный сигнал с частотой порядка 1 Гц и со скважностью, пропорциональной концентрации СО2. Его довольно легко анализировать и такой способ весьма защищен от помех.

Датчики углекислого газа CO2 – виды, преимущества и недостатки

Работает. Впрочем, pwm выход есть и на старой версии этого датчика.

А вот аналоговый выход есть только на «B». Напряжение на нем пропорционально концентрации углекислого газа. Проверим:

Датчики углекислого газа CO2 – виды, преимущества и недостатки

Тоже работает. Такой выход позволяет мастерить совсем простые устройства. Буквально из транзистора, пары резисторов и светодиода можно собрать устройство уровня «загорелся красный светодиод — открой окно». Так что это очень полезное новшество.

Конечно, все подробности отличий MH-Z19B и MH-Z19 можно изучить по pdf.Мои же впечатления такие:
Калибровка по 400 ppm — штука работающая и полезная. Датчик выдает более гладкий сигнал, чем его собрат. Вероятно, внутри показания фильтруются. Датчик при запуске не выдает в порт техническую информацию о себе — предел измерений, точность, что-то еще. Так пожалуй лучше, а то если устройство, которое принимает сигнал, настроено что-то там запускать при превышении СО2, то при приходе «2000» может сработать запуск, а датчик просто информировал о своем пределе измерений.
Что еще сказать. Отслеживается зависимость показаний от качества питания. У MH-Z19B — в меньшей степени, у MH-Z19 — в чуть большей. Но больше всего дуреет S8 при недостаточном питании, тому вообще крышу сносит.

Пока я все это изучал, кажется сработала автокалибровка.

Датчики углекислого газа CO2 – виды, преимущества и недостатки

Причем сразу у двух датчиков: MH-Z14А, который завышал, стал слегка занижать. А MH-Z19B, который занижал, стал завышать.

В этом датчике калибровку можно определить по обнулению 6-го байта в ответе датчика на запрос данных. В первый час после запуска датчика у него нулевое значение, потом оно постепенно возрастает и за 24 часа дрходит до 142. На этой отметке происходит автокалибровка, байт сбрасывается на 0 и все начинается сначала. Выглядит это так:

Датчики углекислого газа CO2 – виды, преимущества и недостатки

Со временем они наверняка придут к почти совпадающим показаниям. У меня так уже было с тремя датчиками из прошлого обзора.

Датчики углекислого газа CO2 – виды, преимущества и недостатки

За неделю тогда они практически спелись в унисон.

Итог: все датчики работают. Все показывают более-менее адекватные цифры. Выбирать я бы стал по удобству применения в конкретном приборе.

Товар предоставлен для написания обзора магазином. Обзор опубликован в соответствии с п.18 Правил сайта.

Оцените статью
Анемометры