레이블이 arduino인 게시물을 표시합니다. 모든 게시물 표시
레이블이 arduino인 게시물을 표시합니다. 모든 게시물 표시

2019년 7월 9일 화요일

ESP32에서 블루투스(SPP profile) 사용하기

ESP32를 사용하면 아두이노에서처럼 따로 HC-05/06 모듈을 붙일 필요 없이 Bluetooth Classic 뿐 아니라 Bluetooth Low Energy(BLE)를 바로 사용할 수 있다.

여기서는 먼저 Bluetooth Classic 프로파일중에 하나인 SPP(Serial Port Profile)을 사용해 보겠다.

아두이노 IDE를 사용하면 Arduino core for ESP32에 필요한 라이브러리가 이미 들어있어 매우 간단하게 사용할 수 있다.

먼저 헤더파일을 include 해 줘야 한다.

#include "BluetoothSerial.h"

그 다음 BluetoothSeria 클래스의l 오브젝트를 선언해준다.

BluetoothSerial bt;

BluetoothSerial 클래스 오브젝트가 선언되면 시리얼 오브젝트와 거의 동일하게 사용할 수 있다. 일단 setup()에서 오브젝트를 초기화 시켜준다.

void setup()
{
  ...
  bt.begin("ESP32_SPP");    
   ...
}

Serial.begin()에서는 파라미터로 통신속도를 넘겨주는데 여기서는 bluetooth device name을 넘겨준다. 다른 기기에서 블루투스 장치를 검색할 때 이 이름으로 보이게 된다.

초기화가 끝나면 Serial 오브젝트와 동일하게 사용해 주면 된다. 즉 bt.read(), bt.write(), bt.available(), bt.print() 등을 Serial과 동일하게 호출할 수 있다.

여기서는 예제로 13번 핀에 LED가 연결되어 있을 때 블루투스로 '+'를 보내면 LED가 켜지고, '-'를 보내면 LED가 꺼지는 코드를 만들어 보겠다.

#include "BluetoothSerial.h"

BluetoothSerial bt;

void setup() {
  Serial.begin(115200);
  bt.begin("ESP32_SPP");
  Serial.println("Waiting for pairing");

  pinMode(13, OUTPUT);
}

void loop() {
 
  if (bt.available()) {
    ch = bt.read();
    if ('+' == ch) {
      digitalWrite(13, HIGH);
      bt.println("LED(13) turned ON");
    } else if ('-' == ch) {
      digitalWrite(13, LOW);
      bt.println("LED(13) turned OFF");
    }
  }
  delay(50);
}


위의 스케치를 ESP32에 업로드하고 시리얼 포트를 열면 다음과 같은 출력이 나올 것이다.


맨 아랫줄에 Waiting for pairing 이라고 출력을 해 블루투스로 연결할 준비가 되었음을 알 수 있다.

이제 컴퓨터 또는 안드로이드폰에서 블루투스 장치를 검색해보면 ESP32_SPP 라는 이름의 장치가 보일 것이다.


ESP32_SPP 를 연결해 주면 된다.

그 다음 시리얼 터미널 프로그램으로 가서 해달 포트를 오픈한다. 여기서는 맥이어서 'ESP32_SPP-ESP32_SPP_SER이라는 이름의 시리얼 포트가 된다.


포트를 오픈한 다음 '+' 또는 '-'를 입력해주면 각각 LED(13) turned ON 또는 LED(13) turned OFF가 터미널에 출력되며 ESP32의 13번 핀에 연결되어 있는 LED가 켜지거나 꺼지게 된다.
* 위의 화면에서는 입력된 글자를 확인하기 위해 local echo를 켜 놓았다. 즉 +/-는 사용자가 터미널에서 입력한 글자이고 LED(13) turned ON/OFF는 ESP32가 출력한 글자이다.






2019년 6월 14일 금요일

ESP32에서 CAN bus 사용하기

CAN버스는 Controller Area Network를 말하는데 이 프로토콜은 주로 자동차에서 널리 이용되고 있다. CAN에 대해 더 자세히 알고 싶으면 다음의 링크들을 참고하면 된다.

- http://www.ni.com/white-paper/2732/en
- http://www.ti.com/lit/an/sloa101b/sloa101b.pdf

ESP32는 CAN 인터페이스를 지원하기 때문에 CAN bus를 쉽게 사용할 수 있다.
이 데모에서는 2개의 ESP32 모듈을 사용해, 첫번째 모듈은 'hellocan' 이라는 메시지를 보내고 두번째 모듈은 받은 메시지의 문자열을 전부 대문자로 바꿔 첫번째 모듈로 돌려보낸다. 그러면 첫번째 모듈은 받은 메시지를 터미널로 출력하게 된다.

ESP32는 CAN controller만을 내장하고 있기 때문에 CAN 버스를 사용하려면 CAN transceiver가 필요하다. 아래 사진의 CAN transceiver를 2개 사용했다.

* aliexpress에서 개당 약 $1 정도로 구입할 수 있다.

ESP32와 CAN transceiver 모듈의 연결은 다음과 같이 해 주면 된다.


CAN library로는 Thomas Barth가 만든 CAN driver를 사용했다.

https://github.com/ThomasBarth/ESP32-CAN-Driver/tree/master/components/can

라이브러리를 다운받아 설치해주면 된다.

첫번째 ESP32 (위의 그림에서 ESP32 (1)에 해당)에는 아래와 같은 코드를 넣어준다.

#include <ESP32CAN.h>
#include <CAN_config.h>

/* the variable name CAN_cfg is fixed, do not change */
CAN_device_t CAN_cfg;

void setup() {
    Serial.begin(115200);
    Serial.println("iotsharing.com CAN demo");
    /* set CAN pins and baudrate */
    CAN_cfg.speed=CAN_SPEED_1000KBPS;
    CAN_cfg.tx_pin_id = GPIO_NUM_5;
    CAN_cfg.rx_pin_id = GPIO_NUM_4;
    /* create a queue for CAN receiving */
    CAN_cfg.rx_queue = xQueueCreate(10,sizeof(CAN_frame_t));
    //initialize CAN Module
    ESP32Can.CANInit();
}

void loop() {
    CAN_frame_t rx_frame;
    //receive next CAN frame from queue
    if(xQueueReceive(CAN_cfg.rx_queue,&rx_frame, 3*portTICK_PERIOD_MS)==pdTRUE){

      //do stuff!
      if(rx_frame.FIR.B.FF==CAN_frame_std)
        printf("New standard frame");
      else
        printf("New extended frame");

      if(rx_frame.FIR.B.RTR==CAN_RTR)
        printf(" RTR from 0x%08x, DLC %d\r\n",rx_frame.MsgID,  rx_frame.FIR.B.DLC);
      else{
        printf(" from 0x%08x, DLC %d\n",rx_frame.MsgID,  rx_frame.FIR.B.DLC);
        /* convert to upper case and respond to sender */
        for(int i = 0; i < 8; i++){
          if(rx_frame.data.u8[i] >= 'a' && rx_frame.data.u8[i] <= 'z'){
            rx_frame.data.u8[i] = rx_frame.data.u8[i] - 32;
          }
        }
      }
      //respond to sender
      ESP32Can.CANWriteFrame(&rx_frame);
    }
}

두번째 ESP32 (위의 그림에서 ESP32 (2)에 해당)에는 아래와 같은 코드를 넣어준다.

#include <ESP32CAN.h>
#include <CAN_config.h>

/* the variable name CAN_cfg is fixed, do not change */
CAN_device_t CAN_cfg;

void setup() {
    Serial.begin(115200);
    Serial.println("iotsharing.com CAN demo");
    /* set CAN pins and baudrate */
    CAN_cfg.speed=CAN_SPEED_1000KBPS;
    CAN_cfg.tx_pin_id = GPIO_NUM_5;
    CAN_cfg.rx_pin_id = GPIO_NUM_4;
    /* create a queue for CAN receiving */
    CAN_cfg.rx_queue = xQueueCreate(10,sizeof(CAN_frame_t));
    //initialize CAN Module
    ESP32Can.CANInit();
}

void loop() {
    CAN_frame_t rx_frame;
    //receive next CAN frame from queue
    if(xQueueReceive(CAN_cfg.rx_queue,&rx_frame, 3*portTICK_PERIOD_MS)==pdTRUE){

      //do stuff!
      if(rx_frame.FIR.B.FF==CAN_frame_std)
        printf("New standard frame");
      else
        printf("New extended frame");

      if(rx_frame.FIR.B.RTR==CAN_RTR)
        printf(" RTR from 0x%08x, DLC %d\r\n",rx_frame.MsgID,  rx_frame.FIR.B.DLC);
      else{
        printf(" from 0x%08x, DLC %d\n",rx_frame.MsgID,  rx_frame.FIR.B.DLC);
        for(int i = 0; i < 8; i++){
          printf("%c\t", (char)rx_frame.data.u8[i]);
        }
        printf("\n");
      }
    }
    else
    {
      rx_frame.FIR.B.FF = CAN_frame_std;
      rx_frame.MsgID = 1;
      rx_frame.FIR.B.DLC = 8;
      rx_frame.data.u8[0] = 'h';
      rx_frame.data.u8[1] = 'e';
      rx_frame.data.u8[2] = 'l';
      rx_frame.data.u8[3] = 'l';
      rx_frame.data.u8[4] = 'o';
      rx_frame.data.u8[5] = 'c';
      rx_frame.data.u8[6] = 'a';
      rx_frame.data.u8[7] = 'n';

      
      ESP32Can.CANWriteFrame(&rx_frame);
    }
}

양쪽의 보드를 동작시키고 첫번째 ESP32의 시리얼 터미널의 출력은 다음과 같다.




* Original credit goes to http://www.iotsharing.com/2017/09/how-to-use-arduino-esp32-can-interface.html



2019년 4월 26일 금요일

아두이노 우노에서 ADXL335 가속도 센서 사용하기 (Using ADXL335 accelerometer on Arduino Uno)

Original Document: https://lastminuteengineers.com/adxl335-accelerometer-arduino-tutorial/

 

가속도 센서의 동작 원리

가속도 센서의 동작원리를 이해하려면 3차원 큐브 속에 공이 들어 있는걸 상상하면 된다.



만일 이 큐브가 우주에 있어 모든것이 무게가 없는 상태라고 한다면 공은 큐브 가운데에 가만히 떠 있을 것이다. 위의 그림에서 큐브의 각 벽면이 특정 축을 나타낸다고 생각해 보자.

박스가 갑자기 왼쪽으로 가속도 1g의 가속도로 가속되어 움직이면 공은 큐브의 X 벽에 부딛히게 될 것이다. 공이 벽X를 때리는 힘을 측정한다면 X축에 1G의 출력값을 얻을 수 있다.



만일 큐브를 지표면으로 가져온다면 공은 벽 Z로 떨어질것이고 벽Z에 1G의 힘을 가할 것이다.




이 경우 큐브는 움직이지 않았지만 그래도 Z축으로 1G의 값을 얻었다. 이는 중력 가속도가 1G의 힘으로 공을 아래로 당기고 있기 때문이다.

* 가속도 센서는 기울어짐 측정 어플리케이션에서 정적 가속도(static acceleration) 뿐 아니고 모션, 쇼크, 진동에 의한 동적 가속도(dynamic acceleration)도 측정한다.

MEMS 가속도 센서의 동작 원리

MEMS(Micro Electro Mechanical Systems) 가속도 센서는 실리콘 웨이퍼 위에 매주 작은 기계가공된 구조(micro-machined structure)로 이루어져 있다.



이 구조는 폴리실리콘 스프링에 매달려 있다. 특정 축에 가속도가 가해지면 이 구조가 편향될 수 있게 해 준다.

편향으로 인해 고정된 플레이트와 매달려 있는 구조 사이의 정전용량(capacitance)이 바뀌게 된다. 이 정전용량의 변화는 축에 가해진 가속도에 비례한다.

센서는 정전용량의 변화를 처리해 아날로그 출력전압으로 변환한다.

ADXL335 가속도 센서의 하드웨어

모듈의 핵심은 아날로그 디바이스에서 나온 소형, 저전력, 저잡음의 3축 MEMS 가속도 센서인 ADXL335이다. 이 센서는 +-3g 범위 내의 값을 측정할 수 있다. 기울기 감지 어플리케이션에서 중력가속도로 인한 정적 가속도 뿐 아니고 모션, 쇼크, 진동에 의한 동적 가속도도 측정할 수 있다.



이 센서는 1.8~3.6V DC에서 동작하고 약 350uA의 전류를 소비한다. 하지만 모듈에 3.3V 레귤레이터가 들어있기 때문에 아두이노같이 5V를 사용하는 마이크로 컴트롤러에서 사용하는데도 문제가 없다.

빵판에서도 사용하기 쉽게 ADXL335의 모든 핀이 2.54mm 피치의 6핀 헤더로 연결되어 있다. 여기에는 X, Y, Z 3축의 아날로그 출력, 2개의 전원핀, 셀프테스트 핀이 들어 있다.

아날로그 출력은 ratiometic한데 이건 즉 0g는 3.3V 공급 전압의 중간(1.65V)을 출력하고, -3g는 0V, 3g는 3.3V를 출력하고 그 사이 값은 정확하게 비례한 전압이 출력된다.

다음은 ADXL335 가속도 센서 IC의 풀 스펙이다.



더 상세한 내용은 데이터쉬트를 참조하면 된다.


ADXL335 가속도 센서 핀아웃




아두이노 우노와 연결

연결은 매우 쉽다. Vcc핀은 우노의 5V핀에, GND핀은 우노의 그라운드 핀에 연결해 준다. 그리고 X, Y, Z 출력은 각각 우노의 A0, A1, A2에 연결해 주면 된다.

정확한 결과를 위해 아두이노 우노의 아날로그 레퍼런스 (AREF) 전압을 변경해 줄 필요가 있다. 여기서는 우노의 3.3V 핀을 우노의 AREF핀에 연결(아래 그림에서 빨간색 점선 부분)해 주면 된다.

  

예제 코드


const int xInput = A0;
const int yInput = A1;
const int zInput = A2;

// initialize minimum and maximum Raw Ranges for each axis
int RawMin = 0;
int RawMax = 1023;

// Take multiple samples to reduce noise
const int sampleSize = 10;

void setup() 
{
 analogReference(EXTERNAL);
 Serial.begin(115200);
}

void loop() 
{
 //Read raw values
 int xRaw = ReadAxis(xInput);
 int yRaw = ReadAxis(yInput);
 int zRaw = ReadAxis(zInput);

 // Convert raw values to 'milli-Gs"
 long xScaled = map(xRaw, RawMin, RawMax, -3000, 3000);
 long yScaled = map(yRaw, RawMin, RawMax, -3000, 3000);
 long zScaled = map(zRaw, RawMin, RawMax, -3000, 3000);

 // re-scale to fractional Gs
 float xAccel = xScaled / 1000.0;
 float yAccel = yScaled / 1000.0;
 float zAccel = zScaled / 1000.0;

 Serial.print("X, Y, Z  :: ");
 Serial.print(xRaw);
 Serial.print(", ");
 Serial.print(yRaw);
 Serial.print(", ");
 Serial.print(zRaw);
 Serial.print(" :: ");
 Serial.print(xAccel,0);
 Serial.print("G, ");
 Serial.print(yAccel,0);
 Serial.print("G, ");
 Serial.print(zAccel,0);
 Serial.println("G");

 delay(200);
}

// Take samples and return the average
int ReadAxis(int axisPin)
{
 long reading = 0;
 analogRead(axisPin);
 delay(1);
 for (int i = 0; i < sampleSize; i++)
 {
 reading += analogRead(axisPin);
 }
 return reading/sampleSize;
}

위의 코드에서는 가장 먼저 센서의 X,Y, Z 출력이 아두이노 우노의 어느 아날로그 입력에 연결되어 있는가를 선언해 준다.

다음은 아날로그입력의 최소/최대값을 지정해 준다. 아두이노 우노의 경우 10-bit ADC를 사용하므로 0~3.3V의 전압은 0~1023 사이의 값으로 변환된다.

sampleSize 변수는 더 정확한 결과를 얻기 위해 각 출력값에 몇개의 샘플값을 사용하는지를 지정한다.

* 주의 
analogReference(EXTERNAL)을 호출하는걸 빼먹으면 내부적으로 생성된 reference 전압과 AREF핀을 쇼트시키게 되므로 아두이노 우노를 망가트릴 수도 있으므로 가장 먼저 호출하는걸 잊어먹으면 안된다.

analogReference(EXTERNAL);
Serial.begin(115200);

loop() 함수에서 매 200ms마다 아날로그 출력값을 읽는다. analogRead() 함수 대신 ReadAxis() 함수를사용한다. 이 함수에서는 10개의 ADC 변환값을 읽어 평균값을 리턴해준다.

// Read raw values
int xRaw = ReadAxis(xInput);
int yRaw = ReadAxis(yInput);
int zRaw = ReadAxis(zInput);

ADXL335 출력을 가속도로 변환하기

센서의 아날로그 출력값을 실제 가속도로 변환해 줘야 한다. 여기서는 아두이노에 기본으로 들어 있는 map() 함수를 사용한다. map(xRaw, RawMin, RawMax, -3000, 3000); 를 호출하면 RawMin(0)은 -3000으로, RawMax(1023)는 3000으로, 0~1023 사이 값은 -3000~3000 사이에 비례하는 값으로 변환된다.

* 여기서 -3000, 3000은 임의의 값이 아니다. 이 값은 센서에서 측정된 실제 중력 가속도(1/1000 G 단위)를 나타낸다. 

  • 센서의 X축이 0V를 출력하면 (xRaw=0) map() 함수는 -3000을 리턴할 것이고 이는 즉 -3G를 나타낸다
  • 센서의 X축이 3.3V를 출력하면 (xRaw=1023) map() 함수는 3000을 리턴할 것이고 이는 즉 3G를 나타낸다.
  • 센서의 X축이 1.65V를 출력하면 (xRaw=511) map() 함수는 0을 리턴할 것이고 이는 즉 0G (가속도가 없음)을 나타낸다.

아래 그림은 모듈을 각각 다른 방향으로 놓았을 때 가속도 센서의 출력값을 보여준다.


ADXL335 셀프 테스트

ADXL335는 최종 어플리케이션에서 센서의 동작을 확인하기 위한 셀프테스트 기능을 가지고 있다.



모듈에 있는 ST(self-test)핀으로 이 기능을 제어한다.

ST핀이 3.3V에 연결되어 있으면 내부적으로 가속도계에 정전력이 가해진다. 이로 인해 사용자는 가속도 센서가 정상적으로 동작하는지 확인할 수 있다.

이로 인한 출력의 변화는 다음과 같다.

  • X축으로 -1.08g (-325mV)
  • Y축으로 +1.08g (+325mV)
  • Z축으로 +1.83g (+550mV)

정상동작시 ST핀은 오픈시켜 놓거나 GND에 연결해주면 된다.


















ㅇ

2019년 4월 19일 금요일

ESP32 딥슬립과 깨어나기 (ESP32 Deep Sleep & Wake-up source)


 

ESP32에서 deep sleep이 왜 필요할까?


어떤 상태에 있는가에 따라 ESP32는 상대적으로 전력소모가 큰 디바이스가 될 수도 있다. 일반적인 동작시 약 75mA 정도의 전류를 소비하지만 WiFi로 데이터를 전송하는 경우는 240mA 정도까지 소비하게 된다.

개발하는것이 외부 아답터를 사용하는 경우라면 전력소모에 대해 별로 신경 쓰지 않겠지만 배터리로 동작하는 경우라면 수 mA라도 크게 신경을 써야 한다. 이 문제의 해결책으로 deep sleep 모드롤 활용해 ESP32의 전력 사용을 줄여줄 수 있다.

ESP32의 다른 슬립모드에 대한 자세한 내용은 이전 포스트를 참조하면 된다.

ESP32 Deep Sleep

Deep Sleep 모드에서는 CPU, 대부분의 RAM과 모든 디지털 페리페럴의 전원이 꺼진다. 전원이 공급되는 부분은 RTC controller, RTC 페리페럴(ULP 코프로세서를 포함), RTC 메모리(slow & fast) 뿐이다.

ULP를 사용하는 경우 약 0.15mA, ULP도 꺼져있는 경우 약 10uA의 전류를 소비한다.



Deep Sleep mode동안 메인 CPU는 전원이 꺼져 있지만 ULP 코프로세서가 센서 값을 읽어 측정된 값에 따라 메인 시스템을 깨울 수 있다. 이런 sleep pattern은 ULP sensor-monitored pattern이라고 한다.

CPU뿐 아니고 칩의 메인 메모리도 전원이 꺼지기 때문에 메모리에 저장된 모든 내용은 사라지고 억세스 할 수 없다.

하지만 RTC 메모리는 전원이 유지되기 때문에 deep sleep mode에서도 내용이 유지되고 메인 프로세서가 깨어난 다음에 내용을 읽을 수 있다. 그렇기 때문에 ESP32가 WiFi와 블루투스를 끄기 전에 연결 데이터를 RTC 메모리에 저장하는 것이다.

그러므로 리부팅 후에 데이터를 사용하려면 전역변수를 선언할 때 RTC_DATA_ATTR attribute를 사용해 RTC 메모리에 저장해야 한다. 예를 들어 RTC_DATA_ATTR int bootCount = 0; 같이 해 주면 된다.

Deep Sleep mode에서는 RTC 모듈을 제외한 칩 전체의 전원이 꺼지기 때문에 RTC recovery memory에 들어 있지 않은 내용은 모두 사라지고 RESET으로 칩을 재시작한다. 즉 프로그램 실행은 다시 한번 맨 처음부터 다시 시작하게 된다는 의미이다.

* TIP
ESP32는 deep sleep에서 깨어날 때 deep sleep wake stub을 실행하는걸 지원한다. 이 함수는 칩이 깨어나자 마자 다른 어떤 코드 (정상적인 초기화 또는 부트로더 코드)보다 먼저 곧바로 실행된다. Wake stub 이 실행된 후에 칩은 sleep mode로 돌아가거나 또는 정상적으로 시작할 수 있다.

다른 sleep mode와 달리 시스템이 deep sleep mode로는 자동으로 들어갈 수 없다. Wake-up 소스를 설정한 후 esp_deep_sleep_start() 함수를 호출하면 곧바로 deep sleep mode로 들어가게 된다.

기본적으로 ESP32는 wake-up 소스에 필요 없는 모든 페리페럴의 전원을 자동으로 꺼 버리지만 옵션으로 모든 페리페럴들애 대해 전원을 끌지 여부를 결정할 수도 있다.


ESP32 Deep Sleep Wake-up Sources

여러가지 방법으로 Deep Sleep mode에서 깨어나게 할 수 있다.

  • Timer
  • Touch pad
  • External wakeup (ext0 & ext1)
Wake-up source는 여러개를 동시에 사용할 수도 있다. 이 경우 여러개의 소스 중 하나가 트리거되면 칩이 깨어나게 된다.

이 소스들은 sleep modㄷ로 들어가기 전에 아무때나 설정할 수 있다.

* 주의
아무 Wake-up source를 설정하지 않고 deep sleep 모드로 들어갈 수도 있는데, 이 경우 칩은 외부에서 리셋을 시켜주기 전에는 무한히 deep sleep mode에 있게 된다.

ESP32 Wake-up Source : Timer

RTC controller는 타이머를 내장하고 있어 정해진 시간 후에 칩을 깨울 수 있다.

시간은 us 단위로 지정하지만 실제 resolution은 선택된 클럭 소스에 따라 결정된다.

타이머를 사용하는 경우 esp_sleep_enable_timer_wakeup() 함수로 sleep wake-up을 활성화 시킬 수 있다.

다음은 타이머를 wake-up 소스로 사용하고 리부팅 될 때 마다 사용하기 위해 RTC 메모리에 데이터를 저장하는 가장 기본적인 예제이다.

#define uS_TO_S_FACTOR 1000000  //Conversion factor for micro seconds to seconds
#define TIME_TO_SLEEP  5        //Time ESP32 will go to sleep (in seconds)

RTC_DATA_ATTR int bootCount = 0;

void setup(){
 Serial.begin(115200);
 delay(1000); //Take some time to open up the Serial Monitor

 //Increment boot number and print it every reboot
 ++bootCount;
 Serial.println("Boot number: " + String(bootCount));

 //Print the wakeup reason for ESP32
 print_wakeup_reason();

 //Set timer to 5 seconds
 esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
 Serial.println("Setup ESP32 to sleep for every " + String(TIME_TO_SLEEP) +
 " Seconds");

 //Go to sleep now
 esp_deep_sleep_start();
}

void loop(){}

//Function that prints the reason by which ESP32 has been awaken from sleep
void print_wakeup_reason(){
 esp_sleep_wakeup_cause_t wakeup_reason;
 wakeup_reason = esp_sleep_get_wakeup_cause();
 switch(wakeup_reason)
 {
  case 1  : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
  case 2  : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
  case 3  : Serial.println("Wakeup caused by timer"); break;
  case 4  : Serial.println("Wakeup caused by touchpad"); break;
  case 5  : Serial.println("Wakeup caused by ULP program"); break;
  default : Serial.println("Wakeup was not caused by deep sleep"); break;
 }
}

ESP32 Wake-up Source : Touch Pad

RTC IO 모듈은 터치센서 인터럽트가 발생할 때 wake-up 시키는 회로를 가지고 있다.

칩이 deep sleep모드로 들어가기 전에 touch pad interrupt를 설정해 줘야 한다.

이 wake-up source를 활성화 하려면 esp_sleep_enable_touchpad_wakeup() 함수를 사용한다.

다음은 touch pad를 wake-up 소스로 사용하고 리부팅 될 때 마다 사용하기 위해 RTC 메모리에 데이터를 저장하는 가장 기본적인 예제이다.

//Define touch sensitivity. Greater the value, more the sensitivity.
#define Threshold 40

RTC_DATA_ATTR int bootCount = 0;
touch_pad_t touchPin;

void callback(){
  //placeholder callback function
}

void setup(){
  Serial.begin(115200);
  delay(1000);

  //Increment boot number and print it every reboot
  ++bootCount;
  Serial.println("Boot number: " + String(bootCount));

  //Print the wakeup reason for ESP32 and touchpad too
  print_wakeup_reason();
  print_wakeup_touchpad();

  //Setup interrupt on Touch Pad 3 (GPIO15)
  touchAttachInterrupt(T3, callback, Threshold);

  //Configure Touchpad as wakeup source
  esp_sleep_enable_touchpad_wakeup();

  //Go to sleep now
  esp_deep_sleep_start();
}

void loop(){}

//Function that prints the reason by which ESP32 has been awaken from sleep
void print_wakeup_reason(){
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason)
  {
    case 1  : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
    case 2  : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
    case 3  : Serial.println("Wakeup caused by timer"); break;
    case 4  : Serial.println("Wakeup caused by touchpad"); break;
    case 5  : Serial.println("Wakeup caused by ULP program"); break;
    default : Serial.println("Wakeup was not caused by deep sleep"); break;
  }
}


//Function that prints the touchpad by which ESP32 has been awaken from sleep
void print_wakeup_touchpad(){
  touch_pad_t pin;
  touchPin = esp_sleep_get_touchpad_wakeup_status();
  switch(touchPin)
  {
    case 0  : Serial.println("Touch detected on GPIO 4"); break;
    case 1  : Serial.println("Touch detected on GPIO 0"); break;
    case 2  : Serial.println("Touch detected on GPIO 2"); break;
    case 3  : Serial.println("Touch detected on GPIO 15"); break;
    case 4  : Serial.println("Touch detected on GPIO 13"); break;
    case 5  : Serial.println("Touch detected on GPIO 12"); break;
    case 6  : Serial.println("Touch detected on GPIO 14"); break;
    case 7  : Serial.println("Touch detected on GPIO 27"); break;
    case 8  : Serial.println("Touch detected on GPIO 33"); break;
    case 9  : Serial.println("Touch detected on GPIO 32"); break;
    default : Serial.println("Wakeup not by touchpad"); break;
  }
}

ESP32 Wake-up Source : External Wake-up

ESP32를 Deep Sleep에서 깨어나게 해 주는 두가지 타입의 외부 트리거가 있다.
  • ext0 - 특정 핀에 의해서만 칩을 wake-up 하고 싶을 때 사용
  • ext1 - 여러 버튼으로 wake-up 하고 싶을 때 사용

ext0 External Wake-up Source

RTC controller는 특정 핀이 미리 지정된 로직 레벨로 될 때 wake-up 시키는 회로를 가지고 있다. 여기에는 RTC GPIO 핀들 (0, 2, 4, 12~15, 25~27, 32~39) 중에 하나를 사용할 수 있다.

이 wake-up source를 활성화 시키려면 esp_sleep_enable_ext0_wakeup(GPIO_PIN, LOGIC_LEVEL) 함수를 사용한다. 이 함수는 두개의 파라미터가 필요하다. 첫번째는 어떤 핀을 사용할 것인가이고 두번째는 핀 상태가 어떨 때  (HIGH 또는 LOW) wake-up 시킬까를 결정한다.

ext0는 wake-up을 위해 RTC IO를 사용하기 때문에 이 wake-up source를 사용ㅎ면 deep sleep 동안에도 RTC 페리페럴에 전원이 공급된다.

이 모드에서 RTC IO 모듈이 활성화 되어 있으므로 내부 pull-up/down 역시 사용할 수 있다. 이 경우 esp_sleep_start()를 호출하기 전에 어플리케이션에서 rtc_gpio_pullup_en(), rtc_gpio_pulldown_en() 함수를 사용해 설정해 줘야 한다.

아래는 푸쉬버튼을 ext0 wake-up source로 동작시키기 위해 어떻게 연결했는가를 보여주는 회로도이다.



다음은 ext0를 wake-up source로 사용하는 가장 기초적인 예제 코드이다.

RTC_DATA_ATTR int bootCount = 0;

void setup(){
  Serial.begin(115200);
  delay(1000); 

  //Increment boot number and print it every reboot
  ++bootCount;
  Serial.println("Boot number: " + String(bootCount));

  //Print the wakeup reason for ESP32
  print_wakeup_reason();

  //Configure GPIO33 as ext0 wake up source for HIGH logic level
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_33,1);

  //Go to sleep now
  esp_deep_sleep_start();
}

void loop(){}

//Function that prints the reason by which ESP32 has been awaken from sleep
void print_wakeup_reason(){
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason)
  {
    case 1  : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
    case 2  : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
    case 3  : Serial.println("Wakeup caused by timer"); break;
    case 4  : Serial.println("Wakeup caused by touchpad"); break;
    case 5  : Serial.println("Wakeup caused by ULP program"); break;
    default : Serial.println("Wakeup was not caused by deep sleep"); break;
  }
}

ext1 External Wake-up Source

ESP32는 여러개의 GPIO 핀을 사용해 deep sleep mode에서 깨어나게 할 수도 있다. 이 경우 RTC GPIO핀들 (32~39)을 사용할 수 있다.

ext1 wake-up source는 RTC controller를 사용하기 때문에 RTC 페리페럴이나 RTC 메모리를 켜 줄 필요가 없다. 즉 내부 pull-up/down 저항을 사용할 수 없다.

내부 pull-up/down 저항을 사용하려면 deep sleep동안 RTC 페리페럴이 켜져 있도록 요청하고 sleep에 들어가기 전에 rtc_gpio_ 함수들을 사용해 pull-up/down 저항을 설정해 줘야 한다.

이 wake-up source를 활성화 시키려면 esp_sleep_enable_ext1_wakeup(BUTTON_PIN_MASK, LOGIC_LEVEL) 함수를 사용한다. 이 함수는 두개의 파라미터가 필요하다. 첫번째는 ESP32에게 어떤 핀들을 사용할지 알려주는 pin mask이다.

두번째는 핀 상태가 어떨 때  (HIGH 또는 LOW) wake-up 시킬까를 결정한다.

  • ESP_EXT1_WAKEUP_ANY_HIGH - 선택된 핀들 중 어느 하나가 HIGH일 때 wake-up
  • ESP_EXT1_WAKEUP_ANY_LOW - 선택된 핀들 중 어느 하나가 LOW일 때 wake-up
PIN MASK를 이해하는 가장 쉬운 방법은 바이너리 형태로 써 보는 것이다.




  • 0 - masked pin. 즉 이 핀은 외부 인터럽트 소스로 사용하지 않음
  • 1 - wake-up source로 활성화 된 핀
LSB가 GPIO0을 나타내고 MSB가 GPIO39를 나타낸다.

만일 어떤 GPIO핀도 wake-up source로 사용하고 싶지 않으면 모든 위치에 '0'을 써 넣으면 된다.

아래는 여러개의 푸쉬버튼을 ext1 wake-up source로 동작시키기 위해 어떻게 연결했는가를 보여주는 회로도이다.




다음은 ext1을 wake-up source로 사용하는 가장 기초적인 예제 코드이다.

//Pushbuttons connected to GPIO32 & GPIO33
#define BUTTON_PIN_BITMASK 0x300000000

RTC_DATA_ATTR int bootCount = 0;

void setup(){
  Serial.begin(115200);
  delay(1000); 

  //Increment boot number and print it every reboot
  ++bootCount;
  Serial.println("Boot number: " + String(bootCount));

  //Print the wakeup reason for ESP32
  print_wakeup_reason();

  //Configure GPIO32 & GPIO33 as ext1 wake up source for HIGH logic level
  esp_sleep_enable_ext1_wakeup(BUTTON_PIN_BITMASK,ESP_EXT1_WAKEUP_ANY_HIGH);

  //Go to sleep now
  esp_deep_sleep_start();
}

void loop(){}

//Function that prints the reason by which ESP32 has been awaken from sleep
void print_wakeup_reason(){
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason)
  {
    case 1  : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
    case 2  : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
    case 3  : Serial.println("Wakeup caused by timer"); break;
    case 4  : Serial.println("Wakeup caused by touchpad"); break;
    case 5  : Serial.println("Wakeup caused by ULP program"); break;
    default : Serial.println("Wakeup was not caused by deep sleep"); break;
  }
}














2018년 11월 23일 금요일

ESP32에서 SPIFFS 사용하기

ESP32는 SPIFFS(Serial Peripheral Interface Flash File System)을 가지고 있다. SPIFFS는 SPI 버스에 연결된 플래쉬 칩을 가지고 있는 ESP32같은 마이크로 컨트롤러를 위해 만들어 진 경랑(lightweight) 파일 시스템이다.

SPIFFS를 사용하면 플래쉬 메모리를 컴퓨터에서 일반 파일을 억세스 하는 것 처럼 사용할 수 있게 해 준다. 물론 더 간단하지만 기능은 제한적이다. 파일을 읽고, 쓰고, 삭제하는 것이 가능하다. 현재 시점에서는 디렉토리를 지원하지 않으므로 모든 파일은 flat한 구조에 저장된다.

ESP32 Filesystem Uploader 설치

아두이노 IDE에서 코드를 작성해 ESP32 파일시스템에 파일을 만들어 내용을 기록해 줄 수 있지만, 이 방법은 파일 내용을 전부 아두이노 스케치 코드에 타이핑 해 줘야만 하므로 유용하지 못하다.
다행히도 아두이노 IDE용 플러그인이 있어 컴퓨터의 폴더에서 ESP32 파일시스템으로 파일을 직접 업로드 할 수가 있다. 이를 위해 ESP32 Filesystem Uploader를 설치해 준다.

* 먼저 최신 버젼의 아두이노 IDE가 설치되어 있고 아두이노용 ESP32 개발환경이 설정되어 있어야만 한다.

1. Arduino-esp32fs-plugin 페이지에서 ESP32FS-v01.zip 을 다운받는다.
2. 아두이노 IDE 디렉토리로 가서 Tools 폴더를 연다.
맥의 경우는 ~/Documents/Arduino 폴더 안에 tools 폴더를 만들어 주면 된다.
3. tools 폴더에서 다운받은 zip 파일 압축을 풀어준다.
4. 아두이노 IDE를 재시동한다.
5. 아두이노 IDE의 Tools 메뉴에 "ESP32 Sketch Data Upload" 항목이 보이면 정상적으로 설치가 된 것이다.


Filesystem Uploader를 사용해서 파일 업로드하기

1. 아두이노 스케치를 만들어 저장한다. 여기서는 파일 업로드 데모이므로 빈 스케치를 사용한다.
2. 스케치 폴더를 열어 준다. Sketch 메뉴에서 'Show Sketch Folder'를 선택하면 스케치가 저장되어 있는 폴더가 열린다.


 3. 폴더 안에 data 라는 이름의 폴더를 만들어 준다.


 4. data 폴더 안에 ESP32 파일시스템에 저장하고 싶은 파일들을 넣어주면 된다. 여기서는 예제로 test_example.txt 라는 파일을 만들어 간단한 내용을 넣어 준다.

5. Tools -> ESP32 Sketch Data Upload 를 선택해서 데이터를 업로드 하면 된다.


예제

1. ESP32_SPIFFS_test 라는 이름으로 스케치를 만들고 스케치 폴더 내에 data 폴더를 만들어 준다.

2. 데이터 폴더 안에 'data.txt'라는 이름의 텍스트 파일을 만들고 간단한 내용을 넣어 준다. 여기서는 'Hello World!'를 넣어주었다.

3. 위의 설명대로 SPIFFS에 파일을 업로드 해 준다.

4. 다음의 스케치 코드를 입력해서 보드에 업로드 해 준다.

#include "SPIFFS.h"

void setup() {
  Serial.begin(115200);

  if (!SPIFFS.begin(true)) {
    Serial.println("# ERROR: can not mount SPIFFS");
    while (1) ;
  }
}

void loop() {
  File f = SPIFFS.open("/data.txt");
  if (!f) {  // can not open file
    Serial.println("# ERROR: can not open file");
    delay(5000);
    return;
  }

  while (f.available()) {
    Serial.write(f.read());
  }
  Serial.println("");

  f.close();
  delay(5000);
}


5. 실행 결과는 다음과 같다.




















2018년 11월 22일 목요일

ESP32에서 인터럽트 사용하기

ESP32는 26개의 GPIO핀을 인터럽트 소스로 사용할 수 있다.

* 아두이노 우노/메가에서는 인터럽트 소스로 사용할 수 있는 핀이 몇개 안된다.




위의 그림에서 빨간색 사각형 안에 들어있는 GPIO 핀들이 인터럽트 소스로 사용 가능한 핀이다.

아두이노에서 인터럽트를 사용하려면 attachInterrupt() 함수로 인터럽트를 설정해 줘야 한다.

attachInterrupt (digitalPinToInterrupt(GPIO), function, mode);

첫번째 파라미터는 인터럽트 소스로 사용할 GPIO 핀 번호인데 번호를 그냥 사용하지 말고 digitalPinToInterrupt(GPIO) 로 넣어줘야 한다. 예를 들어 GPIO 13을 인터럽트 소스로 사용하려면 digitalPinToInterrupt(13) 을 넣어주면 된다.

두번째 파라미터는 인터럽트가 발생했을 때 호출될 함수(Interrupt Service Routine)의 이름이다.

세번째 파라미터는 인터럽트 발생 조건이다. 총 5가지 모드중에 하나를 선택하면 된다.

* LOW - 인터럽트 핀 상태가 LOW면 인터럽트가 발생
* HIGH - 인터럽트 핀 상태가 HIGH면 인터럽트가 발생
* CHANGE - 인터럽트 핀 상태가 바뀌면 인터럽트가 발생. 즉 HIGH에서 LOW로 바뀌거나, LOW에서 HIGH로 바뀌면 인터럽트가 발생
* FALLING - 인터럽트 핀 상태가 HIGH에서 LOW로 바뀔 때 인터럽트가 발생
* RISING - 인터럽트 핀 상태가 LOW에서 HIGH로 바뀔 때 인터럽트가 발생

그러므로 인터럽트를 사용하는 경우 코드의 구조는 다음과 같이 된다.

// 인터럽트가 발생하면 이 함수가 호출됨
void IRAM_ATTR intSvc() 
{
  // 스위치가 눌렸을 때 원하는 동작 코드를 이 부분에 넣어줌
  Serial.println("# Button pressed");
}
void setup()
{
  ...
  attachInterrupt(digitalPinToInterrupt(13), intSvc, LOW);  // GPIO13번 핀에 연결된 스위치가 눌리면 인터럽트가 발생해 intSvc 함수를 호출하도록 설정
  ...
}

void loop()
{
}

위의 코드를 잘 보면 인터럽트가 발생했을 때 호출될 함수 선언에 IRAM_ATTR 이라는 것이 추가되어 있는것을 알 수 있을 것이다. IRAM_ATTR을 넣어줘야 인터럽트 처리 함수가 RAM에 들어가게 된다. IRAM_ATTR이 없으면 인터럽트 처리 함수가 플래쉬에 들어가게 되어 실행 속도가 느려진다.


위와 같이 연결 해 놓고 스위치를 누를 때 마다 LED가 토글되는 코드는 다음과 같다.

#define LED 25
#define SW 13

volatile boolean gLedState = LOW;
 
void IRAM_ATTR toggle() 
{
  gLedState = !gLedState;
  digitalWrite(LED, gLedState);
}


void setup()
{
  pinMode(SW, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SW), toggle, LOW);  // GPIO13번 핀에 연결된 스위치가 눌리면 toggle 함수를 호출하도록 설정
  pinMode(LED, OTUPUT);
}

void loop()
{
}






2018년 11월 20일 화요일

ESP32 PWM 사용하기

ESP32에는 16개의 독립적인 LED PWM 채널을 가지고 있어 각각 다른 속성의 PWM 신호를 만들어 낼 수 있다.

1. 어떤 PWM 채널을 사용할 지 골라야 한다. 0~15 사이의 값을 사용할 수 있다.
2. PWM 주파수를 설정해 줘야 한다. LED의 경우 5000 Hz 정도면 충분하다.
3. PWM duty cycle resolution을 설정해 줘야 한다. 해상도(resolution)은 1비트에서 16비트까지 지정할 수 있다. 여기서는 8 bit 해상도를 사용하는데 그 경우 LED 밝기 값은 0에서 255가 될 수 있다. 만일 16 bit로 설정하면 좀 더 세밀하게 (0~65535) 밝기 값을 조정할 수 있게 된다.

ledcSetup(ch, freq, resolution);   // ch: PWM channel (0~15), freq: PWM 주파수, resolution: PWM  해상도


PWM 주파수는 1초에 몇번 신호를 ON/OFF 할 것인지 결정한다. 위의 그림에서 처럼 PWM 주파수를 4 Hz로 한다면 CH0에 연결되어 있는 LED는 4번 on/off를 반복할 것이다. 8 Hz인 경우 CH1에 연결되어 있는 LED는 8번 on/off를 하게 될 것이다. 아두이노 우노의 경우 PWM 주파수는490 Hz(5,6번 핀은 980 Hz)로 고정되어 있다.

Resolution은 한 주기 내에서 얼마나 세밀하게 시간을 지정할 수 있는가를 결정한다. 8-bit resolution인 경우 2^8 = 256 등분으로 나누지만 10-bit resolution이 되면 2^10 = 1024 등분으로 나뉘어 훨씬 더 정밀하게 제어가 가능해진다. ESP32에서는 1-bit 부터 16-bit까지 resolution을 지정할 수 있다. 아두이노 우노의 경우에는 analogWrite()의 resolution은 8-bit로 고정되어 있어 출력값으로 0~255 사이의 값을 사용해야 한다.

4. 설정한 채널의 출력을 어느 GPIO 핀으로 보낼것인지 지정해야 한다.

ledcAttachPin(gpio, ch);   // gpio : GPIO 핀 번호,  ch : PWM channel

5. 이제 ledcWrite() 함수를 사용해 LED 밝기를 조절할 수 있다.

ledcWrite(ch, duty); // ch : PWM channel, duty : Duty cycle

duty 값은 한 주기 내에서 ON 시간과 OFF 시간의 비율을 결정하는 값이다.


위의 그림을 보면 알 수 있듯이 PWM 주파수가 다르면 같은 duty 값이라도 ON 되어 있는 시간이 달라진다. 다만 전체적으로 ON 시간의 합과 OFF 시간의 합의 비율은 동일하다.




위와 같이 LED를 연결한 경우 LED가 점점 밝아졌다가 다시 점점 어두워지는걸 반복하는 코드는 다음과 같다.

const int ledPin = 16;  // 16 corresponds to GPIO16
// setting PWM properties
const int freq = 5000;
const int ledChannel = 0;
const int resolution = 8;
 

void setup(){
  ledcSetup(ledChannel, freq, resolution);  
// configure LED PWM functionalitites  
  ledcAttachPin(ledPin, ledChannel);  
  // attach the channel to the GPIO to be controlled
}
 void loop(){
  // increase the LED brightness
  for(int dutyCycle = 0; dutyCycle <= 255; dutyCycle++){   
    // changing the LED brightness with PWM
    ledcWrite(ledChannel, dutyCycle);
    delay(15);
  }

  // decrease the LED brightness
  for(int dutyCycle = 255; dutyCycle >= 0; dutyCycle--){
    // changing the LED brightness with PWM
    ledcWrite(ledChannel, dutyCycle);   
    delay(15);
  }
}

PWM 신호의 frequency를 조정할 수 있으므로 16개의 서보모터를 동시에 제어할 수도 있다.


서보모터는 위와 같은 PWM 신호로 제어를 하기 때문에 PWM frequency를 50 Hz로 해 주고 16-bit resolution을 사용하는 경우 duty값 3277이 0도, 4915가 90도, 6554가 180도가 된다.

즉 GPIO16에 연결된 서보모터를 CH0를 사용해서 제어한다고 하면 다음과 같이 할 수 있다.

void setup()
{
  ...
  ledcSetup(0, 50, 16);    // PWM CH 0, Freq. 50 Hz, 16-bit resolution
  ledcAttach(16, 0);         // PWM CH 0을 GPIO 16번으로 출력
  ...
}

// deg는 0~180도 까지
void servoWrite(int ch, int deg)
{
  int duty = deg*18.2 + 3277;

  ledcWrite(ch, duty);
}

void loop()
{
  ...
  servoWrite(0, 90);   // CH 0에 연결된 서보를 90도로
  ...
}







2018년 11월 16일 금요일

ESP32에서 NTP(Network Time Protocol) 서버로부터 시간 가져오기



인터넷에서 정확한 시간을 알기 위해 사용하는 프로토콜로 NTP(Network Time Protocol)이 있다. 곳곳에 운영중인 NTP서버에 접속해 정확한 현재 날짜와 시간을 가져 올 수 있다.



ESP32에서도 NTP를 사용할 수 있게 이미 NTP client 라이브러리가 공개되어 있다.

Download NTP Client Library

위의 링크를 클릭하면 라이브러리를 다운받아 설치해주면 된다.

코드 구조는 다음과 같다.

1. WiFi.h, WiFiUdp.h, NTPClient.h 를 include 해 줘야 한다.

#include <WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>

2. 인터넷 연결을 위해 공유기 설정을 해 준다.

const char* ssid  = "XXXXX";
const char* password = "YYYYY";

위에서 XXXXX 대신 사용할 공유기의 이름(SSID)로, YYYYY 대신 공유기의 암호로 바꿔줘야 한다.

3. 이제 NTP client를 만들어 준다.

WiFiUdp ntpUDP;
NTPClient timeClient(ntpUdp);

4. 공유기에 접속한 후 NTP client를 시작한다.

....
timeClient.begin();

5. 자신의 timezone에 맞게 시간을 조정하기 위해 setTimeOffset() 메소드를 사용할 수 있는데 이때 offset은 초 단위로 지정해 줘야 한다. 즉 1시간은 3600초이므로 한국의 경우 GMT+9 이니까 3600*9 = 32400이 된다.

timeClient.setTimeOffset(32400);

6. NTP 서버에 요청해 시간을 가져온다.

while (!timeClient.update()) {
  timeClient.forceUpdate();
}

7. NTP 서버에게서 읽어온 값을 사람이 볼 수 있는 형태로 변환해준다.

formattedDate = timeClient.getFormattedDate();

getFormattedDate()가 리턴한 값은 다음과 같은 포맷이다.

2018-11-09T07:34:21z

T 앞쪽이 날짜가 되고 T에서 z까지가 시간이므로 각각을 기준으로 원하는 내용을 추출해서 출력해주면 된다.

실행 가능한 전체 코드는 다음과 같다.

#include <WiFi.h>
#include <WiFiUdp.h>

#include <NTPClient.h>

const char* ssid     = "XXXXX";  // XXXXX 를 접속할 공유기 SSID로 변경
const char* password = "YYYYY"   // YYYYY 를 접속할 공유기 암호로 변경

// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

// Variables to save date and time
String formattedDate;
String dayStamp;
String timeStamp;

void setup() {
  Serial.begin(115200);
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  timeClient.begin();    // NTP 클라이언트 초기화
  // 자신의 timezone에 맞게 초 단위로 time offset을 설정해준다. 예를 들어
  // GMT +1 = 3600
  // GMT +8 = 28800
  // GMT -1 = -3600
  // GMT 0 = 0
  timeClient.setTimeOffset(32400);  // 한국은 GMT+9이므로 9*3600=32400
}
void loop() {
  while(!timeClient.update()) {
    timeClient.forceUpdate();
  }
  // formattedDate 은 다음과 같은 형태임
  // 2018-11-12T16:00:13Z
  formattedDate = timeClient.getFormattedDate();
  Serial.println(formattedDate);

  // 날짜 추출
  int splitT = formattedDate.indexOf("T");
  dayStamp = formattedDate.substring(0, splitT);
  Serial.print("DATE: ");
  Serial.println(dayStamp);
  // 시간 추출
  timeStamp = formattedDate.substring(splitT+1, formattedDate.length()-1);
  Serial.print("HOUR: ");
  Serial.println(timeStamp);
  delay(1000);
}


코드의 실행 결과이다.

Connecting to XXXXX
.......
WiFi connected.
IP address:
172.20.10.13
2018-11-09T08:53:26Z
DATE: 2018-11-09
HOUR: 08:53:26
2018-11-09T08:53:27Z
DATE: 2018-11-09
HOUR: 08:53:27
2018-11-09T08:53:28Z
DATE: 2018-11-09
HOUR: 08:53:28