ARM Cortex-M 마이크로컨트롤러 환경에서 멀티태스킹 프로그래밍을 구현할 때, CMSIS-RTOS 인터페이스 표준은 코드의 이식성과 프로젝트 유지보수 효율을 결정짓는 핵심 아키텍처입니다. 과거에 주로 사용되던 v1 규격에서 한 단계 진화한 CMSIS-RTOS v2는 C11 표준 스레드 개념을 반영하여 더욱 현대적이고 직관적인 API 표준을 제공합니다.
특히 STM32 등 최신 MCU 개발 환경에서 하부 RTOS 커널(FreeRTOS, RTX 등)의 고유 API에 종속되지 않고 동일한 코드 베이스로 태스크를 동적으로 관리할 수 있다는 점은 큰 강점입니다. 이번 글에서는 CMSIS-RTOS v2 규격을 기준으로 태스크(Thread)를 안전하게 생성하고 상태를 제어하며, 메시지 큐를 통해 데이터를 통신하는 실무적인 방법을 단계별로 정리해 드립니다.
핵심 요약 3줄
- 현대적인 구조 설계: CMSIS-RTOS v2는 기존 v1의 매크로 정의 방식 대신 구조체 속성(osThreadAttr_t)과 osThreadNew 함수를 통해 태스크를 명확하게 제어합니다.
- 커널 독립적 상태 관리: 내부 커널이 FreeRTOS이든 상관없이 구동, 준비, 차단 등 일관된 상위 4가지 상태 레벨을 통해 리소스를 효율적으로 분배합니다.
- 스레드 세이프 통신: 멀티태스킹 환경의 레이스 컨디션을 방지하기 위해 osMessageQueue API를 사용한 안전한 FIFO 데이터 송수신을 구현합니다.
1. CMSIS-RTOS v2 태스크 생성 3단계
v2 규격에서 태스크를 안전하게 구동하기 위해서는 구조화된 3단계 표준 절차를 밟아야 합니다. 기존 v1에 비해 컴파일 시점의 정적 속성 검증이 강화된 것이 특징입니다.
- 1단계 (태스크 함수 정의): 실제 비즈니스 로직이나 주변장치 제어를 무한 루프 형태로 수행할 void 포인터 인자 형태의 함수를 선언하고 구현합니다.
- 2단계 (속성 설정): osThreadAttr_t 구조체를 활용하여 생성할 태스크의 문자열 이름, 독립 스택 메모리 할당 크기, 커널 스케줄링 우선순위를 명확히 선언합니다.
- 3단계 (커널 등록 및 기동): osThreadNew() 함수를 호출하여 커널 스케줄러가 관리할 객체로 등록합니다.
구현 예제: 태스크 생성 소스코드
#include "cmsis_os2.h" // CMSIS-RTOS v2 표준 헤더 포함
#include "stm32f4xx_hal.h"
// 1. 태스크 함수 정의 (무한 루프 형태로 설계)
void Blink_Task(void *argument) {
while (1) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
// 1000 Tick(기본 설정 시 1000ms) 동안 Blocked 상태로 전환되어 자원 양보
osDelay(1000);
}
}
int main(void) {
// 하드웨어 및 기본 HAL 초기화 후 커널 초기화 수행
HAL_Init();
osKernelInitialize();
// 2. 태스크 속성 정의 (구조체를 활용한 직관적인 제어)
const osThreadAttr_t task_attr = {
.name = "LED_Task", // 디버깅용 태스크 식별 이름
.stack_size = 512, // 할당할 스택 크기 (바이트 단위)
.priority = osPriorityNormal // 스케줄러 우선순위 지정
};
// 3. 커널에 태스크 인스턴스 등록
osThreadNew(Blink_Task, NULL, &task_attr);
// RTOS 스케줄러 시작 (이 함수 호출 이후부터 멀티태스킹 구동)
osKernelStart();
// 스케줄러가 정상 작동하면 아래 루프에는 도달하지 않습니다.
while (1) {
;
}
}
2. 태스크의 상태와 생명 주기 (Task States)
RTOS 커널 내부에서 태스크는 단순히 프로그램이 켜져 있는 상태에 머무는 것이 아니라, CPU 자원의 효율적인 분배를 위해 실시간으로 상태 전환(State Transition)을 반복합니다.
| 태스크 상태 (State) | 동작 특징 및 커널 내부 메커니즘 |
| Running (실행) | 현재 물리적 CPU 제어권을 독점하여 명령어 코드를 실제로 처리하고 있는 단 하나의 태스크 상태 |
| Ready (준비) | 실행에 필요한 모든 조건이 충족되었으나, 자신보다 우선순위가 높은 다른 태스크가 먼저 CPU를 쓰고 있어서 런 큐(Run Queue)에 대기 중인 상태 |
| Blocked (차단/대기) | osDelay()를 호출했거나 메시지 큐, 세마포어 등 특정 이벤트 자원이 도착하기를 기다리며 스케줄링 대상에서 잠시 제외된 상태 |
| Inactive (비활성) | 태스크 객체가 메모리에 할당되지 않았거나, osThreadTerminate()에 의해 완전히 삭제되어 동작을 멈춘 상태 |
3. osThreadNew() 함수의 주요 인자 분석
태스크를 생성하는 핵심 인터페이스인 osThreadNew() 함수의 프로토타입 구조와 매개변수의 의미는 다음과 같습니다.
osThreadId_t osThreadNew(osThreadFunc_t func, void *argument, const osThreadAttr_t *attr);
- func: 태스크가 진입하여 실행할 실제 함수 포인터 주소입니다.
- argument: 태스크가 시작될 때 초기 파라미터로 넘겨받을 사용자 정의 데이터의 void 포인터 주소입니다. 하나의 함수를 여러 태스크 인스턴스가 공유할 때 하드웨어 ID 등을 넘겨주는 용도로 유용합니다. 전달할 데이터가 없다면 NULL을 대입합니다.
- attr: 앞서 1절에서 살펴본 스택 크기, 우선순위 구조체의 주소입니다. 커널이 정한 기본 사양을 그대로 따르고자 할 때는 이 인자에 NULL을 입력하면 됩니다.
4. 메시지 큐를 활용한 태스크 간 통신 (Message Queue)
멀티태스킹 환경에서 서로 다른 태스크가 전역 변수나 배열을 직접 읽고 쓰면, 컨텍스트 스위칭 타이밍이 어긋나면서 데이터가 깨지는 치명적인 버그가 발생합니다. CMSIS-RTOS v2의 메시지 큐는 안전한 FIFO(선입선출) 구조의 커널 보호 통신 채널을 제공합니다.
구현 예제: 메시지 큐 송수신 구조
#include "cmsis_os2.h"
// 메시지 큐 식별자 핸들 변수
osMessageQueueId_t msgQueue;
// 데이터를 생성하여 전송하는 태스크 (우선순위: 보통)
void SenderTask(void *argument) {
uint32_t count = 0;
while (1) {
count++;
// 큐에 데이터 주소를 안전하게 밀어 넣음 (큐 핸들, 데이터 주소, 우선순위, 대기 타임아웃)
osMessageQueuePut(msgQueue, &count, 0, osWaitForever);
osDelay(500); // 500ms 주기로 데이터 발송
}
}
// 데이터를 수신하여 처리하는 태스크 (우선순위: 보통)
void ReceiverTask(void *argument) {
uint32_t received;
osStatus_t status;
while (1) {
// 큐에 데이터가 들어올 때까지 태스크를 Blocked 상태로 두고 대기함
status = osMessageQueueGet(msgQueue, &received, NULL, osWaitForever);
if (status == osOK) {
// 수신 데이터인 received 변수를 활용하여 하드웨어 제어 등 비즈니스 로직 처리
}
}
}
5. 성공적인 개발을 위한 팁
- v2의 바이트 단위 스택 지정을 기억하세요: 기존 CMSIS-RTOS v1이나 FreeRTOS 네이티브 API에서는 태스크 생성 시 스택 크기를 4바이트(Word) 단위로 계산하여 입력하는 경우가 많았습니다. 반면 CMSIS-RTOS v2 규격에서는 완벽한 바이트(Byte) 단위로 스택 크기를 명시하도록 변경되었습니다. 따라서 속성 정의 시 .stack_size = 512라고 지정하면 정확히 512바이트가 할당되므로, 복잡한 로직을 처리하는 태스크라면 1024나 2048 등 충분한 바이트 용량을 인지하고 할당해야 안전합니다.
- 우선순위(Priority) 열거형 표준 준수: CMSIS-RTOS v2는 osPriorityLow, osPriorityNormal, osPriorityHigh, osPriorityRealtime 등 정형화된 우선순위 Enum 값을 제공합니다. 하부 커널이 바뀌더라도 이 표준 등급에 맞춰 내부적으로 스케줄러 스케일링이 매핑되므로, 소스코드 가독성과 유지보수성을 위해 임의의 정수형 숫자 대신 표준 열거형 키워드를 사용하여 아키텍처를 설계하는 편이 좋습니다.
6. 흔히 하는 실수
- HAL_Delay()와 osDelay()의 혼용 에러: STM32 환경에서 기존 베어메탈 코드를 복사해 올 때 가장 많이 하는 실수입니다. HAL_Delay()는 지정된 시간 동안 CPU 레지스터를 붙잡고 밀리초 단위 루프를 도는 Blocking 방식입니다. 이를 태스크 내부에서 실행하면 시스템 틱은 흐르지만 다른 낮은 순위 태스크로 제어권이 넘어가지 못해 전체 시스템의 실시간 스케줄링이 마비됩니다. 타임슬롯을 양보하며 자원을 효율적으로 쓰기 위해서는 무조건 커널 인식형 함수인 osDelay()를 호출해야 합니다.
- osKernelInitialize() 호출 전 API 사용: 시스템 전원 인가 후 main() 함수 초입에서 하드웨어 주변장치 초기화와 RTOS 설정을 동시에 처리하다 보면 코드 순서가 뒤엉키는 경우가 봅니다. osKernelInitialize()가 물리적으로 호출되어 커널 내부 제어 블록이 준비되기 전에 osThreadNew()나 메시지 큐 생성 API를 먼저 호출하면 런타임 Null 포인터 참조 오류가 발생하여 하드 fault로 시스템이 멈추게 되니 실행 순서를 엄격히 제약해야 합니다.
7. 결론
CMSIS-RTOS v2 표준 규격은 단일 MCU 프로젝트에 매몰되지 않고, 향후 더 상위 스펙의 Cortex-M 프로세서나 다른 제조사의 칩셋 아키텍처로 펌웨어를 이식할 때 진가를 발휘하는 유연한 인터페이스 기술입니다. osThreadNew를 기반으로 한 일관된 속성 구조체 선언 방식과 강력한 동기화 기능인 메시지 큐를 적재적소에 조합하면 멀티태스킹에서 발생하는 고질적인 데이터 공유 문제를 깔끔하게 해결할 수 있습니다.
새로운 프로젝트 설계를 시작한다면 하위 호환성에 갇혀 v1 규격을 고집하기보다, 확장성과 안정성이 검증된 v2 인터페이스를 도입하여 글로벌 표준에 부합하는 소스코드 아키텍처를 확보해 보시기 바랍니다. 개발 환경 설정이나 큐 데이터 튜닝 중 발생한 의문점은 언제든 댓글로 남겨주시면 함께 고민해 보도록 하겠습니다.
'Firmware & RTOS > FreeRTOS & Real-time Scheduling' 카테고리의 다른 글
| FreeRTOS 세마포어(Semaphore) vs 뮤텍스(Mutex) 차이점과 올바른 사용법 (0) | 2025.01.14 |
|---|---|
| FreeRTOS 메시지 큐(Queue) 완벽 가이드: 태스크 간 데이터 통신 및 예제 (CMSIS-RTOS v2) (0) | 2025.01.13 |
| FreeRTOS 핵심 개념 완벽 정리: Task, Queue, Semaphore, Mutex 활용법 (0) | 2025.01.11 |
| FreeRTOS 시작하기: CMSIS-RTOS v1 설정 및 태스크 구현 완벽 가이드 (0) | 2025.01.10 |
| FreeRTOS vs CMSIS-RTOS v1 비교: 임베디드(Embedded) 개발 가이드 (0) | 2025.01.09 |