Как стать автором
Обновить

Ардуино на службе здоровья

Время на прочтение 10 мин
Количество просмотров 119K
В статье описан путь от идеи до создания домашнего портативного анализатора качества воздуха (CO2, влажность, температура, давление).

Вступление


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

Рассуждая таким образом я пришел к выводу, что не плохо было бы соорудить на базе Ардуины такой себе показометр влажности. Пользуясь случаем и дешевизной цифровых датчиков, грех было не добавить еще и функцию барометра, термометра и, что немаловажно, функцию измерения количества углекислого газа в воздухе (в старых многоэтажках зачастую плохая вентиляция в квартирах, а сами квартиры на зиму плотно герметизируются). По углекислому газу тоже много страшилок, снижение работоспособности, головная боль, это могут быть симптомы высокой концентрации СО2 в воздухе.

В интернетах так же была найдена такая табличка допустимых концентраций СО2 в воздухе:

Сводная таблица допустимых значений концентрации СО2 в воздухе

Итак, нам понадобится:
  • 1. Arduino Pro Mini цена 2.15$
  • 2. Датчик влажности и температуры DHT-22 он же AM2302 (улучшенная точность по сравнению с DHT-11), цена 4$
  • 3. Датчик давления BMP180, цена 2.98$
  • 4. Модуль с датчиком углекислого газа TGS4161, цена 60$
  • 5. Дисплей типа Nokia 5110, цена 2,35$


Итого: 71.48$ (промышленные приборы с похожим набором датчиков стоят от 300$)

Компоненты для сборки

Датчик углекислого газа

На датчике углекислого газа следует остановиться отдельно. В то время как датчик давления и влажности выдают показания уже в цифре, имеют какие-никакие цепи стабилизации по питанию, то стоимость полностью цифрового датчика СО2 начинается от 120$, многовато как «для поиграться». Самый дешевый в виде модуля к Ардуине китайцы предлагают на базе MG811 за 36$. Посмотрел я на его характеристики, а там дикая зависимость от влажности, наличия посторонних газов в воздухе и решил, что нужно поискать что-то более точное и таким оказался TGS4161, привожу его характеристики:

MG811

Успокоившись на счет стабильности показаний, оформил покупку и датчик приехал через две недели.

Сборка устройства

Когда модуль ко мне приехал, у меня уже все было собрано на макетной плате, не хватало только его. Вооружившись паяльником я обратил свой взор к рекламной картинке (см. выше), мол как его подключать? Вот тут пришлось немного опешить, надежда, что Dout это цифровой выход, не оправдалась, документации на этот модуль вообще нет никакой и нигде, модуль делают китайцы штучно. Что же делать? Пришлось тыкать в ноги модуля тестером, дуть на датчик, привлекать такую-то мать, писать кролику… Методом тыка установлено что модуль содержит два операционных усилителя. Первый из них имеет входное сопротивление 1,5 ТОм (Вы не ошиблись именно тера-Ом, т.е. 1500 гиг-Ом, тогда как обычный цифровой мультиметр имеет входное сопротивление не более 20 мега-Ом), второй сдвоенный ОУ работает как компаратор и повторитель. Aout некий аналоговый сигнал с повторителя, Dout – сигнал с компаратора (активен если СО2 зашкаливает). Назначение вывода TCM установить не удалось.

Как же датчик работает?

Датчик представляет собой электрохимическую ячейку для работы которой нужна высокая температура. Температура обеспечивается встроенным в датчик нагревателем мощностью примерно 0,2Вт. Напряжение на ячейке при концентрации СО2 350ppm и ниже имеет некое стабильное значение, а когда концентрация СО2 растет, напряжение на ячейке тоже изменяется (падает). Для согласования высокого выходного сопротивления ячейки и с целью усиления напряжения применены ОУ.

Чтобы воспользоваться датчиком нужно перевести вольты в концентрацию СО2. Документация на сенсор утверждает, что начальное напряжение на сенсоре может быть любым а вот абсолютное изменение этого напряжения строго по даташиту. Значит нужно как минимум замерить нулевую начальную точку на свежем воздухе, дальше, построить модель сенсора на основе графика его чувствительности задав в качестве точки отсчета измеренное значение. В общем худо-бедно, модель была построена, датчик откалиброван и можно было все собирать вместе в какой-нибудь корпус:

Это первый блин, сильно не ругать

Внутри прибора - лапша

Чувствительность свеже-откалиброванного датчика была такой что он мог достоверно отличать более свежий воздух в парку от загазованного проспекта за 300 метров.

По остальным датчикам: влажность меряет отлично, взяв в руки прибор сразу можно заметить рост влажности, прям таки скачек (еще бы, когда в квартире сейчас влажность всего 29%); давление тоже меряет отлично, как абсолютное так и относительное, подняв и опустив прибор можно заметить разницу в давлении.

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

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

И это только начало

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]='\0';
  outstr[0]='\0';

  if(val < 0.0){
    strcpy(outstr,"-\0");
    val = -val;
  }

  val += roundingFactor;

  strcat(outstr, itoa(int(val),temp,10));  //prints the int part
  if( precision > 0) {
    strcat(outstr, ".\0"); // 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\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++] = '\0';
    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 fail\n\n");
    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 measurement\n");
      }
      else LcdString("error starting pressure measurement\n");
    }
    else LcdString("error retrieving temperature measurement\n");
  }
  else LcdString("error starting temperature measurement\n");

  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);
  
}

Теги:
Хабы:
+47
Комментарии 37
Комментарии Комментарии 37

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн