아두이노/아두이노 일반

샤오미 공기청정기에 사용된다는 미세먼지 센서를 이용한, 아두이노 초미세먼지 측정기 만들기(PMS 7003)

아크리엑터 2019. 12. 29. 20:24
반응형

개요

미세먼지 센서를 이용해서 지금 공기중의 미세먼지를 측정해보자. 미세먼지 센서는 검색을 하다 보니 PMS 7003 이라는 센서가 좋은 것 같아서 비싼 가격이지만 사용해 봤다. 가격이 싼 미세먼지 센서도 있지만, 인터넷 어디선가에서 샤오미 공기청정기에 사용되는 미세먼지 센서라고 강추를 하는 글을 보고, 측정기는 비싼 부품을 사용해야지 정확한 값을 구할 수 있다는 생각에서 본 모듈로 정했다.

이 모듈을 구매할 때는 인터페이스 보드도 꼭 같이 사야한다. 그것이 없으면 선을 뽑아내서 PCB에 연결하기가 너무 어려운 것 같다. 소켓이 같이 들어있어서 이것을 PCB에 연결해서 납땜질을 했는데.... 너무 쉽게 부러져서, 누구든 물어보면 인터페이스 보드도 꼭 같이 사라고 얘길한다. 1천원 더 비쌌었나.... 아니면 몇 백원 더 비샀었나.... 

 

부품

  - 미세먼지 센서(PMS-7003) https://www.aliexpress.com/item/32817074743.html

 

US $18.3 |Laser PM2.5 DUST SENSOR PMS7003 / G7 High precision laser dust concentration sensor digital dust particles-in Sensors

Smarter Shopping, Better Living! Aliexpress.com

www.aliexpress.com

 

미세먼지 센서는 아래와 같이 생겼는데,  파란색 위에 놓은 기판이 인터페이스 보드임. 인터페이스 보드 없이 사용하려면, 왼쪽 상단에 있는 소켓에서 선을 연결하면 되긴한다. 모양이 좀 안 좋아서 그렇지, 작동하는데는 전혀 문제 없다.

이 미세먼지 센서에 있는 레이저LED(?)의 수명이 1년이라고 하는데, 긴 것인가 모르겠다. 1년이 길다고 하는데 종일 켜두는 공기청정기의 경우에는 종일 켜두는 경우도 있는데, 1년 수명은 금방 갈듯하다. 물론, 고장이 나면 미세먼지 센서는 교체를 해야 하는 소모품이 될 것 같다.

이 미세먼지 센서의 구조는 팬을 돌려서, 외부의 공기를 센서내로 끌어들이도록 되어있고, 이 공기가 들어온 내부 공간에는 레이저 빔을 넣어서 산란(?)되는 수준으로 먼지가 얼마 있는지를 찾는 것 같다. 물론, 돋보기 렌즈가 이를 도와준다. 미세먼지 입자의 크기와 산란되는 광의 크기가 비례한다.

https://www.aliexpress.com/item/32817074743.html

 

  - 컬러 TFT LCD 1.44인치(128*128, ST7735) https://www.aliexpress.com/item/33031122936.html

 

US $2.85 |3.3V 1.44 1.8 inch Serial 128*128 128*160 65K SPI Full Color TFT IPS LCD Display Module Board Replace OLED ST7735-in L

Smarter Shopping, Better Living! Aliexpress.com

www.aliexpress.com

측정한 초미세먼지 값을 화면에 출력하기 위한 LCD화면은 컬러로 정했다. 미세먼지 수치에 따라 표시될 색을 다르게 표시하여 미세먼지 수치가 좋은지 나쁜지를 색상으로 바로 인식할 수 있도록 할 목적이다.

 

 

  - 슬라이드 스위치(3PIN)  https://www.aliexpress.com/item/32964400942.html

 

US $0.29 |20Pcs Interruptor on off mini Slide Switch SS12D00 SS12D00G3 3pin 1P2T 2 Position High quality toggle switch Handle le

Smarter Shopping, Better Living! Aliexpress.com

www.aliexpress.com

이것은 단순히 전원을 껐다가 켤수 있는 스위치. 당연히 다른 것으로 교체해도 된다. 

미세먼지 측정기를 가지고 다니면서 언제든지 측정할 수 있도록 하기 위해 배터리를 사용하였는데, 미세먼지 센서를 돌리고 LCD화면을 계속 켜두려고 하니 배터리가 빨리 닳아서, 전원 스위치를 추가하게 되었다.

추가로 고민을 했던 것은 거실에 공기 순환기가 설치되어있는데, 이 센서를 이용하여 거실의 미세먼지 수치가 높아지면 자동으로 공기 순환기가 작동되도록 하려는 생각도 했었다. 그런데, 거실에 있는 공기순환기 작동하는 것이 적외선리모컨이 될 줄 알았는데, 물리적으로 버튼을 눌러야 하는 제품이라서, 포기를 했다. (내 집이라면 컨트롤러 뜯어서 작동되게 했을 텐데, 전세집인지라 나중에 물리적으로 버튼을 누를수 있도록 하는 놈을 하나 만들면 자동화를 하자는 생각으로 포기했다)

브레드보드

TFT-LCD와 먼지센서 연결에는 일반적인 커넥터 및 핀헤더를 사용하였다.(특별한 언급 아님)

지금은 이전에 개발한 것을 역으로 정리를 하고 있는데, 미세먼지 센서의 RX,TX 핀이 기억이 가물가물해서, 아래 배선과 같이 연결을 해서 되지 않는다면, 먼지센서가 연결되는 6번, 7번 핀을 바뀌어야 될 수도 있다. 

 

 

회로

 

 

코드

 

#include <TFT.h>  // Arduino LCD library
#include <SPI.h>
#include <SoftwareSerial.h>

SoftwareSerial mySerial(7,6); // Arudino Uno port RX, TX

// pin definition for the Uno
#define cs   10
#define dc   9
#define rst  8

#define MIN_HEIGHT 2
#define MIN_WIDTH  3
#define MAX_HEIGHT 126
#define MAX_WIDTH  128

#define XCONV(x) (x+MIN_WIDTH)
#define YCONV(y) (y+MIN_HEIGHT)

#define SUBTITLE_MAX_WIDTH 65
#define SUBTITLE_MAX_HEIGHT 35

#define TITLE_MIN_XPOS 0
#define TITLE_MIN_YPOS 0
#define TITLE_MAX_XPOS MAX_WIDTH
#define TITLE_MAX_YPOS 20

#define TITLE_PM1_MIN_XPOS TITLE_MIN_XPOS
#define TITLE_PM1_MIN_YPOS TITLE_MAX_YPOS
#define TITLE_PM1_MAX_XPOS SUBTITLE_MAX_WIDTH
#define TITLE_PM1_MAX_YPOS SUBTITLE_MAX_HEIGHT

#define TITLE_PM2_MIN_XPOS TITLE_MIN_XPOS
#define TITLE_PM2_MIN_YPOS TITLE_PM1_MIN_YPOS+SUBTITLE_MAX_HEIGHT-1
#define TITLE_PM2_MAX_XPOS SUBTITLE_MAX_WIDTH
#define TITLE_PM2_MAX_YPOS SUBTITLE_MAX_HEIGHT

#define TITLE_PM10_MIN_XPOS TITLE_MIN_XPOS
#define TITLE_PM10_MIN_YPOS TITLE_PM2_MIN_YPOS+SUBTITLE_MAX_HEIGHT-1
#define TITLE_PM10_MAX_XPOS SUBTITLE_MAX_WIDTH
#define TITLE_PM10_MAX_YPOS SUBTITLE_MAX_HEIGHT

#define TEXT_SIZE 2

#define VALUE_XPOS 30
#define VALUE_YPOS 25

#define VALUE10_LEVEL1 30
#define VALUE10_LEVEL2 80
#define VALUE10_LEVEL3 150

#define VALUE2_LEVEL1  15
#define VALUE2_LEVEL2  35
#define VALUE2_LEVEL3  75

#define BGCOLOR_LEVEL1()  TFTscreen.fill(0, 0, 255)
#define BGCOLOR_LEVEL2()  TFTscreen.fill(0, 255, 0)
#define BGCOLOR_LEVEL3()  TFTscreen.fill(255, 60, 0)
#define BGCOLOR_LEVEL4()  TFTscreen.fill(255, 0, 0)

#define FGCOLOR_LEVEL1()  TFTscreen.stroke(255, 255, 0)
#define FGCOLOR_LEVEL2()  TFTscreen.stroke(0, 0, 0)
#define FGCOLOR_LEVEL3()  TFTscreen.stroke(0, 0, 0)
#define FGCOLOR_LEVEL4()  TFTscreen.stroke(0, 0, 0)

int pm1, pm2, pm10;

TFT TFTscreen = TFT(cs, dc, rst);

void printTitle()
{
  TFTscreen.fill  (50, 50, 50);
  TFTscreen.rect(XCONV(TITLE_MIN_XPOS), YCONV(TITLE_MIN_YPOS), TITLE_MAX_XPOS, TITLE_MAX_YPOS);

  TFTscreen.setTextSize(2);
  TFTscreen.stroke(255, 255, 255);
  TFTscreen.text("Dust Value", XCONV(TITLE_MIN_XPOS)+3, YCONV(TITLE_MIN_YPOS)+2);
}
void printPM1(char *msg, int value)
{
  char buf[10];
  sprintf(buf, "%2d", value);

  if      (value <= VALUE2_LEVEL1) BGCOLOR_LEVEL1();
  else if (value <= VALUE2_LEVEL2) BGCOLOR_LEVEL2();
  else if (value <= VALUE2_LEVEL3) BGCOLOR_LEVEL3();
  else                             BGCOLOR_LEVEL4();

//  TFTscreen.fill(190, 170, 200);
  TFTscreen.stroke(0, 0, 0);
  TFTscreen.rect(XCONV(TITLE_PM1_MIN_XPOS), YCONV(TITLE_PM1_MIN_YPOS)-1, 
                 TITLE_PM1_MAX_XPOS, TITLE_PM1_MAX_YPOS);
//  TFTscreen.fill(50, 50, 50);
  TFTscreen.rect(XCONV(TITLE_PM1_MAX_XPOS)-1, YCONV(TITLE_PM1_MIN_YPOS)-1, 
                 MAX_WIDTH-TITLE_PM1_MAX_XPOS+1, TITLE_PM1_MAX_YPOS);

  TFTscreen.setTextSize(TEXT_SIZE);
//  TFTscreen.stroke(255, 255, 255);
  if      (value <= VALUE2_LEVEL1) FGCOLOR_LEVEL1();
  else if (value <= VALUE2_LEVEL2) FGCOLOR_LEVEL2();
  else if (value <= VALUE2_LEVEL3) FGCOLOR_LEVEL3();
  else                             FGCOLOR_LEVEL4();

  TFTscreen.text(msg, XCONV(TITLE_PM1_MIN_XPOS)+2, YCONV(TITLE_PM1_MIN_YPOS)+10);
  TFTscreen.text(buf, XCONV(TITLE_PM1_MAX_XPOS)+3, YCONV(TITLE_PM1_MIN_YPOS)+10); 
  TFTscreen.setTextSize(1);   
  TFTscreen.text("ug/m3", XCONV(TITLE_PM1_MAX_XPOS)+VALUE_XPOS, YCONV(TITLE_PM1_MIN_YPOS)+VALUE_YPOS);                 
}

void printPM2_5(char *msg, int value)
{
  char buf[10];
  sprintf(buf, "%2d", value);

  if      (value <= VALUE2_LEVEL1) BGCOLOR_LEVEL1();
  else if (value <= VALUE2_LEVEL2) BGCOLOR_LEVEL2();
  else if (value <= VALUE2_LEVEL3) BGCOLOR_LEVEL3();
  else                             BGCOLOR_LEVEL4();
  
  TFTscreen.stroke(0, 0, 0);
  TFTscreen.rect(XCONV(TITLE_PM2_MIN_XPOS), YCONV(TITLE_PM2_MIN_YPOS)-1, 
                 TITLE_PM2_MAX_XPOS, TITLE_PM2_MAX_YPOS);
  TFTscreen.rect(XCONV(TITLE_PM2_MAX_XPOS)-1, YCONV(TITLE_PM2_MIN_YPOS)-1, 
                 MAX_WIDTH-TITLE_PM2_MAX_XPOS+1, TITLE_PM2_MAX_YPOS);

  if      (value <= VALUE2_LEVEL1) FGCOLOR_LEVEL1();
  else if (value <= VALUE2_LEVEL2) FGCOLOR_LEVEL2();
  else if (value <= VALUE2_LEVEL3) FGCOLOR_LEVEL3();
  else                             FGCOLOR_LEVEL4();

  TFTscreen.setTextSize(TEXT_SIZE);
//  TFTscreen.stroke(0, 0, 0);
  TFTscreen.text(msg, XCONV(TITLE_PM2_MIN_XPOS)+2, YCONV(TITLE_PM2_MIN_YPOS)+10);
  TFTscreen.text(buf, XCONV(TITLE_PM2_MAX_XPOS)+3, YCONV(TITLE_PM2_MIN_YPOS)+10);    
  TFTscreen.setTextSize(1);
  TFTscreen.text("ug/m3", XCONV(TITLE_PM2_MAX_XPOS)+VALUE_XPOS, YCONV(TITLE_PM2_MIN_YPOS)+VALUE_YPOS);                 
}

void printPM10(char *msg, int value)
{
  char buf[10];
  sprintf(buf, "%2d", value);
  
  if      (value <= VALUE10_LEVEL1) BGCOLOR_LEVEL1();
  else if (value <= VALUE10_LEVEL2) BGCOLOR_LEVEL2();
  else if (value <= VALUE10_LEVEL3) BGCOLOR_LEVEL3();
  else                              BGCOLOR_LEVEL4();

  TFTscreen.stroke(0, 0, 0);
  TFTscreen.rect(XCONV(TITLE_PM10_MIN_XPOS), YCONV(TITLE_PM10_MIN_YPOS)-1, 
                 TITLE_PM10_MAX_XPOS, TITLE_PM10_MAX_YPOS);
  TFTscreen.rect(XCONV(TITLE_PM10_MAX_XPOS)-1, YCONV(TITLE_PM10_MIN_YPOS)-1, 
                 MAX_WIDTH-TITLE_PM10_MAX_XPOS+1, TITLE_PM10_MAX_YPOS);

  if      (value <= VALUE10_LEVEL1) FGCOLOR_LEVEL1();
  else if (value <= VALUE10_LEVEL2) FGCOLOR_LEVEL2();
  else if (value <= VALUE10_LEVEL3) FGCOLOR_LEVEL3();
  else                             FGCOLOR_LEVEL4();

  TFTscreen.setTextSize(TEXT_SIZE);
//  TFTscreen.stroke(255, 255, 255);
  TFTscreen.text(msg, XCONV(TITLE_PM10_MIN_XPOS)+2, YCONV(TITLE_PM10_MIN_YPOS)+10);
  TFTscreen.text(buf, XCONV(TITLE_PM10_MAX_XPOS)+3, YCONV(TITLE_PM10_MIN_YPOS)+10);    
  TFTscreen.setTextSize(1);
  TFTscreen.text("ug/m3", XCONV(TITLE_PM10_MAX_XPOS)+VALUE_XPOS, YCONV(TITLE_PM10_MIN_YPOS)+VALUE_YPOS);                 
}

void printScreen()
{
  TFTscreen.background(0, 0, 0);

  TFTscreen.stroke(100, 100, 100);

  printTitle();
  
  printPM1  ("PM1"  , 0);
  printPM2_5("PM2.5", 0);
  printPM10 ("PM10" , 0);
  

}


int getDustValue()
{
  int chksum=0,res=0;
  unsigned char pms[32]={0,};
  char buf[10];
  
  
  if(mySerial.available()>=32){
    chksum = 0;
    pms[0] = mySerial.read();
    if(pms[0]!=0x42) return;
    chksum += pms[0];
    
    pms[1] = mySerial.read();
    if (pms[1]!=0x4d ) return;
    chksum += pms[1];

    for(int j=2; j<32 ; j++){
      pms[j]=mySerial.read();
      if(j<30) chksum+=pms[j];
    }
//    Serial.println();
//
//    Serial.print("check sum = ");
//    Serial.print(pms[30]);
//    Serial.print(pms[31]);
//
//
//    Serial.print("  ");
//    Serial.print(chksum);
//
//    Serial.print("  ");
//    Serial.print(chksum>>8);
//    Serial.println(chksum);
//    
    
    if(pms[30] != (unsigned char)(chksum>>8) 
        || pms[31]!= (unsigned char)(chksum) ){
      Serial.println("checksum error");
      return res;
    }
    if(pms[0]!=0x42 || pms[1]!=0x4d ) {
      Serial.println("start value error");
      return res;
    }

  pm1 = pms[10]*10 + pms[11];
  pm2 = pms[12]*10 + pms[13];
  pm10 = pms[14]*10 + pms[15];

  Serial.print("Dust raw data debugging :  ");
//  Serial.print(pm1);
//  Serial.print(" ");
//  Serial.print(pm2);
//  Serial.print(" ");
//  Serial.print(pm10);
//  Serial.print(" ");
  
  Serial.print("1.0ug/m3:");
  Serial.print(pms[10]);
  Serial.print(pms[11]);
  Serial.print("  ");
  Serial.print("2.5ug/m3:");
  Serial.print(pms[12]);
  Serial.print(pms[13]);
  Serial.print("  ");
  Serial.print("10ug/m3:");
  Serial.print(pms[14]);
  Serial.println(pms[15]);
  } 
  return 1;
}

void setup() {
  // initialize the serial port
  Serial.begin(9600);
  mySerial.begin(9600);

  Serial.println("Program Starting...");
  // initialize the display
  TFTscreen.begin();
  TFTscreen.setRotation(4);

  pm1 = pm2 = pm10 = 0;
  
  printScreen();
}

void loop() {
  static int old_pm1=0, old_pm2=0, old_pm10=0;

  getDustValue();

  if (pm1  != old_pm1)  printPM1  ("PM1"  , pm1);
  if (pm2  != old_pm2)  printPM2_5("PM2.5", pm2);
  if (pm10 != old_pm10) printPM10 ("PM10" , pm10);
 
  old_pm1 = pm1;
  old_pm2 = pm2;
  old_pm10 = pm10;
  
  delay(100);
}

결과물

만능기판에 Wire로 연결해서 만들어 봄
LCD를 보드에 연결한 화면

 

케이스에 넣어서 미세먼지 측정한 화면

위의 사진처럼, PMS7003 미세먼지 센서는  PM1, PM2.5, PM10  을 측정할 수 있다. 초미세먼지라는 기준이 PM2.5 이하로 생각했는데, PM1도 측정이 가능하구만...

케이스를 만들어 넣을 때는 미세먼지 센서의 공기 입출입구를 열어주어야 한다.  

이번에 만든 미세먼지 센서를 이용해서, 운전중의 차량내의 미세먼지를 측정해봤더니, 차안의 공기는 아주 안정적으로 좋음 상태였다. 에어컨 필터에서 미세먼지를 거의 막아주는 것 같았다. 당연히, 이것을 알았기에 차량용 미세먼지를 막아주는 공기청정기를 별도로 구입할 필요가 없다는 것도 증명을 했다.

집안의 미세먼지를 측정하니, 집에서 요리를 할 때, 특히 삼겹살 등 고기를 구울 때는 100이 넘는 수치가 나왔었고, 이때는 공기청정기를 돌려도 쉽게 수치가 낮아지지는 않았었다. 고기를 구울 때는 창문을 열어두고 굽고, 약 15분 정도 환기시켜 준후에 문을 닫고 공기청정기를 돌리는 식으로 사용하고 있다.

미세먼지센서를 공기청정기의 바람이 나오는 곳에 가져가니, 미세먼지 수치가 0 수준으로 떨어지는 것을 보니, 공기청정기가 잘 작동하고 있었다.  랜탈로 사용하는 공기청정기라서 신뢰도가 낮았었는데, 직접 측정해보니,... 신뢰도가 높아졌다.

반응형