Mr.Doors


Rambler's Top100



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

Arduino_logoCоздание домашнего портативного анализатора качества воздуха (CO2, влажность, температура, давление) на базе Ардуино.

Вступление

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

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

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

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

 

 

Нам понадобятся:

1. Arduino Pro Mini

2. Датчик влажности и температуры DHT-22 он же AM2302 (улучшенная точность по сравнению с DHT-11)

3. Датчик давления BMP180

4. Модуль с датчиком углекислого газа TGS4161

5. Дисплей типа Nokia 5110

Примечание по пункту 5 :  почему не модуль-дисплей для Arduino, типа этого. У меня есть и такой, стоимость выше нокиевского, а качество — углы обзора — хуже, большие габариты. Модуль на основе нокиевского дисплея имеет высокий контраст (удобно использовать без подсветки даже в сумерках, а на солнце так вообще супер), больше помещается информации и вроде как меньше проводов для подключения. Да еще и низкое энергопотребление.

Суммарно стоимость компонентов окажется около 70 $ (промышленные приборы с похожим набором датчиков стоят от 300$)

Компоненты для сборки вы можете прибрести на всем известном ныне Aliexpress либо в других магазинах, выше в списке я дал пару ссылок на магазин Banggood

Arduino_board

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

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

arduino_sensors


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

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

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

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

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

arduino_device

 

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

arduino_compilation

 

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

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

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

oscillogramme

 


Рабочий скетч для 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);
 
}

 

Автор статьи: St007

Источник: habrahabr.ru

 

Lamoda RU