Датчики углекислого газа 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. В прошлом тесте все было наоборот. Так что некоторый дрейф присутствует у всех.
Для сглаживания локальных всплесков и провалов снятых значений я применил фильтр Кальмана. Это простой и эффективный способ отбросить лишнее и сконцентрироваться на важном в показаниях приборов. Для настройки фильтра требуется определить его коэффициент, для чего я добавил в выводимую программой таблицу несколько колонок, а потом построил по ним графики.

Про анемометры:  ДЗ-1-СН4 сигнализатор (детектор) загазованности метана

Датчики углекислого газа 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 Правил сайта.

Оцените статью
Анемометры
Добавить комментарий