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

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) 너무 넉넉하게 줄 수는 없어도 최소한 필요한 공간보다는 크게 공간을 제공해 줘야만 한다.

2015년 8월 10일 월요일

아두이노에서 RTOS 사용하기 (FreeRTOS in Arduino)

아두이노에 포팅된 FreeRTOS를 사용하면 아두이노에서도 RTOS를 사용해 멀티 쓰레드로 프로그램을 만들 수 있다. FreeRTOS를 설치하려면 먼저 브라우져에서 아래 주소로 이동한다.




Download ZIP을 클릭해 zip 파일을 다운받는다. 다운받은 파일의 압축을 풀면 디렉토리 내에 아래와 같은 파일들이 만들어진다.



이 중 libraries 디렉토리 내에 아래와 같은 3개의 서브디렉토리가 들어있다.



이 3개의 서브디렉토리를 자신의 아두이노 라이브러리 디렉토리에 복사 해 주면 된다. (아두이노에 외부 라이브러리 설치하는 법 참조)
아두이노 DUE를 사용하면 FreeRTOS_ARM을 사용하고 아두이노 UNO, MEGA등 AVR 프로세서를 사용한 아두이노의 경우 FreeRTOS_AVR을 사용하면 된다. 

라이브러리 설치가 끝나면 아두이노를 재시동하고 예제 -> FreeRTOS_AVR -> frBlink 파일을 선택해 보드에 업로드 해서 LED가 깜빡이면 FreeRTOS가 정상적으로 동작하고 있는 것이다. 

#include <FreeRTOS_AVR.h>

const uint8_t LED_PIN = 13;
SemaphoreHandle_t sem;

static void Thread1(void* arg) {
  while (1) {
    xSemaphoreTake(sem, portMAX_DELAY);
    digitalWrite(LED_PIN, LOW);
  }
}

static void Thread2(void* arg) {
  pinMode(LED_PIN, OUTPUT);
  while (1) {
    digitalWrite(LED_PIN, HIGH);
    vTaskDelay((200L * configTICK_RATE_HZ) / 1000L);
    xSemaphoreGive(sem);
    vTaskDelay((200L * configTICK_RATE_HZ) / 1000L);
  }
}

void setup() {
  portBASE_TYPE s1, s2;

  sem = xSemaphoreCreateCounting(1, 0);  // 세마포어를 생성
  s1 = xTaskCreate(Thread1, NULL, configMINIMAL_STACK_SIZE, NULL, 2, NULL); // Thread1 생성
  s2 = xTaskCreate(Thread2, NULL, configMINIMAL_STACK_SIZE, NULL, 1, NULL); // Thread2 생성
  vTaskStartScheduler();  // 태스크 스케쥴러 시작
  while(1) {}
}

void loop() {}

코드에서는 두개의 쓰레드 (Thread1, Thread2)가 동시에 실행되어 Thread2에서 LED를 켜고 200ms 후에 semaphore를 POST 해주어 Thread1이 semaphore를 받으면 LED를 끄게 되어 있다. Thread1은 LED를 끄고 나면 곧바로 다시 semaphore를 기다리지만 Thread2에서 200ms를 기다린 후 LED를 켜고 다시 200ms 후에 semaphore를 POST 하기 전에는 계속 semaphore를 기다리게 된다. 



그리고 일반 스케치와 다르게 loop() 에서는 아무 동작을 하지 않는다.