
xSemaphoreCreateRecursiveMutex() 사용법을 학습한다Mutex와 Binary Semaphore는 모두 0 또는 1의 값을 가지는 세마포어 구조체를 기반으로 합니다. 그러나 설계 목적과 소유권 메커니즘에서 근본적인 차이가 있습니다.
| 특성 | Mutex | Binary Semaphore |
|---|---|---|
| 주요 목적 | 공유 자원 보호 (상호배제) | Task 간 동기화 (이벤트 알림) |
| 소유권 | 있음 (Take한 Task만 Give 가능) | 없음 (어느 Task든 Give 가능) |
| Priority Inheritance | 지원 | 미지원 |
| ISR에서 Give | 불가능 | 가능 (xSemaphoreGiveFromISR) |
| 초기 상태 | 사용 가능 (1) | 사용 불가 (0) |
| Recursive 지원 | 별도 API 사용 시 가능 | 불가능 |
Mutex의 초기 상태는 반납(available)이므로, 생성 직후 첫 번째 Task가 즉시 획득할 수 있습니다. Binary Semaphore의 초기 상태는 반대로 획득 불가(unavailable)이므로, 다른 Task 또는 ISR이
Give를 호출하기 전까지Take는 차단됩니다. 이 차이가 두 기법의 사용 목적을 명확히 구분합니다.
Mutex는 xSemaphoreTake()를 호출한 Task가 해당 Mutex의 소유자가 됩니다. FreeRTOS는 소유자가 아닌 Task가 xSemaphoreGive()를 호출하는 것을 허용하지 않으며, 이를 위반하면 pdFALSE를 반환합니다.
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
SemaphoreHandle_t xMutex;
/* 잘못된 예: Task B가 Task A 소유의 Mutex를 반납 시도 */
void vTaskA(void *pvParameters) {
xSemaphoreTake(xMutex, portMAX_DELAY);
/* Mutex를 반납하지 않은 채로 종료 */
vTaskDelay(pdMS_TO_TICKS(1000));
}
void vTaskB(void *pvParameters) {
vTaskDelay(pdMS_TO_TICKS(500));
/* Task A 소유의 Mutex를 Task B가 반납 시도 -> pdFALSE 반환 */
BaseType_t result = xSemaphoreGive(xMutex);
if(result == pdFALSE) {
printf("[오류] 소유권 없는 Mutex Give 실패\n");
}
}
/* 올바른 예: Take와 Give는 반드시 동일 Task에서 수행 */
void vTaskA(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
shared_resource_access();
xSemaphoreGive(xMutex); /* 반드시 같은 Task에서 반납 */
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
[자원 보호가 목적인가?]
YES -> Mutex 사용
NO -> [ISR에서 신호를 보내야 하는가?]
YES -> Binary Semaphore 사용
NO -> Binary Semaphore 또는 EventGroup 사용
Mutex를 사용해야 하는 경우:
/* UART, SPI, I2C 등 공유 주변기기 보호 */
void spi_transfer(uint8_t *data, uint16_t len) {
if(xSemaphoreTake(xSpiMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
xSemaphoreGive(xSpiMutex);
}
}
Binary Semaphore를 사용해야 하는 경우:
/* ISR -> Task 간 이벤트 전달 */
void EXTI0_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* ISR에서 Binary Semaphore Give */
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void vEventTask(void *pvParameters) {
while(1) {
/* ISR이 Give하기를 기다림 */
xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
process_event();
}
}
Priority Inversion은 낮은 우선순위 Task가 Mutex를 보유한 채로 중간 우선순위 Task에 의해 선점되어, 결과적으로 높은 우선순위 Task가 간접적으로 차단되는 현상입니다.
[Priority Inversion 발생 시나리오]
시각 Task L(우선순위 1) Task M(우선순위 2) Task H(우선순위 3)
0 Mutex 획득
1 작업 수행 중 실행 대기 (Mutex 없음)
2 선점됨 실행 시작
3 작업 수행 중 Mutex 대기 (Blocked)
4 작업 수행 중 차단 유지
5 실행 재개 작업 완료
6 Mutex 반납
7 Mutex 획득, 실행 시작
결과: Task H (우선순위 3)가 Task M (우선순위 2)보다 늦게 실행됨
FreeRTOS의 Mutex는 Priority Inheritance를 자동으로 적용합니다. 높은 우선순위 Task가 Mutex 획득을 대기하는 순간, FreeRTOS는 Mutex를 보유한 Task의 우선순위를 요청자 수준으로 일시 상승시킵니다.
[Priority Inheritance 적용 시 시나리오]
시각 Task L(우선순위 1) Task M(우선순위 2) Task H(우선순위 3)
0 Mutex 획득 (우선순위 1)
1 작업 수행 중 Mutex 요청
2 우선순위 3으로 상승 실행 대기 Blocked
3 빠르게 실행 완료
4 Mutex 반납, 우선순위 1로 복원
5 Mutex 획득, 실행
6 실행 재개
결과: Task H가 Task M보다 먼저 실행됨 (우선순위 역전 완화)
Priority Inheritance 활성화 조건:
/* FreeRTOSConfig.h */
#define configUSE_MUTEXES 1 /* 반드시 1로 설정 */
/* Priority Inheritance는 xSemaphoreCreateMutex() 사용 시 자동 적용 */
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
/* Binary Semaphore로 생성 시 Priority Inheritance 미적용 */
SemaphoreHandle_t xBinSem = xSemaphoreCreateBinary(); /* PI 없음 */
Priority Inheritance는 Priority Inversion을 완전히 해결하지 않으며, 일부 시나리오에서는 여전히 문제가 발생할 수 있습니다.
한계 1: 체인형 Priority Inversion
Task L -> Mutex A 보유
Task M -> Mutex B 보유, Mutex A 대기
Task H -> Mutex B 대기
결과: L의 우선순위가 M 수준으로만 상승, H 수준까지 전파 안 됨
(FreeRTOS는 체인 전파를 완전히 지원하지 않음)
한계 2: 일시적 완화만 제공
/*
* Priority Inheritance는 우선순위 역전을 줄이지만,
* 완전한 해결은 Priority Ceiling Protocol (PCP) 등의
* 별도 기법이 필요합니다.
*
* 안전 필수(Safety-Critical) 시스템에서는 AUTOSAR OS나
* OSEK/VDX 등 PCP를 지원하는 RTOS를 검토해야 합니다.
*/
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>
SemaphoreHandle_t xMutex;
void vLowPriorityTask(void *pvParameters) {
while(1) {
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("[Low] Mutex 획득, 현재 우선순위: %lu\n",
(uint32_t)uxTaskPriorityGet(NULL));
/* 긴 작업: 이 구간에서 High Task가 Mutex를 요청하면 우선순위가 상승 */
vTaskDelay(pdMS_TO_TICKS(300));
printf("[Low] Mutex 반납 직전 우선순위: %lu (상속 중일 경우 상승)\n",
(uint32_t)uxTaskPriorityGet(NULL));
xSemaphoreGive(xMutex);
printf("[Low] Mutex 반납 후 우선순위: %lu (원래 값으로 복원)\n",
(uint32_t)uxTaskPriorityGet(NULL));
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void vMidPriorityTask(void *pvParameters) {
/* Mutex와 무관한 작업 수행, Low Task의 실행을 방해하는 역할 */
while(1) {
printf("[Mid] 실행 중\n");
vTaskDelay(pdMS_TO_TICKS(150));
}
}
void vHighPriorityTask(void *pvParameters) {
vTaskDelay(pdMS_TO_TICKS(50)); /* Low Task가 Mutex를 먼저 획득하도록 대기 */
while(1) {
printf("[High] Mutex 요청 -> Low Task 우선순위 상속 유발\n");
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("[High] Mutex 획득 성공\n");
vTaskDelay(pdMS_TO_TICKS(50));
xSemaphoreGive(xMutex);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
int main(void) {
xMutex = xSemaphoreCreateMutex();
configASSERT(xMutex != NULL);
/* 우선순위: Low=1, Mid=2, High=3 */
xTaskCreate(vLowPriorityTask, "Low", 256, NULL, 1, NULL);
xTaskCreate(vMidPriorityTask, "Mid", 256, NULL, 2, NULL);
xTaskCreate(vHighPriorityTask, "High", 256, NULL, 3, NULL);
vTaskStartScheduler();
while(1);
}
일반 Mutex는 동일 Task에서 이중으로 xSemaphoreTake()를 호출하면 Self-Deadlock이 발생합니다. 재귀 함수 호출이나 여러 레이어에서 동일 Mutex를 사용하는 경우 이 문제가 발생합니다.
SemaphoreHandle_t xMutex;
/* 문제: function_b가 function_a 내부에서 호출되며 동일 Mutex를 재획득 시도 */
void function_b(void) {
xSemaphoreTake(xMutex, portMAX_DELAY); /* Self-Deadlock 발생 */
/* 작업 */
xSemaphoreGive(xMutex);
}
void function_a(void) {
xSemaphoreTake(xMutex, portMAX_DELAY);
function_b(); /* 내부에서 동일 Mutex 재획득 -> 영구 대기 */
xSemaphoreGive(xMutex);
}
Recursive Mutex는 동일 Task가 동일 Mutex를 여러 번 획득할 수 있도록 내부 카운터를 관리합니다. 획득 횟수만큼 반납해야 완전히 해제됩니다.
[Recursive Mutex 동작]
Task A: Take -> 카운터 1
Task A: Take -> 카운터 2 (차단 없음)
Task A: Take -> 카운터 3 (차단 없음)
Task A: Give -> 카운터 2
Task A: Give -> 카운터 1
Task A: Give -> 카운터 0 (완전 해제, 다른 Task 획득 가능)
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);
Recursive Mutex를 동적으로 생성합니다. 활성화를 위해 FreeRTOSConfig.h에 다음 설정이 필요합니다.
/* FreeRTOSConfig.h */
#define configUSE_RECURSIVE_MUTEXES 1
Recursive Mutex 전용 API:
일반 Mutex API와 혼용하면 안 됩니다. Recursive Mutex는 전용 API를 사용해야 합니다.
| 일반 Mutex | Recursive Mutex |
|---|---|
xSemaphoreTake() | xSemaphoreTakeRecursive() |
xSemaphoreGive() | xSemaphoreGiveRecursive() |
xSemaphoreCreateMutex() | xSemaphoreCreateRecursiveMutex() |
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
SemaphoreHandle_t xRecursiveMutex;
void function_b(void) {
/* 동일 Task에서 재획득 가능: 카운터 증가 */
if(xSemaphoreTakeRecursive(xRecursiveMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
printf("[function_b] Mutex 재획득 (카운터 증가)\n");
/* 작업 수행 */
xSemaphoreGiveRecursive(xRecursiveMutex); /* 카운터 감소 */
printf("[function_b] Mutex 반납 (카운터 감소)\n");
}
}
void function_a(void) {
if(xSemaphoreTakeRecursive(xRecursiveMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
printf("[function_a] Mutex 첫 번째 획득 (카운터 1)\n");
function_b(); /* 내부에서 동일 Mutex 재획득: 차단 없음 */
xSemaphoreGiveRecursive(xRecursiveMutex); /* 카운터 0으로 완전 해제 */
printf("[function_a] Mutex 최종 반납 (카운터 0)\n");
}
}
void vTask(void *pvParameters) {
while(1) {
function_a();
vTaskDelay(pdMS_TO_TICKS(500));
}
}
int main(void) {
xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
configASSERT(xRecursiveMutex != NULL);
xTaskCreate(vTask, "Task", 256, NULL, 2, NULL);
vTaskStartScheduler();
while(1);
}
주의 1: Take와 Give 횟수 반드시 일치
/* 잘못된 예: Give 횟수가 Take보다 적음 */
void bad_recursive(void) {
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY); /* 카운터 1 */
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY); /* 카운터 2 */
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY); /* 카운터 3 */
xSemaphoreGiveRecursive(xRecursiveMutex); /* 카운터 2 */
xSemaphoreGiveRecursive(xRecursiveMutex); /* 카운터 1 */
/* 카운터가 0이 되지 않아 다른 Task가 영원히 대기 */
}
/* 올바른 예 */
void good_recursive(void) {
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
xSemaphoreGiveRecursive(xRecursiveMutex);
xSemaphoreGiveRecursive(xRecursiveMutex);
xSemaphoreGiveRecursive(xRecursiveMutex); /* 카운터 0: 완전 해제 */
}
주의 2: API 혼용 금지
/* 오류: Recursive Mutex에 일반 API 혼용 */
SemaphoreHandle_t xRecMutex = xSemaphoreCreateRecursiveMutex();
xSemaphoreTakeRecursive(xRecMutex, portMAX_DELAY); /* 올바름 */
xSemaphoreGive(xRecMutex); /* 오류: 일반 Give 사용 금지 */
/* 반드시 전용 API 사용 */
xSemaphoreGiveRecursive(xRecMutex); /* 올바름 */
주의 3: ISR에서 사용 불가
Recursive Mutex도 일반 Mutex와 동일하게 ISR에서 사용할 수 없습니다.
[Recursive Mutex가 필요한 경우]
1. 재귀 함수 내에서 Mutex 보호가 필요한 경우
- 트리 탐색, 재귀 파싱 등
2. 레이어드 아키텍처에서 상위/하위 레이어가 동일 자원 접근 시
- 드라이버 레이어 -> HAL 레이어 -> 동일 Mutex 사용
3. 콜백 함수가 Mutex를 보유한 상태에서 호출될 수 있는 경우
[일반 Mutex로 충분한 경우 (권장)]
- 재귀 호출이 없고 단일 진입점에서만 자원 접근
- Recursive Mutex는 카운터 관리 오버헤드가 추가되므로
불필요한 경우 일반 Mutex 사용 권장
SPI 드라이버에서 상위 레이어(디바이스 드라이버)와 하위 레이어(HAL)가 동일 Mutex를 사용하는 구조를 구현합니다.
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>
#include <string.h>
#define SPI_TIMEOUT_MS 50
typedef struct {
SemaphoreHandle_t xMutex;
uint32_t transferCount;
} SpiDriver;
static SpiDriver spiDrv = {0};
/* HAL 레이어: 저수준 SPI 전송 (Mutex 내부에서 호출 가능) */
static BaseType_t hal_spi_transmit(const uint8_t *data, uint16_t len) {
/*
* Recursive Mutex를 사용하므로, 이미 Mutex를 보유한 상태에서
* 호출되어도 차단 없이 카운터만 증가합니다.
*/
if(xSemaphoreTakeRecursive(spiDrv.xMutex,
pdMS_TO_TICKS(SPI_TIMEOUT_MS)) != pdTRUE) {
return pdFALSE;
}
/* HAL_SPI_Transmit() 호출을 시뮬레이션 */
printf("[HAL] SPI 전송: %u 바이트\n", len);
spiDrv.transferCount++;
xSemaphoreGiveRecursive(spiDrv.xMutex);
return pdTRUE;
}
/* 디바이스 드라이버 레이어: HAL 위에서 동작 */
BaseType_t device_write_register(uint8_t reg, uint8_t value) {
uint8_t txBuf[2] = { reg, value };
if(xSemaphoreTakeRecursive(spiDrv.xMutex,
pdMS_TO_TICKS(SPI_TIMEOUT_MS)) != pdTRUE) {
return pdFALSE;
}
printf("[Device] 레지스터 0x%02X 에 0x%02X 쓰기\n", reg, value);
/* Mutex를 보유한 채로 HAL 호출: Recursive Mutex이므로 차단 없음 */
hal_spi_transmit(txBuf, sizeof(txBuf));
xSemaphoreGiveRecursive(spiDrv.xMutex);
return pdTRUE;
}
BaseType_t device_burst_write(uint8_t startReg, const uint8_t *data, uint8_t count) {
if(xSemaphoreTakeRecursive(spiDrv.xMutex,
pdMS_TO_TICKS(SPI_TIMEOUT_MS)) != pdTRUE) {
return pdFALSE;
}
printf("[Device] 연속 쓰기: 레지스터 0x%02X 부터 %u 바이트\n", startReg, count);
/* 각 레지스터에 대해 반복 쓰기: 동일 Mutex를 반복 재획득 */
for(uint8_t i = 0; i < count; i++) {
device_write_register(startReg + i, data[i]);
}
xSemaphoreGiveRecursive(spiDrv.xMutex);
return pdTRUE;
}
BaseType_t spi_driver_init(void) {
spiDrv.xMutex = xSemaphoreCreateRecursiveMutex();
if(spiDrv.xMutex == NULL) {
return pdFALSE;
}
spiDrv.transferCount = 0;
return pdTRUE;
}
void vConfigTask(void *pvParameters) {
uint8_t config[4] = { 0x01, 0x02, 0x03, 0x04 };
while(1) {
printf("\n[Config] 디바이스 초기화 시퀀스 시작\n");
device_burst_write(0x10, config, 4);
printf("[Config] 초기화 완료, 총 전송 횟수: %lu\n\n",
spiDrv.transferCount);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
int main(void) {
if(spi_driver_init() != pdTRUE) {
while(1);
}
printf("=== Recursive Mutex SPI 드라이버 실습 ===\n\n");
xTaskCreate(vConfigTask, "Config", 256, NULL, 2, NULL);
vTaskStartScheduler();
while(1);
}
3개의 공유 자원을 여러 Task가 조합하여 접근하는 환경에서 획득 순서 통일 원칙을 적용합니다.
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>
/*
* 자원 획득 순서 규칙: xMutexA -> xMutexB -> xMutexC
* 모든 Task는 이 순서를 반드시 준수해야 합니다.
*/
SemaphoreHandle_t xMutexA; /* 자원 A: 센서 데이터 */
SemaphoreHandle_t xMutexB; /* 자원 B: 처리 버퍼 */
SemaphoreHandle_t xMutexC; /* 자원 C: 출력 장치 */
#define ACQUIRE_TIMEOUT_MS 200
/*
* 안전한 다중 Mutex 획득 함수
* 획득 실패 시 이미 보유한 Mutex를 모두 반납하고 재시도 가능한 상태로 복귀
*/
typedef enum {
RESOURCE_NONE = 0,
RESOURCE_A = 1,
RESOURCE_B = 2,
RESOURCE_C = 4
} ResourceMask;
BaseType_t acquire_resources(ResourceMask mask, uint32_t timeoutMs) {
/* 항상 A -> B -> C 순서로 획득 */
if(mask & RESOURCE_A) {
if(xSemaphoreTake(xMutexA,
pdMS_TO_TICKS(timeoutMs)) != pdTRUE) {
printf("[오류] 자원 A 획득 타임아웃\n");
return pdFALSE;
}
}
if(mask & RESOURCE_B) {
if(xSemaphoreTake(xMutexB,
pdMS_TO_TICKS(timeoutMs)) != pdTRUE) {
printf("[오류] 자원 B 획득 타임아웃, A 반납\n");
if(mask & RESOURCE_A) xSemaphoreGive(xMutexA);
return pdFALSE;
}
}
if(mask & RESOURCE_C) {
if(xSemaphoreTake(xMutexC,
pdMS_TO_TICKS(timeoutMs)) != pdTRUE) {
printf("[오류] 자원 C 획득 타임아웃, B/A 반납\n");
if(mask & RESOURCE_B) xSemaphoreGive(xMutexB);
if(mask & RESOURCE_A) xSemaphoreGive(xMutexA);
return pdFALSE;
}
}
return pdTRUE;
}
void release_resources(ResourceMask mask) {
/* 획득 역순으로 반납: C -> B -> A */
if(mask & RESOURCE_C) xSemaphoreGive(xMutexC);
if(mask & RESOURCE_B) xSemaphoreGive(xMutexB);
if(mask & RESOURCE_A) xSemaphoreGive(xMutexA);
}
/* Task 1: 자원 A, B 사용 (센서 -> 버퍼) */
void vTask_AB(void *pvParameters) {
while(1) {
if(acquire_resources(RESOURCE_A | RESOURCE_B,
ACQUIRE_TIMEOUT_MS) == pdTRUE) {
printf("[Task_AB] 자원 A, B 작업 수행\n");
vTaskDelay(pdMS_TO_TICKS(50));
release_resources(RESOURCE_A | RESOURCE_B);
}
vTaskDelay(pdMS_TO_TICKS(300));
}
}
/* Task 2: 자원 B, C 사용 (버퍼 -> 출력) */
void vTask_BC(void *pvParameters) {
while(1) {
if(acquire_resources(RESOURCE_B | RESOURCE_C,
ACQUIRE_TIMEOUT_MS) == pdTRUE) {
printf("[Task_BC] 자원 B, C 작업 수행\n");
vTaskDelay(pdMS_TO_TICKS(50));
release_resources(RESOURCE_B | RESOURCE_C);
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
/* Task 3: 자원 A, C 사용 (센서 -> 출력 직접) */
void vTask_AC(void *pvParameters) {
while(1) {
if(acquire_resources(RESOURCE_A | RESOURCE_C,
ACQUIRE_TIMEOUT_MS) == pdTRUE) {
printf("[Task_AC] 자원 A, C 작업 수행\n");
vTaskDelay(pdMS_TO_TICKS(50));
release_resources(RESOURCE_A | RESOURCE_C);
}
vTaskDelay(pdMS_TO_TICKS(700));
}
}
/* Task 4: 자원 A, B, C 모두 사용 */
void vTask_ABC(void *pvParameters) {
while(1) {
if(acquire_resources(RESOURCE_A | RESOURCE_B | RESOURCE_C,
ACQUIRE_TIMEOUT_MS) == pdTRUE) {
printf("[Task_ABC] 자원 A, B, C 전체 작업 수행\n");
vTaskDelay(pdMS_TO_TICKS(100));
release_resources(RESOURCE_A | RESOURCE_B | RESOURCE_C);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
int main(void) {
xMutexA = xSemaphoreCreateMutex();
xMutexB = xSemaphoreCreateMutex();
xMutexC = xSemaphoreCreateMutex();
configASSERT(xMutexA != NULL);
configASSERT(xMutexB != NULL);
configASSERT(xMutexC != NULL);
printf("=== 다중 자원 Deadlock 방지 실습 ===\n\n");
xTaskCreate(vTask_AB, "Task_AB", 256, NULL, 2, NULL);
xTaskCreate(vTask_BC, "Task_BC", 256, NULL, 2, NULL);
xTaskCreate(vTask_AC, "Task_AC", 256, NULL, 2, NULL);
xTaskCreate(vTask_ABC, "Task_ABC", 256, NULL, 1, NULL);
vTaskStartScheduler();
while(1);
}
센서 데이터 수집을 Binary Semaphore로 동기화하고, 수집된 데이터는 Mutex로 보호하는 구조를 구현합니다.
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>
#include <string.h>
SemaphoreHandle_t xDataReadySemaphore; /* Binary Semaphore: ISR -> Task 동기화 */
SemaphoreHandle_t xDataMutex; /* Mutex: 공유 데이터 보호 */
typedef struct {
int16_t adcRaw;
float voltage;
uint32_t timestamp;
} SensorSample;
#define SAMPLE_BUFFER_SIZE 8
static SensorSample sampleBuffer[SAMPLE_BUFFER_SIZE];
static uint8_t writeIndex = 0;
static uint8_t sampleCount = 0;
/* ISR 시뮬레이션 Task: 실제 환경에서는 ADC ISR이 이 역할 수행 */
void vAdcIsrSimTask(void *pvParameters) {
int16_t adcValue = 0;
while(1) {
vTaskDelay(pdMS_TO_TICKS(200)); /* ADC 변환 완료 주기 시뮬레이션 */
adcValue = (int16_t)(1000 + (xTaskGetTickCount() % 1024));
/*
* 실제 ISR에서는 xSemaphoreGiveFromISR() 사용
* 여기서는 Task에서 시뮬레이션하므로 xSemaphoreGive() 사용
*/
xSemaphoreGive(xDataReadySemaphore);
/*
* ISR에서 공유 데이터를 직접 쓰지 않음
* 원시 ADC 값 전달은 별도 Queue 사용 권장
* 여기서는 단순화하여 전역 변수에 직접 기록
*/
if(xSemaphoreTake(xDataMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
sampleBuffer[writeIndex].adcRaw = adcValue;
sampleBuffer[writeIndex].voltage = adcValue * 3.3f / 4096.0f;
sampleBuffer[writeIndex].timestamp = xTaskGetTickCount();
writeIndex = (writeIndex + 1) % SAMPLE_BUFFER_SIZE;
if(sampleCount < SAMPLE_BUFFER_SIZE) sampleCount++;
xSemaphoreGive(xDataMutex);
}
}
}
/* 데이터 처리 Task: Binary Semaphore로 이벤트 수신 후 Mutex로 데이터 접근 */
void vProcessTask(void *pvParameters) {
while(1) {
/* ADC 변환 완료 이벤트 대기 */
if(xSemaphoreTake(xDataReadySemaphore,
portMAX_DELAY) == pdTRUE) {
/* Mutex로 공유 데이터 접근 보호 */
if(xSemaphoreTake(xDataMutex,
pdMS_TO_TICKS(50)) == pdTRUE) {
uint8_t idx = (writeIndex == 0)
? (SAMPLE_BUFFER_SIZE - 1)
: (writeIndex - 1);
printf("[Process] ADC Raw: %d, Voltage: %.3fV, Tick: %lu\n",
sampleBuffer[idx].adcRaw,
sampleBuffer[idx].voltage,
sampleBuffer[idx].timestamp);
xSemaphoreGive(xDataMutex);
}
}
}
}
/* 통계 Task: 주기적으로 버퍼 전체를 분석 */
void vStatisticsTask(void *pvParameters) {
while(1) {
vTaskDelay(pdMS_TO_TICKS(2000));
if(xSemaphoreTake(xDataMutex,
pdMS_TO_TICKS(100)) == pdTRUE) {
if(sampleCount == 0) {
xSemaphoreGive(xDataMutex);
continue;
}
float sum = 0.0f;
float minV = 3.3f, maxV = 0.0f;
uint8_t count = sampleCount;
for(uint8_t i = 0; i < count; i++) {
float v = sampleBuffer[i].voltage;
sum += v;
if(v < minV) minV = v;
if(v > maxV) maxV = v;
}
xSemaphoreGive(xDataMutex);
printf("\n--- 전압 통계 (최근 %u 샘플) ---\n", count);
printf("평균: %.3fV, 최솟값: %.3fV, 최댓값: %.3fV\n\n",
sum / count, minV, maxV);
}
}
}
int main(void) {
xDataReadySemaphore = xSemaphoreCreateBinary();
xDataMutex = xSemaphoreCreateMutex();
configASSERT(xDataReadySemaphore != NULL);
configASSERT(xDataMutex != NULL);
printf("=== Mutex + Binary Semaphore 혼합 사용 실습 ===\n\n");
xTaskCreate(vAdcIsrSimTask, "AdcSim", 256, NULL, 3, NULL);
xTaskCreate(vProcessTask, "Process", 256, NULL, 2, NULL);
xTaskCreate(vStatisticsTask, "Stats", 256, NULL, 1, NULL);
vTaskStartScheduler();
while(1);
}
UART, SPI, 상태 머신을 각각 일반 Mutex, Recursive Mutex, Binary Semaphore로 보호하는 통합 시스템을 구현합니다.
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
/* 동기화 객체 */
SemaphoreHandle_t xUartMutex; /* 일반 Mutex: UART 보호 */
SemaphoreHandle_t xSpiRecursiveMutex; /* Recursive Mutex: 레이어드 SPI 보호 */
SemaphoreHandle_t xAlarmSemaphore; /* Binary Semaphore: 알람 이벤트 동기화 */
/* ============================================================
* UART 드라이버 (일반 Mutex)
* ============================================================ */
BaseType_t uart_log(const char *format, ...) {
char buf[128];
int len;
va_list args;
va_start(args, format);
len = vsnprintf(buf, sizeof(buf), format, args);
va_end(args);
if(len <= 0) return pdFALSE;
if(xSemaphoreTake(xUartMutex, pdMS_TO_TICKS(50)) != pdTRUE) {
return pdFALSE;
}
printf("%s", buf);
xSemaphoreGive(xUartMutex);
return pdTRUE;
}
/* ============================================================
* SPI 드라이버 (Recursive Mutex)
* ============================================================ */
static BaseType_t spi_hal_write(uint8_t byte) {
if(xSemaphoreTakeRecursive(xSpiRecursiveMutex,
pdMS_TO_TICKS(50)) != pdTRUE) {
return pdFALSE;
}
/* HAL_SPI_Transmit 시뮬레이션 */
(void)byte;
xSemaphoreGiveRecursive(xSpiRecursiveMutex);
return pdTRUE;
}
BaseType_t spi_device_write_reg(uint8_t reg, uint8_t val) {
if(xSemaphoreTakeRecursive(xSpiRecursiveMutex,
pdMS_TO_TICKS(50)) != pdTRUE) {
return pdFALSE;
}
spi_hal_write(reg); /* Recursive: 내부 HAL 호출 가능 */
spi_hal_write(val);
xSemaphoreGiveRecursive(xSpiRecursiveMutex);
return pdTRUE;
}
BaseType_t spi_device_init_sequence(void) {
if(xSemaphoreTakeRecursive(xSpiRecursiveMutex,
pdMS_TO_TICKS(50)) != pdTRUE) {
return pdFALSE;
}
/* 여러 레지스터 초기화: 각 호출에서 Recursive Mutex 재획득 */
spi_device_write_reg(0x00, 0x01); /* 활성화 */
spi_device_write_reg(0x01, 0x10); /* 설정 1 */
spi_device_write_reg(0x02, 0x20); /* 설정 2 */
xSemaphoreGiveRecursive(xSpiRecursiveMutex);
return pdTRUE;
}
/* ============================================================
* Task 구현
* ============================================================ */
void vSensorTask(void *pvParameters) {
/* 디바이스 초기화 */
if(spi_device_init_sequence() == pdTRUE) {
uart_log("[Sensor] 디바이스 초기화 완료\n");
}
uint16_t sampleIndex = 0;
while(1) {
/* SPI로 센서 읽기 시뮬레이션 */
spi_device_write_reg(0x10, (uint8_t)(sampleIndex & 0xFF));
if(sampleIndex % 10 == 0) {
uart_log("[Sensor] 샘플 %u 수집 완료\n", sampleIndex);
}
/* 임계 조건 발생 시 알람 발행 */
if(sampleIndex == 50) {
uart_log("[Sensor] 임계 조건 감지, 알람 발행\n");
xSemaphoreGive(xAlarmSemaphore);
}
sampleIndex++;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void vAlarmTask(void *pvParameters) {
while(1) {
/* 알람 이벤트 대기 */
xSemaphoreTake(xAlarmSemaphore, portMAX_DELAY);
uart_log("[Alarm] 알람 수신, 대응 시퀀스 실행\n");
/* 알람 대응: SPI 명령 전송 */
spi_device_write_reg(0xFF, 0x01); /* 안전 모드 진입 명령 */
uart_log("[Alarm] 안전 모드 진입 완료\n");
}
}
void vMonitorTask(void *pvParameters) {
while(1) {
vTaskDelay(pdMS_TO_TICKS(3000));
uart_log("\n[Monitor] 시스템 정상 동작 중\n\n");
}
}
int main(void) {
xUartMutex = xSemaphoreCreateMutex();
xSpiRecursiveMutex = xSemaphoreCreateRecursiveMutex();
xAlarmSemaphore = xSemaphoreCreateBinary();
configASSERT(xUartMutex != NULL);
configASSERT(xSpiRecursiveMutex != NULL);
configASSERT(xAlarmSemaphore != NULL);
uart_log("=== 계층적 드라이버 통합 실습 ===\n\n");
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 3, NULL);
xTaskCreate(vAlarmTask, "Alarm", 256, NULL, 2, NULL);
xTaskCreate(vMonitorTask, "Monitor", 256, NULL, 1, NULL);
vTaskStartScheduler();
while(1);
}
/* 증상: 특정 Task가 Mutex 획득 후 영구 차단 */
void problematic_function(int depth) {
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY); /* 매 호출마다 +1 */
if(depth > 0) {
problematic_function(depth - 1);
}
if(depth == 0) {
return; /* Give 없이 반환: 카운터가 depth+1만큼 누적 */
}
xSemaphoreGiveRecursive(xRecursiveMutex); /* depth==0인 경우 실행 안 됨 */
}
/* 수정: 모든 경로에서 Give 보장 */
void fixed_function(int depth) {
xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY);
if(depth > 0) {
fixed_function(depth - 1);
}
xSemaphoreGiveRecursive(xRecursiveMutex); /* 반드시 모든 경로에서 실행 */
}
/*
* 증상: 높은 우선순위 Task의 응답 시간이 예상보다 길고 불규칙
* 원인: Binary Semaphore를 자원 보호에 잘못 사용
*/
/* 잘못된 예: 공유 자원 보호에 Binary Semaphore 사용 */
SemaphoreHandle_t xSharedDataSem = xSemaphoreCreateBinary();
/* xSemaphoreGive(xSharedDataSem) 로 초기화 필요 */
void vHighPriorityTask(void *pvParameters) {
while(1) {
/*
* Binary Semaphore는 Priority Inheritance를 지원하지 않음
* 낮은 우선순위 Task가 Semaphore를 보유하면 그대로 차단됨
*/
xSemaphoreTake(xSharedDataSem, portMAX_DELAY);
access_shared_data();
xSemaphoreGive(xSharedDataSem);
}
}
/* 수정: 공유 자원 보호에는 Mutex 사용 */
SemaphoreHandle_t xSharedDataMutex = NULL; /* xSemaphoreCreateMutex()로 초기화 */
void vHighPriorityTask_fixed(void *pvParameters) {
while(1) {
/*
* Mutex 사용 시 Priority Inheritance 자동 적용
* 낮은 우선순위 Task의 우선순위가 일시 상승하여 빠르게 자원 해제
*/
xSemaphoreTake(xSharedDataMutex, portMAX_DELAY);
access_shared_data();
xSemaphoreGive(xSharedDataMutex);
}
}
/*
* 잘못된 예: Mutex 보유 중 시간이 걸리는 작업 수행
* 다른 Task의 대기 시간이 증가하고 시스템 응답성이 저하됨
*/
void bad_mutex_usage(void) {
xSemaphoreTake(xMutex, portMAX_DELAY);
read_sensor(); /* 수십 ms 소요 */
compute_result(); /* CPU 집약적 연산 */
transmit_over_network(); /* 수백 ms 소요 가능 */
update_shared_variable(); /* 실제 공유 자원 접근 */
xSemaphoreGive(xMutex);
}
/*
* 올바른 예: Mutex 보유 시간을 공유 자원 접근 구간으로 최소화
*/
void good_mutex_usage(void) {
int16_t rawData;
float result;
/* Mutex 없이 수행 가능한 작업을 먼저 처리 */
rawData = read_sensor();
result = compute_result(rawData);
/* 공유 자원 접근 구간만 Mutex 보호 */
xSemaphoreTake(xMutex, portMAX_DELAY);
update_shared_variable(result);
xSemaphoreGive(xMutex);
}
Mutex vs Binary Semaphore
Priority Inheritance
xSemaphoreCreateMutex() 사용 시 자동 적용, configUSE_MUTEXES=1 필요Recursive Mutex
configUSE_RECURSIVE_MUTEXES=1 설정 및 전용 API(TakeRecursive, GiveRecursive) 사용 필수복잡한 자원 보호 설계 원칙
| 개념 | 설명 |
|---|---|
| Mutex 소유권 | Take한 Task만 Give 가능, 위반 시 pdFALSE 반환 |
| Binary Semaphore 초기 상태 | unavailable(0), 첫 Take는 Give 이후에 성공 |
| Priority Inheritance | Mutex 보유 Task 우선순위를 요청자 수준으로 일시 상승 |
| Priority Inversion | 낮은 우선순위 Task가 높은 우선순위 Task를 간접 차단하는 현상 |
| Recursive Mutex | 동일 Task의 중복 Take를 허용, 내부 카운터로 관리 |
| xSemaphoreTakeRecursive() | Recursive Mutex 전용 획득 함수 |
| xSemaphoreGiveRecursive() | Recursive Mutex 전용 반납 함수 |
| 획득 순서 통일 | 다중 Mutex Deadlock 방지를 위한 전 시스템 규칙 |
| Mutex 보유 시간 최소화 | 공유 자원 접근 구간 외 작업은 Mutex 밖에서 수행 |
중첩된 데이터 구조를 재귀적으로 직렬화하여 UART로 출력하는 함수를 Recursive Mutex로 보호하십시오.
요구사항:
Priority Inheritance 적용 전후의 고우선순위 Task 응답 시간을 측정하고 차이를 분석하십시오.
요구사항:
xTaskGetTickCount()로 Mutex 요청부터 획득까지의 지연 시간 측정여러 Task가 공유 자원(A, B, C)에 접근하는 요청을 중앙에서 관리하는 자원 관리자 Task를 구현하십시오.
요구사항:
/*
* 증상: 예상치 못한 동작 또는 Crash
* 원인: Recursive Mutex에 일반 xSemaphoreTake() 사용
*/
SemaphoreHandle_t xRecMutex = xSemaphoreCreateRecursiveMutex();
/* 오류 */
xSemaphoreTake(xRecMutex, portMAX_DELAY); /* 일반 API 사용 금지 */
xSemaphoreGive(xRecMutex); /* 일반 API 사용 금지 */
/* 수정 */
xSemaphoreTakeRecursive(xRecMutex, portMAX_DELAY);
xSemaphoreGiveRecursive(xRecMutex);
/*
* 증상: Mutex를 사용하지만 우선순위 역전 문제가 그대로 발생
* 원인: FreeRTOSConfig.h에서 configUSE_MUTEXES가 0
*/
/* FreeRTOSConfig.h 확인 */
#define configUSE_MUTEXES 1 /* 반드시 1로 설정 */
/*
* 추가 확인: xSemaphoreCreateMutex()를 사용했는지 점검
* xSemaphoreCreateBinary()는 Priority Inheritance 미지원
*/
/*
* 증상: 간헐적 Deadlock (재현이 어렵고 타이밍에 의존)
* 원인: 서로 다른 Task에서 Mutex 획득 순서가 다름
* 진단: 각 Task의 Mutex 획득 순서를 코드 리뷰로 전수 검사
*/
/* 각 Task의 주석에 획득 순서를 명시하여 리뷰 시 확인 용이하게 작성 */
void vTaskA(void *pvParameters) {
/* Mutex 획득 순서: xMutexSensor -> xMutexBuffer */
xSemaphoreTake(xMutexSensor, portMAX_DELAY);
xSemaphoreTake(xMutexBuffer, portMAX_DELAY);
/* 작업 */
xSemaphoreGive(xMutexBuffer);
xSemaphoreGive(xMutexSensor);
}
void vTaskB(void *pvParameters) {
/* Mutex 획득 순서: xMutexSensor -> xMutexBuffer (TaskA와 동일 순서 유지) */
xSemaphoreTake(xMutexSensor, portMAX_DELAY);
xSemaphoreTake(xMutexBuffer, portMAX_DELAY);
/* 작업 */
xSemaphoreGive(xMutexBuffer);
xSemaphoreGive(xMutexSensor);
}