본문 바로가기

아두이노우노 R4

Arduino를 사용하여 ESP32 보드에 FreeRTOS 구현하기 2

반응형

 

Arduino를 사용하여 ESP 32 장치에 FreeRTOS 솔루션 구현 튜토리얼 4과 중 2과 

 

이전 수업에서는 튜토리얼에 사용된 브레드보드 회로를 제작하고 Arduino IDE를 사용하여 다운로드한 간단한 프로그램으로 테스트했습니다. 이번에는 동일한 회로를 사용하되, FreeRTOS 기술을 활용하도록 프로그램을 수정해 보겠습니다. 본론으로 들어가기에 앞서, FreeRTOS의 실시간 프로그래밍 방식에 대해 알아보겠습니다. 

 

FreeRTOS 프로그래밍 개요

 

Arduino IDE에서 작성된 일반적인 C++ 프로그램은 세 가지 주요 부분으로 구성됩니다.

 

  • 정의, 선언, 포함된 라이브러리를 포함하는 헤더 섹션입니다.
  • 변수와 프로그램 객체를 초기화하고 콘솔 연결, 웹 서비스 등의 서비스를 시작하는 설정 섹션입니다. 또한 한 번만 실행되는 코드도 포함됩니다.
  • 프로그램이 완료되고 실행이 중지될 때까지 계속해서 실행되는 루프 섹션입니다.

 

프로그래머는 조건 분기를 사용하여 프로세서가 취하는 실행 경로를 정의하고, 실행 방향을 결정합니다. 일반적으로 한 번에 하나의 프로그램이 실행되지만, 여러 스레드를 정의하여 여러 처리 분기를 동시에 실행할 수 있습니다.

 

FreeRTOS 환경에서는 독립적으로 실행 가능한 여러 코드 스레드(작업이라고 함)를 정의하고, 운영 체제가 작업 우선순위와 현재 상태에 따라 특정 시점에 실행할 작업을 결정하도록 하는 접근 방식을 사용합니다. 이 시스템의 핵심은 실행 흐름을 제어하는 작업 스케줄러입니다. Arduino IDE에 구현된 FreeRTOS에서 이 스케줄러는 "비협조적 선점형 라운드 로빈" 스케줄링 알고리즘을 사용하여 할당된 우선순위와 실행 준비 상태에 따라 다양한 작업에 처리 시간 단위를 할당합니다. 각 시간 단위(기본값 1밀리초)가 시작될 때 시스템은 실행할 작업을 결정하고, 필요한 경우 현재 실행 중인 작업을 다른 작업으로 교체합니다. 각 작업은 사용할 힙 메모리 영역과 마지막 실행 시 현재 처리 상태에 대한 정보가 포함된 작업 제어 블록(TCB) 영역을 갖습니다. 한 작업에서 다른 작업으로 프로세스를 전환하려면 프로그램 포인터를 새 작업의 TCB로 이동하고 새 작업의 메모리 영역을 가리켜야 합니다. 하지만 "비협조적 선제 라운드 로빈"이란 도대체 무슨 뜻일까요?

 

  • 비협조적이란 스케줄러가 허가 없이 작업을 교체할 수 있음을 의미합니다. 작업은 처리 포기를 거부할 수 없지만, 중요한 기능을 수행하는 경우 실행 상태를 유지할 가능성을 높이기 위해 (프로그래밍 방식으로) 더 높은 우선순위를 할당할 수 있습니다. 매우 중요한 처리 요구 사항의 경우 교체를 방지하는 다른 방법도 있지만, 이 튜토리얼에서는 다루지 않습니다.
  • 선점형이란 높은 우선순위 인터럽트 및 세마포어 상태 변경과 같은 특정 이벤트로 인해 시간 슬라이스가 완료되기 전에 프로세스 교환이 발생할 수 있음을 의미합니다.
  • 라운드 로빈은 동일한 우선순위를 가진 여러 작업이 타임 슬라이스 시작 시점에 실행될 준비가 되어 있는 경우, 시스템이 각 타임 슬라이스마다 작업을 교체하여 작업 간 처리 균형을 맞추는 것을 의미합니다. 이는 운영 체제 오버헤드를 다소 증가시키지만, 컨텍스트 교체가 빠르게 수행될 수 있기 때문에 일반적으로 좋은 균형점으로 간주됩니다.

 

구성할 수 있는 FreeRTOS 작업의 다른 변형도 있고, 다른 기능을 제공하는 다른 실시간 운영 체제도 있지만, 이것이 Arduino IDE와 ESP 32 하드웨어에 구현된 기본 기능이며, 대부분의 취미 생활자의 요구 사항을 잘 충족합니다.

 

각 작업은 언제든지 네 가지 상태 중 하나에 속하며, 운영 체제는 각 상태를 추적합니다. 각 시간 분할이 시작될 때 OS는 상태를 업데이트합니다. 상태는 다음과 같습니다.

 

  • RUNNING — 현재 실행 중인 작업(멀티프로세싱 시스템의 경우 작업들).
  • 준비 — 프로세서가 사용 가능하면 언제든지 처리를 계속할 수 있습니다.
  • 차단됨 — 실행할 준비가 되지 않았습니다. 작업이 리소스가 사용 가능해지기를 기다리거나, 타이머 이벤트를 기다리거나, 인터럽트 또는 큐 메시지를 기다리고 있을 수 있습니다
  • SUSPENDED — 프로그램적으로 대기 상태에 놓였습니다.

작업 실행이 올바른 순서로 이루어지도록 조정하고 시간을 조정해야 하는 상황이 많습니다. 타이머, 세마포어, 뮤텍스, 큐, 인터럽트가 이러한 목적으로 일반적으로 사용되며, 이후 레슨에서 자세히 살펴보겠습니다. 오늘은 여러 작업을 생성하고 실행하며, 각 작업 간에 정보를 공유하는 방법을 살펴보겠습니다.

 

프로그램 개요

 

이번 수업에서는 아래와 같이 여러 가지 작업을 만들어 보겠습니다.

 

  • 블링커 작업 — LED를 켜고 끄는 작업을 합니다. 일반 함수를 생성하고 GPIO 핀, 주기 시간, 시간 배율, 그리고 현재 상태 값을 포함하는 구조체 변수를 전달합니다. 이 함수는 일반적인 아두이노 코드를 사용하여 LED를 켜고 끄고 상태를 업데이트합니다. 구조체는 전역 변수로 정의되어 있으므로 다른 작업에서 값을 읽고 수정할 수 있습니다. 그런 다음 이 함수 정의를 사용하여 빨간색, 녹색, 파란색 LED 조명을 각각 독립적으로 제어하는 세 가지 작업을 생성합니다.
  • Tally Task — 현재 켜져 있는 LED의 개수를 표시합니다. 이 작업은 위 세 가지 작업의 상태 값을 읽어 OLED 디스플레이에 켜진 LED의 총 개수를 표시합니다. 0.1초마다 디스플레이를 업데이트하도록 설정합니다.
  • Highwater Task — 직렬 콘솔에서 실행 중인 각 작업의 여유 공간을 표시합니다. 시작 후 5초 후에 한 번 실행되고, 그 후에는 더 이상 처리되지 않도록 일시 중지되도록 설정합니다. (자세한 내용은 나중에 설명합니다.)
  • 속도 향상 작업 - 버튼을 누를 때마다 사이클 속도를 50%씩 증가시킵니다. 이는 간단한 인터럽트 시스템을 보여줍니다. 이 시스템은 버튼을 누를 때만 실행됩니다.

 

아래 프로그램 설명을 살펴보면 이 부분들이 어떻게 연결되는지 알 수 있을 겁니다. 시작해 볼까요?

 

프로그램 코드

 

Arduino IDE에서 새 프로젝트를 열면 아래 코드를 복사하여 붙여넣을 수 있습니다. 사용되는 FreeRTOS 함수에 대한 자세한 내용은 이전에 인용된 설명서를 참조하고 여기에서 확인할 수 있습니다 . 첫 번째 코드 부분은 다음과 같습니다. 

 

// 1 - define freeRTOS settings
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
#define SYSTEM_RUNNING_CORE 0

// 2 - define LCD display settings
#define OLED_ADDR 0x3C
#define OLED_RESET 4
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 3 - define task functions
void TaskBlink(void *pvParameters);
void TaskTally(void *pvParameters);
void TaskSpeedup(void *pvParameters);
void TaskHighWater(void *pvParameters);


// 4 - define global variables for led status
int greenLED = 0;  // LED status indicator 0-off, 1-on
int redLED = 0;
int blueLED = 0;
int *greenPtr = &greenLED;  //pointers to pass to tasks to update led status
int *redPtr = &redLED;
int *bluePtr = &blueLED;
float speedMult = 1.0; // define speed multipier value

 

위에 표시된 섹션에서는 여러 가지 사항을 초기화하고 정의합니다.

 

  1. ESP 32에 대한 FreeRTOS 설정을 정의하고 있습니다. 대부분의 ESP 32 보드에는 대칭형 멀티프로세서 모드로 실행되는 두 개의 프로세스 코어가 포함되어 있습니다. 작업을 생성할 때 실행할 코어를 지정할 수 있습니다.
  2. OLED 디스플레이의 매개변수를 설정하고 필요한 라이브러리를 포함합니다. 이는 표준 아두이노 코드입니다.
  3. 작업 생성에 사용될 함수의 함수 프로토타입을 만드세요. 모든 함수는 VOID 반환 인수와 전달되는 데이터에 대한 일반 포인터를 가져야 합니다. 또 다른 중요한 점은 함수가 호출 프로그램으로 반환해서는 안 된다는 것입니다. 함수는 프로그램적으로 삭제될 때까지 무한 루프로 실행됩니다.
  4. 여기서는 여러 작업에서 읽거나 업데이트할 수 있는 전역 변수를 정의합니다. 각 LED의 상태를 저장하는 변수(예: greenLED)와 LED 상태 포인터를 저장하는 변수(예: *greenPtr)가 있습니다. LED 상태 포인터를 전달하면 작업에서 값을 읽고 업데이트할 수 있습니다. 또한, LED 사이클 시간을 변경할 수 있도록 speedMult라는 부동 소수점 변수도 정의합니다.

 

다음 코드 섹션: 

 

// 5 - define structure for data to pass to each task
struct BlinkData {
  int pin;
  int delay;
  float *speedMult;
  int *ptr;
};

// 6 - load up data for each LED task
static BlinkData blinkGreen = { 15, 2500, &speedMult, &greenLED };
static BlinkData blinkRed = { 4, 3300, &speedMult, &redLED };
static BlinkData blinkBlue = { 16, 1800, &speedMult, &blueLED };

// 7 - set up task handles for the RTOS tasks
TaskHandle_t taskGreen;
TaskHandle_t taskRed;
TaskHandle_t taskBlue;
TaskHandle_t taskSpeed;
TaskHandle_t taskTally;
TaskHandle_t taskHighwater;

// 8 - define function for highwater mark
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);

// 9 - define ISR routine to service button interrupt
int pinButton = 23; //GPIO pin used for pushbutton
void IRAM_ATTR buttonPush() 
{
  vTaskResume(taskSpeed); // run the speed program once
}

 

5. BlinkData라는 구조체 데이터 타입을 정의하여 태스크와 정보를 교환합니다. GPIO 핀 번호와 초기 지연은 값으로 입력되고, 속도 배율과 상태 변수는 프로그램 실행 시 동적으로 변경되므로 참조로 복사됩니다.

6. TaskBlink 함수를 사용하여 세 개의 작업을 생성할 것이므로, 각 LED에 대해 사용할 값을 갖는 별도의 BlinkData 구조체 변수를 생성합니다. 여기서도 핀과 지연 값, 그리고 속도 배율기와 상태 포인터를 전달합니다.

7. FreeRTOS가 각 작업을 생성할 때 시스템은 해당 작업을 참조하는 데 사용할 수 있는 핸들 구조체(FreeRTOS에서 정의)를 반환합니다. 이러한 작업 참조를 포함하기 위해 전역 변수인 TaskHandle_t를 생성합니다.

8. 다음은 실행 중인 작업의 하이워터 마크를 가져오는 데 사용되는 FreeRTOS 함수의 함수 프로토타입입니다. 하이워터 마크는 작업에 할당되었지만 사용되지 않은 메모리 양(바이트)을 나타냅니다. 작업 초기화 시 지정된 메모리 할당량을 조정하는 데 사용할 수 있습니다. 이 함수의 사용 방법은 나중에 살펴보겠습니다.

9. 버튼을 눌렀을 때 시스템에 어떤 동작을 할지 알려주는 데 사용됩니다. 다음 코드 섹션에서 GPIO 핀에 연결될 표준 아두이노 인터럽트 서비스 요청입니다.

 

이제 시작 코드를 실행할 준비가 되었습니다... 

 

// the setup function runs once when you press reset or power the board
void setup() {

  // 10 - initialize serial communication at 115200 bits per second:
  Serial.begin(115200);

  // 11 - setup button for interupt processing
  pinMode(pinButton, INPUT_PULLUP);
  attachInterrupt(pinButton, buttonPush, FALLING); 

  // 12 - Now set up tasks to run independently.
  xTaskCreatePinnedToCore
    (
    TaskBlink, // name of function to invoke
    "TaskBlinkGreen" , // label for the task
    4096 , // This stack size can be checked & adjusted by reading the Stack Highwater   
    (void *)&blinkGreen, // pointer to global data area
    2 , // Priority, 
    &taskGreen, //pointer to task handle
    ARDUINO_RUNNING_CORE // run on the arduino process core
    );

  xTaskCreatePinnedToCore
    (
    TaskBlink, 
    "TaskBlinkRed",
    4096,
    (void *)&blinkRed, 
    2,
    &taskRed, 
    ARDUINO_RUNNING_CORE
    );

  xTaskCreatePinnedToCore
    (
    TaskBlink, 
    "TaskBlinkBlue",  
    4096,  
    (void *)&blinkBlue,
    2, 
    &taskBlue, 
    ARDUINO_RUNNING_CORE
    );

  xTaskCreatePinnedToCore
  (
    TaskTally,
    "TaskTally", 
    4096,  
    NULL, 
    2, 
    &taskTally, 
    SYSTEM_RUNNING_CORE
    );

xTaskCreatePinnedToCore
    (
    TaskSpeed,
    "Speed", 
    4096,  
    NULL, 
    2,
    &taskSpeed, 
    SYSTEM_RUNNING_CORE
    );  

xTaskCreatePinnedToCore
  (
    TaskHighWater,
    "High Water Mark Display", 
    4096,
    NULL,
    2,
    &taskHighwater, 
    SYSTEM_RUNNING_CORE
    ); 

} // end of setup

 

10. 직렬 콘솔을 시작하는 일반적인 Arduino 루틴입니다.

11. 여기서는 GPIO 23번 핀을 설정하여 값이 감소할 때 buttonPush라는 인터럽트를 생성합니다. 이 인터럽트는 위 9단계에서 정의한 루틴을 트리거합니다.

12. 마지막으로 FreeRTOS 작업을 생성합니다. FreeRTOS 함수 xTaskCreatePinnedToCore 를 사용하여 시스템에 실행할 프로세서 코어를 지정합니다. 또는 마지막 인수를 생략하고 사용 가능한 모든 프로세서에서 작업을 실행할 수 있는 xTaskCreate 를 사용할 수도 있습니다. 작업 생성 함수에 전달되는 인수는 다음과 같습니다.

 

  • 작업을 시작하기 위해 호출할 함수
  • 작업을 호출할 이름(텍스트)
  • 작업에 대한 메모리 할당(바이트). 첫 실행 시 하이워터 마크를 읽은 후 이 값을 조정할 수 있습니다.
  • 참조로 함수에 전달된 변수(또는 NULL)
  • 작업 우선순위(높은 작업이 먼저 처리됨)
  • 작업 핸들 이름
  • 실행할 프로세서 코어

 

다음은 가장 흥미롭지 않은 코드 섹션입니다. 

 

// Now the task scheduler, which takes over control of scheduling individual tasks, is automatically started.

void loop() {
  // Hooked to Idle task, it will run whenever CPU is idle (i.e. other tasks blocked)
  // DO NOTHING HERE...
 
  /*--------------------------------------------------*/
  /*---------------------- Tasks ---------------------*/
  /*--------------------------------------------------*/
}

 

설정 함수가 끝나면 FreeRTOS 리소스에 대한 호출이 있을 경우 FreeRTOS 스케줄러가 자동으로 시작됩니다. FreeRTOS 함수 vTaskStartScheduler 는 시스템에서 이 함수로 호출됩니다. 일반적으로 void loop() 함수는 아무 작업도 수행하지 않으며, 모든 실행은 태스크에서 이루어집니다. 여기에 코드를 추가하면 가장 낮은 우선순위로 실행되는 FreeRTOS IDLE 태스크의 일부로 실행됩니다.

 

다음으로 작업 함수를 구현합니다. 각 함수를 살펴보겠습니다.

 

태스크블링크 

 

void TaskBlink(void *xStruct)  // This is a task template
{
  BlinkData *data = (BlinkData *)xStruct; //passed data cast to BlinkData structur
  // unpack data from the BlinkData structure passed by reference
  int pin = data->pin;
  int delay = data->delay;
  float *speedMult = data->speedMult;
  int *statePtr = data->ptr;

  // set pinMode on output pin
  pinMode(pin, OUTPUT);


  while(true)  // A Task shall never return or exit.
  {
    int delayInt = (delay * (*speedMult)); // get nearest int to new delay time
    digitalWrite(pin, HIGH);           // turn the LED on (HIGH is the voltage level)
    *statePtr = 1;                     // set the LED state to 1
    vTaskDelay(pdMS_TO_TICKS(delayInt));  // unblock delay for on cycle time
    digitalWrite(pin, LOW);            // turn the LED off by making the voltage LOW
    *statePtr = 0;                     // set the LED state to 0
    vTaskDelay(pdMS_TO_TICKS(delayInt)); // unblock delay for off cycle                
  }
}

 

  1. 이 함수에서는 먼저 BlinkData 구조체에서 참조로 전달된 데이터를 변수 pin과 delay에, 그리고 speedMult에 대한 참조 포인터와 적절한 핀 색상(greenPtr, redPtr, bluePtr)에 대한 핀 상태 포인터를 풀어냅니다.
  2. GPIO 핀의 핀 모드를 출력으로 설정합니다.
  3. 종료되지 않는 처리 루프에 들어갑니다. 이 루프에서는 초기 설정과 현재 속도 배율을 기반으로 현재 켜짐/꺼짐 지연 시간을 결정하고, LED를 켜고 끄고, 핀 상태 변수를 0(꺼짐) 또는 1(켜짐)로 변경합니다.
  4. 상태 변경 간의 지연 시간은 FreeRTOS의 vTaskDelay 라는 함수를 사용하여 구현됩니다. 이 함수는 대기할 시간 단위(틱) 수를 나타내는 인수를 받습니다. 또한 밀리초를 틱으로 변환하는 pdMS_TO_TICKS 라는 함수도 사용합니다 . 이렇게 하면 Free RTOS 설치 시 시간 단위 설정과 관계없이 입력하는 지연 시간은 밀리초에 해당합니다.
  5. vTaskDelay 함수는 비차단 지연 함수입니다. 이 작업이 대기하는 동안 BLOCKED 상태로 전환되고 다른 작업을 예약할 수 있습니다 . 

 

태스크탤리 

 

void TaskTally(void *pvParameters)  // This is a task template
{
  (void)pvParameters; // Not used anywhere - the input is NULL

  // 1 - initialization
  TickType_t xLastWaitTime;  // define the last update time (in ticks)

  // initialize xLastWaitTime
  xLastWaitTime = xTaskGetTickCount();

  // initialize the lcd display (once)
  Adafruit_SSD1306 Display(OLED_RESET);
  Display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  Display.clearDisplay();
  Display.display();
  
  // 2 - display loop
  while (true)  // A Task shall never return or exit.
  {
    // calcualte numer of lamps lit from global variables
    int numLit = redLED + greenLED + blueLED;

    //display data on LCD display
    Display.clearDisplay();
    Display.setTextSize(1);
    Display.setTextColor(WHITE);
    Display.setCursor(40, 0);
    Display.println("LEDs Lit");
    Display.setTextSize(2);
    Display.setTextColor(WHITE);
    Display.setCursor(60, 15);
    Display.println(String(numLit));
    Display.display();
    // short unblocked delay between readings (1/10 second)
    xTaskDelayUntil(&xLastWaitTime, pdMS_TO_TICKS(100));
  }
}

 

 

  1. 초기화 섹션에서는 xLastWaitTime이라는 변수를 정의하고 현재 틱 횟수로 초기화합니다. 또한 일반적인 Arduino 호출을 사용하여 OLED 디스플레이를 초기화합니다.
  2. 이 함수는 무한 루프를 돌며 켜진 LED 수를 계산하고 이 정보로 OLED 디스플레이를 업데이트합니다. 그런 다음 xTaskDelayUntil 함수를 사용하여 다음 실행 시간을 이전 실행 시간으로부터 0.1초 후로 설정합니다. vTaskDelay처럼 함수 실행 사이의 대기 시간을 정의하는 대신, 실행 시작 사이의 클록 시간 차이를 설정합니다. 

태스크하이워터 

 

void TaskHighWater(void *pvParameters)
{
  while (true) //run forever
  {
    vTaskDelay(pdMS_TO_TICKS(5000));   // wait 5 seconds so system is fully running
    // display highwater marks for all 6 tasks
    Serial.println("***************************");
    Serial.print("High Water Mark for Green LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskGreen));
    Serial.print("High Water Mark for red LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskRed));
    Serial.print("High Water Mark for Blue LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskBlue));
    Serial.print("High Water Mark for Tally : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskTally));
    Serial.print("High Water Mark for Highwater Task: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskHighwater));
    //Serial.print("High Water Mark for Speedup: ");
    //Serial.println(uxTaskGetStackHighWaterMark(taskSpeedup));
    Serial.flush(); // make sure last data is written
    
    vTaskSuspend(NULL); // run this task only once
  }
}

 

이 작업은 모든 작업이 시작될 때까지 5초 동안 대기하고, 각 프로세스의 고수위 통계를 읽어 직렬 콘솔에 출력한 후 자동으로 일시 중단됩니다. 이후 레슨에서 주기적으로 실행되도록 수정하겠습니다.

 

태스크스피드 

 

void TaskSpeed(void *pvParameters)  // This is a task template
{
  (void)pvParameters;
  vTaskSuspend(NULL); // start in suspended state

  while (true)  // A Task shall never return or exit.
  {
  speedMult = 0.667 * speedMult; // increase speed by 50% by cutting cycle time by 1/3
  Serial.println("Speed increased");
  vTaskSuspend(NULL); // run this task only once
  }
}

 

이 작업은 스스로 일시 중단하면서 시작하나요? 네, 이 작업은 푸시 버튼에 연결된 인터럽트 서비스 루틴에 의해 트리거되며, 버튼을 누를 때까지 실행되지 않도록 해야 합니다. 버튼을 누르면 작업 상태가 READY로 변경되고 스스로 한 번 실행된 후 다시 일시 중단됩니다. 이 작업이 실행되면 모든 LED 지연의 대기 시간이 67% 감소하는데, 이는 사이클 속도가 50% 증가하는 것과 같습니다.

 

속도를 어떻게 낮추나요? 버튼을 하나 더 추가하는 것도 좋을 것 같습니다. 하지만 다음 레슨에서는 시스템 콘솔의 입력 명령으로 속도를 설정할 수 있도록 코드를 수정하겠습니다.

 

전체 코드 목록

 

레슨 2의 전체 코드 목록입니다. 보드에 로드하여 어떤 일이 일어나는지 확인해 보세요. 또한, rtosLesson1이라는 새 스케치로 저장하세요. 이후 레슨에서 이 스케치를 참조하고 수정하겠습니다. 

 

/*********
This code was developed for use with the tutorial series entitled
"Implementing FreeRTOS Solutions on ESP 32 Devices using Arduino"  

Refer to https://medium.com/@tomw3115/implementing-freertos-solutions-on-esp-32-devices-using-arduino-114e05f7138a for more information.

This software is provided as-is for hobbyist use and educational purposes only.

published by Tom Wilson - September 2024 *********/

// 1 - define freeRTOS settings
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif
#define SYSTEM_RUNNING_CORE 0

// 2 - define LCD display settings
#define OLED_ADDR 0x3C
#define OLED_RESET 4
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_SSD1306.h>

// 3 - define task functions
void TaskBlink(void *pvParameters);
void TaskTally(void *pvParameters);
void TaskSpeedup(void *pvParameters);
void TaskHighWater(void *pvParameters);


// 4 - define global variable for led status
int greenLED = 0;  // LED status indicator 0-off, 1-on
int redLED = 0;
int blueLED = 0;
int *greenPtr = &greenLED;  //pointers to pass to tasks to update led status
int *redPtr = &redLED;
int *bluePtr = &blueLED;
float speedMult = 1.0; // define speed multipier value

// 5 - define structure for data to pass to each task
struct BlinkData {
  int pin;
  int delay;
  float *speedMult;
  int *ptr;
};

// 6 - load up data for each LED task
static BlinkData blinkGreen = { 15, 2500, &speedMult, &greenLED };
static BlinkData blinkRed = { 4, 3300, &speedMult, &redLED };
static BlinkData blinkBlue = { 16, 1800, &speedMult, &blueLED };

// 7 - set up task handles for the RTOS tasks
TaskHandle_t taskGreen;
TaskHandle_t taskRed;
TaskHandle_t taskBlue;
TaskHandle_t taskSpeed;
TaskHandle_t taskTally;
TaskHandle_t taskHighwater;

// 8 - define function for highwater mark
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);

// 9 - define ISR routine to service button interrupt
int pinButton = 23; //GPIO pin used for pushbutton
void IRAM_ATTR buttonPush() 
{
  vTaskResume(taskSpeed); // run the speed program once
}

// the setup function runs once when you press reset or power the board
void setup() {

  // initialize serial communication at 115200 bits per second:
  Serial.begin(115200);

  // setup button for interupt processing
  pinMode(pinButton, INPUT_PULLUP);
  attachInterrupt(pinButton, buttonPush, FALLING);

  // Now set up tasks to run independently.
  xTaskCreatePinnedToCore
    (
    TaskBlink, // name of function to invoke
    "TaskBlinkGreen" , // label for the task
    4096 , // This stack size can be checked & adjusted by reading the Stack Highwater   
    (void *)&blinkGreen, // pointer to global data area
    2 , // Priority, 
    &taskGreen, //pointer to task handle
    ARDUINO_RUNNING_CORE // run on the arduino process core
    );

  xTaskCreatePinnedToCore
    (
    TaskBlink, 
    "TaskBlinkRed",
    4096,
    (void *)&blinkRed, 
    2,
    &taskRed, 
    ARDUINO_RUNNING_CORE
    );

  xTaskCreatePinnedToCore
    (
    TaskBlink, 
    "TaskBlinkBlue",  
    4096,  
    (void *)&blinkBlue,
    2, 
    &taskBlue, 
    ARDUINO_RUNNING_CORE
    );

  xTaskCreatePinnedToCore
  (
    TaskTally,
    "TaskTally", 
    4096,  
    NULL, 
    2, 
    &taskTally, 
    SYSTEM_RUNNING_CORE
    );

xTaskCreatePinnedToCore
    (
    TaskSpeed,
    "Speed", 
    4096,  
    NULL, 
    2,
    &taskSpeed, 
    SYSTEM_RUNNING_CORE
    );  

xTaskCreatePinnedToCore
  (
    TaskHighWater,
    "High Water Mark Display", 
    4096,
    NULL,
    2,
    &taskHighwater, 
    SYSTEM_RUNNING_CORE
    ); 
} //end of setup

// Now the task scheduler, which takes over control of scheduling individual tasks, is automatically started.
 
void loop() {
  // Hooked to Idle task, it will run whenever CPU is idle (i.e. other tasks blocked)
  // DO NOTHING HERE...
 
  /*--------------------------------------------------*/
  /*---------------------- Tasks ---------------------*/
  /*--------------------------------------------------*/
}
void TaskBlink(void *xStruct)  // This is a task template
{
  BlinkData *data = (BlinkData *)xStruct; //passed data cast as BlinkData structure
  // unpack data from the BlinkData structure passed by reference
  int pin = data->pin;
  int delay = data->delay;
  float *speedMult = data->speedMult;
  int *statePtr = data->ptr;

  // set pinMode on output pin
  pinMode(pin, OUTPUT);


  while(true)  // A Task shall never return or exit.
  {
    int delayInt = (delay * (*speedMult)); // get nearest int to new delay time
    digitalWrite(pin, HIGH);           // turn the LED on (HIGH is the voltage level)
    *statePtr = 1;                     // set the LED state to 1
    vTaskDelay(pdMS_TO_TICKS(delayInt));  // unblock delay for on cycle time
    digitalWrite(pin, LOW);            // turn the LED off by making the voltage LOW
    *statePtr = 0;                     // set the LED state to 0
    vTaskDelay(pdMS_TO_TICKS(delayInt)); // unblock delay for off cycle                
  }
}

void TaskTally(void *pvParameters)  // This is a task template
{
  (void)pvParameters;

  TickType_t xLastWaitTime;  //updated after first call by vTaskDelaUntil

  // initialize xLastWaitTime
  xLastWaitTime = xTaskGetTickCount();

  // initialize the lcd display (once)
  Adafruit_SSD1306 Display(OLED_RESET);
  Display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  Display.clearDisplay();
  Display.display();

  while (true)  // A Task shall never return or exit.
  {
    // calcualte numer of lamps lit from global variables
    int numLit = redLED + greenLED + blueLED;

    //display data on LCD display
    Display.clearDisplay();
    Display.setTextSize(1);
    Display.setTextColor(WHITE);
    Display.setCursor(40, 0);
    Display.println("LEDs Lit");
    Display.setTextSize(2);
    Display.setTextColor(WHITE);
    Display.setCursor(60, 15);
    Display.println(String(numLit));
    Display.display();
    // short unblocked delay between readings (1/10 second)
    xTaskDelayUntil(&xLastWaitTime, pdMS_TO_TICKS(100));
  }
}

void TaskHighWater(void *pvParameters)
{
  while (true) //run forever
  {
    vTaskDelay(pdMS_TO_TICKS(5000));   // wait 5 seconds so system is fully running
    // display highwater marks for all 6 tasks
    Serial.println("***************************");
    Serial.print("High Water Mark for Green LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskGreen));
    Serial.print("High Water Mark for Red LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskRed));
    Serial.print("High Water Mark for Blue LED : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskBlue));
    Serial.print("High Water Mark for Tally : ");
    Serial.println(uxTaskGetStackHighWaterMark(taskTally));
    Serial.print("High Water Mark for Highwater Task: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskHighwater));
    Serial.print("High Water Mark for Speed: ");
    Serial.println(uxTaskGetStackHighWaterMark(taskSpeed));
    Serial.flush(); // make sure last data is written
    
    vTaskSuspend(NULL); // run this task only once
  }
}

void TaskSpeed(void *pvParameters)  // This is a task template
{
  (void)pvParameters;
  vTaskSuspend(NULL); // start in suspended state

  while (true)  // A Task shall never return or exit.
  {
  speedMult = 0.667 * speedMult; // increase speed by 50%
  Serial.println("Speed increased");
  vTaskSuspend(NULL); // run this task only once
  }
}

 

결과

 

이제 세 개의 LED 표시등이 깜박임 작업에 전달한 설정에 따라 각기 다른 속도로 켜졌다 꺼졌다 하는 것을 볼 수 있습니다. OLED 디스플레이는 특정 시점에 켜진 LED의 개수를 표시합니다. 버튼을 누를 때마다 LED가 50%씩 더 빠른 속도로 깜박이기 시작합니다. 버튼을 여러 번 누르면 OLED 디스플레이가 0.1초마다 한 번씩만 샘플링하기 때문에 속도를 따라가지 못할 수 있습니다. 예상 화면은 다음과 같습니다. 

 

이제 Arduino IDE에서 시리얼 모니터를 열고 보드를 재부팅하세요. 5초 후에 아래와 비슷한 하이 워터마크 목록이 나타날 것입니다.  

 

 

이는 각 작업에서 사용 가능한 메모리 용량을 나타냅니다. 이러한 작업의 코드는 시간이 지남에 따라 메모리 요구량이 증가하지 않으므로 코드의 xTaskCreatePinnedToCore 문에서 메모리 할당량을 줄일 수 있을 것입니다. 처음에 메모리 할당량을 너무 낮게 예측하여 작업의 메모리가 부족해지면 어떻게 될까요? 보드가 예기치 않게 반복적으로 재부팅될 것입니다.

 

결론

 

다시 한번 깜박이는 LED 보드를 최종 결과로 얻었습니다. 하지만 더 실용적인 문제에 활용할 수 있는 몇 가지 기술을 익히셨기를 바랍니다. 여러분의 과제는 온도 센서, 리드 스위치, 근접 센서, 이미지 인식, 라이더 또는 다양한 입력을 읽는 것입니다. 여러분의 응답은 모터, 조명, 서보, 디스플레이, 웹소켓, MQTT 연결 또는 ESP 32에서 실행 가능한 다른 모든 것을 제어할 수 있습니다. 다음 과정에서는 여러 과제 간의 상호 작용을 제어하고 동기화하는 추가적인 방법, 특히 타이머, 세마포어, 뮤텍스, 큐를 살펴보겠습니다. 

 

 

반응형

더욱 좋은 정보를 제공하겠습니다.~ ^^