Сравним три популярных датчика пыли. Стоит ли переплачивать за более дорогой? Какие наиболее распространенные ошибки допускают при работе с бюджетными датчиками? Ответы под катом.
Об устройстве чуть позже, сначала посмотрим внимательнее на датчики.
SDS011
Первый датчик лазерный, фирмы Nova Fitness. Он предназначен для определения количества пыли размером от 0,3 до 10 мкм в воздухе. Датчик разделяет пыль на две категории — размером от 0,3 до 2,5 мкм и от 2,5 до 10 мкм. Это общепринятая классификация, и наиболее опасна для здоровья пыль от 0,3 до 2,5 мкм.
Как и у многих современных датчиков, у этого два типа представления данных. Он способен передавать сведения в последовательный порт, а кроме того, имеет два выхода, скважность сигналов на которых пропорциональна концентрации пыли двух размеров.
Прочие сведения:
диапазон измерений: 0 — 999 мкг/м3
напряжение питания: 5 вольт
потребляемый ток: 100 мА
ток в режиме энергосбережения: 2 мА
температура окружающей среды: -20-50°С
время отклика: 1 сек
частота отправки сообщений в компорт: 1 раз в секунду.
минимальный определяемый размер частиц пыли: менее 0,3мкм.
размеры: 71х70х23 мм
Датчик снабжен центробежным микровентилятором.
В комплект входит переходник с последовательного порта на usb.
Тут pdf на с подробной информацией по датчику:
https://inovafitness.de/downloads/
А тут описание протокола обмена по UART интерфейсу.
https://nettigo.eu/attachments/415
GP2Y1014AU0F
Второй датчик производства фирмы Sharp. Он дешевле, компактнее и проще первого. Определяет пыль по принципу фотометрии. Краткие характеристики:
потребляемый ток: 20 мА
напряжение питания: 5 вольт
чувствительность: 0,5 вольт/100мкг/м3
размеры: 46х30x17,6мм
диапазон измерений: 0 — 500 мкг/м3
Вентилятора у датчика нет, что с одной стороны является преимуществом, но с другой обещает не такую быструю реакцию датчика на изменение атмосферы.
Вот описание датчика:
http://www.sharp-world.com/products/device/lineup/data/pdf/datasheet/gp2y1010au_appl_e.pdf
Samyoung DSM501a
Третий датчик для определения пыли он использует тот же метод, что и Шарп, но в отличие от него, данные выдает по двум калибрам пыли — до 2,5мкм и от 2,5 и выше. Для связи передачи данных используется широтно-импульсная модуляция, что немного сложнее обработке, но намного надежнее с точки зрения помехозащищенности, чем аналоговый сигнал.
Его краткая характеристика:
напряжение питания: 4,5-5,5 вольт
потребляемый ток: 90ма
минимальный размер обнаруживаемой пыли: 1мкм
диапазон измерений: 0-1400 мкг/м3
У этого датчика есть выход, который используется для регулировки чувствительности второго канала. Таким образом, мы можем менять границу разделения пыли на две фракции. Для улучшения циркуляции воздуха используется микро-печка из резистора, которая нагревает воздух и за счет конвекции он быстрее обновляется у сенсора.
Описание:
http://www.platan.ru/pdf/datasheets/samyoung/DSM501.pdf
Разумеется, фаворитом соревнований является лазерный датчик. Он и дороже и современнее своих конкурентов. Мне его прислали бесплатно, на обзор, и, как многие думают, я его должен хвалить уже из-за этого. Это не так, но сказать пару хороших слов можно сразу, едва вынув датчик из упаковки. Его легко подключить к компьютеру через прилагаемый переходник, скачать программу снятия данных и вуаля! График пыли строится на вашем экране.
Программа у меня провисела в фоне несколько дней, прежде чем я занялся сравнением датчиков, и уже в эти дни я понял, очень мало я знаю о пыли в своем доме.
Теперь, когда с соперниками разобрались, кратко остановимся на тех деталях, которые будут использованы для изготовления стенда.
Экран 128х160 с интерфейсом SPI, размером 1,8 дюйма.
Часы реального времени DS3231, соединение по шине I2C
«Черный ящик» OpenLog — будет висеть на компорте и записывать на флешку все, что контроллер отправляет в порт.
И конечно же Arduino Pro Mini — мозг и память всего проекта.
В качестве переходного звена, объединяющего все элементы, будет макетная платка. В основном на ней разъемы для подключения датчиков, но еще имеются резистор и электролитик — они требуются для нормального функционирования датчика Sharp.
Два резистора с джамперами — они служат для переключения порога чувствительности второго канала датчика DSM501, разъем микро-usb для питания в отсутствии компьютера и линейка резисторов для безопасного подключения экрана.
Вот что у меня получилось в результате.
Первым делом я решил протестировать датчик фирмы Sharp. Аналоговый выход обещал легкое подключение и обработку данных. Но все оказалось не совсем так. бОльшую часть времени датчик простаивает. Один раз в 10 мс на датчик нужно подать короткий импульс длительностью 320 мкс, который включит светодиод подсветки пыли. Потом, через 280 мкс, когда на выходе датчика окажется ответный сигнал, нужно успеть его снять и определить его амплитуду. Она-то и характеризует количество пыли в воздухе. Как только сигнал оцифрован, напряжение с подсветки снимается и все отключается до следующего импульса. Конечно, такой алгоритм снижает износ датчика и еще, как упомянуто в мануале, позволяет отличить пыль от дыма. Жаль, там не развивается эта мысль и остается только догадываться, как их отличить на практике.
Простым вольтметром сигнал с датчика не померить и пришлось снимать осциллограмму.
Желтый график — управление светодиодом подсветки пыли. Отрицательный сигнал включает диод.
Голубой — ответ датчика.
Фиолетовый я ввел для наглядности, он включается непосредственно перед тем, как контроллер приступает к измерению напряжения сигнала и выключается после того, как напряжение измерено. Что интересно, мы видим, что сигнал от датчика начинает спадать не после отключения диода подсветки, а еще при его работе. Так что в измерениях важно не промахнуться по времени и все точно выполнять по инструкции.
Второй важный момент — грамотно перевести полученные вольты в микрограммы на кубометр. Для этого надо вычесть из сигнала постоянную полку и по графику вычислить концентрацию пыли. Постоянная составляющая сигнала у каждого датчика своя. Все требует настройки.
Программисту на заметку:
В демонстрационных программах, которые можно найти на гитхабе, со временем промашка. Чтобы успеть все правильно измерить, нужно приступать уже через 200 мкс после включения диода, а не чрез 280, как там пишут.
delayMicroseconds(200); // was 280, but real 320mks at 200 setting
Итак. Что же мы имеем с гуся? А с гуся мы имеем почти ничего. Показания скачут вокруг нулевых значений. В принципе, это и не удивительно, я пылесошу каждый день. Но хотелось бы меньшего разброса показаний. Если поступить по-варварски и ввести в отверстие датчика какой-то предмет, то показания сразу взлетают на максимум, так что датчик рабочий. Просто для относительно чистой комнаты не очень чувствительный.
Второй датчик тоже заработал не сразу. Халява не прошла, и первый попавшийся в интернете код не заработал. Датчик показывал погоду на Марсе, и пришлось разбираться детально. Для начала я решил исключить плохое питание. Может оно и хорошее, но на всякий случай я припаял между + и — электролитик. Затем, в соответствии с мануалом, надо учитывать только импульсы от 10 до 90 мс. Да, импульсы у этого датчика следуют не с постоянной частотой и скважностью, а хаотично. И программно нужно их суммировать и вычислять процент присутствия импульсов относительно общего времени наблюдения. Так вот, в эту сумму не попадают импульсы менее 10 и более 90 мс. Третье: надо научиться пользоваться настройками датчика. При замыкании провода управления на землю через резистор 18,2КОм первый выход датчика становится столь же чувствительным, как и второй. Что не имеет практического смысла. При замыкании на землю через резистор 47 КОм первый выход начинает видеть пыль только крупнее 1,75 мкм. Таким образом, если в датчик залетит пылинка 1,5 мкм, то на втором выходе будет сигнал, а на первом — нет. При полном отключении управляющего провода от земли первый выход начинает замечать только пыль крупнее 2,5 мкм. Что нам и нужно. Мы снимаем показания со второго выхода, вычитаем их них показания с первого и получаем, таким образом, количество пыли в диапазоне от 1 до 2,5 мкм.
Но это еще не все. Остается перевести длительность присутствия сигнала на выходах датчика в количество пыли. А для этого в нашем распоряжении лишь график. График в программу не засунешь, я нашел онлайн сервис по оцифровке графиков и подбору полиномов для их более-менее точного моделирования. До меня это сделал какой-то парень из Штатов, но там у них свой стандарт, они меряют пыль в пылинках на одну сотую кубического фута. А у нас микрограммы на кубометр. Так что формула из готовых скриптов мне решительно не подходила.
Программисту на заметку:
Вот моя формула:
y = -0.088496207228664*x^4 — 2.5505502324503*x^3 — 21.92053783603786*x^2 + 172.17128476610927*x — 90.1119605706346
Где
х — процент времени, когда присутствует сигнал от датчика, 0-100%
y — концентрация пыли, мкг/м3
Формула очень хорошо коррелирует с графиком из датшита, но совсем не коррелирует с действительностью, увы.
После всех этих плясок с бубном ничего сильно хорошего не получилось: датчик показывал то чистейший горный воздух, то тяжелую атмосферу шлифовального цеха.
Удивительно, но если вернуться к старой формуле из примеров использования датчика, той, с штуками пылинок на сотую кубофута, то выдаваемые датчиком значения численно более-менее правдоподобны, для чистой комнаты. Но как только поднимается пыль, они начинают отставать.
А вот лазерный датчик хлопот не доставил. Байты из порта прочитались сразу. Сигнал с ШИМ — выходов тоже равномерный и красивый.
Показания очень хорошо соотносятся с происходящими вокруг датчика событиями. Это было заметно еще по первому включению, с выводом графика на десктоп. Пики в левой части графика — пайка. Датчик лежал на столе, где я паял стенд. По ощущениям, дыма особого не было, но график решительно полз вверх каждый раз, когда я включал паяльник. Короткий всплеск справа — расстилание кровати. Оказывается, даже чистая постель — источник огромного количества пыли.
Еще один неочевидный вывод — хороший пылесос совсем не гоняет пыль по комнате, а действительно ее всасывает. Включение пылесоса совершенно не отражалось на графике. А вот строительный пылесос — совсем другое дело. Мне нужно было просверлить пару небольших отверстий в ванной комнате. Там вытяжка, там маленькие отверстия, но все-таки я подогнал строительный пылесос, чтобы пылью от сверления не портить эксперимент с датчиками пыли. Эффект получился совсем не такой, как я ожидал.
Концентрация пыли мгновенно выросла в 20 раз и спадала потом еще полчаса. Кстати, проветривание резко снижает количество пыли в воздухе. Наверное, не во всех областях это так, но мне вот повезло.
Код скрипта
/////////////////// © Tykhon, 2019 ////////////////////////////////////
#include "Wire.h"
#include <SoftwareSerial.h>
#include <TFT.h>
/////////////////// for DSM501 sensor ////////////////////////////////////
int pinV2 = 6;
int pinV1 = 5;
unsigned long durationV1;
unsigned long durationV2;
unsigned long starttimeV1;
unsigned long starttimeV2;
unsigned long lowpulseoccupancyV1 = 0;
unsigned long lowpulseoccupancyV2 = 0;
float one_to_two_point_five;
float ratioV1 = 0;
float ratioV2 = 0;
boolean flagV1 = true;
boolean flagV2 = true;
boolean V1 = false;
boolean V2 = false;
float concentrationV1 = 0;
float concentrationV2 = 0;
int allV1, allV2, goodV1, goodV2;
/////////////////// for Sharp sensor ////////////////////////////////////
unsigned long LastCheckSharp;
const int sharpLEDPin = 4; // Arduino digital pin 2 connect to sensor LED.
const int sharpVoPin = A6; // Arduino analog pin A6 connect to sensor Vo.
static unsigned long VoRawTotal = 0;
int VoRawCount = 0;
static float Voc = 0.45;
float dustDensity = 0.0;
float Vo = 0.0;
float Dust = 0.0;
/////////////////// for general purposes ////////////////////////////////
unsigned long starttime;
unsigned long endtime;
unsigned long sampletime_ms = 30000;
unsigned long now;
unsigned long loops;
#define DS1307_ADDR 0x68 // RTC address
/////////////////// for LCD ////////////////////////////////////////////
#define TFT_CS 10
#define TFT_RST 9
#define TFT_DC 8
int line_25[85] = {};
int line_10[85] = {};
byte lineIndex;
TFT TFTscreen = TFT(TFT_CS, TFT_DC, TFT_RST);
char one_to_two_point_fivec[5], Dustc[5], Pm25fc[5], Pm10fc[5];
////////////////// for SDS 011 sensor //////////////////////////////////
int rxPin = 3;
int txPin = 2;
SoftwareSerial mySerial(3, 2); //RX, TX
unsigned int Pm25 = 0;
unsigned int Pm10 = 0;
float Pm25f = 0.0;
float Pm10f = 0.0;
int sdspoll = 0;
unsigned int Pm25sum = 0;
unsigned int Pm10sum = 0;
unsigned long lastsds;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup()
{
Serial.begin(9600);
mySerial.begin(9600);
pinMode(pinV1,INPUT);
pinMode(pinV2,INPUT);
pinMode(sharpLEDPin, OUTPUT);
starttime = millis();
Wire.begin();
TFTscreen.begin();
TFTscreen.setRotation(2);
TFTscreen.background(20,0,0);
TFTscreen.stroke(200,200,200);
TFTscreen.setTextSize(2);
TFTscreen.text("DSM", 2, 5);
TFTscreen.text("Sharp", 2, 35);
TFTscreen.stroke(0,250,250);
TFTscreen.text("Pm10", 2, 65);
TFTscreen.stroke(180,10,250);
TFTscreen.text("Pm2.5", 2, 95);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////// for date and time //////////////////////////////////
byte bcdToDec(byte val) {
return ( (val/16*10) + (val%16) );
}
/////////////////// returns date and time //////////////////////////////////
String getdate(){
Wire.beginTransmission(DS1307_ADDR);
byte zero = 0x00;
Wire.write(zero);
Wire.endTransmission();
Wire.requestFrom(DS1307_ADDR, 7);
int secondint = bcdToDec(Wire.read());
int minuteint = bcdToDec(Wire.read());
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); ///
}
//////////////////// draws graph ////////////////////////////////////////
void tft_graph(){
byte k = 30;
TFTscreen.stroke(20,0,0);
TFTscreen.fill(20,0,0);
TFTscreen.rect(0, TFTscreen.height()-45, TFTscreen.width(), 45);
TFTscreen.stroke(250,180,10);
TFTscreen.line(5, 90+k, 5, 121+k);
TFTscreen.line(5, 121+k, 90, 121+k);
int maxvalue = 0;
for (int i = 0; i < (sizeof(line_10)/sizeof(int)); i++){
maxvalue = max(maxvalue, line_10[i]);
maxvalue = max(maxvalue, line_25[i]);
};
int g = 5+(sizeof(line_10)/sizeof(int));
for (int i = lineIndex-1; i >= 0; i--){
int x1 = g;
g--;
int y1 = map(line_10[i],maxvalue,0,90+k,120+k);
int y2 = map(line_25[i],maxvalue,0,90+k,120+k);
TFTscreen.stroke(10,180,250);
TFTscreen.point(x1, y1);
TFTscreen.stroke(180,10,250);
TFTscreen.point(x1, y2);
};
for (int i = (sizeof(line_10)/sizeof(int))-1; i >= lineIndex; i--){
int x1 = g;
g--;
int y1 = map(line_10[i],maxvalue,0,90+k,120+k);
int y2 = map(line_25[i],maxvalue,0,90+k,120+k);
TFTscreen.stroke(10,180,250);
TFTscreen.point(x1, y1);
TFTscreen.stroke(180,10,250);
TFTscreen.point(x1, y2);
};
}
///////////////////// prints data on screen ///////////////////////////
void tft_output(){
TFTscreen.stroke(20,0,0);
TFTscreen.text(one_to_two_point_fivec, 70, 5);
TFTscreen.text(Dustc, 70, 35);
TFTscreen.text(Pm10fc, 70, 65);
TFTscreen.text(Pm25fc, 70, 95);
String one_to_two_point_fives = String(one_to_two_point_five);
String Dusts = String(Dust);
String Pm10fs = String(Pm10f);
String Pm25fs = String(Pm25f);
one_to_two_point_fives.toCharArray(one_to_two_point_fivec,5);
Dusts.toCharArray(Dustc,5);
Pm10fs.toCharArray(Pm10fc,5);
Pm25fs.toCharArray(Pm25fc,5);
TFTscreen.stroke(255,255,255);
TFTscreen.text(one_to_two_point_fivec, 70, 5);
TFTscreen.text(Dustc, 70, 35);
TFTscreen.text(Pm10fc, 70, 65);
TFTscreen.text(Pm25fc, 70, 95);
}
////////////////// reads serial port and decodes data //////////////////////
void ProcessSerialData()
{
uint8_t mData = 0;
uint8_t i = 0;
uint8_t mPkt[10] = {0};
uint8_t mCheck = 0;
while (mySerial.available() > 0)
{
// from www.inovafitness.com
// packet format: AA C0 PM25_Low PM25_High PM10_Low PM10_High 0 0 CRC AB
mData = mySerial.read(); delay(2);//wait until packet is received
if (mData == 0xAA) //head1 ok
{
mPkt[0] = mData;
mData = mySerial.read();
if (mData == 0xc0) //head2 ok
{
mPkt[1] = mData;
mCheck = 0;
for (i = 0; i < 6; i++) //data recv and crc calc
{
mPkt[i + 2] = mySerial.read();
delay(2);
mCheck += mPkt[i + 2];
}
mPkt[8] = mySerial.read();
delay(1);
mPkt[9] = mySerial.read();
if (mCheck == mPkt[8]) //crc ok
{
mySerial.flush();
Pm25 = (uint16_t)mPkt[2] | (uint16_t)(mPkt[3] << 8);
Pm10 = (uint16_t)mPkt[4] | (uint16_t)(mPkt[5] << 8);
if (Pm25 > 9999)
Pm25 = 9999;
if (Pm10 > 9999)
Pm10 = 9999;
return;
}
}
}
}
}
////////////////// sends SLEEP command to SDS011 //////////////////////
void SDS011sleep()
{
while (mySerial.available() > 0)
{
uint8_t sleep_command[] = {0xAA, 0xB4, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x05, 0xAB};
for (uint8_t i = 0; i < 19; i++) {
mySerial.write(sleep_command[i]);
}
mySerial.flush();
}
}
////////////////// sends WAKEUP command to SDS011 //////////////////////
void SDS011wakeup()
{
while (mySerial.available() > 0)
{
uint8_t wakeup_command[] = {0xAA, 0xB4, 0x06, 0x01, 0x01, 0xC5};
for (uint8_t i = 0; i < 6; i++) {
mySerial.write(wakeup_command[i]);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void loop()
{
loops +=1;
now = millis();
////////////// Sharp ////////////////////////
if ((now - LastCheckSharp) >= 10) {
LastCheckSharp = now;
digitalWrite(sharpLEDPin, LOW);
delayMicroseconds(200); // was 280, but real 320mks at 200 setting
int VoRaw = analogRead(sharpVoPin);
digitalWrite(sharpLEDPin, HIGH);
VoRawTotal += VoRaw;
VoRawCount++;
};
//////////// DSM 501 ////////////////////////
V1 = digitalRead(pinV1);
if ((flagV1)&&!(V1)) { // start period of low V1
starttimeV1 = millis();
flagV1 = false;
};
if (!(flagV1)&&(V1)) { // stop period of low V1
durationV1 = millis() - starttimeV1;
flagV1 = true;
allV1+=1;
if ((durationV1 <= 90)&&(durationV1 >= 10)) {
lowpulseoccupancyV1 += durationV1;
goodV1+=1;
};
};
V2 = digitalRead(pinV2);
if ((flagV2)&&!(V2)) { // start period of low V2
starttimeV2 = millis();
flagV2 = false;
};
if (!(flagV2)&&(V2)) { // stop period of low V2
durationV2 = millis() - starttimeV2;
flagV2 = true;
allV2+=1;
if ((durationV2 <= 90)&&(durationV2 >= 10)) {
lowpulseoccupancyV2 += durationV2;
goodV2+=1;
};
};
//////////// SDS 011 ////////////////////////
if ((now - lastsds)> 1000) {
lastsds = now;
sdspoll ++;
ProcessSerialData();
Pm25sum += Pm25;
Pm10sum += Pm10;
};
//////////// output ///////////////////////////////
endtime = millis();
if ((endtime-starttime) > sampletime_ms)
{
Serial.print(getdate());
//////////////////////////
Vo = 1.0 * VoRawTotal / VoRawCount;
Vo = Vo / 1024.0 * 5.0;
float dV = Vo - Voc;
if (dV < 0 ) {dV = 0; Voc = Vo;};
Dust = 172.0*dV;
Serial.print(" Sharp: ");
Serial.print(Dust);
VoRawCount = 0;
VoRawTotal = 0;
//////////////////////////
ratioV1 = (lowpulseoccupancyV1*100.0)/(endtime-starttime); // Integer percentage 0=>100
ratioV2 = (lowpulseoccupancyV2*100.0)/(endtime-starttime); // Integer percentage 0=>100
// concentrationV1 = 1.1*pow(ratioV1,3)-3.8*pow(ratioV1,2)+520*ratioV1+0.62; // using spec sheet curve in pcs in 1/100 ft3
// concentrationV2 = 1.1*pow(ratioV2,3)-3.8*pow(ratioV2,2)+520*ratioV2+0.62; // using spec sheet curve in pcs in 1/100 ft3
concentrationV1 = -0.0885*pow(ratioV1,4) - 2.55055*pow(ratioV1,3)- 21.920538*pow(ratioV1,2) + 172.171285*ratioV1 - 90.112;
concentrationV2 = -0.0885*pow(ratioV2,4) - 2.55055*pow(ratioV2,3)- 21.920538*pow(ratioV2,2) + 172.171285*ratioV2 - 90.112;
if (concentrationV1 < 0) {concentrationV1 = 0.0;};
if (concentrationV2 < 0) {concentrationV2 = 0.0;};
one_to_two_point_five = concentrationV2 - concentrationV1;
if (one_to_two_point_five < 0) {one_to_two_point_five = 0;};
Serial.print(" DSM501_>2.5: ");
Serial.print(concentrationV1);
Serial.print(" DSM501_>1.0: ");
Serial.print(concentrationV2);
Serial.print(" DSM501_1-2.5: ");
Serial.print(one_to_two_point_five);
lowpulseoccupancyV1 = 0;
lowpulseoccupancyV2 = 0;
loops = 0;
allV1 = 0;
goodV1 = 0;
allV2 = 0;
goodV2 = 0;
///////////// SDS011 /////////////////
Pm25f = Pm25sum/(sdspoll*10.0);
Pm10f = Pm10sum/(sdspoll*10.0);
Serial.print(" Pm2.5 ");
Serial.print(Pm25f,2);
Serial.print(" Pm10 ");
Serial.print(Pm10f,2);
sdspoll = 0;
Pm25sum = 0;
Pm10sum = 0;
line_25[lineIndex] = int(Pm25f*100.0);
line_10[lineIndex] = int(Pm10f*100.0);
lineIndex++;
if (lineIndex >= 85) {lineIndex = 0;};
//////////// finish ///////////////
tft_output();
Serial.println();
tft_graph();
starttime = millis();
} // if 30 sec passed
} // loop
Выводы
Я пару дней гонял стенд с тремя датчиками в разных условиях. В области малых концентраций пыли для измерений годится только лазерный датчик. Он же и самый быстрый на отклик. У меня нет возможностей проверить показания этого датчика. Но мне достаточно того, что в его измерениях отражались все события в комнате, которые так или иначе должны были влиять на пыль. Расстилание кровати, проветривание, пайка, работа пылесоса, ремонтные работы — все это тут же сказывалось на графике. Что до шума от вентилятора, то он не очень большой. И у датчика есть режим энергосбережения, при котором вентилятор не работает. Два других датчика скорее подойдут для контроля относительно грязной атмосферы. Если выбирать из них, то Samyoung DSM 501 выглядит предпочтительнее. Его преимущества — разделение пыли по фракциям, лучшая помехозащищенность, большой диапазон и улучшенная циркуляция воздуха.
Плюсы датчика SDS011
+ высокая корреляция показаний прибора и обстановки с пылью вокруг
+ стабильность показаний
+ скорость реакции
+ невысокая (для лазерных датчиков) стоимость.
Минусы датчика SDS011
— шум от вентилятора
— относительно высокое энергопотребление
Обновление от 24.05.19
В итоге, для постоянной работы я оставил один только датчик пыли — разумеется SDS011. У него обнаружилась полезная особенность. Для экономии электричества и ресурса он может работать в перывистом режиме — заданный период он ожидает, потом включается на 30 секунд, измеряет концентрацию пыли и выдает ее в порт. Потом снова засыпает. Еще одна полезная настройка — датчик может самостоятельно посылать данные в порт, а может ждать запроса от контроллера. Мне показалось, что 30 секунд недостаточно, чтобы датчик вышел на стабильные показания, так что я провел серию экспериментов. В скетч добавил режим, в котором датчик принудительно включается и выключается. Пробовал запускать его на минуту и две, регулярно опрашивать и усреднять показания. В итоге оказалось, что как раз-таки 30 секунд — оптимальный период работы.
В новой версии скетча добавлен автоматический режим, когда датчик сам включается и выключается, и ручной. Добавлены шкалы на графике. В итоге экран прибора выглядит примерно так:
Код скетча
///////////////////////////////////////////////////////////////////////
/// © tykhon, 2019
///////////////////////////////////////////////////////////////////////
#include "Wire.h"
#include <SoftwareSerial.h>
#include <TFT.h>
/////////////////// for general purposes ////////////////////////////////
boolean automode = true; // manual on/off sensor or auto.
// Auto: sensot itself turn on (for 30 secs. )and off (for 1-30 mins) and gathers statistics.
// Manual: sensor never shuts off by itself, but the program turns it on (for work_time) and off (work_period - work_time) and handle all the incoming data.
byte mode = 3; // 0 for countinuous work of sensor 1-30 for work with 1-30 min delays. Set 0 for automode = false; 1-30 for automode = true
byte work_period = 2; // period of time (work+delay) in min 1 to 30; Recommended: 2, 3, 4, 5, 6, 10, 15, 20, 30. Relevant only if automode = false;
byte work_time = 1; // time of work of sensor in min 1 to 29, should be less than work_period; Relevant only if automode = false;
/////////////////// for LCD ////////////////////////////////////////////
#define TFT_CS 10
#define TFT_RST 9
#define TFT_DC 8
int line_25[135] = {};
int line_10[135] = {};
byte lineIndex;
TFT TFTscreen = TFT(TFT_CS, TFT_DC, TFT_RST);
char Pm25fc[5], Pm10fc[5], maxvaluec[4], line10meanc[4], line25meanc[4], hour_scalec[4];
////////////////// for SDS 011 sensor //////////////////////////////////
int rxPin = 3;
int txPin = 2;
SoftwareSerial mySerial(3, 2); //RX, TX
unsigned int Pm25 = 0;
unsigned int Pm10 = 0;
unsigned int Pm25sum = 0;
unsigned int Pm10sum = 0;
float Pm25f = 0.0;
float Pm10f = 0.0;
#define DS1307_ADDR 0x68 // RTC address
unsigned int current_minute, lastminute;
boolean active = false;
unsigned int loops = 0;
/////////////////// for date and time //////////////////////////////////
byte bcdToDec(byte val) {
return ( (val/16*10) + (val%16) );
}
/////////////////// returns date and time //////////////////////////////////
String getdate(){
Wire.beginTransmission(DS1307_ADDR);
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); ///
}
/////////////////// gets data from sensor //////////////////////////////////
void print_Pm(){
Pm25f = Pm25/10.0;
Pm10f = Pm10/10.0;
String dataString = getdate()+" Pm2.5: "+String(Pm25f,2)+" Pm10: "+String(Pm10f,2);
dataString.replace(".",",");
dataString.replace(" ","\t");
Serial.println(dataString);
line_25[lineIndex] = Pm25;
line_10[lineIndex] = Pm10;
lineIndex++;
if (lineIndex >= sizeof(line_10)/sizeof(int)) {lineIndex = 0;};
}
//////////////////// draws graph ////////////////////////////////////////
void tft_graph(){
// x1 y1 x2 y2
unsigned int line10sum = 0;
unsigned int line25sum = 0;
unsigned int elements = 0;
TFTscreen.stroke(20,0,0);
TFTscreen.fill(20,0,0);
TFTscreen.rect(0, TFTscreen.height()-65, TFTscreen.width(), 64);
TFTscreen.stroke(250,180,10);
TFTscreen.line(20, 65, 20, 118);
TFTscreen.line(20, 118, 156, 118);
int maxvalue = 0;
for (int i = 0; i < (sizeof(line_10)/sizeof(int)); i++){
maxvalue = max(maxvalue, line_10[i]);
maxvalue = max(maxvalue, line_25[i]);
if (line_10[i] > 0) {
line10sum += line_10[i];
line25sum += line_25[i];
elements ++;
};
};
float line10mean = line10sum / (elements*10);
float line25mean = line25sum / (elements*10);
maxvalue = int(ceil( float(maxvalue)/50.0)*50);
String maxvalues = String(maxvalue/10);
maxvalues.toCharArray(maxvaluec,4);
String line10means = String(int(round(line10mean+0.25)));
line10means.toCharArray(line10meanc,4);
String line25means = String(int(round(line25mean+0.25)));
line25means.toCharArray(line25meanc,4);
TFTscreen.setTextSize(1);
TFTscreen.stroke(255,255,255);
TFTscreen.text(maxvaluec, 1, 65);
int hours_count = sizeof(line_10)/sizeof(int);
int period = work_period;
if (automode) {period = mode;};
hours_count = hours_count * period;
hours_count = hours_count / (-60);
int onestep = 1;
if (hours_count < -5) {onestep = 2;};
for (int i = 0; i >= hours_count; i = i - onestep) {
String hour_scales = String(i);
hour_scales.toCharArray(hour_scalec,4);
int posx = 15 +(sizeof(line_10)/sizeof(int)) + i*(60/period);
TFTscreen.stroke(250,180,10);
TFTscreen.line(posx+5, 118, posx+5, 121);
TFTscreen.stroke(255,255,255);
int shift = posx+5;
if (shift > 155) {shift = shift-10;};
TFTscreen.text(hour_scalec, shift, 121);
};
TFTscreen.stroke(10,180,250);
TFTscreen.text(line10meanc, 1, 85);
TFTscreen.stroke(180,10,250);
TFTscreen.text(line25meanc, 1, 105);
TFTscreen.setTextSize(2);
int g = 20+(sizeof(line_10)/sizeof(int));
for (int i = lineIndex-1; i >= 0; i--){
int x1 = g;
g--;
int y1 = map(line_10[i],maxvalue,0,66,118);
int y2 = map(line_25[i],maxvalue,0,66,118);
TFTscreen.stroke(10,180,250);
TFTscreen.line(x1, y1, x1, 118);
TFTscreen.stroke(180,10,250);
TFTscreen.line(x1, y2, x1, 118);
};
for (int i = (sizeof(line_10)/sizeof(int))-1; i >= lineIndex; i--){
int x1 = g;
g--;
int y1 = map(line_10[i],maxvalue,0,66,118);
int y2 = map(line_25[i],maxvalue,0,66,118);
TFTscreen.stroke(10,180,250);
// TFTscreen.point(x1, y1);
TFTscreen.line(x1, y1, x1, 118);
TFTscreen.stroke(180,10,250);
// TFTscreen.point(x1, y2);
TFTscreen.line(x1, y2, x1, 118);
};
}
///////////////////// prints data on screen ///////////////////////////
void tft_output(){
TFTscreen.stroke(20,0,0);
TFTscreen.text(Pm10fc, 70, 2);
TFTscreen.text(Pm25fc, 70, 32);
String Pm10fs = String(Pm10f);
String Pm25fs = String(Pm25f);
Pm10fs.toCharArray(Pm10fc,5);
Pm25fs.toCharArray(Pm25fc,5);
TFTscreen.stroke(255,255,255);
TFTscreen.text(Pm10fc, 70, 2);
TFTscreen.text(Pm25fc, 70, 32);
}
////////////////// reads serial port and decodes data //////////////////////
void ProcessSerialData()
{
uint8_t mData = 0;
uint8_t i = 0;
uint8_t mPkt[10] = {0};
uint8_t mCheck = 0;
while (mySerial.available() > 0)
{
// from www.inovafitness.com
// packet format: AA C0 PM25_Low PM25_High PM10_Low PM10_High 0 0 CRC AB
mData = mySerial.read(); delay(2);//wait until packet is received
if (mData == 0xAA) //head1 ok
{
mPkt[0] = mData;
mData = mySerial.read();
if (mData == 0xc0) //head2 ok
{
mPkt[1] = mData;
mCheck = 0;
for (i = 0; i < 6; i++) //data recv and crc calc
{
mPkt[i + 2] = mySerial.read();
delay(2);
mCheck += mPkt[i + 2];
}
mPkt[8] = mySerial.read();
delay(1);
mPkt[9] = mySerial.read();
if (mCheck == mPkt[8]) //crc ok
{
mySerial.flush();
Pm25 = (uint16_t)mPkt[2] | (uint16_t)(mPkt[3] << 8);
Pm10 = (uint16_t)mPkt[4] | (uint16_t)(mPkt[5] << 8);
if (Pm25 > 9999)
Pm25 = 9999;
if (Pm10 > 9999)
Pm10 = 9999;
return;
} else {Serial.println('crc not ok');};
} else {Serial.println('head not ok');};
}
}
}
////////////////// sends work mode command to SDS011 //////////////////////
void SDS011workmode(byte mode)
{
byte cs = 7 + mode;
uint8_t sleep_command[] = {0xAA, 0xB4, 0x08, 0x01, mode, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, cs, 0xAB};
// ^^ 0 for continuous, 1-30 for 1-30 min delay between turns on. ^^ checksum: 07 for 0x00, 08 for 0x01 and so on
for (uint8_t i = 0; i < 19; i++) {
mySerial.write(sleep_command[i]);
}
}
////////////////// sends SLEEP command to SDS011 //////////////////////
void SDS011sleep()
{
uint8_t sleep_command[] = {0xAA, 0xB4, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x05, 0xAB};
for (uint8_t i = 0; i < 19; i++) {
mySerial.write(sleep_command[i]);
}
}
////////////////// sends WAKEUP command to SDS011 //////////////////////
void SDS011wakeup()
{
uint8_t wakeup_command[] = {0xAA, 0xB4, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x06, 0xAB};
for (uint8_t i = 0; i < 19; i++) {
mySerial.write(wakeup_command[i]);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup()
{
Serial.begin(9600);
mySerial.begin(9600);
Wire.begin();
SDS011wakeup();
delay(10);
SDS011wakeup();
if (automode == false) {SDS011workmode(0);} else {SDS011workmode(mode);};
TFTscreen.begin();
delay(100);
TFTscreen.setRotation(1);
TFTscreen.background(20,0,0);
TFTscreen.setTextSize(2);
TFTscreen.stroke(0,250,250);
TFTscreen.text("Pm10", 2, 2);
TFTscreen.stroke(180,10,250);
TFTscreen.text("Pm2.5", 2, 32);
// tft_graph();
if (automode == false) { //(mode != 0)&&(
delay(30000);
// SDS011sleep();
}
ProcessSerialData();
print_Pm();
tft_output();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void loop()
{
getdate();
if (automode == false) {
if (((current_minute+work_time)%work_period == 0)&&(active == false)) {
SDS011wakeup();
active = true;
};
if (active == true) {
ProcessSerialData();
loops ++;
Pm25sum += Pm25;
Pm10sum += Pm10;
// /*
Serial.print (loops);
Serial.print (" Pm25 = ");
Serial.print (Pm25);
Serial.print (" Pm10 = ");
Serial.println (Pm10);
// */
};
if (((current_minute)%work_period == 0)&&(active == true)) {
SDS011sleep();
active = false;
Pm25 = Pm25sum/loops;
Pm10 = Pm10sum/loops;
Pm25sum = 0;
Pm10sum = 0;
loops = 0;
print_Pm();
tft_output();
tft_graph();
};
} else { // if sensor goes on and off by itself
if (((current_minute)%mode == 0)&&(lastminute != current_minute)) {
ProcessSerialData();
print_Pm();
tft_output();
tft_graph();
lastminute = current_minute;
}
} // if sensor goes on and off by itself
delay(1000);
} // loop
Товар предоставлен для написания обзора магазином. Обзор опубликован в соответствии с п.18 Правил сайта.
Вот немного дешевле
С большим удовольствием проникся темой которая меня уже давно интересует (дочка астматик-аллергик) и качество воздуха очень интересно.
Жаль у автора нет дома системы очистки воздуха, с ней все веселей.
Я смотрел в сторону
но 5рублей пока пресноводное не дает потратить.
Под брендом xiaomi вроде бы вот такой у этого же продавца все измеряет хорошо
Единственное, что у него только rx-tx, выхода pwm нет. Но в большинстве случаев этого порта хватает. И нет проблем с пересчетом ))
Особенно шокирован был показаниями датчика при включении ультразвукового увлажнителя. С тех пор включаю только когда влажность совсем низкая (раньше работал почти постоянно весь отопительный сезон).
Да и думается мне, что аэрозоль очень быстро испаряется.
Или как вы объясните появление пыли от ультразвукового увлажнителя?
Обсуждалось тыщу раз на профильных форумах. УЗ увлажнитель распыляет растворённые примеси — ну там кальций и всё прочее, что у вас в чайнике откладывается после кипячения. В УЗ надо заливать дистиллят, или как максимум воду после обратного осмоса.
Может, и не полезно этим дышать.
От них по-любому вреда нет, это ж вода.
Что?
Ну как бэ он даёт аэрозоль воды в воздухе (это такие мелкие-мелкие капельки, а не пар-газ), которую и регистрирует датчик.И что тут странного????? И что тут вредного? Если вода чистая конечно для лёгких.
Ну так вроде давно уже водопроводную воду перестали заливать в такие увлажнители.Как появился осмос.
(если вода чистая конечно для лёгких).
Впрочем и соль из воды не сильно вредна для человека.
Дистиллят постоянно пить вреднее.
Ещё один свидетель вымывания солей…
Заливал дистиллят для эксперимента, картина похожая, но показания не такие большие. Может, это подтверждает то, что «шкалит» действительно аэрозоль. А может растворяется налёт в увлажнителе.
В итоге ради перестраховки стал включать увлажнитель сильно меньше.
В инете внятного ничего на эту тему не нашёл. Как провести более точное исследование, пока не знаю :)
Но сам факт довольно интересен. Возможно, это можно использовать для проверки датчиков пыли.
Были б на i2c шине.
Если делать измеритель качества воздуха то не очень понятно как 2 датчика с uart подключить.
Есть еще датчик пыли от Sensirion, из плюсов — можно подключать по I2C, из минусов — стоит как крыло от Шаттла.
И как их обслуживать? Неужели можно прям со всех щелей там достать всю пыль?
А вот остальные «игрушки» явно для применения в бытовых очистителях воздуха как показометры.Их и чистить проще и реже.Внутри они не отличаются от дымовых пожарных датчиков.ИК излучающий диод и фотодиод в лабиринте под углом друг к другу.Ну ещё для селекции размера частиц могли применить микро щелевые диафрагмы и дифракционные фильтры.С их чисткой могут быть нюансы, если тонкие и нежные или на органической плёнке растворимой в спирту итд.
Ничего сложного в чистке нет.По быстрому продуваете компрессором(пылесосом на выдув, грушей-клизмой...).Ну раз в квартал.
ЕСЛИ они у вас работают постоянно, круглосуточно и вам нужна точность.
Раз в год, в 2 года, разбираете (полная разборка) и протираете ватными палочками и зубочистками с ватой и спиртом внутри.Потому что часть пыли жирная(все типы сажи, не считая масел и пыльцы, частиц кожи итд. итп.) и прилипает к поверхностям фотодиода и лазеров намертво.
Лопасти вентилятора тоже чистятся, как и сам вентилятор(я их мою в ПК стекломоем на спирту с ПАВ, с распылителя, сняв предварительно с устройства конечно).
Ну уметь тоже надо, учиться… В.И.Л.
По моим ощущениям, воздух — весьма инерционная система, а тут фиксируются события по 20 секунд в другом конце квартиры.
Может пыль поднимал не пылесос, а вибрации от перфоратора?
Может быть попробовать включить строительный пылесос в максимально герметичном объёме? Заклеить скотчем в картонную коробку. Или в мешок от мусора?
Ну и попробовать включить его без перфоратора…
Лежит в столе датчик nova, надо бы на выходных его к погодке всё же приделать…
Вот к примеру графики моего за сутки:
Сходимость данных с дорогущим BAM1020 очень высокая. Так что можно смело использовать ))
Если у Вас есть — может выложите куда-нибудь? На яндекс-диск например?
1) Вы как-то боритесь с тем что лазер имеет ограниченный срок жизни? К примеру включаете раз в 5 минут или что-то подобное
2) Корпус. Как их нормально закрепить? Жалательно фото :)
А показал он значения больше 700 по pm2.5…
В доме при этом было видно едва различимый дым
aliexpress.com/item/PM1-0-PM-2-5-PM10-9/32929301485.html
Вроде работает.
Датчик PMS5003 — народ хвалит его.
Хотя я бы лучше сейчас купил устройство еще и с датчиком улекислого газа:
aliexpress.com/item/Indoor-Outdoor-PM2-5/32887570197.html
Вариант с выносным датчиком становится интереснее, особенно в контексте умного дома, если есть управление отоплением, вентиляцией и увлажнителем/очистителем воздуха.
При наличии в контроллере DMA все еще проще — сказать ему, чтобы он получил нужное количество результатов АЦП, и подождать пока он завершит :)