FreeRTOS의 코드는 다음과 같은 네이밍 규칙을 사용한다.
.
FreeRTOS에서 변수명을 결정할 때 접두어는 아래와 같이 결정한다.
c
: char
타입을 의미한다.s
: int16_t
타입 (short
)을 의미한다.i
: int32_t
타입 (int
)을 의미한다.x
: BaseType_t
타입을 의미한다. 상당히 자주 사용되는데, 구조체나 인스턴스 핸들 등 일반적인 타입을 제외하면 대부분 x
다.u
: unsigned
를 의미한다.p
: 포인터 변수를 의미한다.FreeRTOS에서 함수명을 결정할 때 접두어는 아래와 같이 결정한다.
v
: void
반환값이 없는 함수를 의미한다.x
: 변수명의 접두어인 x
와 같은 의미다. BaseType_t
를 반환하는 함수다.pv
: void*
타입을 반환하는 함수다.prv
: private 함수를 의미한다. 대표적으로 아무런 task도 실행되지 않을 때 실행되는 idle task가 호출하는 callback 함수 (hook)가 이 접두어를 사용하고 있다.FreeRTOS에서 매크로명은 그 매크로가 선언된 소스파일의 이름을 따라 결정된다.
예를 들어 무한정 기다림을 의미하는 portMAX_DELAY
는 portable.h
라는 커널 소스에 선언돼있다.
문서에 따르면 semaphore 관련 API는 거의 매크로로 구성돼있지만, 함수명 네이밍 규칙을 따라간다고 명시돼있다.
FreeRTOS에서 하나의 task는 하나의 스레드(thread)를 의미한다.
각 task에는 우선순위를 할당하며 숫자가 높을수록 큰 우선순위를 의미한다.
configMAX_PRIORITIES
까지 할당이 가능하다.Task는 return value가 없으며 (void*) 타입으로 여러 자료형을 매개변수로 받을 수 있다.
Task는 일회용 task와 주기적 task 2가지 종류가 있다.
일회용 task는 기능을 한 번 수행한 뒤 마지막에 스스로를 삭제한다. (꼭 해줘야 함.)
void 태스크이름( void* pvParameters ) {
/* 어쩌고 저쩌고 */
vTaskDelete(NULL);
}
주기적 task는 무한 loop가 들어있다. Delay 또는 suspend 함수 등으로 task의 state가 blocked(waiting)로 변하지 않는 이상 계속해서 기능을 빠르게 반복 실행한다.
void 태스크이름( void* pvParameters ) {
while(true) {
/* 어쩌고 저쩌고 */
}
}
각 task마다 local stack 공간이 할당된다. 이 공간은 메모리의 .bss
영역 또는 .heap
영역에 들어간다.
configSUPPORT_DYNAMIC_ALLOCATION
과 configSUPPORT_STATIC_ALLOCATION
값을 어떻게 설정하느냐에 따라, 그리고 task 생성 함수를 xTaskCreate
나 xTaskCreateStatic
중 어느 것을 사용하느냐에 따라 할당 방법이 달라진다.6가지 API가 주로 사용되며 앞 3개가 중요하고, 뒤 3개는 잘 안 쓴다.
xTaskCreate()
.
pvTaskCode
는 task의 기능이 선언된 함수의 함수포인터를 말한다.pcName
은 디버깅 용도로 사용하는 문자열이며 task의 이름을 말한다.usStackDepth
는 task마다 할당되는 stack 메모리를 말한다. 단위는 WORD
이며 우리가 사용하는 ARM Cortex-M보드에서는 1WORD == 4Byte
라는 점을 기억하자.pvParameters
는 task 함수로 전달할 매개변수를 말한다. 없다면 NULL
쓰면 된다. 전달할 매개변수를 (void*)
타입으로 캐스팅한 뒤 여기다가 넣어주면 task 함수에서 사용할 수 있다.pxCreatedTask
는 task를 제어하기 위한 TaskHandle_t
타입 핸들을 말한다. Task의 우선순위를 바꾸거나, task를 멈추거나 등 task에 대한 설정은 모두 이 핸들을 통해서 이뤄진다.vTaskDelay()
.
running
에서 blocked(waiting)
으로 변경하는 함수다. 설정된 시간 xTicksToDelay
동안 해당 task는 blocked
task가 되며, 다음 우선순위를 가진 task가 실행된다.tick
단위 시간을 사용하는 것 보다 pdMS_TO_TICKS()
매크로를 사용해서 우리에게 편한 ms(밀리초) 단위를 tick
으로 변환해서 사용하는 것이 좋다. 1 Tick
이 의미하는 시간이 다르기 때문이다.vTaskDelayUntil()
.
vTaskDelay()
와 똑같은 기능을 수행하지만, 작동 방식이 조금 다르다.
vTaskDelay()
는 호출 시점부터 지정된 시간만큼 blocked 되는 반면,
vTaskDelayUntil()
은 호출 시점과 관계 없이 목표 절대 시간 주기에 맞춰 blocked 된다.
vTaskSuspend()
와 vTaskResume()
vTaskPrioritySet()
전체 소스코드는 저작권을 고려해 올리지 않는다.
실습 내용은 다음과 같다.
10
의 task1을 만든다.9
의 task2를 만든다.xTaskCreate((TaskFunction_t)Task1, "Task1", 128, NULL, 10, &xHandle1);
xTaskCreate((TaskFunction_t)Task2, "Task2", 128, (void*)Param, 9, &xHandle2 );
8
로 낮춘다.vTaskSuspend(xHandle1);
vTaskPrioritySet(xHandle1, 8);
vTaskResume(xHandle1);
vTaskDelay()
를 주석처리 해보면서 두 task가 어떻게 작동하는지 확인한다.vTaskDelay()
를 주석처리 하지 않았을 때 결과다.b
를 출력하고 1초간 blocked 된다.a
를 출력하고 1초간 blocked 된다.b
를 출력하고 1초간 blocked 되는 과정이 반복된다.vTaskDelay()
를 주석했을 때 결과다.vTaskDelay()
를 만난 뒤 blocked 되면 task1이 시작된다.a
를 출력한다.b
를 1회 출력한 뒤 다시 blocked 된다.b
가 한 번 찍힌다.)a
를 출력하는 과정이 반복된다.vTaskDelay()
를 주석했을 때 결과다.vTaskDelay()
를 주석했을 때 결과다. (위 사진과 같은 결과)이번 실습을 통해 다음 세 가지를 배울 수 있었다.
우리는 앞서 각 task마다 지정된 크기의 local stack을 갖고있다고 배웠다. 만일 task 내에서 해당 stack size를 넘는 데이터를 선언하고 연산을 수행한다면 stack overflow가 발생할 것이다. 이 치명적인 fault를 어떻게 발견할 수 있을까? FreeRTOS는 stack overflow를 발견하기 위한 기초적인 두 가지 알고리즘을 제공한다.
FreeRTOSConfig.h
헤더파일 안에는 configCHECK_FOR_STACK_OVERFLOW
라는 상수가 정의돼있다. 이 상수는 0, 1, 2
세 가지 값을 가질 수 있다. 0
일 경우 stack overflow를 컨트롤러가 따로 검사하지는 않는다.
저작권을 존중하므로 전체 실습 소스코드는 공유하지 않는다.
IPC를 사용하다보면 필연적으로 공유자원을 사용하고 싶은 욕구가 생긴다.
.
buttonCnt
라는 공유자원을 사용하고 있다.buttonCnt++
라는 단순한 한 줄 짜리 연산도 어셈블리어로 무려 5줄이나 차지하고 있다. 저 어셈블리 연산은 atomic 하지 않기 때문에 얼마든지 저 사이에 접근이 일어날 수 있다.buttonCnt == 100
이면 특정 동작을 수행하도록 조건문을 만들었는데, buttonCnt++
연산 도중 task B에 의해 한 번 더 증가해 buttonCnt = 101
이 됐다고 하자. 그럼 task A의 특정 동작은 수행될 수가 없다.∴ 따라서, critical section problem은 OS가 반드시 중요하게 다뤄야하는 문제다! Critical section에 대한 소유권은 반드시 단일 task가 독점하도록 보장해줘야 한다.
taskENTER_CRITICAL()
, taskEXIT_CRITICAL()
이라는 커널 API를 제공한다. 이 API는 인터럽트보다 우선순위가 높다!!저작권을 존중하므로 전체 실습 소스코드는 공유하지 않는다.
FreeRTOS의 critical section 보호 방법 중 하나인 taskENTER_CRITICAL
을 사용한다.
xSemaphoreCreateBinary()
, xSemaphoreCreateCounting()
xSemaphoreTake()
, xSemaphoreTakeFromISR()
xSemaphoreGive()
, xSemaphoreGiveFromISR()
vSemaphoreDelete()
0
과 1
값으로만 이뤄진 semaphore다. Critical section에 대해 p연산을 수행하면 semaphore가 0
이 되서 다른 task의 접근을 막는다. 접근을 요청한 task는 blocked 상태가 되며 들어갈 때 까지 기다린다. (무한정 기다리는 건 아니고, take 함수에서 기다리는 시간을 설정할 수 있다.)0
부터 사용자가 설정한 수까지 값을 가지는 semaphore다. 예를 들어 공유자원이 10칸짜리 배열일 때, semaphore를 10
으로 초기화하면, counting semaphore는 남은 공유자원의 개수를 의미한다. semaphore가 0
이 되면 해당 배열이 가득 찼음을 의미하며 접근을 요청한 task는 blocked 상태가 되며 기다린다.Clicked[]
give()
연산으로 semaphore 값 증가한다.take()
중인 task가 blocked에서 running이 되며 몇 번째 semaphore이고 현재 semaphore의 값이 무엇인지 출력한다. Sem번호(semaphore 값)
Clicked[12], [13], [14]
가 먼저 출력되는 것을 확인할 수 있다. 그럼 이제 semaphore가 0, 1, 2
로 늘었을 것이고, task는 연속해서 3번 take()
연산을 수행할 수 있다.(P.s. ISR에서 무거운 연산을 처리할 경우 버튼에 대한 인터럽트가 온전히 실행되지 않을 수 있다. 실제로 버튼을 굉장히 빠르게 5번 눌렀지만, 4번(11, 12, 13, 14)만 인식이 됐다. ISR은 최대한 작게 만들어야 다른 인터럽트나 task에 악영향을 최소한으로 만들 수 있다. ISR이 무겁고 속도가 느릴 수 밖에 없다면 뒤에서 배울 deferred interrupt processing 이라는 좋은 방법을 사용해서 해결할 수 있다.)
Mutex는 binary semaphore과 거의 같은 기능을 수행하지만, 중대한 차이가 하나 있다.
바로 우선순위 역전 현상을 예방해줄 메커니즘을 포함하고 있다는 점이다.
Semaphore는 critical section 및 공유자원에 대한 독점권을 보장하는 훌륭한 동기화 수단이지만, 치명적인 문제인 ‘우선순위 역전 현상’을 유발할 수 있다.
take()
연산을 수행해 critical section이 lock이 걸린다.take()
연산을 수행한다. 이때 semaphore가 0
이므로 task2는 blocked state로 진입한다.give()
연산으로 semaphore를 반환한다.우선순위 역전 현상을 해결하는 방법으로 FreeRTOS는 우선순위 상속(inheritance)을 사용한다.
give()
하자마자 다시 원래 우선순위로 돌아간다.Context switching이 무엇인지는 간략하게 알고있지만, 정확한 정의는 잘 모르는 경우가 많다. 이번 기회에 context switching이 무엇인지, 무엇 때문에 오래 걸려서 잦은 수행을 피해야하는지 알아보자.
오해하기 쉽지만, 인터럽트는 반드시 빠르게 처리해야 하는 것이 아니다. 인터럽트는 단순히 HW가 SW 적인 작업을 수행하기 위해 CPU에게 비동기적으로 기능을 요청하는 신호일 뿐이고, ISR은 우선순위가 다른 task보다 높은 하나의 task일 뿐이다. ISR은 다른 task와 마찬가지로 제 시간 안에만 처리하면 되는 메커니즘일 뿐이다. 그렇기 때문에 다른 인터럽트를 놓치지 않도록 ISR을 경량으로 유지하는 것이 중요하다.
FreeRTOS는 ISR를 최대한 경량화할 것을 권장하며 다음과 같은 네 가지 근거를 든다.
FreeRTOS는 꼭 ISR이 크고 복잡한 기능을 수행해야 한다면, 그 기능을 대신 수행할 높은 우선순위의 task를 생성한 뒤 위임할 것을 강력히 권장한다. 이 task가 바로 deferred interrupt processing task다.
이번 실습에서는 수 MB의 데이터를 처리하는 매우 무거운 연산은 버튼 인터럽트 ISR에서 처리하는 경우와 ISR()이 semaphore give()
해서 깨운 deferred processing task에서 처리하는 경우로 나눠 결과를 살펴본다. 차이를 알기위해 xTaskGetTickCount()
함수로 현재 SYSTICK의 값을 출력한다.
ISR을 최대한 경량화해야하는 이유에 대해 잘 알 수 있었던 실습이었다.
Task와 task 간의 communication을 위해서 FreeRTOS에서 마련한 세 가지 방법은 다음과 같다.
이 중 semaphore에 대해서는 앞서 다뤘으니, 이번에는 나머지 IPC 방법들에 대해 다뤄보자.
Event flag는 비트마스킹을 사용해 task로부터 event를 수신하는 방법이다. 임의의 task 또는 ISR에서 event flag를 set()
하면, 대응하는 다른 task 또는 ISR에서 get()
해서 특정 event를 수행하도록 만드는 것이 flag를 사용한 통신의 핵심이다.
예를 들어, 하나의 동작을 구성하는 n개의 작은 task가 있다고 가정하자. i번째 task는 i번째 인덱스의 bit에 대응하도록 event flag bit 배열을 만든다. 특정 task에서 문제가 발생해서 k번째 bit가 1
이 됐다. 모든 task를 수행한 뒤 event & 0 == event
라면 동작을 수행할 수 있지만, k번째 bit가 1
이므로 문제가 발생했고 그것이 k번 task에서 발생했음을 알 수 있다.
EventGroupHandle_t xEventGroupCreate()
EventBits_t xEventGroupSetBits(EventGroupHandle_t handle, EventBits_t uxBitsToSet)
xEventGroup
은 event group에 대한 handle을 의미한다. Create 함수의 반환 객체를 여기다가 넣어주면 된다.uxBitsToWaitFor
는 기다리고 있는 bit나 event를 ‘+’기호를 사용해서 표현한다.xClearOnExit
는 이름 그대로 event를 전달 받았을 때 해당 bit를 다시 초기화할 것인지 여부를 설정한다. 이를 pdTRUE
로 설정하지 않으면 마치 event가 미친듯이 빠른 속도로 연속해서 들어오는 것으로 인식한다. 특수한 경우 아니면 pdTRUE
로 고정할 것.xWaitForAllBits
는 기다리는 bits를 OR연산 처리할 것인지 AND연산 처리할 것인지 여부를 결정한다. pdFALSE
면 OR연산 처리되서 기다리는 bit 중 어느 하나라도 들어오면 성공 처리하고, pdTRUE
면 AND연산 처리되서 기다리는 bit 모두 들어와야 성공 처리한다.xTicksToWait
은 event를 기다리는 시간을 의미한다. 별 상관없으면 보통 portMAX_DELAY
를 사용해서 계속 기다리도록 설정한다.void xEventGroupDelete(EventGroupHandle_t handle)
xEventGroupSetBits()
로 group_id
에게 ENGINE_OIL_PRES_OK
event를 10번 전송한다.ENGINE_OIL_PRES_OK
를 xEventGroupWaitBits()
로 기다리다가 받자마자 ***Event Arrived!***
문장을 송출한다.지금까지 배운 IPC인 semaphore와 event flag는 bit값을 바꾸면서 서로 약속된 의미로 해석한 뒤 특정 기능을 수행하는 방식으로 돼있다면, message queue는 정수, 실수, 구조체, 심지어는 사진, 음악도 전송할 수 있는 아주 강력한 IPC 기능이다.
x
를 write한다.x
를 다시 queue에 write한다.Y
를 초기화한다. 이때 read와 동시에 delete한다.FreeRTOS의 queue는 ‘copy’ 방식을 채택한다. 물론 queue에 무엇이 저장될지 알 수 없기 때문에 포인터를 사용한 reference 방식이 좋겠지만, 당연히 task에서 선언한 변수는 local stack에 저장되기 때문에 포인터를 사용했다가는 엄청난 fault가 발생할 것이 예상되기 때문이다.
Message Queue를 활용한 통신의 종류는 다음과 같다.
task_id
라는 uint16_t
형의 양수를 선언해서 queue의 해당 정보가 어느 task로부터 전송됐는지를 표현하는 방법이다.qid
라는 message queue에서 xMessage
를 기다리는 동안 blocked 될 것이다.qid
에 10의 배수를 xQueueSend()
함수로 전송한다.
좋은 글 감사합니다.