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월 18일 화요일

LTE modem 사용

LTE modem 모델은 Huawei E8372h-153


aliexpress에서 $30~40에 구할 수 있다. 워낙 많은 통신사에서 사용하는 모델이다 보니 제품 윗면의 로고는 다를 수 있는데 신경 쓸 필요 없음






 뒷면에보면 모델명을 확인할 수 있음


기본적으로 내부에 안테나가 들어있어 그대로 사용해도 되지만 신호 수신을 좋게하기 위해 옆면에 외부 안테나를 부착할 수 있는 포트가 마개로 덮혀 있음



이런식으로 외부 안테나 부착이 가능함


로고가 표시돠어 있는 윗면을 커넥터 반대쪽으로 살짝 밀면 뚜껑이 간단하게 분리된다. 내부에 위의 사진과 같이 MicroSD와 SIM card를 꼽을 수 있는 커넥터가 있다. SIM카드를 삽입하고 뚜껑을 덮어주면 됨

통신망은 LG U+의 LTE를 사용.

전원을 연결하면 잠시 후 제일 왼쪽 LED가 초록색으로 점멸하기 시작


무선랜을 선택해 LTE모뎀에 접속 (SSID, WiFi key는 뚜껑을 열어보면 확인할 수 있음. 장치 홈페이지에서 관리자 ID/PW는 둘 다 admin/admin 

맨 처음 연결하면 프로파일을 만들어 줘야 함. LG U+ LTE이므로 프로파일 이름은 임의로 설정, ID/PW는 없음, APN은 internet.lguplus.co.kr 을 넣어주면 됨

정상적으로 설정이 끝나 통신망에 접속하면 제일 왼쪽 LED가 푸른색으로 켜져 있음

제일 오른쪽 LED는 무선랜 상태를 표시. 녹색으로 켜져 있음 정상 동작하는 것임

* 이 방식의 장점은 LTE 데이터를 여러 장비에서 공유가 가능. 또한 USB를 꼭 컴퓨터가 아니고 휴대폰 충전기나 휴대용 배터리에 연결해서 사용할 수 있음
 

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;
  }
}














ESP32의 슬립모드와 전력소모 (ESP32 Sleep modes and their power consumption)

Original document : Insight into ESP32 Sleep Modes & Their Power Consumption


Inside ESP32 chip

ESP32의 전력관리에 대해 이해하기 위해 먼저 ESP32 칩 내부의 구조를 알 필요가 있다. 아래 그림은 ESP32 칩 내부의 기능별 블록 다이어그램이다.



ESP32칩에는 듀얼코어 32-bit 마이크로프로세서와 448KB의 ROM, 520KB의 SRAM과 4MB의 플래쉬가 들어 있다.

또한 WiFI, Bluetooth, Cryptographic accelerator, RTC 및 다양한 페리페럴을 가지고 있다.

ESP32 Power Modes


ESP32의 전력 관리 모드로는 다음의 5가지가 있다.
  • Active mode
  • Modem Sleep mode
  • Light Sleep mode
  • Deep Sleep mode
  • Hibernation mode
각 모드별로 서로 다른 특징과 소비전력 절약 기능을 가지고 있다. 각 모드에 대해 하나씩 확인해 보자.

Active mode

일반적으로 사용하는 모드이다. 이 모드에서는 칩의 모든 기능이 활성화 된다.

액티브 모드에서는 칩의 모든 기능(WiFi, 프로세서 코어, 블루투스 등)을 켜 놓기 때문에 동작시 240mA 이상의 전류를 소모할 수 있다. 또한 WiFi와 블루투스를 동시에 사용하는 경우 순간적으로 최대 790mA까지 전류를 소모하는 경우도 있음을 주의해야 한다.



ESP32 데이터쉬트를 보면 액티브 모드에서 RF가 동작시 전력소모는 다음과 같다.



확실히 가장 많은 전류를 소비하는 비효율적인 모드이다. 그러므로 전력소모를 줄이고 싶으면 기능을 사용하지 않는 경우 적절한 다른 전력 모드로 스위칭 해 필요없는 기능을 비활성화시켜 줘야만 한다.

Modem Sleep mode

이 모드에서는 WiFi, 블루투스, RF를 제외한 모든 부분이 활성화 된다. 이 모드에서 slow speed라면 약 3mA, high speed라면 약 20mA 정도의 전류를 소비한다.



WiFI/블루투스 연결을 유지하려면 CPU, WiFi, 블루투스, RF가 Association Sleep pattern이라는 미리 지정된 간격으로 깨어나야 한다.

이 sleep pattern동안 전원 모드는 active mode와 modem sleep mode를 왔다갔다 해야 한다.

ESP32가 스테이션 모드에서 공유기에 연결되었을 때만 modem sleep 모드에 들어갈 수 있다. 이 경우 DTIM beacon mechanism을 통해 공유기 연결을 유지한다.

전력 소비를 줄이기 위해 ESP32는 DTIM beacon interval 사이에는 WiFi 모듈을 비활성화 시키고 다음번 Beacon interval이 돌아오기 전에 자동으로 깨어나게 한다.

Sleep time은 공유기의 DTIM Beacon interval time에 의해 결정되는되 일반적으로 100ms ~ 1000ms 정도 된다.

Light Sleep mode

 Light sleep mode는 modem sleep mode와 비슷하고 association sleep pattern도 따른다. 차이점은 디지털 페리페럴, 대부분의 RAM, CPU가 clock-gated가 된다.


Light Sleep mode동안 CPU는 클럭펄스를 꺼 버림으로서 pause되지만 RTC와 ULP 코프로세서는 활성화 되어 있다. 그러므로 modem sleep mode보다도 더 전력소모를 줄여 약 0.8mA 정도의 전류를 소모한다.



Light Sleep mode로 들어가기 전에 ESP32는 내부상태를 저장하고 sleep에서 깨어나면 동작을 재개하는데 이를 Full RAM Retention이라 한다.

Wake-up 소스를 설정한 후 esp_light_sleep_start() 함수를 사용해 light sleep mode로 들어갈 수 있다.

Deep Sleep mode

Deep Sleep mode에서는 CPU, 대부분의 RAM과 모든 디지털 페리페럴이 꺼진다. 전원이 켜져 있는 부분은 RTC 컨트롤러, RTC 페리페럴(ULP 코프로세서를 포함), RTC 메모리 (slow & fast) 뿐이다.

이 경우 ULP 코프로세서가 켜져 있는 경우 약 0.15mA, 그렇지 않은 경우 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 소스에 필요 없는 모든 페리페럴의 전원을 자동으로 꺼 버리지만 옵션으로 모든 페리페럴들애 대해 전원을 끌지 여부를 결정할 수도 있다.

Hibernation mode

Deep Sleep mode와 다르게 Hibernation mode는 내부 8MHz 오실레이터와 ULP 코프로세서까지도 전원을 꺼 버린다. RTC recovery 메모리까지도 전원을 꺼 버리기 때문에 hibernation mode에서는 어떤 데이터도 보관할 수 없다.

Slow clock상의 RTC timer와 일부 RTC GPIO만 활성화 되어 있고 나머지는 모두 전원이 꺼진다. 활성화 된 부분이 hibernation mode에서 깨어나는걸 담당한다.

이 모드는 전력소비를 극단적으로 줄여 약 2.5uA의 전류를 소비한다.














2019년 4월 12일 금요일

ESP32에서 멀티코어 사용하기 (Use multicore on ESP32)

ESP32는 2개의 Xtensa 32-bit LX6 마이크로 프로세서 코어를 가지고 있다. 아두이노 IDE에서 코드를 실행하면 디폴토로 코어 1에서 실행된다. 여기서는 태스크를 만들어 두번째 코어에서 코드를 실행하는 방법을 설명한다. 그렇게 하면 두개의 코어에서 동시에 코드를 실행해서 멀티태스킹을 할 수 있다.

코드가 어느 코어에서 실행되고 있는가를 확인하려면 xPortGetCoreID() 함수를 사용하면 된다.

아래의 간단한 테스크 코드를 실행시켜 보면 setup(), loop() 함수 모두 코어 1에서 실행되는걸 확인할 수 있다.

void setup()
{
  Serial.begin(115200);
  Serial.print("* setup() is running on core ");
  Serial.println(xPortGetCoreID());
}

void loop()
{
  Serial.print("* loop() is running on core ");
  Serial.println(xPortGetCoreID());
  delay(100);
}




ESP32를 아두이노 IDE에서 사용할 때 기본적으로 리얼타임 OS인 FreeRTOS를 지원한다. 그러므로 FreeRTOS를 사용하면 여러개의 태스크를 독립적으로 병렬로 실행할 수 있다.

태스크는 무언가를 수행하는 코드의 조각이다. 예를 들어 LED를 깜빡이거나 네트웍 통신, 센서 읽기, 센서값을 네트워그로 전송등 다양한 작업을 수행할 수 있다.

코드의 특정 부분이 특정 코어에서 실행되도록 할당하려면 태스크를 만들어 줘야 한다. 태스크를 만들 때 우선순위 뿐 아니고 어느 코어에서 실행할지 고를 수 있다. 우선순위는 0에서 시작하고 0이 가장 우선순위가 낮다. 프로세서는 우선순위가 높은 태스크를 먼저 실행한다.





1. 태스크를 만들려면 먼저 태스크 핸들을 만들어 준다.

TaskHandle_t Task1;

2. setup()에서 xTaskCreatePinnedToCore 함수를 사용해 특정 코어에 고정된 태스크를 만들어 준다.

xTaskCreatePinnedToCore (
  Task1code,      // 태스크를 구현한 함수
  “Task1”,        // 태스크 이름
  10000,          // 스택 크기 (word단위)
  NULL,           // 태스크 파라미터
  0,              // 태스크 우선순위
  &Task1,         // 태스크 핸들
  0 );            // 태스크가 실행될 코어


3. 만들어 진 태스크가 실행할 코드가 들어있는 함수를 만들어 줘야만 한다. 위의 예제에서는 Task1Code 함수를 만들어 줘야 한다.

void Task1Code( void *param)
{
  while (1) {
    // 태스크 1이 실행할 코드가 들어갈 부분
    // 무한루프로 실행됨
  }
}


코드가 실행되는 도중 만들어 진 태스크를 삭제하고 싶으면 vTaskDelete() 함수를 사용하면 된다. 이 함수의 파리미터로 지우고자 하는 태스크의 핸들(위의 예제에서는 Task1)을 넘겨준다.

예제


ESP32의 2번핀에 LED1을, 4번핀에 LED2를 연결하고 두개의 서로 다른 태스크가 각각 LED1은 1000ms마다, LED2는 700ms마다 깜빡이게 만든다. 여기서 Task1은 코어 0에서, Task2는 코어 1에서 실행된다.




#define LED1 2
#define LED2 4

TaskHandle_t Task1;
TaskHandle_t Task2;

void setup()
{
  Serial.begin(115200);
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);

  xTaskCreatePinnedToCore(
    blink1000,         // 태스크 함수
    "Task1",           // 테스크 이름
    10000,             // 스택 크기(워드단위)
    NULL,              // 태스크 파라미터
    1,                 // 태스크 우선순위
    &Task1,            // 태스크 핸들
    0);                // 실행될 코어

  xTaskCreatePinnedToCore(
    blink700,          // 태스크 함수
    "Task2",           // 테스크 이름
    10000,             // 스택 크기(워드단위)
    NULL,              // 태스크 파라미터
    1,                 // 태스크 우선순위
    &Task2,            // 태스크 핸들
    1);                // 실행될 코어
}

void blink1000 ( void *param )
{
  Serial.print("# Task 1 running on core ");
  Serial.println(xPortGetCoreID());

  while (1) {
    digitalWrite(LED1, HIGH);
    delay(1000);
    digitalWrite(LED1, LOW);
    delay(1000);
  }
}

void blink700 ( void *param )
{
  Serial.print("# Task 2 running on core ");
  Serial.println(xPortGetCoreID());

  while (1) {
    digitalWrite(LED2, HIGH);
    delay(700);
    digitalWrite(LED2, LOW);
    delay(700);
  }
}

void loop()
{
}