임시 블로그 이름

아두이노-DHT11 소자에서 온도/습도 읽어오기 본문

엔지니어링

아두이노-DHT11 소자에서 온도/습도 읽어오기

paeton 2015. 9. 30. 14:19

아두이노 공기질 측정기를 만들기 전에, 미리 아두이노에 익숙해 져야겠다는 생각이 들었다.

패키지로 온 여러 소자들을 뒤져보다 보니 이상하게 생긴 소자가 있길래 뭔가 하고 검색을 해보니 온도/습도를 측정해서 출력해주는 센서다.


검색을 해보니 HardCopyWorld라는 곳에 한글로 정리가 잘 되어있어서 참고 했다.

같이 보면 좋을것 같다.

http://www.hardcopyworld.com/ngine/aduino/index.php/archives/190


또, 물론 이 소자를 위한 아두이노 라이브러리가 존재한다.

스케치 프로그램에서 Sketch –> Include Library –> Manage Library로 가서 DHT11을 검색하면, 이 소자를 구동시키기 위한 라이브러리를 쉽게 인스톨 할 수 있다.

다른 사람이 만들어 놓은 라이브러리를 잘 가져다 쓰는 것도 물론 중요하지만, 그 이전에 본인이 직접 해보지 않으면 본인 것으로 만들기 쉽지 않다.

기억을 위해서, 정리를 위해서 여기에 기록을 남겨둔다.



A. 소자

1. 소자 생김새


imageimage3

데이터 시트에 있는 그림이다.


왼쪽 그림을 자세히 보면 왼쪽부터 VCC, GND, S라고 적혀있는데, VCC는 바이어스 전압으로 아두이노에서 나오는 5V전압 포트를 연결하면 되고, GND는 그라운드 전압이다. 이것도 아두이노에 있는 GND포트에 연결해주면 된다. 그리고 S라고 되어있는 포트는 아무래도 Signal을 의미하는 것 같은데, 이걸로 소자와 보드가 통신을 한다. 따라서 이걸 아두이노에 있는 DIGITAL PORT에 연결해주면 된다. 그리고 저 포트의 순서는 보드를 만들어파는 제조사 마다 다른 것 같다.


오른쪽 그림처럼 보드 없이 딸랑 센서만 파는 경우도 있는데, 다리 4개 중에 1개는 안쓰는 다리고, Pull-Up 저항같은 것들을 본인이 직접 만들어줘야 되므로 귀찮다.


2. 소자의 스펙

전자부품을 작동시키기 위해서는 먼저 데이터시트를 봐야 한다.

그래야 이 부품의 작동 환경, 작동 방법, 스펙등을 확인하고 적절하게 작동시킬 수 있기 때문이다.

데이터 시트는 여기에 있다. http://www.micropik.com/PDF/dht11.pdf


데이터 시트를 보면, 먼저 이 소자의 스펙이 나와있다.

image6

image9


습도 (RH, Relative Humidity)는 20% ~ 90% 까지 잴 수 있고, 온도는 섭씨 0도 ~ 50도 까지 측정 할 수 있다.

오차는 습도의 경우 5%이고, 온도의 경우는 2도이다. 그닥 정확한 소자는 아니라는 이야기다.


그리고, 좀 더 디테일한 것에서 보면, 두 값 모두 해상도가 8bit인것을 알 수 있다.

좀 이상한게, 소자에서 출력으로 주는 값은 온도, 습도 각각 16bit 출력을 준다. (뒤에 바로 설명한다)

아마도 출력은 크게 만들어 놓고, 스펙만 제한해 놓은것 같다.


3. 데이터 형식

그리고 그 다음은 어떻게 소자와 통신하는지를 알려준다.

Single-Wire Two-Way라고 써있으므로 1개 신호 라인으로 서로 신호를 주고 받고 하는 것으로 보인다. (그게 바로 serial 통신이다)

image14


총 40bit의 데이터를 전달한다고 되어있고, 가장 높은 bit를 먼저 전달한다고 되어있다.

데이터 [40bit]: 습도 정수부 [8bit] + 습도 소수부 [8bit] + 온도 정수부 [8bit] + 온도 소수부 [8bit] + 체크섬 [8bit]


전송된 데이터의 에러 체크를 위해서 체크섬을 제공하는데, 그냥 앞에 32개 비트를 모두 자리 맞춰서 더하면 체크섬이 나오는지 확인하면 된다는 이야기다.


4. 구동 방법

image18

위 그림은 전체적으로 어떻게 소자와 통신하는지 나와있는 그림이다.

전체적으로 보면, 아두이노에서 소자에 일정한 자극을 주면, 이에 반응해서 소자가 아두이노로 데이터를 날려주는 구조다.

MCU Signal은 아두이노에서 소자에 주는 신호이고,

DHT Signal은 아두이노 신호에 반응해서 소자에서 아두이노로 날려주는 신호다.


데이터 시트를 자세히 읽어보면, 소자는 평소에는 저전력모드로 있다가, 아두이노에서 자극을 주면 온도와 습도를 체크해서 데이터를 날려주고 다시 저전력 모드로 돌아간다고 되어있다.

그러니까, 계속해서 실시간으로 온도 모니터링을 하려면, 계속해서 자극을 줘야 한다는 이야기다.




소자에 자극을 전달하는 시퀀스에 대해서 좀 더 자세히 설명이 나와있으므로 알아보면,

image23

시퀀스는 다음과 같다.

1. HIGH 상태 (준비 상태)

2. LOW 상태 18 [ms]이상 유지 (소자에 자극 준다)

3. HIGH 상태 (DHT 소자가 주는 LOW 신호를 받기 위한 준비)

4. DHT 소자가 LOW 신호 80 [us] 유지

5. DHT 소자가 HIGH 신호 80 [us] 유지

6. 데이터 전송 시작



1bit의 데이터 전송은 다음과 같이 LOW와 HIGH 신호의 쌍으로 이루어진다.

1. 50 [us]의 LOW 신호 유지

2. HIGH 신호 유지

image28

0인 경우 신호 다이어그램


image31

1인 경우 신호 다이어그램


0과 1의 구분은 유지되는 HIGH 신호의 길이로 구분한다.

0인 경우 HIGH 신호가 짧게 (26~28 [us])로 유지되고, 1인 경우 HIGH 신호가 길게 (~70 [us])로 유지 된다고 나와있다.






B. 아두이노 코딩

결론 부터 이야기 하면, DHT11을 구동시킬 수 있는 클래스 라이브러리를 만들었고, 소스를 보면서 이야기 해보겠다.

소스는 맨위에 링크된 HardCopyWorld에서 올려놓은 소스를 참고해서 내 식대로 짰다.


0. 아두이노 세팅 및 코딩


unnamed

빵판과 DHT11 그리고 아두이노 보드를 위 사진과 같이 세팅했다.

앞서 이야기 했듯이, DHT11의 포트는 보드 제조사마다 다른것 같다. 위 사진의 경우 가장 왼쪽이 Signal, 중간이 +5V, 오른쪽이 GND 이다.

DIGITAL PORT는 2번을 사용했다.


아두이노에 올려줄 소스코드는 아래와 같다.

보다시피 별거 없고 1초마다 DHT11에서 온도와 습도를 읽어서 시리얼포트로 출력해 주는 코드 이다.

// 2015.09.30 Jihan Kim #include "myDHT11.h" int SIGNAL_PIN_NUMBER = 2; CmyDHT11 myDHT11(SIGNAL_PIN_NUMBER); void setup() { Serial.begin(9600); } void loop() { float temp = 0.f; float humid = 0.f; if (myDHT11.read(&temp, &humid) == 0) { Serial.print("Temperature: "); Serial.print(temp); Serial.print(" Humidity: "); Serial.println(humid);

} delay(1000); }



1. DHT11 구동 클래스 (CmyDHT11)


헤더파일

// myDHT11.h
// 2015.09.30 Jihan Kim

#ifndef _MYDHT11_h
#define _MYDHT11_h

#if defined(ARDUINO) && ARDUINO >= 100
    #include "arduino.h"
#else
    #include "WProgram.h"
#endif

const int PACKET_LENGTH = 40;    // [bits]
const int DECISION_THRESHOLD = 30;    // [us]

class CmyDHT11
{
public:
    CmyDHT11(uint8_t targetPortNum);
    ~CmyDHT11();


    int read(float* pTemperature, float* pHumidity);
private:

    unsigned long waitFor(uint8_t pinNumber, uint8_t targetStatus, unsigned long maxTime);
    uint8_t checkBit(unsigned long time);
    uint8_t m_TargetPort;
};


#endif


소스파일

// 
// 2015.09.30 Jihan Kim
// 

#include "myDHT11.h"


CmyDHT11::CmyDHT11(uint8_t targetPort)
{
    m_TargetPort = targetPort;

    // maintain its HIGH status
    pinMode(m_TargetPort, OUTPUT);
    digitalWrite(m_TargetPort, HIGH);
}

CmyDHT11::~CmyDHT11()
{
    ;
}


uint8_t CmyDHT11::checkBit(unsigned long time)
{
    if (time < DECISION_THRESHOLD)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}



unsigned long CmyDHT11::waitFor(uint8_t pinNumber, uint8_t targetStatus, unsigned long maxTime)
{
    unsigned long startCount = micros();
    unsigned long endCount = startCount + maxTime;

    while (digitalRead(pinNumber) != targetStatus)
    {
        if (micros() > endCount)
        {
            return -1;
        }
    }

    return micros() - startCount;
}



int CmyDHT11::read(float* pTemperature, float* pHumidity)
{
    pinMode(m_TargetPort, OUTPUT);
    digitalWrite(m_TargetPort, LOW);
    delay(18); // Low로 18[ms] 유지해준다.
    digitalWrite(m_TargetPort, HIGH); // HIGH로 복귀

    pinMode(m_TargetPort, INPUT);

    int t0 = waitFor(m_TargetPort, LOW, 40);    // DHT11이 준비를 기대린다. LOW 까지 최대 40 [us] 기다린다
    if (t0 < 0)
    {
        Serial.println("Error");
        return -1;
    }

    int t1 = waitFor(m_TargetPort, HIGH, 90);  // Tx 준비를 체크한다. HIGH까지 최대 90 [us] 까지 기다린다
    if (t1 < 0)
    {
        Serial.println("Error");
        return -1;
    }

    int t2 = waitFor(m_TargetPort, LOW, 90);   // Tx 준비를 체크한다. LOW까지 최대 90 [us] 까지 기다린다
    if (t2 < 0)
    {
        Serial.println("Error");
        return -1;
    }


    // 데이터 전송 시작. 40 bit 읽는다.
    // 습도 정수부 [8bit] + 습도 소수부 [8bit] + 온도 정수부 [8bit] + 온도 소수부 [8bit] + 체크섬 [8bit]
    // HIGH가 유지되는 시간이 짧으면 0, 길면 1이다.
    // 30 [us]를 기준으로 한다.
    int pTimeArray[PACKET_LENGTH];
    for (int idx = 0; idx < PACKET_LENGTH; ++idx)
    {
        if (waitFor(m_TargetPort, HIGH, 50) < 0)    // 데이터 송신 기다린다. HIGH까지 최대 50 [us] 기다린다.
        {
            return -1;
        }

        int dataDuration = waitFor(m_TargetPort, LOW, 80);    // 데이터 송신 받는다. 최대 80 [us] 기다린다.
        if (dataDuration < 0)
        {
            return -1;
        }

        pTimeArray[idx] = dataDuration;
    }

    int RH_Int = 0;
    int RH_Dec = 0;
    int Temp_Int = 0;
    int Temp_Dec = 0;
    int checkSum = 0;
    int bitValue = 0;
    for (int idx = 0; idx < 8; ++idx)
    {
        // 습도 정수부
        RH_Int += (checkBit(pTimeArray[idx]) << (7 - idx));

        // 습도 소수부
        RH_Dec += (checkBit(pTimeArray[idx + 8]) << (7 - idx));



        // 온도 정수부
        Temp_Int += (checkBit(pTimeArray[idx + 16]) << (7 - idx));

        // 온도 소수부
        Temp_Dec += (checkBit(pTimeArray[idx + 24]) << (7 - idx));


        // 체크섬
        checkSum += (checkBit(pTimeArray[idx + 32]) << (7 - idx));
    }




    if (RH_Int + RH_Dec + Temp_Int + Temp_Dec == checkSum)
    {
        *pTemperature = Temp_Int + Temp_Dec*0.01f;
        *pHumidity = RH_Int + RH_Dec*0.01f;
        return 0;
    }
    else
    {
        return -1;
    }
}




C. 결과

위의 소스를 돌려보면 아래처럼 온도와 습도가 출력됨을 알 수 있다.

serialPortCapture





E. 어려운 점


타이밍…. 그리고 타이밍…..


1. 데이터 시트의 부정확함

위 시퀀스 다이어그램 나와있는 시간들이 대체로 부정확 하다.

먼저 LOW를 18 [ms]를 준 이후에 HIGH가 20 [us] ~ 40 [us]정도로 유지된다고 나와있는데, 실제로 카운팅을 해보면 보통 8 [us]가 나온다.

그리고 그 뒤에 나오는 두 번의 80 [us]의 LOW-HIGH 들은 실제로 70 [us] ~ 80 [us]를 왔다 갔다 한다.


데이터를 받는 부분도 마찬가지다. ‘0’을 나타내는 신호의 경우 26 [us] ~ 28 [us]라고 되어있으나 실제로 카운팅을 해보면 8 [us] ~ 26 [us] 까지 다양하게 나오고, ‘1’을 나타내는 경우도 50 [us] ~ 70 [us]까지 다양하게 나온다.

이러한 문제 때문에 내가 참고한 원래 소스 (이 글 맨위에 있는 HardCopyWorld에서 링크한)도 신호의 절대적인 길이를 측정하는 것이 아니라, 최대 시간을 주고 현재 입력이 얼마나 유지 되는지를 체크하는 방식으로 코드가 짜여져 있는 것 같다.


무슨말인가 하면, 위의 코드에서 waitFor() 함수를 보자.

unsigned long CmyDHT11::waitFor(uint8_t pinNumber, uint8_t targetStatus, unsigned long maxTime)
{
    unsigned long startCount = micros();
    unsigned long endCount = startCount + maxTime;

    while (digitalRead(pinNumber) != targetStatus)
    {
        if (micros() > endCount)
        {
            return -1;
        }
    }

    return micros() - startCount;
}


이 함수는 targetStatus가 나올때 까지 현재 status가 유지되는 시간을 카운트 하는 함수다.

즉, waitFor(PORT_NUMBER, LOW, 40) 이라고 하면, 현재는 HIGH 입력이 들어오고 있는데, LOW로 바뀔때 까지 얼마의 시간이 걸리는지를 계산해서 리턴하고, 이때 최대 40 [us]까지 대기한다는 뜻이다.


만약 데이터 시트에 있는 그 시간대로 카운팅을 해서 신호를 분석하려고 하면, 위에 말한 타이밍 오차 때문에 제대로 동작하도록 만들 수가 없다.

(처음에는 이런식으로 구현해 보려고 했으나, 실제로 동작하는 시간을 재 보고 멘붕한 뒤에 HardCopyWorld의 소스를 분석해보니 이렇게 짤 수 밖에 없다는 것을 알게 되었다.)



2. 디버깅

처음에는 디버깅을 위해서 모든 시간을 재면서 Serial.print() 명령으로 그때 그때 결과를 출력했다.

그런데 처음 몇 번 (소자에 자극 신호를 줄때) 은 제대로 동작했지만, 뒤로 갈 수록 이상한 결과가 출력되고 제대로 동작 하지 않았다.


무엇이 문제인지 알아보기 위해서 HardCopyWorld의 소스코드에다가 출력문만 넣었더니, 똑같은 현상이 나타나면서 제대로 동작하는 않는것을 확인했다.


타이밍 단위가 [us]이다 보니, 간단한 시리얼 출력만으로도 마이크로프로세서의 시간이 밀리게 되고, 그래서 타이밍을 놓쳐서 제대로 데이터를 읽어올 수가 없었던 것이었다.




F. 결론


아두이노와 DHT11을 이용해서 온도와 습도를 잰다는 것은 타이밍이나 여러가지 소스코드 준비가 확실히 되고 포트의 전압 하나하나가 투명하게 처리가 되는 데서부터 조금이라도 디버깅이 쉽게 된다는 것은 분명히 알겠다.

Comments