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

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



ESP32에서 FreeRTOS 사용 - Message Queue 사용하기

FreeRTOS에서

- Queue는 태스크 간 교환되는 데이터/메시지를저장하는데 사용
- 데이터는 queue에 카피됨 (포인터로 참조되는게 아님)
- 여러 태스크가 억세스 할 수 있음
- Queue Read는 blocking 동작 (큐에 데이터가 없으면 데이터를 기다리기 위해 태스크는 blocked 상태가 됨)
- Queue Write는 blocking 동작 (큐에 빈 공간이 없으면 빈 공간이 생기는걸 기디리기 위해 태스크는 blocked 상태가 됨)
- mulitple queue에 대해 blocking 동작 (큐 그룹에 데이터가 들어오는걸 기다리기 위해 태스크는 blocked 상태가 됨)

* 데모
이 데모에서는 3개의 태스크를 만드는데, 2개의 태스크는 sending 태스크이고 1개는 receiving 태스크이다. sending 태스크는 sender id와 태스크의 카운트 값을 가지고 있는 데이터를 보낸다.

// structure that hold data
typedef struct{
  int sender;
  int counter;
}Data;

// this variable hold queue handle 
xQueueHandle xQueue;

void setup() {
  Serial.begin(115200);
  // create the queue which size can contains 5 elements of Data
  xQueue = xQueueCreate(5, sizeof(Data));
  xTaskCreate(
      sendTask1,     // Task function. 
      "sendTask1",   // name of task. 
      10000,         // Stack size of task 
      NULL,          // parameter of the task 
      2,             // priority of the task 
      NULL);         // Task handle to keep track of created task 
  xTaskCreate(
      sendTask2,     // Task function. 
      "sendTask2",   // name of task. 
      10000,         // Stack size of task 
      NULL,          // parameter of the task 
      2,             // priority of the task 
      NULL);         // Task handle to keep track of created task 
  xTaskCreate(
      receiveTask,   // Task function. 
      "receiveTask", // name of task. 
      10000,         // Stack size of task 
      NULL,          // parameter of the task 
      1,             // priority of the task 
      NULL);         // Task handle to keep track of created task 
}

void loop() {
}

void sendTask1( void * parameter )
{ 
  BaseType_t xStatus;   // keep the status of sending data 
  // time to block the task until the queue has free space 
  const TickType_t xTicksToWait = pdMS_TO_TICKS(100);
  Data data;    // create data to send 
  data.sender = 1;   // sender 1 has id is 1
  data.counter = 1;
  for(;;){
    Serial.println("sendTask1 is sending data");
    // send data to front of the queue 
    xStatus = xQueueSendToFront( xQueue, &data, xTicksToWait );
    // check whether sending is ok or not 
    if( xStatus == pdPASS ) {   // increase counter of sender
      data.counter = data.counter + 1;
    }
    // we delay here so that receiveTask has chance to receive data
    delay(1000);
  }
  vTaskDelete( NULL );
}
 
// this task is similar to sendTask1 
void sendTask2( void * parameter )
{
  BaseType_t xStatus;
  const TickType_t xTicksToWait = pdMS_TO_TICKS(100);
  Data data;
  data.sender = 2;
  data.counter = 1;
  for(;;){
    Serial.println("sendTask2 is sending data");
    xStatus = xQueueSendToFront( xQueue, &data, xTicksToWait );
    if( xStatus == pdPASS ) {
      data.counter = data.counter + 1;
    }
    delay(1000);
  }
  vTaskDelete( NULL );
}
 
void receiveTask( void * parameter )
{
  BaseType_t xStatus;   // keep the status of receiving data 
  // time to block the task until data is available 
  const TickType_t xTicksToWait = pdMS_TO_TICKS(100);
  Data data;
  while (1) {        // receive data from the queue
    xStatus = xQueueReceive( xQueue, &data, xTicksToWait );
    if(xStatus == pdPASS){     // check whether receiving is ok or not 
      // print the data to terminal 
      Serial.print("receiveTask got data: ");
      Serial.print("sender = ");
      Serial.print(data.sender);
      Serial.print(" counter = ");
      Serial.println(data.counter);
    }
  }
  vTaskDelete( NULL );
} 
 
 
 
 
 

2018년 4월 5일 목요일

ESP32에서 FreeRTOS 사용 - TASK 만들기

여기서는 ESP32에서 FreeRTOS를 사용하는 방법을 설명하겠다.

아두이노 ESP32는 기본적으로 FreeRTOS상에서 만들어졌지만 한개의 태스크만 사용한다면 FreeRTOS의 훌륭한 기능들을 활용하지 못하고 낭비하는 것이다.

예제 코드에서는 어플리케이션에 기본 아두이노 태스크 외에 하나의 태스크를 더 추가해 준다. 즉 어플리케이션은 2개의 태스크를 가지게 된다. 아두이노 태스크는 시리얼 포트로 'this is Arduino task'란 글자를 출력하고 두번째 태스크는 'this is additional task'라는 글자를 출력한다.

태스크를 만들기 위해 xTaskCreate() API 함수를 사용했다. FreeRTOS API에 대해서는 FreeRTOS ebook을 참고해라. xTaskCreate() 함수에 대한 설명은 FreeRTOS ebook의 48페이지에 있다.

ESP32 아두이노가 FreeRTOS 기반으로 만들어 졌기 때문에 따로 FreeRTOS 헤더파일을 include 하지 않아도 된다.

void setup() {
  Serial.begin(115200);
  // 새 태스크 생성
  xTaskCreate(
      additionalTask,           // Task 함수 이름
      "additional Task",        // Task 이름
      10000,                    // Task 스택 크기
      NULL,                     // Task 파라미터
      1,                        // Task 우선순위
      NULL);                    // Task handle
}

// loop() 함수는 ESP32 loopTask에 의해 무한히 호출됨

void loop() {
  Serial.println("this is Arduino Task");
  delay(1000);
}


// additionalTask 가 생성될 때 이 함수가 호출됨
void additionalTask( void * parameter )
{

  for(;;){
    Serial.println("this is additional Task");
    delay(1000);
  }
  // 종료되면 Task 삭제
  // 하지만 위의 무한루프 때문에 호출될 수 없음

  vTaskDelete( NULL );
}


보드에 업로드 해 실행한 결과는 다음과 같다.





2016년 2월 2일 화요일

아두이노에서 FreeRTOS 사용하기 - 4. Mutual Exclusion (상호배제)


이번 주제인 mutual exclusion(상호 배제)는 여러 태스크가 같은 자원에 동시에 억세스 하지 못하도록 보장해 주는 메커니즘이다. 

두개의 태스크가 동시에 같은 자원(여기서는 가상의 프린터 )를 사용하는 경우 태스크가 실행되는 도중에 RTOS에 의해 선점되면 출력 결과가 아래와 같은 식으로 되어 버린다.

Printer 1: +++++++++++Printer 2: -----------------------------
++++++++++++++++++
Printer 1: Printer 2: -----------------------------
+++++Printer 2: -----------------------------
++++++++++++++++++++++++
Printer 2: -----------------------------
PrintePrinter 2: -----------------------------
r 1: ++++++++Printer 2: -----------------------------
+++++++++++++++++++++
Printer 1: Printer 2: -----------------------------
+++++++++++++++++++++++++++++
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++Printer 2: -----------------------------
++++++++++++++
Printer 1: ++++++Printer 2: -----------------------------
+++++++++++++++++++++++
Printer 2: -----------------------------

위의 결과가 나온 코드이다.

#include <FreeRTOS_AVR.h>

#define MS(x) ((unsigned long)(x)/portTICK_PERIOD_MS)

void vPrint(void *);

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

  xTaskCreate(vPrint, "Printer1", 200, (void *)"Printer 1: +++++++++++++++++++++++++++++", 1, NULL);
  xTaskCreate(vPrint, "Printer2", 200, (void *)"Printer 2: -----------------------------", 2, NULL);

  vTaskStartScheduler();
  while (1) ;
}

void loop()
{
}

void vPrint(void *pvParameters)
{
  unsigned char led_state = 0;

  while (1) {
    printer((char *)pvParameters);
    vTaskDelay(MS(random(100)));
  }
}

void printer(char *p)
{
  while (*p != '\0') {
    Serial.print(*p++);
    delay(1);
  }
  Serial.println(" ");
}

하지만 원하는 것은 한 태스크가 프린터로 출력하는 동안 다른 태스크가 방해하지 못하게 하는 것이다.

Printer 1: +++++++++++++++++++++++++++++
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 1: +++++++++++++++++++++++++++++

이런식으로 출력되게 하기 위해 한 태스크가 프린터를 사용하는 동안 다른 태스크는 프린터에 접근하지 못하고 대기하게 하기 위해 MUTEX를 사용한다.

#include <FreeRTOS_AVR.h>

#define MS(x) ((unsigned long)(x)/portTICK_PERIOD_MS)

SemaphoreHandle_t xMutex;

void vPrint(void *);

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

  xMutex = xSemaphoreCreateMutex();
  if (NULL != xMutex) {
    xTaskCreate(vPrint, "Printer1", 200, (void *)"Printer 1: +++++++++++++++++++++++++++++", 1, NULL);
    xTaskCreate(vPrint, "Printer2", 200, (void *)"Printer 2: -----------------------------", 2, NULL);

    vTaskStartScheduler();
  } else {
    Serial.println("Create mutex failed.");
  }
  while (1) ;
}

void loop()
{
}

void vPrint(void *pvParameters)
{
  unsigned char led_state = 0;

  while (1) {
    xSemaphoreTake( xMutex, portMAX_DELAY);       // block forever until the semaphore is given
    // Critical section start
    printer((char *)pvParameters);
    // Critical section end
    xSemaphoreGive( xMutex );
    vTaskDelay(MS(random(500)));
  }
}

void printer(char *p)
{
  while (*p != '\0') {
    Serial.print(*p++);
    delay(1);
  }
  Serial.println(" ");
}

위의 코드에서 빨간색으로 표시된 부분을 Critical Section(임계영역)이라고 한다. 여러 태스크가 공유하지만 동시에 억세스하면 안되는 자원을 억세스 하는건 critical section 내에서만 해야 한다. C.S.는 xSemaphoreTake()로 시작하고 xSemaphoreGive()로 끝난다. 한 태스크가 C.S. 내에 있는 코드를 실행하고 있는 동안 다른 태스크가 C.S. 안으로 들어가려고 하면 xSemaphoreTake()에서 블럭되어 현재 C.S. 내에서 실행중이 태스크가 빠져 나오면서 xSemaphoreGive()를 호출할 때 까지 sleep 상태로 대기하게 되므로 동시에 두개 이상의 태스크가 C.S. 내로 진입할 수 없게 된다.  
 
 
1. CS에 아무 태스크도 진입하지 않은 상태

 2.태스크1이 xSemaphoreTake() 를 호출해 키를 가지고 CS에 진입

 3. 태스크1이 CS 내부에서 공유자원인 프린터를 사용

 4. 태스크1이 아직 CS 내에 있는데 태스크2도 CS에 진입하기 위해 xSemaphoreTake()를 호출했으나 키가 없어 CS에 진입하지 못하고 sleep 상태로 키가 돌아오기를 대기


 5. 태스크1이 CS에서 빠져나오며 xSemaphoreGive()를 호출해 키를 돌려줌

 6. Sleep상태로 기다리던 태스크2가 키가 돌아왔으므로 깨어나 CS에 진입
 
 
위의 예제에서는 태스크간에 공유하는 자원으로 프린터를 예로 들었지만 그 이외에 서로 공유하는 전역변수들 역시 억세스 할 때는 C.S. 내에서 해야 한다. 
 
그리고 태스크가 C.S.  내에 들어 있는 동안 그 C.S.에 진입하려고 하는 태스크는 블럭되어 실행되지 못하므로 ISR과 마찬가지로 C.S. 내에서도 가능한 빨리 작업을 마치고 빠져 나와야 한다.

2016년 2월 1일 월요일

아두이노에서 FreeRTOS 사용하기 - 3. 태스크 동기화


멀티태스킹 프로그램을 만들다 보면 태스크 간 또는 태스크와 ISR 간에 동기화가 필요한 경우가 있다. 

이 예제에는 LED라는 이름의 태스크가 실행되는데, 이 태스크는 외부에 연결된 Push button이 눌린 이벤트가릴 발생할 때 마다 led의 상태를 토글한다. 또 하나의 태스크인 PBSW는 스위치의 상태를 감시하다 버튼이 눌리면 그 이벤트를 LED 태스크에 통보한다.

LED 태스크는 버튼이 눌린 이벤트가 발생할 때 까지 대기해야 하는데 이를 위해 세마포어를 사용하게 된다. 여기서는 binary semaphore를 사용한다.



세마포어는 다음 함수로 만들 수 있다.

vSemaphoreCreateBinary ( xSemaphoreHandle xSemaphore );

세마포어에 대한 동작은 ‘take’ 또는 ‘give’ 두가지 밖에 없다.

xSemaphoreTake ( vSemaphoreHandle xSemaphore, portTickType xBlockTime );
xSemaphoreGive ( vSemaphoreHandle xSemaphore );

위의 두 매크로 모두 정상적으로 실행되었으면 pdTrue를 리턴하고, 에러가 발생하면 pdFalse를 리턴한다. xBlockTime 파라미터는 semaphore가 available할 때 까지 대기 시간이다. Queue 함수에서 사용된 것과 동일한 역할을 한다.

만일 Queue 또는 semaphore등을 ISR에서 사용해야 하는 경우에는 특별한 함수나 매크로를 사용해야만 한다. 이 함수나 매크로들은 이름의 마지막이 FromISR 로 끝난다. 예) xSemaphoreTakeFromISR, xSemaphoreGiveFromISR 

#include <FreeRTOS_AVR.h>

#define MS(x) ((unsigned long)(x)/portTICK_PERIOD_MS)

#define LED_PIN 13
#define SW_PIN 3

SemaphoreHandle_t xSemaphore;

void vLED(void *);
void vPBSW(void *);

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

  vSemaphoreCreateBinary( xSemaphore );

  xTaskCreate(vLED, "LED", 200, NULL, 1, NULL);
  xTaskCreate(vPBSW, "PBSW", 200, NULL, 1, NULL);

  vTaskStartScheduler();
  while (1) ;
}

void loop()
{
}

void vLED(void *pvParameters)
{
  unsigned char led_state = 0;

  Serial.print("Task ");
  Serial.print((char *)pvParameters);
  Serial.println(" started...");

  pinMode(LED_PIN, OUTPUT);          // init LED
  while (1) {
    xSemaphoreTake( xSemaphore, portMAX_DELAY);       // block forever until the semaphore is given
    led_state =! led_state;                          // toggle LED state
    digitalWrite(LED_PIN, led_state);         // apply LED state
  }
}

void vPBSW(void *pvParameters)
{
  unsigned char last_state = 1;
  unsigned char cur_state;

  Serial.print("Task ");
  Serial.print((char *)pvParameters);
  Serial.println(" started...");

  pinMode(SW_PIN, INPUT_PULLUP);  // init Pushbutton switch
  while (1) {
    cur_state = digitalRead(SW_PIN);
    if ((1==last_state) && (0==cur_state)) {    // button pressed
      xSemaphoreGive( xSemaphore );
      last_state = 0;
    } else if ((1==last_state) && (0==cur_state)) {    // button released
      last_state = 1;
    }
    vTaskDelay(10/portTICK_PERIOD_MS);
  }
}

위의 코드에서 vLED 태스크는 xSemaphoreTake() 함수에서 실행을 중단하고 vPBSW 태스크에서 xSemaphoreGive() 함수를 호출해 줄 때 까지는 잠자는 상태로 대기하게 된다. 물론 세마포어를 사용하지 않고 전역변수를 이용해서도 유사하게 동작하게 할 수 있지만 그 경우는 busy waiting이 되기 때문에 상당히 비효율적이다.

void LED()
{
  ...
  while (1) {
    if (1 == gFlag) {
    led_state =! led_state;                          // toggle LED state
    digitalWrite(LED_PIN, led_state);         // apply LED state
    gFlag = 0; 
  }
} 

void PBSW()
{
  ...
  while (1) {    
    if ((1==last_state) && (0==cur_state)) {    // button pressed
      gFlag = 1;
      last_state = 0;
    } else if ((1==last_state) && (0==cur_state)) {    // button released
      last_state = 1;
    }
    ...
  }
}

이 경우 LED 태스크는 쉬지 않고 계속 gFlag 변수값이 1이 되는지를 확인해야 한다. 그에 비해 세마포어를 사용하면 스위치가 눌리지 않는 한 LED 테스크는 별도로 CPU 자원을 사용하지 않고 sleep 상태를 유지하므로 그 시간에 다른 작업을 더 할 수 있다.



2016년 1월 28일 목요일

아두이노에서 FreeRTOS 사용하기 - 1


RTOS를 사용하는 가장 큰 목적이 멀티태스킹이므로 어떻게 멀티태스크 환경을 설정하고, 태스크를 만들고, 선점 스케줄러를 시작시키는가를 보여주기 위해 두개의 독립적인 태스크가 동시에 실행되는 프로그램을 만들어 보겠다. 

(아두이노에 FreeRTOS를 설치하는 방법은 이전 포스트를 참고 - http://arsviator.blogspot.kr/2015/08/rtos-freertos-in-arduino.html?q=freertos)

이 예제에는 두개의 독립적인 태스크가 있다. 첫번째는 “LED”라는 이름으로 LED를 일정한 시간 간격으로 깜빡이게 하는 것이고, 두번째는 “Temperature”라는 이름으로 주기적으로 외부 온도 센서의 측정값을 읽어 시리얼 포트로 결과를 출력하는 것이다. 두 태스크는 같은 우선순위로 실행되고, 스케줄러는 다른 태스크가 실행될 수 있도록 실행중인 태스크를 선점한다. 

#include <FreeRTOS_AVR.h>

#define MS(x) (((unsigned long)(x)*configTICK_RATE_HZ)/1000L)

static void Led(void* arg);
static void Light(void* arg);

static void Led(void* arg) {
  boolean ledState = false;

  pinMode(13, OUTPUT); 
  while (1) {
    digitalWrite(13, ledState);
    ledState = !ledState;
    vTaskDelay(MS(500));
  }
}

static void Light(void* arg) {  
  short l = 0;
  TickType_t xLastWakeTime = xTaskGetTickCount();
  const TickType_t xWakePeriod = 2000/portTICK_PERIOD_MS;    // 2 sec

  while (1) {
    l = analogRead(0);
    Serial.print("Light: ");
    Serial.println(l);
    vTaskDelayUntil(&xLastWakeTime, xWakePeriod);
  }
}

void setup() {
  portBASE_TYPE s1, s2;

  Serial.begin(115200);

  s1 = xTaskCreate(Led, NULL, 200, NULL, 1, NULL);
  s2 = xTaskCreate(Light, NULL, 200, NULL, 1, NULL);

  vTaskStartScheduler();     // start scheduler
  while(1);
}

void loop() {
}


LED 태스크는 먼저 LED가 연결된 (여기서는 아두이노에 기본으로 연결되어 있는 13번) 핀을 출력모드로 설정한 후 일정한 시간 간격으로 LED를 깜빡이게 한다. 

Light 태스크 역시 일정한 시간 간격으로 A0 핀에 연결된 CDS값을 AD변환하여 조도를 측정해 결과 값을 시리얼 포트로 출력해 준다.

태스크는 무한 루프를 포함한 함수로 구성되고, 태스크 함수의 프로토타입은 다음과 같다.

void TaskFunction (void *pvParameters);

위의 코드를 보면 두개의 태스크에서 시간을 delay하기 위해 서로 다른 함수를 사용한 것을 알 수 있다. LED task에서는 vTaskDelay() 함수를 사용한데 비해 Light task에서는 vTaskDelayUntil() 함수를 사용하고 있다.

void vTaskDelay ( portTickType xTicksToDelay );
void vTaskDelayUntil ( portTickType *pxPreviousWakeTime, portTickType xTimeIncrement );

두 함수 모두 스케줄러에 일정 시간동안 delay를 요청하는건 동일하나 vTaskDelay()는 함수가 호출된 시점부터 일정한 tick 동안 태스크 실행을 블럭하는데 비해 vTaskDelayUntil() 함수는 이전에 블럭에서 깨어난 시간인 pxPreviousWakeTime 의 값 부터 xTimeIncrement 만큼만 태스크의 실행을 블럭한다. vTaskDelayUntil() 함수를 사용하면 루프의 내용을 한번 실행하는데 얼마의 시간이 거리건 상관 없이 일정한 시간 간격으로 루프를 실행할 수 있다. 또한 pxPreviousWakeTime 변수는 태스크가 블럭에서 깨어나는 순간 값이 자동으로 업데이트 된다.

만일 다음과 같은 두개의 태스크가 있는 경우를 생각해 보자. 여기서 foo() 함수는 호출되면 실행되는데 0.5초의 시간이 걸린다고 가정한다.

void task1(…)
{
  while (1) {
    foo();
    vTaskDelay(1000/portTICK_PERIOD_MS);
  }
}

void task2(…)
{
  portTICK_TYPE xLastWakeTime = xTaskGetTickCount();

  while (1) {
    foo();
    vTaskDelayUntil(&xLastWakeTime, 1000/portTICK_PERIOD_MS);
  }
}

두 코드는 비슷해 보이지만 task1의 경우 0초에 foo() 함수를 호출하고 0.5초에 vTaskDelay() 함수를 호출해서 1초를 블럭한 뒤 다시 1.5초에 foo()를 호출하게 된다. 즉 foo() 함수는 1.5초에 한번씩 호출되는 것이다. 그에 비해 task2의 경우 foo() 함수의 실행시간이 얼마가 걸리건 상관 없이 foo() 함수는 정확히 1초에 한번씩 호출되게 된다. (물론 foo() 함수의 실행시간은 delay 시간 (여기서는 1초) 보다는 짧아야 한다) 그림으로 보면 좀 더 이해하기 쉬울 것이다. 


태스크를 생성했으면 스케줄러를 실행시켜 주면 이제부터 생성된 태스크들이 동시에 실행을 시작하게 된다. 스케줄러가 시작되면 모든 통제권을 RTOS가 가지게 되므로 vTaskStartScheduler()를 호출한 뒤에는 무한루프를 넣어주고, loop() 함수는 사용하면 안된다.

또하나 주의할 것은 태스크를 생성할 때 태스크가 사용할 스택의 크기를 지정해줘야 하는데 이 스택 크기를 너무 작게 잡으면 프로그램의 동작이 완전 멈춰버릴 수 있으므로 충분한 공간을 확보해 줘야만 한다.FreeRTOS 예제 코드에 보면 태스크 생성시 xTaskCreate(Thread1, NULL, configMINIMAL_STACK_SIZE, NULL, 2, NULL); 같이 스택 크기를 configMINIMAL_STACK_SIZE로 지정해 주는데 이 경우 태스크의 코드가 조금만 커져도 (로컬 변수를 몇개 추가하거나 다른 함수를 중복호출 하거나 등) 프로그램 실행이 완전히 멈춰버린다. 아두이노 우노같은 경우 SRAM 공간이 충분하지 않으므로 (SRAM 2KB) 너무 넉넉하게 줄 수는 없어도 최소한 필요한 공간보다는 크게 공간을 제공해 줘야만 한다.