티스토리 뷰

입문

LED 온도계 2

Just4Fun 2016. 3. 22. 21:25

이번 글에서는 실제로 DS18B20에서 온도 데이터를 읽어 오는 기능을 구현해 보도록 하겠다.

프로그램을 작성하기 이전에 한가지 생각해 봐야 하는 점이 있다.  블로그의 첫번째 글인 "hello, world"에서 언급한 바와 같이 프로그래머가 예상했던대로 보드에서 결과가 나오는 경우는 모든 것이 문제가 없는 단 한가지의 경우에 해당되고, 제대로 동작되지 않을 경우는 무수히 많다.  특히 클럭 신호도 없는 단 하나의 신호선을 통해서 데이터 송수신이 이루어 진다면 더더욱 그 결과를 신뢰하기가 어렵게 된다.  이미 앞의 글에서 설명했듯이 신호가 유지되는 길이를 이용하여 데이터값이 '0'인지 '1'인지 판단해야 되는 DS18B20의 경우 내장된 크리스탈도 없으므로 정확한 시간에 맞춰 동작된다는 보장도 없다.  이러한 상황에서 AVR에서 읽어 들인 데이터가 제대로 된 데이터인지, 아무 의미없는 값인지 어떻게 판단할 수 있을지 한번쯤 고민해봐야 되고, 100% 정확한 데이터를 수신했다는 것을 증명할 수 있는 방법을 찾아야 한다.

수신된 데이터에 에러가 없는지 판단하기 위한 가장 좋은 방법은 오실로스코프와 같은 계측기를 가지고 송수신 신호를 측정하면 될 것이다.  그러나 만약 그런 고가의 측정 장비가 없는 경우에는 수신된 데이터만 가지고 데이터의 에러 유무를 판단할 수 있어야 한다.  

읽어온 데이터만 가지고 올바른 데이터를 수신했는지 아는 방법은 미리 예상하고 있는 값이 수신되었는지 비교해 보면 알 수 있다.  레지스터중에 몇개는 다른 레지스터와 구별되는 초기값을 가지는 경우가 있다.  그런 레지스터 값을 읽어서 초기값대로 읽혀 왔는지 비교하면 된다.

DS18B20의 datasheet 문서를 분석하여 독특한 초기값을 가지는 레지스터가 있는지 찾아 본다.  64-Bit Lasered ROM Code라는 것을 이용하면 AVR에서 데이터를 제대로 읽어 왔는지 알수 있을것 같다.

위의 그림에서 보이는 것처럼 첫번째 LSB 8비트값은 부품코드인 0x28 값을 가진다.  이 값은 DS18B20이 가지는 고정된 값이므로, 이 레지스터 값을 읽어서 수신된 데이터가 0x28이 맞는지 아닌지 확인해 보도록 하자.

이번 프로젝트는 시간을 이용하여 데이터값이 '0'인지, '1'인지 판단해야하므로 프로젝트를 시작하기 전에 CPU의 클럭 주파수를 정확히 알고 진행하여야 한다.  LED 제어 1 프로젝트를 참고하여 정확하게 1초마다 한번씩 LED가 점멸되는지 확인한다.  실제 CPU는 16MHz 크리스탈을 기준으로 동작됨에도 불구하고 F_CPU값만 1000000으로 설정한다고 해서 1MHz로 CPU가 동작되지 않는다.  

이전 글에서 설명하지 않았던 코드는 다음과 같다.

#define PORTB_REG   0x23
#define PORTD_REG   0x29
struct port
{
    uint8_t   pin;
    uint8_t   ddr;
    uint8_t   port;
};
 
volatile struct port *const portb = (void*)PORTB_REG;
volatile struct port *const portd = (void*)PORTD_REG;
 
#define PUSH_BTN            0x01
#define DS18B20             0x02
 
#define RESET_PULSE_DELAY   500
#define WRITE_START_GAP     2
#define WRITE_SAMPLE_TIME   70
#define READ_START_GAP      3
#define READ_SAMPLE_TIME    10
#define READ_SLOT_TIME      60
 
 
static inline void high_dq(void)
{
    portb->port |= DS18B20;
}
 
static inline void low_dq(void)
{
    portb->port &= ~DS18B20;
}
 
static inline void out_dq(void)
{
    portb->ddr |= DS18B20;
}
 
static inline void in_dq(void)
{
    portb->ddr &= ~DS18B20;
}
 
static inline uint8_t get_dq(void)
{
    return (portb->pin & DS18B20);
}

24번 줄에 나와 있는 코드처럼 DQ 신호를 제어하는 함수 이름 앞에 "inline"이라고 된것을 볼 수 있다.  inline 함수는 #define으로 사용하는 매크로처럼 동작되는 함수라고 보면 된다.  함수 호출을 하게 되면 호출된 함수에서 사용되는 CPU 내부 레지스터를 미리 스택에 저장해 두었다가 호출된 함수가 끝나면 스택에 저장된 레지스터 값들을 원래대로 환원 시켜주는 절차를 수행하게 된다.  그러면 쓸데없이 많은 시간이 낭비되는데 이러한 낭비를 줄이기 위하여 inline으로 선언된 함수는 함수 호출을 하는것이 아니라 함수 호출하는 부분에 매크로처럼 코드가 삽입되게 된다.  조금의 코드사이즈가 증가되는 단점이 있지만 성능향상의 이점이 있기 때문에 유용하게 사용할 수 있는 코딩 기법이다.  그러나 항상 inline 함수가 코드속에 삽입되지는 않는다.  경우에 따라서는 일반 함수 호출처럼 처리되는 경우도 있다.  컴파일된 결과물에 대한 어셈블 코드를 보면 inline함수가 코드내에 삽입되었는지, 함수 호출이 되었는 지 간단히 알 수 있다.

초기화 코드는 다음과 같다.

void setup(void)
{
    portb->ddr  = 0x00;
    portb->port = PUSH_BTN;
 
    portd->ddr  = 0xFF;
    portd->port = 0x00;
}

PORT B에는 푸쉬버튼과 DS18B20이 연결되어 있으므로 모든 핀을 입력으로 설정해 둔다.  4번라인처럼 입력으로 설정된 핀을 high 상태로 설정하면 AVR내부에 핀마다 연결되어 있는 풀업저항이 enable되어 푸쉬버튼이 연결된 PB0핀의 상태가 high가 된다.  스위치를 누르면 low 상태가 되어 스위치가 눌리어 졌는지 알수 있게 된다.

PORT D에는 LED가 연결되어 있으므로 모든 핀을 출력으로 설정한다.

이제 이 상태에서 DS18B20의 내부에 있는 ROM Code 값을 읽어 보기로 하자.  ROM code를 읽기 위해 DS18B20으로 주는 명령어는 0x33이다.  이 명령을 주고 연속해서 8바이트를 읽어 오면 ROM code가 읽히게 된다.  코드를 만들면 다음과 같다.

#define READ_ROM    0x33
uint8_t     rom[8];
 
void read_rom_code(void)
{
    int     i;
 
    reset_presence();
    write_byte(READ_ROM);
 
    for (i=0; i<8; i++)
    {
        rom[i] = read_byte();
    }
}

read_rom_code() 함수를 setup() 함수 마지막 부분에서 함수 호출하면 배열 rom[]에 ROM code값이 들어갈 것이다.  loop() 함수에서 rom[]에 있는 값들을 차례대로 LED로 나타내면 어떤 값을 읽어 왔는지 알수 있다.  푸쉬 버튼을 한 번 누를때마다 rom[] 안에 있는 값들을 차례대로 출력하면 눈으로 LED를 봤을때 어떤 값인지 확인하기 좋을 것이다.

loop() 함수는 다음과 같이 만들면 된다.

void wait_until_button_press(void)
{
    while (portb->pin & PUSH_BTN);
}
 
void loop(void)
{
    int     i;
 
    for (i=0; i<8; i++)
    {
        portd->port = rom[i];
        wait_until_button_press();
        _delay_ms(500);
    }
}

모든 코드를 작성하였으면 프로젝트를 빌드하여 보드에서 실행해 보자.  실행 되자마자 8개중에 두개의 LED에 불이 켜지면 일단 정상일 가능성이 높다.  PD7에서부터 시작하여 PD0까지 순서대로 읽어 가며, 불이 켜지면 '1', 불이 꺼져있으면 '0'의 값으로 표시하면 된다. "0 0 1 0 1 0 0 0"으로 읽혔다면 0x28을 읽어 온 것이다. 위에서 설명했던 DS18B20의 부품번호에 해당되는 값이므로 DS18B20과 통신이 제대로 이루어진것 같다.  푸쉬버튼을 한번씩 눌러가며 나머지 값들도 계속해서 읽어 보자.

가지고 있는 DS18B20은 다음과 같은 값으로 읽혀졌다.

rom[] = {0x28,0x39,0x0f,0x33,0x05,0x00,0x00,0xff};

그런데 문제가 있다. 0x28 다음에 나오는 6바이트 데이터는 부품마다 부여되는 일련번호이다.  즉, 모든 DS18B20은 서로 다른 일련번호를 가지고 있으므로 이 값들이 제대로 읽혀온 값인지 아닌지 100% 확신 할수가 없다는 것이다.   실제로 임베디드 프로그램을 하다보면 처음 몇번은 정상적으로 동작하다가도 어느 순간부터 비정상적으로 동작되는 경우가 너무나 많기 때문에, 이번 경우처럼 첫 데이터가 맞는 데이터라 하더라도 그 이후에 나오는 데이터가 맞다고 확신을 못하는 것이다.  이런 이유로 부품 제조회사에서 마지막 한 바이트를 CRC 값으로 제공하는 것이다.  CRC는 쉽게 말해서 수신된 데이터에 오류가 있는지 없는지 검사하는데 사용되는 값이다.

CRC에 대한 설명은 다소 내용이 어려우므로 나중에 하기로 하고, 여기에서는 간단하게 두가지 방법으로 CRC 검사를 할 수 있다는 정도로 설명을 하도록 하겠다.

CRC 검사를 하는 한가지 방법은 CRC 계산식을 매번 돌려서 CRC검사를 하는것이다.  다른 방법 한가지는 미리 CRC 검사를 할 수 있는 CRC 테이블을 만든 후 테이블에서 CRC값을 가지고 와서 CRC 검사를 하는 것이다.  두가지 방법 각각 장단점이 있는데, 검사식을 이용하는 방법은 별도의 테이블을 만들 필요가 없으므로 메모리를 적게 사용하는 장점이 있다.  단점으로는 엄청나게 많은 반복문을 수행하여야 하므로 성능이 떨어지게 된다.   반면 테이블을 이용하는 방법은 성능 향상은 되지만 테이블을 만들어야 하므로 메모리 사용량이 늘어나게 된다.  DS18B20의 경우 8비트 CRC계산을 하므로 256바이트 크기의 테이블이 필요하다.  이정도 크기는 컴퓨터와 같이 메모리 크기가 몇 GB인 경우에는 전혀 문제거리가 안되지만 AVR과 같이 메모리 크기가 1~2KB 인경우 전체 메모리의 12 ~ 25%를 단지 CRC 검사용 테이블을 만드는데 사용하게 되는 것이다.  따라서 성능을 크게 따지지 않는다면 검사식을 이용하는 방법을 사용하면 좋고, 성능이 문제 된다면 테이블 방식을 사용하는것이 좋다.

DS18B20에서 사용하는 CRC 검사용 프로그램은 다음과 같다. 

먼저 검사식을 이용한 방법이다.

#define CRC8INIT 0x00
#define CRC8POLY 0x8C // = X^8+X^5+X^4+X^0
 
uint8_t crc8(uint8_t *data, int len)
{
    uint8_t     crc = CRC8INIT;
    uint8_t  i;
 
    while (len--)
    {
        crc = crc ^ *data++;
        for (i=0; i<8; i++)
        {
            if (crc & 1)
                crc = (crc >> 1) ^ CRC8POLY ;
            else
                crc >>= 1;
        }
    }
 
    return crc;
}

다음으로는 테이블 방식이다.

uint8_t crc_table[256];
 
void make_crc_table(void)
{
    uint8_t    crc;
    int        i,j;
 
    for (i=0; i<256; i++)
    {
        crc = i;
        for (j=0; j<8; j++)
        {
            crc = (crc & 0x01) ? CRC8POLY ^ (crc >> 1) : (crc >> 1);
        }
        crc_table[i] = crc;
    }
}
 
uint8_t crc8(uint8_t *data, int len)
{
    uint8_t     crc = CRC8INIT;
 
    while (len--)
    {
        crc = crc_table[crc ^ *data++];
    }
 
    return crc;
}

make_crc_table() 함수는 프로그램이 시작될 때 한번만 실행하면 된다.  그 이후에 CRC 검사를 할 때에만 테이블을 이용하면 된다.

DS18B20에서 읽은 ROM code를 위의 CRC 검사코드를 통해서 확인해 보도록 하겠다.  ROM code 마지막 데이터가 CRC인데, CRC 바이트 앞부분까지의 데이터만 가지고 CRC 검사기를 돌리면 CRC 값과 동일한 값이 나와야 되고, 만약 CRC 바이트까지 포함하여 CRC검사기를 돌리면 그 결과값이 0이 나오면 데이터에 오류가 없는 정상적인 데이터이다.  일반적으로 CRC검사를 할 때에는 CRC값까지 같이 포함하여 검사한다.  그 결과가 0이 나오면 데이터에 에러가 없다는 것이 되고 0이 아닌 값이 나오면 데이터에 에러가 발생했다고 판단할 수 있기 때문이다.

int main(int argc, char *argv[])
{
    uint8_t rom[] = {0x28,0x39,0x0f,0x33,0x05,0x00,0x00,0xff};
    uint8_t crc = 0;
 
    make_crc_table();
 
    crc = crc8(rom, sizeof(rom));
 
    printf("CRC = %02X\n", crc);
}

위의 코드는 테이블 방식으로 DS18B20에서 수신한 ROM code를 검사한 것이다.  결과는 0으로 나왔다.  즉, 수신된 ROM code 데이터에 에러가 없다는 뜻이다.

참고로, Atmel Studio를 설치하면 그 안에 1-Wire 프로토콜을 사용하는 디바이스의 CRC 계산을 해주는 코드가 이미 들어 있다. 사용하는 방법은 다음과 같다.

#include <util/crc16.h>
 
// Dallas iButton test vector.
uint8_t serno[] = { 0x02, 0x1c, 0xb8, 0x01, 0, 0, 0, 0xa2 };
 
int  checkcrc(void)
{
    uint8_t crc = 0, i;
 
    for (i = 0; i < sizeof (serno); i++)
        crc = _crc_ibutton_update(crc, serno[i]);
 
    return crc; // must be 0
}

DS18B20과의 통신이 정상적으로 수행되는것이 확인 되었으므로 원래 이 프로젝트의 목표인 온도값을 읽어보기로 하자.

온도값을 읽기 위해서는 먼저 DS18B20에게 온도를 측정한 다음에 이를 디지털 데이터로 변환하여 scratchpad 레지스터에 저장하라는 명령인 0x44를 수행하여야 한다.  이 과정을 수행하는데 어느정도의 시간이 소요된다.  측정하는 온도의 해상도 설정에 따라 그 시간이 달라지는데 자세한 내용은 datasheet 문서를 보면 나온다.  그러나 여기에서는 온도를 변환하는 동안 DS18B20이 DQ신호를 low로 유지하다가 변환이 끝나면 high상태로 올려준다고 datasheet에 나와 있으므로 시간을 따로 측정하지 않고 DQ신호만 모니터링 하는 방법을 이용하여 변환이 완료되었는지 확인 하도록 하겠다.

온도 변환이 완료되면 scratchpad 레지스트를 읽어오기 위한 명령인 0xbe를 수행한다. 그 이후에 9바이트의 데이터를 읽어 오면 된다.  Scratchpad 레지스터는 다음과 같은 구성으로 이루어져 있다.

 

단, 여기서 온도 변환 명령과 read scratchpad 명령전에 ROM code 명령을 수행하는 부분이 선행 되어야 한다.  이유는 하나의 DQ 신호에 여러개의 DS18B20을 연결 시킬수 있기 때문에 각각의 디바이스를 구별하기 위하여 이름을 불러주는 과정이 필요하다.  그러나 이번 프로젝트와 같이 하나의 부품만 연결되어 있다면 굳이 이름을 불러줄 필요가 없게된다.  따라서 ROM 명령을 건너뛴다는 명령어를 먼저 내려 보내 줘야 한다.  그 명령어는 0XCC 이다.

최종적으로 온도를 읽어 오는 코드는 다음과 같다.

#define SKIP_ROM        0xcc
#define CONVERT_T       0x44
#define READ_SCRATCH    0xbe
 
uint8_t     scratchpad[9];
 
void read_scratchpad(void)
{
    int     i;
 
    reset_presence();
    write_byte(SKIP_ROM);
    write_byte(CONVERT_T);
    in_dq();
    while(!get_dq());
    reset_presence();
    write_byte(SKIP_ROM);
    write_byte(READ_SCRATCH);
 
    for (i=0; i<9; i++)
    {
        scratchpad[i] = read_byte();
    }
}

읽어 온 온도값을 LED를 이용하여 표시해 본다.  ROM Code를 표시했을때처럼 푸쉬버튼을 누를때마다 한 바이트 값을 표시하도록 한다.

void loop(void)
{
    int     i;
 
    for (i=0; i<9; i++)
    {
        portd->port = scratchpad[i];
        wait_until_button_press();
        _delay_ms(500);
    }
}

가지고 있는 DS18B20에서 읽어 온 값은 다음과 같다.

0x6c, 0x01, 0x4b, 0x46, 0x7f, 0xff, 0x04, 0x10, 0x5d

위의 데이터의 마지막 값 0X5D가 CRC 값이다. 위의 값을 CRC 검사기로 돌려본 결과 0의 값을 출력하였다.  데이터에 오류 없이 잘 수신되었음을 확인할 수 있다.

이제 마지막으로 온도값만 추출하여 LED로 표시하는 코드를 만들어 보겠다.  

Scratchpad 레지스터중에 실제 온도를 나타내는 레지스터는 Byte0과 Byte1이다.  

위의 그림에서 LS byte가 Scratchpad 레지스터의 Byte0이고 MS byte가 Byte 1이다.  16비트 데이터중에 MSB 5비트는 부호를 나타내고 LSB 4비트는 소수점 아래 온도값이다.  이 프로젝트에서는 LED 8개를 이용하여 온도 값을 표현하여야 하므로 소수점 아래 부분은 떼어내고 정수부분만 표시하도록 하겠다. 그렇게 하기 위해서 다음과 같은 코드를 이용한다.

temp = ((scratchpad[1] << 4) & 0xF0) | ((scratchpad[0] >> 4) & 0x0F);

최종 코드는 다음과 같다.

void loop(void)
{
    read_scratchpad();
    portd->port = ((scratchpad[1] << 4) & 0xF0) | ((scratchpad[0] >> 4) & 0x0F);
    _delay_ms(500);
}

물론 LED로 온도를 표시하기 전에 읽어온 데이터에 대해서 CRC 검사를 하여 에러가 없음을 확인하는 코드를 추가 하면 더 좋은 코드가 될 것이다.  표시되는 온도값을 다른 온도계와 비교하여 맞는값이 나오는지 확인해보고, 손으로 DS18B20을 잡았을 때 온도가 서서히 올라가는지, 손을 떼었을때 다시 처음 온도값으로 서서히 떨어지는지 확인해 보자.

 

위의 동영상은 22도 상태에서 시작하여 손으로 DS18B20을 잡아서 28도까지 높였다가, 손을 떼어 다시 차츰 온도가 내려가는 것을 보여주고 있다.

 

이번 프로젝트에서 작성한 소스코드를 파일로 첨부한다.

main.c
다운로드
crc8.c
다운로드

 

입문 과정 목차

'입문' 카테고리의 다른 글

_delay_ms  (10) 2016.03.27
LED 온도계 3  (6) 2016.03.24
LED 온도계 1  (6) 2016.03.20
푸쉬 버튼과 LED제어 연동 2  (1) 2016.03.15
푸쉬 버튼과 LED제어 연동 1  (0) 2016.03.12
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함