반응형

아두이노 프로젝트#12아두이노 나노(Nano) OLED 탁상시계 (with Calendar & 온도계)

 쉽게 구할 수 있는 나노와 OLED를 가지고 책상 위에 올려놓을 수 있는 멋진 미니 탁상시계를 만들어보세요!

또한 3D 프린팅을 이용해서 꼭 맞는 깔끔한 케이스를 만드는 과정도 담았으니 끝까지 보셨으면 합니다.

 

 

실습 재료 목록

 : 아래 표와 이미지를 참고하여 실습재료를 준배해 주세요

 

 

[ 주요 부품 스팩 ]

 나노(Nano Pin Map) 보드 핀 맵은 아래와 같습니다. 

OLED 사용을 위한 관련 지식 및 자료

(실습에 사용되는 OLED는 SSD1306 드라이버를 사용하는 I2C 타입입니다)

 

아래는 우노 보드와의 기본 연결이며, 나노보드와의 연결도 동일합니다.

우노 보드와의 기본 연결이며, 나노보드와의 연결도 동일합니다

 

우노보드와 나노보드에서 OLED와의 I2C 통신을 위한 SDA핀과 SCL핀의 번호는 동일하며,  기타 메가보드나 레오나르도보드 사용 시의 핀 번호는 다르기 때문에 아래 표를 참고하세요.  (SDA핀과 SCL핀 번호는 보드마다 지정되어 있으니 반드시 해당 핀으로 연결해야 I2C 통신이 됩니다.)

추가적으로 OLED관련해서 다른 타입의 OLED를 사용하려 하거나 관련 정보가 필요하다면 아래 게시글을 먼저 살펴보시기 바랍니다. 

※ 【 아두이노모듈#29】 OLED 처음 사용 설명서 #1 (SPI, I2C 주요 5종 사용법 안내)

 

DS3231  RTC 모듈 핀 맵

 'SCL, SDA, VCC, GND' 핀은 사용상의 편의를 위해 좌우측에 동일한(서로 연결된) 핀을 배치하였기 때문에 

연결하기 편한 쪽으로 연결하면 됩니다.

DS3231 모듈의 특징은,  DS1307 모듈보다 정밀한 RTC기능(리얼타임클럭)을 제공하며  보드에 온도센서를 포함하고 있어,  RTC 모듈의 주변 온도값 데이터를 얻을 수 있는 특징이 있습니다. 가격도 매우 저렴하기 때문에,  DS1307보다는 DS3231 RTC모듈을 적극 추천합니다. 

 

브레드 보드 내부 핀의 연결 구조

 

 

 아두이노 탁상시계 회로 연결도

 

 아두이노 코드

 

/**** 참고 : https://simple-circuit.com/arduino-ssd1306-oled-ds3231-rtc/ ***/ 
/* 편집 : 상세한 한글 주석과 연도 표시를 한국식 (yyyy/mm/dd)으로 수정함          */
/* 기타 추가 자료 참고는 여기를 방문하세요 : http://RasINO.tistory.com         */
/**************************************************************************/
#include <SPI.h>
#include <Wire.h>                // I2C 장치 사용을 위한 선언(현재 버전의 IDE사용시 생략가능)
#include <Adafruit_GFX.h>        // Adafruit graphics library 사용
#include <Adafruit_SSD1306.h>    // Adafruit SSD1306 OLED 드라이버 사용
 
#define OLED_RESET -1            // -1은 아두이노 보드 리셋과 연동 됨.
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RESET);  // 128 x 64 OLED 사용시 

#define button1    11   // 버튼1을 나노 11번 핀으로 (설정버튼-무브) 설정함
#define button2     3   // 버튼2는 나는 3번 핀으로 (숫자 증가버튼) 설정함

void setup(void) {
  Serial.begin(9600);
  pinMode(button1, INPUT_PULLUP);  // 풀업 저항을 생략하기 위해 INPUT_PULLUP으로 선언함
  pinMode(button2, INPUT_PULLUP);  // 풀업 저항을 생략하기 위해 INPUT_PULLUP으로 선언함
  delay(1000);
   
  // OLED 초기화
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // OLED후면 설정(납땜)에 따라 주소를 0x3D로 바꿀 수 있음
  
  display.clearDisplay();  // 디스플레이 버퍼에 들어 있는 내용을 클리어 함
  display.display();       // 현재의 디스플레이 버퍼 내용을 출력해봄
 
  display.setTextColor(WHITE, BLACK);
  display.drawRect(117, 56, 3, 3, WHITE);   // 온도 표시를 위한 기호 ( ° ) 출력
  draw_text(0, 56, "TEMPERATURE =", 1);
  draw_text(122, 56, "C", 1);
}
 
char Time[]     = "  :  :  ";
char Calendar[] = "20  /  /  ";
char temperature[] = " 00.00";
char temperature_msb;
byte i, second, minute, hour, day, date, month, year, temperature_lsb;
 
void display_day(){
  switch(day){
    case 1:  draw_text(40, 0, " SUNDAY  ", 1); break;
    case 2:  draw_text(40, 0, " MONDAY  ", 1); break;
    case 3:  draw_text(40, 0, " TUESDAY ", 1); break;
    case 4:  draw_text(40, 0, "WEDNESDAY", 1); break;
    case 5:  draw_text(40, 0, "THURSDAY ", 1); break;
    case 6:  draw_text(40, 0, " FRIDAY  ", 1); break;
    default: draw_text(40, 0, "SATURDAY ", 1);
  }
}
 
void DS3231_display(){
  // Convert BCD to decimal  (10진수 형태의 시간과 날자를 출력하기 위한 변환)
  second = (second >> 4) * 10 + (second & 0x0F);
  minute = (minute >> 4) * 10 + (minute & 0x0F);
  hour   = (hour >> 4)   * 10 + (hour & 0x0F);
  date   = (date >> 4)   * 10 + (date & 0x0F);
  month  = (month >> 4)  * 10 + (month & 0x0F);
  year   = (year >> 4)   * 10 + (year & 0x0F);
  // End conversion

  // 각 요소별로 10의 자리와 1의 자리를 분리하여 저장
  Time[7]     = second % 10 + 48;     // 초의 1의 자리값 저장
  Time[6]     = second / 10 + 48;     // 초의 10의 자리값 저장
  Time[4]     = minute % 10 + 48;
  Time[3]     = minute / 10 + 48;
  Time[1]     = hour   % 10 + 48;
  Time[0]     = hour   / 10 + 48;
  Calendar[9] = date   % 10 + 48;
  Calendar[8] = date   / 10 + 48;
  Calendar[6] = month  % 10 + 48;
  Calendar[5] = month  / 10 + 48;
  Calendar[3] = year   % 10 + 48;
  Calendar[2] = year   / 10 + 48;
 
  // 온도가 0도씨 이하가 되면 온도 앞에 '-' 기호를 붙여줌
  if(temperature_msb < 0){
    temperature_msb = abs(temperature_msb);
    temperature[0] = '-';
  }
  else
    temperature[0] = ' ';
    
  
  temperature_lsb >>= 6;   // temperature_lsb = temperature_lsb >> 6 (온도하위비트 비트이동 후 다시 저장)  
  temperature[2] = temperature_msb % 10  + 48;  // 온도값 1의 자리 저장
  temperature[1] = temperature_msb / 10  + 48;  // 온도값 10의 자리 저장
  if(temperature_lsb == 0 || temperature_lsb == 2){
    temperature[5] = '0';
    if(temperature_lsb == 0) temperature[4] = '0';
    else                     temperature[4] = '5';
  }
  if(temperature_lsb == 1 || temperature_lsb == 3){
    temperature[5] = '5';
    if(temperature_lsb == 1) temperature[4] = '2';
    else                     temperature[4] = '7';
  }
 
  draw_text(4,  14, Calendar, 2);    // 한국식(yyyy/mm/dd) 형태로 날짜 출력
  draw_text(16, 35, Time, 2);        // 시간 출력 
  draw_text(80, 56, temperature, 1); // 온도 출력
}

// 날짜, 요일, 시간 등 수정을 위한 블링크 루틴
void blink_parameter(){
  byte j = 0;
  while(j < 10 && digitalRead(button1) && digitalRead(button2)){
    j++;
    delay(25);
  }
}

// edit함수 : 버튼 눌림에 따른 날짜, 시간 등 각각의 수정된 값들을 저장하는 루틴 
// 버튼1을 눌러 수정모드가 된 후 버튼2를 눌러 각각의 값을 변경함
byte edit(byte x_pos, byte y_pos, byte parameter){
  char text[3];
  sprintf(text,"%02u", parameter);
  while(!digitalRead(button1));                      // 버튼1이 눌러지면(GND 신호이면)
  while(true){
    while(!digitalRead(button2)){                    // 버튼2가 눌러지면(GND 신호이면)
      parameter++;
      if(i == 0 && parameter > 31)                   // 날짜가(date > 31)이면  date = 1 로
        parameter = 1;
      if(i == 1 && parameter > 12)                   // 달이(month > 12) 이면  month = 1 로
        parameter = 1;
      if(i == 2 && parameter > 99)                   // 연도가(year > 99) 이면 year = 0 로
        parameter = 0;
      if(i == 3 && parameter > 23)                   // 시간이(hours > 23) 이면 hours = 0 로
        parameter = 0;
      if(i == 4 && parameter > 59)                   // 분이(minutes > 59) 이면 minutes = 0 로
        parameter = 0;
      sprintf(text,"%02u", parameter);               // 파라메터 숫자 값을 문자열 값으로 변환함
      draw_text(x_pos, y_pos, text, 2);              //  
      delay(200);                                    // 200ms 기다려줌
    }
    draw_text(x_pos, y_pos, "  ", 2);
    blink_parameter();
    draw_text(x_pos, y_pos, text, 2);
    blink_parameter();
    if(!digitalRead(button1)){                       // If button B1 is pressed
      i++;                                           // Increament 'i' for the next parameter
      return parameter;                              // Return parameter value and exit
    }
  }
}

// 전달받는 파라메터 값에 따라 문자를 OLED 디스플레이에 출력해주는 루틴
// 표시할 x y 좌표위치와, 출력할 내용(저장된 주소값), 문자 크기 
void draw_text(byte x_pos, byte y_pos, char *text, byte text_size) {
  display.setCursor(x_pos, y_pos);
  display.setTextSize(text_size);
  display.print(text);
  display.display();
}
 
void loop() {
  if(!digitalRead(button1)){                         // 버튼1(수정)이 눌러 진다면
    i = 0;
    while(!digitalRead(button1));                    // 버튼1이 눌렀다 떼어지는 것을 체크
    while(true){
      while(!digitalRead(button2)){                  // 버튼2가 눌러질 때마다
        day++;                                       // 요일 증가
        if(day > 7) day = 1;                         // 7요일 째에는 다시 SUNDAY로 리셋  
        display_day();                               // 버튼 누를 때마다 요일을 OLED화면에 표시 
        delay(200);                                  // 200 ms 기다려줌
      }
      draw_text(40, 0, "         ", 1);              // 깜빡임을 위해 요일 자리의 내용을 공백으로 지움
      blink_parameter();                             
      display_day();                                 // 다시 요일을 화면에 표시
      blink_parameter();                             
      if(!digitalRead(button1))                      // 버튼1이 (다시) 눌러진다면
        break;                                       // 요일 변경 루틴을 빠져 나감 
    }
 
    date   = edit(100, 14, date);                    // date 날짜 수정
    month  = edit(64, 14, month);                    // month 달 수정
    year   = edit(28, 14, year);                     // year 연도 수정
    hour   = edit(16, 35, hour);                     // hour 시간 수정
    minute = edit(52, 35, minute);                   // minutes 분 수정
    // 시간의 십진값을 BCD 코드 값으로 변환 (Convert decimal to BCD)
    minute = ((minute / 10) << 4) + (minute % 10);   // (십의 자리값)+(일의 자리값)
    hour = ((hour / 10) << 4) + (hour % 10);
    date = ((date / 10) << 4) + (date % 10);
    month = ((month / 10) << 4) + (month % 10);
    year = ((year / 10) << 4) + (year % 10);
    // 변환 구간 끝
 
    // 위에서 수정한 요일, 날짜, 시간등의 데이터 값을 I2C통신을 통해 DS3231 RTC 모듈에 기록함 
    Wire.beginTransmission(0x68);               // I2C 통신에 필요한 DS3231 모듈의 고유한 주소 값(0x68) 호출
    Wire.write(0);                              // 기록할 레지스터 주소 지정(Send register address)
    Wire.write(0);                              // 0초 리셋 후 초 발진기 스타트(Reset seconds and start oscillator)
    Wire.write(minute);                         // minute 분   값 기록
    Wire.write(hour);                           // hour   시간 값 기록
    Wire.write(day);                            // day    요일 값 기록
    Wire.write(date);                           // date   날   값 기록
    Wire.write(month);                          // month  달   값 기록
    Wire.write(year);                           // year   연도 값 기록
    Wire.endTransmission();                     // 전송 종료
    delay(200);                                 // 200ms 기다려 줌
  }

  // 아래부터는 기본적으로 요일, 날짜, 시간등의 데이터 값을 I2C통신을 통해 DS3231 RTC 모듈로부터 읽은 다음 OLED에 표시하는 코드
  Wire.beginTransmission(0x68);                 // DS3231의 호출 주소를 통해 I2C 통신을 시작함
  Wire.write(0);                                // 읽어올 레지스터 주소 지정(Send register address)
  Wire.endTransmission(false);                  // I2C 재시작(restart)
  Wire.requestFrom(0x68, 7);                    // 'DS3231'로부터 날짜와 시간 값들이 저장되어 있는 7 byte(8bit씩 7개)의 값을 읽어 들인다
  second = Wire.read();                         // 0번 레지스터로부터 초 값을 읽어 'second'에 저장
  minute = Wire.read();                         // 1번 레지스터로부터 분 값을 읽어 'minuts'에 저장
  hour   = Wire.read();                         // 2번 레지스터로부터 시간 값을 읽어 'hour'에 저장
  day    = Wire.read();                         // 3번 레지스터로부터 요일 값을 읽어 'day'에 저장
  date   = Wire.read();                         // 4번 레지스터로부터 날 값을 읽어 'date'에 저장
  month  = Wire.read();                         // 5번 레지스터로부터 달 값을 읽어 'month'에 저장
  year   = Wire.read();                         // 6번 레지스터로부터 연도 값을 읽어 'year'에 저장
  Wire.beginTransmission(0x68);                 // DS3231의 호출 주소를 통해 I2C 통신을 시작함
  Wire.write(0x11);                             // 읽어올 레지스터 주소 지정(0x11)
  Wire.endTransmission(false);                  // I2C 재시작(restart)
  Wire.requestFrom(0x68, 2);                    // 'DS3231'로부터 온도값이 저장되어 있는 2 byte(8bit씩 2개)의 값을 읽어 들인다
  temperature_msb = Wire.read();                // 상위(읽는 순서대로) 온도 비트를 읽어 'temperature_msb' 저장한다 
  temperature_lsb = Wire.read();                // 하위(읽는 순서대로) 온도 비트를 읽어 'temperature_lsb' 저장한다
 
  display_day();                                // 요일 출력 루틴 호출하여 출력
  DS3231_display();                             // 시간과 날자 출력 루틴 호출하여 출력
 
  delay(50);                                    // 50ms 딜레이 줌
}

아두이노 코드 파일 다운로드

Nano_OLED_DS3231_II_kr_1.zip
0.00MB

 

 만약 코드 업로드시 에러가 난다면, arduino nano 보드의 프로세서 옵션을  Old Bootloader로 선택해서 다시 업로드 해보세요.   아두이노 IDE 버전이 1.8.8 버전 이상을 사용하거나,  CH340, CH341과 같은 호환 통신칩셋을 사용한 나노 보드의 경우 업로드시 IDE메뉴,  툴 》 프로세서 "ATmega328P" 옵션에서,   "ATmega328P (Old Bootloader)"로 선택해 주어야 에러가 나지 않습니다. 

화면을 클릭하면 확대 가능합니다.

 

코드 업로드 및 동작 확인

: 회로 전체를 조립한 후 회로를 조립하고 코드를 업로드한 후 동작을 확인해 보세요

 미니 브레드보드 블럭을 끼워 연결하는 방향에 따라,  여러 가지 형태의 시계를 만들 수 있어 좋습니다.

  

3D 모델링 툴을 활용한 탁상시계 케이스 디자인

: 회로 조립과 동작은 시켰는데, 뭔가 아쉽죠?  그래서 3D 프린터로 깔끔하게 케이스를 만들어 봤습니다.  물론 주변에 아크릴 케이스 같은 걸 활용해도 좋습니다. 

 3D 모델링을 위해 사용한 디자인 툴은 라이노3D (Rhino 3D)입니다. 

3D 모델링 툴은 Rhino, fusion360, 123 Design 등 여러가지가 있지만, 비용이나, 각각의 장단점 및 특색이 있기 때문에 누구의 추천이 아니라, 본인이 지향하는 방향과 맞는 툴을 선택하시는 것이 좋습니다. 

 

모델링의 순서는 브레드보드에 꽂은 형태의 완성품을 가지고 가로x세로x높이를 측정한 다음, 케이스의 두께를 결정하고 OLED 디스플레이의 위치를 설계하는 순서로 설계했고요,  케이스 우측에는 나노의 USB mini 케이블 단자를 여유 있게 꽂을 수 있는 크기의 사각 구멍을 뚫어 주었습니다. 

그리고, 케이스의 커버는 접착제 없이 끼우고 뺄수 있는 디자인을 위해,  치수 계산을 좀 정밀하게 하여, 어느 정도 단단히 끼우거나 빠질 수 있도록 설계했습니다. 

 그리고 한 가지, 3D 프린팅을 위한 디자인에서는 프린팅되는 형태를 감안한 설계가 필요한데요,  가령 예를 들어, 위쪽 커버를 만들 때,  로고를 전면에 붙이면서, 양각으로 튀어나오게 하다 보니, 반대편에도 홈에 끼우기 위한 라인이 양각으로  즉,  상하 모두 튀어나오는 디자인으로 설계되다 보면, 3D 프린팅 과정에서 결과물이 뭉개지거나 기대하는 모양을 얻기가 어렵습니다.  따라서 로고를 음각으로 하여 뒤집어 출력하도록 하면 평평한 바닥부터 위쪽을 쌓아 올리듯 했을 때 문제없이 프린팅 됩니다.   #캡스톤디자인

 

《 3D 프린팅의 순서(생성 파일 기준) 》

: 3D 프린팅을 위해서는 프린팅에 필요한 파일을 생성해야 합니다.  모델링 설계 단계부터 생성되는 파일이 각각 있기 때문에  생성파일의 유형을 가지고 "3D 제품 모델(링) 프린팅"의 전체 흐름을 파악할 수 있습니다.  

 

아래가 그 순서입니다. (모델링 툴에 따라 파일 확장자는 다를 수 있습니다)

1. 3D 모델링 툴을 이용하여 모델링 파일 생성하기.  라이노의 경우 "xxx.3dm" 으로 작업 파일 저장 후 

   다음 단계에 사용될 파일(대표적으로, "xxx.stl" )을 Export(출력) 함.

2.  큐라(Cura)라는 대표적인 슬라이싱 프로그램을 통해 3D 모델링 파일을 한 층씩 슬라이싱 한 데이터 형태로 만들어 G-Code파일을 Export(출력)함.  :  FDM 방식의 3D 프린터는 X-Y-Z 축의 좌표 형태로 움직이면서 익스투루더의 노즐로 가열된 필라멘트를 출력시켜 한 층씩 쌓아가면서 출력물을 만들게 됩니다.  이를 위해 큐라 프로그램에서 3D 프린터에서 필요한 X-Y-Z축 좌표 코드 즉, G-Code라 하는데,  이 G-Code형태의 파일을 Export(출력)해서 , 이 파일을 가지고 3D프린터기에 입력하면 해당되는 출력물을 얻게 됩니다.   

 정리하면, 

[  3D 모델링 툴(라이노)에서 "xxx.stl" 파일 생성 》  큐라(Cura)에서 G-Code 파일("xxx.gcode") 생성 》 3D프린터로 출력 ]

아래는 대표적인 슬라이싱 프로그램인 큐라에서의 셋팅 모습입니다. 

 

아래는 큐라에서 생성한 G-Code파일을 열어본 모습입니다.  X-Y-Z 축의 좌표 등과 같은 데이터가 들어 있는 것을 알 수 있습니다. 

 

아래는 큐라에서 생성한 G-Code파일을 입력하여 모델링 파일을 출력하고 있는 모습입니다.

(프린터 모델은 CR-10 S4 기종을 사용하고 있습니다)

아래는 최종 출력물 모습입니다.

최종 조립전에, 케이스의 높이로 인해 OLED가 너무 깊숙이 들어가 있어, OLED를 최대한 잘 보이도록 위로 돌출시키기 위해,  OLED의 핀헤더를 좀 더 긴 것으로 연결하여 교체해보았습니다. (납땜 작업을 위한 인두기와 솔더 필요)

 

 

그럼,  조립회로를 넣어 최종 조립해보겠습니다.

케이스 하나 만들어 넣었을 뿐인데,  무언가 완성감 있는 만족도가 있어 좋습니다. 

 

 3D 모델링 툴을 활용한 탁상시계 케이스 디자인에 사용한 파일 다운로드 받기

  1.  3D 모델링 작업 파일 (라이노 3D Ver5 )

 

Nano_OLED_case5.3dm
0.49MB

 2.   큐라(Cura)와 같은 슬라이싱 툴에서 편집 가능한 파일 

NaOLED2.stl
1.18MB

 3.  FDM 방식의 3D 프린터에서 바로 출력 가능한 G-Code 파일 

 

NaOLED3.gcode
1.34MB

 

※ 위의 파일을 다운받아 그대로 출력해보셔도 좋고, 아니면, 본인의 보드 디자인에 따라,  이래저래 바꾸어 보며, 연습해보시면 학습에 많은 도움이 될 것으로 기대합니다.  때문에,  파일의 편집 수정 요청은 말아주셨으면 합니다. ^^;

 

 아래 영상도 함께 참고하면 전체 흐름을 파악하는데 도움되실 듯합니다.

< 전체 학습 및 제작과정 영상 보기 >

 

 

 

 

 

 

 

 

반응형