Датчик углекислого газа в воздухе (CO2) MH-Z19B — головоломка | mysku

Датчик углекислого газа в воздухе (CO2) MH-Z19B — головоломка

Перейти в магазин

Чтобы окончательно решить исход вековечной вражды в офисе между «теми, кому дует» и «теми, кому душно» решил разориться на бюджетный датчик содержания CO2 в атмосферном воздухе и прикрутить к нему сирену. Поскольку цена на такие датчики — совсем негуманная, выбрал вариант «MH-Z19B», который оказался самым бюджетным.

К сожалению, датчик подарил головоломку. Подробности — под катом.

(Внимание — в обзоре много «программизма», ругани в адрес китайских даташитов, присутствует шестнадцатеричная математика — так что если тема DIY вам не близка, проходите мимо, иначе будете разочарованы).

Датчик «MH-Z19B» сделан китайской компанией «Winsen» (даташит, PDF) и неоднократно упоминался на Habrahabr и Geektimes. Это вторая ревизия, с буквой «b» в названии, по результатам китайской «работы над ошибками». Первую ревизию одним неверным движением можно было ввести в режим калибровки, для которой требовалась атмосфера с нулевым содержанием CO2. Бедолагам, которые в это влетали, приходилось искать баллон с чистым азотом или кислородом, чтобы её организовать. В ревизии «b» китайцы сделали так, что модуль «эталонной» считает смесь с 400ppm углекислоты — то есть его можно, теоретически, перекалибровать просто в лесу или в парке.

Модуль пришёл в паре с небольшим шлейфом, основная плата имеет надпил, позволяющий отломать кусок с разъёмом и вместо этого припаять гребёнки. Модуль работает в двух режимах — UART (передавая показания по последовательному порту на 9600 бод) и PWM, контакты слева и справа, соответственно:

Для удобства я обрезал шлейф и насадил на провода дюпоновские наконечники. Правда, выяснилось, что часть проводов вообще ни к чему не подключена. Напротив, PWM оказался не выведен на шлейф, к контакту пришлось дополнительно подпаиваться, завернув из предосторожности датчик в плёнку поплотнее:

Датчик работает по следующему принципу — он получает по UART девятибайтовые команды (последний байт — CRC) и отвечает также девятибайтовыми пакетами. Замер концентрации СО2 выполняется командой с байтом 0x86:

Также показания можно прочитать, померяв ширину PWM-сигнала:

Тут подстерегала первая проблема — как узнать текущую размерность измерений? Даташит упоминает, что датчик может мерять в диапазаонах от нуля как до 2000, так и от нуля до 5000ppm:

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

Почему-то ни одна инструкция в интернете этого вопроса не касается — никто толком не интересовался настройками датчика и просто принимают их как данность. Кряхтим и пишем код для Ардуино, который пошлёт модулю нужную команду:

  • 2000 ppm: «2000» в десятичной системе это «0x07 d0» в шестнадцатеричной, получается — третий байт команды будет 0x07, четвёртый байт — 0xD0, CRC (девятый байт) 0x8F
  • 5000 ppm: третий байт 0x13, четвёртый байт 0x88, CRC (девятый байт) 0xCB

(CRC вычисляем по формуле из того же даташита, (NOT(Byte1 + Byte2 + Byte3 + Byte4 + Byte5 + Byte6 + Byte7)) +1)

Пробуем и так, и эдак, но в результате получаем на выходе дикие неправдоподобные значения концентрации СО2, вылетающие за паспортные диапазоны показаний датчика в разы. Уже испугавшись, что запороли дорогой прибор, через какое-то время нагугливаем ссылку revspace.nl/MHZ19 со словами

According to the MH-Z19B datasheet, you can configure the measurement range by putting the desired range in byte 3 and 4. However, unlike what the MH-Z19B datasheet says, you can set the range using the following command (in this case 0x07d0 = 2000 ppm in byte 6 and 7)

Замечательные китайцы ухитрились ошибиться в даташите! Материмся, и вместо третьего и четвёртого байта пишем в шестой и седьмой. Благодаря подсказке неизвестного голландца — модуль воскресает.

Поскольку теперь доверия модулю нет никакого, решил разобраться с его показаниями досконально и сравнить результаты по UART и PWM. Пишем код под Ардуино, который сначала, в блоке setup, даёт команду установки размерности, а потом в цикле loop делает замеры. Модуль располагаем на сквозняке у форточки.

Код (финальный, уже включающий все позднейшие *открытия*

#include <SoftwareSerial.h>
#define pwmPin 10
SoftwareSerial swSerial(A0, A1); // RX, TX

void setup() {
Serial.begin(9600);
swSerial.begin(9600);
pinMode(pwmPin, INPUT);

/*
Источник - https://revspace.nl/MHZ19
2000 ppm range: 0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x07, 0xD0, 0x8F
5000 ppm range: 0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88, 0xCB
*/

// Этот вариант ("A") с записью команды в 6й и 7й байт - работает
//           bytes:                         3     4           6     7
byte setrangeA_cmd[9] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88, 0xCB}; // задаёт диапазон 0 - 5000ppm
unsigned char setrangeA_response[9];
swSerial.write(setrangeA_cmd,9);
swSerial.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);

/*
// Этот вариант ("B") с записью команды в 3й и 4й байт, согласно даташиту - НЕ работает и поэтому закомментирован
//           bytes:                         3     4           6     7
byte setrangeB_cmd[9] = {0xFF, 0x01, 0x99, 0x00, 0x00, 0x00, 0x13, 0x88, 0xCB};
unsigned char setrangeB_response[9];
swSerial.write(setrangeB_cmd,9);
swSerial.readBytes(setrangeB_response, 9);
int setrangeB_i;
byte setrangeB_crc = 0;
for (setrangeB_i = 1; setrangeB_i < 8; setrangeB_i++) setrangeB_crc+=setrangeB_response[setrangeB_i];
setrangeB_crc = 255 - setrangeB_crc;
setrangeB_crc += 1;
if ( !(setrangeB_response[0] == 0xFF && setrangeB_response[1] == 0x99 && setrangeB_response[8] == setrangeB_crc) ) {
Serial.println("Range CRC error: " + String(setrangeB_crc) + " / "+ String(setrangeB_response[8]) + " (bytes 3 and 4)");
} else {
Serial.println("Range was set! (bytes 3 and 4)");
}
delay(1000);
*/

}

void loop() {

byte measure_cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79};
unsigned char measure_response[9];
unsigned long th, tl, ppm = 0, ppm2 = 0, ppm3 = 0;

// ***** узнаём концентрацию CO2 через UART: *****
swSerial.write(measure_cmd,9);
swSerial.readBytes(measure_response, 9);
int i;
byte crc = 0;
for (i = 1; i < 8; i++) crc+=measure_response[i];
crc = 255 - crc;
crc += 1;
if ( !(measure_response[0] == 0xFF && measure_response[1] == 0x86 && measure_response[8] == crc) ) {
Serial.println("CRC error: " + String(crc) + " / "+ String(measure_response[8]));
}
unsigned int responseHigh = (unsigned int) measure_response[2];
unsigned int responseLow = (unsigned int) measure_response[3];
unsigned int ppm = (256*responseHigh) + responseLow;

// *****  узнаём концентрацию CO2 через PWM: *****
do {
th = pulseIn(pwmPin, HIGH, 1004000) / 1000;
tl = 1004 - th;
ppm2 =  2000 * (th-2)/(th+tl-4); // расчёт для диапазона от 0 до 2000ppm
ppm3 =  5000 * (th-2)/(th+tl-4); // расчёт для диапазона от 0 до 5000ppm
} while (th == 0);

Serial.print(ppm);
Serial.print(" <- ppm (UART) ");
Serial.print((ppm/5)*2);
Serial.println(" <- two fifths of it"); // Потом пришло озарение
Serial.print(th);
Serial.println(" <- Milliseconds PWM is HIGH");
Serial.print(ppm2);
Serial.println(" <- ppm2 (PWM) with 2000ppm as limit");
Serial.print(ppm3);
Serial.println(" <- ppm3 (PWM) with 5000ppm as limit");

Serial.println("-----------");
delay(5000);
}

Все замеры проводим, подключив модуль к питанию от 5 вольт; при 3.3 вольтах он выдаёт очевидно некорректные значения по верху диапазона.

В режиме PWM при заданном диапазоне значений от 0 до 2000ppm получаем, в условиях центра города, заполночь у форточки во двор, 1208ppm, что, безусловно, завышено. UART выдаёт нам близкое значение 1227ppm — различия вполне объяснимы ошибками оцифровки PWM-показаний.

Для сравнения, вот примерные дапазоны концентраций СО2, найденные в интернете:

— 350 — 450 ppm: Нормальный уровень на открытом воздухе.
— < 600 ppm: Приемлемые уровни. Уровень. рекомендованный для спален, детских садов и школ.
— 600 — 1000 ppm: Жалобы на несвежий воздух, возможно снижение концентрации внимания.
— 1000 ppm: Максимальный уровень стандартов ASHRAE (American Society of Heating, Refrigerating and Air-Conditioning Engineers) и OSHA (Occupational Safety & Health Administration).
— 1000 — 2500 ppm: Общая вялость, снижение концентрации внимания, возможна головная боль.
— 2500 — 5000 ppm: Возможны нежелательные эффекты на здоровье.

Переключаем датчик в диапазон 0 — 5000ppm. В режиме PWM у той же форточки во двор получаем 478ppm, что гораздо больше похоже на правду. Но вот в режиме UART наш датчик снова выдаёт совершенно неправдоподобное значение 1213ppm.

Десять раз перепроверив формулы, по близости показаний начинаю догадываться, что датчик считает с ошибками в арифметике. Модифицирую код, чтобы формула расчёта концентрации CO2 по данным PWM рассчитывалась с подстановкой всех вариантов верхнего предела значений. В момент какого-то озарения также дополнительно модифицирую код, чтобы значение, полученное по UART, дополнительно выводилось домноженным на 2000/5000:

диапазон 0 — 2000ppm

Range was set! (bytes 6 and 7)
1227 <- ppm (UART) 490 <- two fifths of it
606 <- Milliseconds PWM is HIGH
1208 <- ppm2 (PWM) with 2000ppm as limit
3020 <- ppm3 (PWM) with 5000ppm as limit

диапазон 0 — 5000ppm

Range was set! (bytes 6 and 7)
1213 <- ppm (UART) 484 <- two fifths of it
241 <- Milliseconds PWM is HIGH
478 <- ppm2 (PWM) with 2000ppm as limit
1195 <- ppm3 (PWM) with 5000ppm as limit

Делаем выводы:

  • в качестве рабочего диапазона работы датчика надо задавать 0 — 5000ppm
  • несмотря на это, для расчёта значений, полученных через PWM, надо в формулу подставлять константу 2000, а не 5000
  • для получения правдивых показаний от UART надо полученное значение умножать на 2/5
  • китайским даташитам, даже на вид добротным, верить нельзя
  • готовые скетчи для Ардуино из интернета брать нельзя
  • показания всех китайских «показометров» надо проверять хотя бы на соответствие здравому смыслу и внутреннюю непротиворечивость

После этого расследования датчик поселится в приборе в офисе, в компании с барометром, датчиком влажности воздуха, содержания пыли и ESP8266 для управления всем этим добром. Хоть я и вдохновился изначально приборчиками в переговорках московского Google, мой вариант не будет иметь цифр и экрана, а вместо этого получит более простую цветовую индикацию, как светофор — а подробные логи будет выкладывать по WiFi на сервер. Впрочем, это уже отдельная история.

P.S. Товар куплен за свои кровные:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Наверх