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

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월 19일 금요일

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()
{
}






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


















2018년 11월 9일 금요일

ESP32에서 터치센서 사용하기 (Using ESP32 Capacitive Touch Sensor)



이전 포스트(ESP32 GPIO 레퍼런스)에서 소개했던 것 처럼 ESP32에는 총 10개의 터치 센서가 들어 있다. 이를 사용하면 아무런 추가 하드웨어 없이 터치 스위치를 만들 수 있다. 10개 터치 센서의 핀 매핑은 다음과 같다.

static const uint8_t T0 = 4;
static const uint8_t T1 = 0;
static const uint8_t T2 = 2;
static const uint8_t T3 = 15;
static const uint8_t T4 = 13;
static const uint8_t T5 = 12;
static const uint8_t T6 = 14;
static const uint8_t T7 = 27;
static const uint8_t T8 = 33;
static const uint8_t T9 = 32;

사용법 역시 매우 간단하다. 푸쉬버튼을 사용하는 경우 먼저 setup()에서 스위치가 연결된 핀을 pinMode()로 설정을 해 주고 digitalRead()로 상태를 읽어야 하는데 터치의 경우 별도의 설정은 필요 없고 touchRead() 함수로 값을 읽으면 된다.

아래 예제 코드는 T0 (4번핀)를 터치 스위치로 사용해서 LED를 토글시키는 것이다.


#define TOUCH_SW T0    // connected to 4
#define LED A13        // connected to 15

int touchVal = 100;

void setup()
{
  Serial.begin(115200);
  Serial.println("ESP32 Touch Example");
  pinMode(LED, OUTPUT);
}

void loop()
{
  static boolean led_state = LOW;
  static boolean prevState = LOW; 
  static boolean touchState;

  touchVal = touchRead(TOUCH_SW);     // read touch switch state
  touchState = touchVal < 50 ? HIGH:LOW;
  if ((LOW == prevState) && (HIGH == touchState))  {    
    Serial.println("Switch touched");
    led_state = !led_state;
    digitalWrite(LED, led_state);      
  }
  prevState = touchState;
  delay(50);
}






2018년 11월 7일 수요일

ESP32 GPIO 레퍼런스

ESP32에는 총 48개의 핀이 있는데 각 핀들은 여러가지 기능을 가지고 있다. 물론 ESP32 개발보드에서 모든 핀을 다 사용할 수 있는것은 아니다. 그래서 여기서는 어떤 핀을 사용해야 하고 어떤 핀을 사용하지 않아야 하는지에 대해 설명하겠다.

ESP32는 다음과 같은 peripheral을 가지고 있다.

* 18 ADC(Analog-to-Digital Converter) channel
* 3 SPI
* 3 UART
* 2 I2C
* 16 PWM
* 2 DAC (Digital-to-Analog Converter)
* 2 I2S
* 10 Capacitive sensing GPIO

ADC와 DAC 기능은 특정 핀에 고정되어 있다. 하지만 UART, I2C, SPI, PWM등의 기능은 어느 핀에 사용할지 결정해서 코드에서 지정해 줘야 한다.

소프트웨어에서 핀의 속성을 정의해 줄 수 있지만, 각 핀들은 디폴트로 지정되어 있는 기능들이 있다. ESP32 DEVKIT V1-DOIT의 경우 디폴트 핀 배열은 다음과 같다.




또한 특정한 기능의 핀들은 프로젝트마다 사용하기에 적합할 수도 그렇지 않을 수도 있다.

아래 그림은 ESP-WROOM-32의 핀 배치이다. 만일 칩만 가지고 자작 보드를 만든다면 아래 그림을 레퍼런스로 삼을 수 있다.



Input Only Pins

GPIO 34부터 39까지는 입력 전용 핀이다. 그리고 이 핀들은 내부적으로 풀업/풀다운 저항이 없다. 출력으로 사용할 수 없으므로 입력으로만 사용해야 한다.

* GPIO 34
* GPIO 35
* GPIO 36
* GPIO 37
* GPIO 38
* GPIO 39

ESP-WROOM-32에 들어 있는 SPI Flash

일부 보드들은 GPIO 6부터 11까지의 핀도 사용할 수 있게 되어 있다. 하지만 이 핀들은 ESP-WROOM-32 칩에 내장된 SPI Flash에 연결되어 있기 때문에 다른 용도로 사용하는걸 권장하지 않는다.

* GPIO 6 (SCK/CLK)
* GPIO 7 (SDO/SD0)
* GPIO 8 (SDI/SD1)
* GPIO 9 (SHD/SD2)
* GPIO 10 (SWP/SD3)
* GPIO 11 (CSC/CMD)

Capacitive Touch GPIO (정전식 터치 입력)

ESP32는 10개의 정전식 터치 입력 센서를 가지고 있다. 이 센서는 사람의 피부 같이 전하를 가진 것들의 전하 변화를 측정할 수 있다. 그러므로 손가락으로 GPIO를 터치할 때 유도되는 변하를 감지할 수 있다. 이 핀들은 전하 패드에 연결해 기계식 버튼을 대치할 수 있다. 정전식 터치 핀은 ESP32를 deep sleep에서 깨어나게 하는데 사용할 수도 있다.

이 내부 터치 센서는 다음의 GPIO에 연결되어 있다.

* T0 (GPIO 4)
* T1 (GPIO 0)
* T2 (GPIO 2)
* T3 (GPIO 15)
* T4 (GPIO 13)
* T5 (GPIO 12)
* T6 (GPIO 14)
* T7 (GPIO 27)
* T8 (GPIO 33)
* T9 (GPIO 32)

ADC (Analog-to-Digital Converter)

 ESP32는 18개의 12-bit ADC 채널을 가지고 있다. (ESP8266의 경우는 1개의 10-bit ADC만 가지고 있음) 다음의 GPIO 는 각각 해당 ADC 채널로 사용될 수 있다.

 * ADC1_CH0 (GPIO 36)
 * ADC1_CH1 (GPIO 37)
 * ADC1_CH2 (GPIO 38)
 * ADC1_CH3 (GPIO 39)
 * ADC1_CH4 (GPIO 32)
 * ADC1_CH5 (GPIO 33)
 * ADC1_CH6 (GPIO 34)
 * ADC1_CH7 (GPIO 35)
 * ADC2_CH0 (GPIO 4)
 * ADC2_CH1 (GPIO 0)
 * ADC2_CH2 (GPIO 2)
 * ADC2_CH3 (GPIO 15)
 * ADC2_CH4 (GPIO 13)
 * ADC2_CH5 (GPIO 12)
 * ADC2_CH6 (GPIO 14)
 * ADC2_CH7 (GPIO 27)
 * ADC2_CH8 (GPIO 25)
 * ADC2_CH9 (GPIO 26)

ADC 입력 채널은 12-bit 해살도를 가지고 있다. 즉 아날로그 값을 읽으면 0~4095 사이의 값을 얻을 수 있다. 여기서 0은 0V, 4095는 3.3V를 의미한다. 또한 코드에서 각 채널의 해설도나 ADC 범위를 조정할 수도 있다.

ESP32 ADC 핀은 리니어 한 속성을 가지고 있지 않으므로 주의해야 한다. 0V와 0.1V, 또는 3.2V와 3.3V를 거의 구별하지 못할 것이다. 그러므로 ADC를 사용할 때 이 점을 염두에 두어야만 한다. 아래 그래프와 같은 속성을 가진다.


DAC (Digital-to-Analog Converter)

ESP32에는 2개의 8-bit DAC 채널이 있어 디지털 신호를 아날로그 전압으로 바꿔 출력해 준다.

* DAC1 (GPIO 25)
* DAC2 (GPIO 26)

RTC GPIO

ESP32는 RTC GPIO를 지원한다. ESP32가 deep sleep 모드에 있을 때 RTC의 low-power subsystem에 연결된 GPIO를 사용할 수 있다. 이 RTC GPIO는 ULP(Ultra Low Power) 코프로세서가 실행중일 때 ESP32를 deep sleep에서 깨어나게 하는데 사용된다. 다음의 GPIO들이 외부 wake up 소스로 사용될 수 있다.

* RTC_GPIO0 (GPIO 36)
* RTC_GPIO3 (GPIO 39)
* RTC_GPIO4 (GPIO 34)
* RTC_GPIO5 (GPIO 35)
* RTC_GPIO6 (GPIO 25)
* RTC_GPIO7 (GPIO 26)
* RTC_GPIO8 (GPIO 33)
* RTC_GPIO9 (GPIO 32)
* RTC_GPIO10 (GPIO 4)
* RTC_GPIO11 (GPIO 0)
* RTC_GPIO12 (GPIO 2)
* RTC_GPIO13 (GPIO 15)
* RTC_GPIO14 (GPIO 13)
* RTC_GPIO15 (GPIO 12)
* RTC_GPIO16 (GPIO 14)
* RTC_GPIO17 (GPIO 27)

PWM (Pulse Width Modulation)

ESP32 LED PWM controller는 16개의 독립적인 채널을 가지고 있어 각각 다른 속성의 PWM 신호를 만들어 내도록 설정할 수 있다. 출력으로 사용할 수 있는 모든 핀은 PWM 으로 사용할 수 있다. (즉 GPIO 34~39는 PWM을 만들 수 없다)

PWM 신호를 만들어주려면 코드에서 다음의 파라미터를 정해줘야 한다.

* Signal Frequency
* Duty Cycle
* PWM Channel
* 신호를 출력할 GPIO핀

I2C

아두이노 IDE에서 ESP32를 사용할 때는 다음의 디폴트 핀을 사용해야만 한다.

* GPIO 21 (SDA)
* GPIO 22 (SCL)

SPI

디폴트로 SPI핀은 다음과 같이 매핑되어 있다.

Interrupt

모든 GPIO 핀은 인터럽트로 설정할 수 있다.

Enable (EN)

Enable (EN)은 3.3V 레귤레이터의 enable 핀이다. 이 핀은 풀업이 되어 있으므로, GND에 연결하면 3.3V 레귤레이터가 비활성화 된다. 즉 이 핀을 푸쉬버튼에 연결해 놓으면 ESP32를 재시동하는데 사용할 수 있다.

GPIO current draw

ESP32 데이터쉬트에 있는 "권장하는 동작 조건(Recommended Operating Condition)"에 따르면 GPIO마다 최대 40mA의 전류를 흘릴 수 있다.























2018년 7월 2일 월요일

ESP32에서 FreeRTOS 사용 - TASK에 parameter 넘겨주기

FreeRTOS에서 태스크를 만드는 방법은 이전 포스트에서 설명했다. 이때 사용하는 API에 보면 몇개의 파라미터가 있는데 "void *pvParameters", "UBaseType_t uxPriority", "TaskHandle_t *pxCreatedTask"를 사용하는 법을 설명하겠다.

BaseType_t xTaskCreate(  TaskFunction_t pvTaskCode,
                         const char * const pcName,
                         uint16_t usStackDepth,
                         void *pvParameters,
                         UBaseType_t uxPriority,
                         TaskHandle_t *pxCreatedTask)

데모

1) Task Parameter
"void *pvParameters"는 TaskFunction_t에 입력을 전달해주기 위해 사용한다. 예를 들어 void hello_world_task(void *param) 이라는 태스크를 가지고 있으면 "void *pvParameters"는 "void *param"이다. 우리는 이 예제에서 태스크를 만들고 그 태스크에 파라미터를 전달할 때 이 파라미터가 태스크 함수에 전달되고, 터미널에 이 파라미터를 출력한다.

void setup() {
  Serial.begin(115200);
  char task1Param[13] = "task1Param";

  xTaskCreate (task1,   // task function
               "task1", // name of task
               10000,   // stack size of task
               (void *) task1Param,  // parameter of the task
               1,       // priority of the task
               NULL);   // task handle to keep track of created task
}

void loop() {
}

// this function will be invoked when additionalTask was created
void task1 ( void * parameter )
{
  Serial.println((char *)parameter);
  while (1) {    // loop forever
    Serial.println("task1 is running");
    delay(1000);
  }
  vTaskDelete( NULL ); // delete a task when finish,
                       // this will not happen
                       // because this is infinitely loop
}

2) Task Priorities

FreeRTOS는 3가지 종류의 스케쥴링 방식을 지원한다.

- Co-operative scheduling
실행중인 태스크는 running 상태에서 blocked 상태로 바뀌거나 또는 taskYIELD()를 호출할 때 컨택스트 스위칭이 발생한다. 그럼 나머지 태스크 중에 가장 높은 우선순위를 가지고 있는 태스크가 실행되게 된다.

- Prioritized pre-emptive scheduling with time slicing
높은 우선순위(priority)를 가진 태스크가 낮은 우선순위를 가진 태스크를 preempt한다. 같은 우선순위를 가진 태스크 끼리는 time tick 인터럽트가 발생할 때 마다 순서를 바꿔 실행한다. (RTOS는 시간을 측정하기 위한 타이머 인터럽트를 가지고 있는데, 매번 인터럽트가 발생할 때 마다 RTOS는 태스크를 unblock 또는 깨울 시간인지 체크한다.)

- Priotitized pre-emptive scheduling without time slicing
높은 우선순위(priority)를 가진 태스크가 낮은 우선순위를 가진 태스크를 preempt한다. 같은 우선순위를 가진 태스크는 매번 time tick 인터럽트가 발생할 때마다 순서를 바꾸지 않는다. (실행중인 태스크는 더 높은 우선순위를 가진 태스크에 의해 preempt 될 때 까지 계속 실행한다.)

2-1) Prioritized pre-emptive scheduling with time slicing 데모
현재 Arduino ESP32의 FreeRTOS는 Prioritized pre-emptive scheduling with time slicing을 사용하도록 설정되어 있으므로 여기서는 이 방식의 스케쥴링 데모를 만들어 보았다. 2개의 태스크를 만드는데 task1은 우선순위 1, task2는 우선순위 4를 가진다. task1은 "task1 is running"과 "task1 is ending"이라는 글자를 터미널에 출력한다. task2는 "task2 is running"과 "task2 is ending"이라는 글자를 터미널에 출력한다. 그러므로 대부분의 시간은 task2가 터미널에 출력하는데 사용된다.


void setup() {
  Serial.begin(115200);
  char task1Param[12] = "taskParam";
  xTaskCreate(
      task1,        // Task function
      "task1",      // name of task
      10000,        // Stack size of task
      (void *)task1Param,  // parameter of the task
      1,            // priority of the task 
      NULL);        // Task handle to keep track of created task 
  xTaskCreate(
      task2,        // Task function. 
      "task2",      // name of task. 
      10000,        // Stack size of task 
      (void *)task1Param,  // parameter of the task 
      4,            // priority of the task 
      NULL);        // Task handle to keep track of created task 
}

void loop() {
}
 
// this function will be invoked when additionalTask was created 
void task1( void * parameter )
{
  Serial.println((char *)parameter);

  while(1){
    Serial.println("task1 is running");
    Serial.println("task1 is ending");
  }
  vTaskDelete( NULL );
}

// this function will be invoked when additionalTask was created 
void task2( void * parameter )
{
  Serial.println((char *)parameter);
  while (1){
    Serial.println("task2 is running");
    Serial.println("task2 is ending");
  }
  vTaskDelete( NULL );
} 
 
 
 
2-2) Task Handle

Task Handle은 만들어 진 테스크의 레퍼런스를 가지고 있기 때문에 나중에 그 태스크를 삭제하거나 우선순위를 바꾸는 등의 작업을 할 수 있다.



2-2-1) Task Handle 데모
위의 2-1) 데모 코드를 재사용하는데 요구사항을 약간 바꿔 보았다. task2가 20번 실행되면 task1의 우선순위를 4로 바꾸고 task2의 우선순위를 1로 바꿔주어 이제부터는 터미널에 task1이 출력하는 내용이 대부분을 차지하게 된다.

// task handle variables to hold instances of tasks
TaskHandle_t t1 = NULL;
TaskHandle_t t2 = NULL;
int count = 0;

void setup() {
  Serial.begin(115200);
  char task1Param[12] = "taskParam"; 
 
  xTaskCreate(
      task1,     // Task function. 
      "task1",   // name of task. 
      10000,     // Stack size of task 
      (void *)task1Param,  // parameter of the task 
      1,         // priority of the task 
      &t1);      // Task handle to keep track of created task 
  xTaskCreate(
      task2,     // Task function. 
      "task2",   // name of task. 
      10000,     // Stack size of task 
      (void *)task1Param,  // parameter of the task 
      4,         // priority of the task 
      &t2);      // Task handle to keep track of created task 
}

void loop() {
}
 
// this function will be invoked when additionalTask was created 
void task1( void * parameter )
{
  Serial.println((char *)parameter);
  while (1){
    Serial.println("task1 is running");
    Serial.println("task1 is ending");
  }
  vTaskDelete( NULL );
}

// this function will be invoked when additionalTask was created 
void task2( void * parameter )
{
  Serial.println((char *)parameter);

  while (1) {
    count++;
    // if count is 20 then we swap the priority of 2 tasks 
    if(20 == count){
      vTaskPrioritySet( t1, 4 );
      vTaskPrioritySet( t2, 1 );
    }
    Serial.println("task2 is running");
    Serial.println("task2 is ending");
  }
  vTaskDelete( NULL );
}




 2-3) Idle Task Hook Function

Idle 태스크(백그라운드 태스크)는 다음과 같은 특징을 가진다.
- 스케쥴러에 의해 만들어 짐
- 가장 낮은 우선순위를 가짐 (우선순위 0)
- 다른 모든 태스크들이 blocked 상태일 때 실행상태가 됨

Idle 태스크에 함수를 hook하는것이 가능하다. 이 함수는 idle 태스크가 실행될 때 호출된다.
이 태스크를 사용해 다음과 같은 일을 하는데 사용할 수 있다.
 - 백그라운드에서 어떤 작업 수행
- 시스템의 spare time 측정
- 할 일이 없을 떼 마이크로 프로세서를 low power mode로 진입시킴
- 태스크가 삭제될 때 리소스 정리

주의)  태스크 hook 함수내에서 idle 태스크를 block시키는 어떠한 작업도 하면 안된다.

task hook 함수를 사용하려면 단순히 "bool vApplicationIdleHook(void)"  프로토타입의 함수를 구현해 주고 esp_register_freertos_idle_hool(vApplicationIdleHook)을 호출해 hook 함수를 등록해 주면 된다.

2-3-1) 데모
task hook 함수를 만들어 주고 이 함수가 호출될 때 마다 카운터를 1씩 증가시킨다. 생성된 태스크 함수에서는 단지 이 값을 출력한다.

#include "esp_freertos_hooks.h"
int counter = 0;

void setup() {
  Serial.begin(115200);
  esp_register_freertos_idle_hook(vApplicationIdleHook);
  char task1Param[12] = "task1Param";

  xTaskCreate(
      task1,     // Task function. 
      "task1",   // name of task. 
      10000,     // Stack size of task 
      (void *)task1Param,   // parameter of the task 
      1,         // priority of the task 
      NULL);     // Task handle to keep track of created task 
}


void loop() {
}
 
// this function will be invoked when additionalTask was created 
void task1( void * parameter )
{
  Serial.println((char *)parameter);

  while (1) {
    Serial.print("task1 is running and counter : ");
    Serial.println(counter);
    if(counter > 30000)
    {
      counter = 0;  
    }
    // block this task so that Idle task has chance to run 
    delay(10);
  }
  vTaskDelete( NULL );
}

// define task hook function 
bool vApplicationIdleHook(void)
{
  counter++;
}