RTOS #15

홍태준·2026년 3월 22일

RTOS

목록 보기
15/20
post-thumbnail

Week 3 Day 5: Semaphore 실습

학습 목표

  • Binary Semaphore의 구조와 ISR-Task 동기화 패턴을 이해한다
  • Counting Semaphore의 동작 원리와 자원 풀 관리 방법을 학습한다
  • 두 세마포어의 사용 시나리오를 구분하고 적절히 선택하는 기준을 파악한다
  • 생산자-소비자 패턴 기반의 버퍼 동기화 실습을 수행한다

1. Binary Semaphore

1.1 Binary Semaphore의 구조

Binary Semaphore는 0 또는 1의 값만 가지는 세마포어입니다. Mutex와 동일한 내부 구조체를 사용하지만, 소유권 개념이 없으며 Priority Inheritance를 지원하지 않습니다.

항목설명
초기 상태사용 불가 (0)
최대값1
소유권없음
Priority Inheritance미지원
ISR에서 Give가능 (xSemaphoreGiveFromISR)
주요 용도Task 간 또는 ISR-Task 간 이벤트 동기화

Binary Semaphore의 초기 상태는 0(unavailable)입니다. 따라서 생성 직후 xSemaphoreTake()를 호출하면 다른 Task 또는 ISR이 xSemaphoreGive()를 호출하기 전까지 차단됩니다. 이 특성이 이벤트 신호 전달에 적합한 이유입니다.

1.2 생성 및 기본 사용

/* FreeRTOSConfig.h */
#define configUSE_COUNTING_SEMAPHORES    1   /* Binary Semaphore는 별도 설정 불필요 */
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

SemaphoreHandle_t xBinarySemaphore;

int main(void) {
    /* Binary Semaphore 생성: 초기 상태 = 0 (unavailable) */
    xBinarySemaphore = xSemaphoreCreateBinary();
    configASSERT(xBinarySemaphore != NULL);

    /* ... */
    vTaskStartScheduler();
    while(1);
}

1.3 ISR-Task 동기화 패턴

Binary Semaphore의 가장 일반적인 사용 패턴은 ISR에서 이벤트를 감지하고 Task에 신호를 전달하는 것입니다.

[ISR-Task 동기화 흐름]

ISR 발생
    |
    v
xSemaphoreGiveFromISR()  -> Binary Semaphore 값 0 -> 1
    |
    v
portYIELD_FROM_ISR()     -> 컨텍스트 전환 요청
    |
    v
Task: xSemaphoreTake()   -> Binary Semaphore 값 1 -> 0, 실행 재개
    |
    v
이벤트 처리
SemaphoreHandle_t xButtonSemaphore;

/* GPIO 인터럽트 서비스 루틴 */
void EXTI0_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /* ISR 전용 Give 함수 사용 */
    xSemaphoreGiveFromISR(xButtonSemaphore, &xHigherPriorityTaskWoken);

    /* 더 높은 우선순위의 Task가 깨어났다면 즉시 컨텍스트 전환 */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

    /* HAL 인터럽트 플래그 클리어 */
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
}

/* 이벤트 처리 Task */
void vButtonTask(void *pvParameters) {
    while(1) {
        /* ISR이 Give를 호출하기 전까지 차단 상태 유지 */
        if(xSemaphoreTake(xButtonSemaphore, portMAX_DELAY) == pdTRUE) {
            /* 버튼 이벤트 처리 */
            process_button_event();
        }
    }
}

1.4 Binary Semaphore 사용 시 주의사항

주의 1: 연속 신호 누락 가능성

Binary Semaphore는 최대값이 1이므로, Task가 처리하기 전에 ISR이 두 번 이상 발생하면 두 번째 신호부터는 인터럽트 접근이 누락됩니다.

[신호 누락 시나리오]

ISR 1회 발생  -> Give -> 세마포어 값 = 1
ISR 2회 발생  -> Give -> 세마포어 값 = 1 (변화 없음, 신호 누락)
Task 처리     -> Take -> 세마포어 값 = 0 (1회 처리만 수행)

해결 방법: 빠른 신호 처리가 필요한 경우 Counting Semaphore 또는 Queue 사용

주의 2: ISR에서 일반 Give 함수 사용 금지

/* 오류: ISR 내에서 일반 xSemaphoreGive() 사용 금지 */
void EXTI0_IRQHandler(void) {
    xSemaphoreGive(xBinarySemaphore);        /* 오류: ISR에서 사용 불가(Task 환경 전용)*/
}

/* 올바름: ISR 전용 함수 사용 */
void EXTI0_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

2. Counting Semaphore

2.1 Counting Semaphore의 구조

Counting Semaphore는 0 이상의 정수 값을 가지는 세마포어입니다. 최대값은 생성 시 지정하며, 값이 0이 되면 xSemaphoreTake() 호출이 차단됩니다.

항목설명
초기 상태생성 시 지정 (0 또는 최대값)
최대값생성 시 지정 (1 이상의 정수)
소유권없음
Priority Inheritance미지원
ISR에서 Give가능 (xSemaphoreGiveFromISR)
주요 용도이벤트 카운팅, 자원 풀 관리

2.2 생성 함수

SemaphoreHandle_t xSemaphoreCreateCounting(
    UBaseType_t uxMaxCount,      /* 세마포어의 최대값 */
    UBaseType_t uxInitialCount   /* 초기값 */
);
/* FreeRTOSConfig.h */
#define configUSE_COUNTING_SEMAPHORES    1   /* Counting Semaphore 사용 시 필수 */

초기값 설정 기준:

[용도에 따른 초기값 선택]

이벤트 카운팅 용도:
  uxInitialCount = 0
  의미: 처음에는 처리할 이벤트 없음, Give 호출 시마다 카운트 증가

자원 풀 관리 용도:
  uxInitialCount = uxMaxCount
  의미: 처음에는 모든 자원이 사용 가능, Take 호출 시마다 사용 가능 자원 감소

2.3 이벤트 카운팅 패턴

ISR이 빠르게 반복 발생하여 이벤트가 누적될 수 있는 경우에 사용합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

#define MAX_EVENT_COUNT    10

SemaphoreHandle_t xEventSemaphore;

/* ISR: 센서 데이터 수신 인터럽트 */
void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /* 수신 이벤트 카운트 증가 */
    xSemaphoreGiveFromISR(xEventSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

/* 이벤트 처리 Task */
void vUartProcessTask(void *pvParameters) {
    while(1) {
        /*
         * 세마포어 값이 0이면 차단
         * ISR이 연속으로 발생한 경우 카운트만큼 반복 처리
         */
        if(xSemaphoreTake(xEventSemaphore, portMAX_DELAY) == pdTRUE) {
            process_uart_data();
        }
    }
}

int main(void) {
    /* 초기값 0: 처음에는 처리할 이벤트 없음 */
    xEventSemaphore = xSemaphoreCreateCounting(MAX_EVENT_COUNT, 0);
    configASSERT(xEventSemaphore != NULL);

    xTaskCreate(vUartProcessTask, "UartProc", 256, NULL, 2, NULL);
    vTaskStartScheduler();
    while(1);
}

2.4 자원 풀 관리 패턴

DMA 채널, 통신 슬롯 등 동시에 사용 가능한 자원의 수를 제한하는 경우에 사용합니다.

#define DMA_CHANNEL_COUNT    4

SemaphoreHandle_t xDmaChannelSemaphore;

/*
 * DMA 채널 획득
 * 사용 가능한 채널이 없으면 차단
 */
BaseType_t dma_channel_acquire(TickType_t xTimeout) {
    return xSemaphoreTake(xDmaChannelSemaphore, xTimeout);
}

/*
 * DMA 채널 반납
 * 전송 완료 후 반드시 호출
 */
void dma_channel_release(void) {
    xSemaphoreGive(xDmaChannelSemaphore);
}

void vDmaUserTask(void *pvParameters) {
    while(1) {
        /* 사용 가능한 DMA 채널 확보 (최대 100ms 대기) */
        if(dma_channel_acquire(pdMS_TO_TICKS(100)) == pdTRUE) {
            /* DMA 전송 수행 */
            start_dma_transfer();
            wait_dma_complete();

            /* 채널 반납 */
            dma_channel_release();
        } else {
            /* 타임아웃: 채널 획득 실패 처리 */
            handle_dma_busy();
        }

        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

int main(void) {
    /* 초기값 = 최대값: 처음에는 모든 채널 사용 가능 */
    xDmaChannelSemaphore = xSemaphoreCreateCounting(DMA_CHANNEL_COUNT, DMA_CHANNEL_COUNT);
                                                     
    configASSERT(xDmaChannelSemaphore != NULL);

    /* 4개의 Task가 DMA 채널을 경쟁하여 사용 */
    for(int i = 0; i < 4; i++) {
        xTaskCreate(vDmaUserTask, "DmaUser", 256, NULL, 2, NULL);
    }

    vTaskStartScheduler();
    while(1);
}

2.5 현재 카운트 조회

/*
 * uxSemaphoreGetCount(): 현재 세마포어 값 조회
 * 자원 풀에서 남은 자원 수 확인 등에 활용
 */
UBaseType_t uxRemaining = uxSemaphoreGetCount(xDmaChannelSemaphore);
printf("사용 가능한 DMA 채널: %u / %d\n", uxRemaining, DMA_CHANNEL_COUNT);

3. 사용 시나리오 비교

3.1 Binary Semaphore vs Counting Semaphore

특성Binary SemaphoreCounting Semaphore
값 범위0 또는 10 ~ 지정한 최대값
연속 이벤트 처리누락 가능카운트 누적 가능
자원 풀 관리불가가능
단일 이벤트 동기화적합가능하나 불필요한 오버헤드
생성 함수xSemaphoreCreateBinary()xSemaphoreCreateCounting()

3.2 Binary Semaphore vs Mutex 비교

특성Binary SemaphoreMutex
주요 목적이벤트 동기화공유 자원 보호
소유권없음있음 (Take한 Task만 Give)
Priority Inheritance미지원지원
ISR Give가능불가
초기 상태unavailable (0)available (1)

3.3 시나리오별 선택 기준

[세마포어 선택 흐름도]

Task 간 신호 전달이 목적인가?
    YES -> 단일 이벤트인가?
               YES -> Binary Semaphore
               NO  -> 이벤트 누적이 발생할 수 있는가?
                          YES -> Counting Semaphore
                          NO  -> Binary Semaphore

    NO  -> 공유 자원을 보호하는가?
               YES -> Mutex (Priority Inheritance 필요 여부 고려)
               NO  -> 여러 자원을 동시에 제한하는가?
                          YES -> Counting Semaphore (자원 풀)
                          NO  -> EventGroup 또는 Queue 고려

Binary Semaphore를 사용해야 하는 경우:

/* 1. ISR -> Task 단방향 이벤트 전달 */
void ADC_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xAdcDoneSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

/* 2. 특정 Task가 다른 Task의 완료를 기다리는 경우 */
void vWorkerTask(void *pvParameters) {
    perform_heavy_computation();
    xSemaphoreGive(xWorkDoneSemaphore);   /* 완료 신호 */
    vTaskDelete(NULL);
}

void vControllerTask(void *pvParameters) {
    xTaskCreate(vWorkerTask, "Worker", 256, NULL, 1, NULL);
    xSemaphoreTake(xWorkDoneSemaphore, portMAX_DELAY);   /* 완료 대기 */
    use_computation_result();
}

Counting Semaphore를 사용해야 하는 경우:

/* 1. 빠른 ISR이 반복 발생하여 이벤트가 누적되는 경우 */
/* 2. 동시 접근 수를 N개로 제한하는 자원 풀 */

#define MAX_CONNECTIONS    5

SemaphoreHandle_t xConnectionSlot;

BaseType_t network_connect(void) {
    /* 슬롯이 없으면 차단 */
    return xSemaphoreTake(xConnectionSlot, pdMS_TO_TICKS(200));
}

void network_disconnect(void) {
    xSemaphoreGive(xConnectionSlot);   /* 슬롯 반납 */
}

4. 실습: 버퍼 동기화

실습 1: Counting Semaphore를 이용한 생산자-소비자 패턴

생산자 Task가 원형 버퍼에 데이터를 쓰고, 소비자 Task가 Counting Semaphore로 데이터 가용 여부를 추적하여 읽는 구조를 구현합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>
#include <string.h>

#define BUFFER_SIZE       8
#define PRODUCER_DELAY    200    /* ms */
#define CONSUMER_DELAY    350    /* ms */

typedef struct {
    uint32_t data;
    uint32_t timestamp;
} BufferItem;

static BufferItem  ringBuffer[BUFFER_SIZE];
static uint8_t     writeIdx   = 0;
static uint8_t     readIdx    = 0;

/*
 * xItemsAvailable: 소비 가능한 항목 수 (초기 0)
 * xSpaceAvailable: 생산 가능한 빈 슬롯 수 (초기 BUFFER_SIZE)
 */
SemaphoreHandle_t xItemsAvailable;
SemaphoreHandle_t xSpaceAvailable;
SemaphoreHandle_t xBufferMutex;      /* 버퍼 인덱스 보호용 Mutex */

/* 생산자 Task */
void vProducerTask(void *pvParameters) {
    uint32_t producedCount = 0;

    while(1) {
        vTaskDelay(pdMS_TO_TICKS(PRODUCER_DELAY));

        /* 빈 슬롯 확보 대기 */
        if(xSemaphoreTake(xSpaceAvailable, pdMS_TO_TICKS(500)) != pdTRUE) {
            printf("[Producer] 버퍼 포화, 슬롯 대기 타임아웃\n");
            continue;
        }

        /* 버퍼 쓰기: Mutex로 인덱스 보호 */
        xSemaphoreTake(xBufferMutex, portMAX_DELAY);

        ringBuffer[writeIdx].data      = producedCount;
        ringBuffer[writeIdx].timestamp = xTaskGetTickCount();

        printf("[Producer] 항목 %lu 쓰기 (인덱스 %u)\n", producedCount, writeIdx);

        writeIdx = (writeIdx + 1) % BUFFER_SIZE;
        producedCount++;

        xSemaphoreGive(xBufferMutex);

        /* 소비 가능한 항목 수 증가 */
        xSemaphoreGive(xItemsAvailable);
    }
}

/* 소비자 Task */
void vConsumerTask(void *pvParameters) {
    BufferItem item;

    while(1) {
        vTaskDelay(pdMS_TO_TICKS(CONSUMER_DELAY));

        /* 소비 가능한 항목 대기 */
        if(xSemaphoreTake(xItemsAvailable, pdMS_TO_TICKS(1000)) != pdTRUE) {
            printf("[Consumer] 버퍼 비어있음, 대기 타임아웃\n");
            continue;
        }

        /* 버퍼 읽기: Mutex로 인덱스 보호 */
        xSemaphoreTake(xBufferMutex, portMAX_DELAY);

        item = ringBuffer[readIdx];

        printf("[Consumer] 항목 %lu 읽기 (인덱스 %u, 지연 %lu ms)\n",
               item.data,
               readIdx,
               xTaskGetTickCount() - item.timestamp);

        readIdx = (readIdx + 1) % BUFFER_SIZE;

        xSemaphoreGive(xBufferMutex);

        /* 빈 슬롯 수 증가 */
        xSemaphoreGive(xSpaceAvailable);
    }
}

/* 버퍼 상태 모니터링 Task */
void vMonitorTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(2000));

        UBaseType_t items = uxSemaphoreGetCount(xItemsAvailable);
        UBaseType_t space = uxSemaphoreGetCount(xSpaceAvailable);

        printf("\n--- 버퍼 상태 ---\n");
        printf("  사용 중: %u / %d\n", (unsigned)(BUFFER_SIZE - space), BUFFER_SIZE);
        printf("  가용 슬롯: %u\n", (unsigned)space);
        printf("  소비 대기 항목: %u\n\n", (unsigned)items);
    }
}

int main(void) {
    /* 항목 카운터: 초기 0 (비어있음) */
    xItemsAvailable = xSemaphoreCreateCounting(BUFFER_SIZE, 0);

    /* 슬롯 카운터: 초기 BUFFER_SIZE (모두 비어있음) */
    xSpaceAvailable = xSemaphoreCreateCounting(BUFFER_SIZE, BUFFER_SIZE);

    xBufferMutex = xSemaphoreCreateMutex();

    configASSERT(xItemsAvailable != NULL);
    configASSERT(xSpaceAvailable != NULL);
    configASSERT(xBufferMutex    != NULL);

    printf("=== Counting Semaphore 버퍼 동기화 실습 ===\n\n");

    xTaskCreate(vProducerTask, "Producer", 256, NULL, 2, NULL);
    xTaskCreate(vConsumerTask, "Consumer", 256, NULL, 2, NULL);
    xTaskCreate(vMonitorTask,  "Monitor",  256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

실습 2: 다중 생산자-소비자 (Multi-Producer Multi-Consumer)

여러 생산자와 여러 소비자가 동일 버퍼에 접근하는 구조로 확장합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>

#define BUFFER_SIZE       16
#define NUM_PRODUCERS     2
#define NUM_CONSUMERS     3

static uint32_t ringBuffer[BUFFER_SIZE];
static uint8_t  writeIdx = 0;
static uint8_t  readIdx  = 0;

SemaphoreHandle_t xItemsAvailable;
SemaphoreHandle_t xSpaceAvailable;
SemaphoreHandle_t xWriteMutex;    /* 쓰기 인덱스 보호 */
SemaphoreHandle_t xReadMutex;     /* 읽기 인덱스 보호 */

void vProducerTask(void *pvParameters) {
    uint32_t taskId       = (uint32_t)(uintptr_t)pvParameters;
    uint32_t producedVal  = taskId * 1000;

    while(1) {
        vTaskDelay(pdMS_TO_TICKS(100 + taskId * 50));

        if(xSemaphoreTake(xSpaceAvailable, pdMS_TO_TICKS(300)) != pdTRUE) {
            printf("[Producer %lu] 슬롯 없음, 스킵\n", taskId);
            continue;
        }

        xSemaphoreTake(xWriteMutex, portMAX_DELAY);

        ringBuffer[writeIdx] = producedVal;
        printf("[Producer %lu] 값 %lu 쓰기 (인덱스 %u)\n",
               taskId, producedVal, writeIdx);

        writeIdx = (writeIdx + 1) % BUFFER_SIZE;
        producedVal++;

        xSemaphoreGive(xWriteMutex);
        xSemaphoreGive(xItemsAvailable);
    }
}

void vConsumerTask(void *pvParameters) {
    uint32_t taskId = (uint32_t)(uintptr_t)pvParameters;
    uint32_t value;

    while(1) {
        vTaskDelay(pdMS_TO_TICKS(150 + taskId * 30));

        if(xSemaphoreTake(xItemsAvailable, pdMS_TO_TICKS(500)) != pdTRUE) {
            printf("[Consumer %lu] 항목 없음, 대기\n", taskId);
            continue;
        }

        xSemaphoreTake(xReadMutex, portMAX_DELAY);

        value = ringBuffer[readIdx];
        printf("[Consumer %lu] 값 %lu 읽기 (인덱스 %u)\n",
               taskId, value, readIdx);

        readIdx = (readIdx + 1) % BUFFER_SIZE;

        xSemaphoreGive(xReadMutex);
        xSemaphoreGive(xSpaceAvailable);
    }
}

int main(void) {
    xItemsAvailable = xSemaphoreCreateCounting(BUFFER_SIZE, 0);
    xSpaceAvailable = xSemaphoreCreateCounting(BUFFER_SIZE, BUFFER_SIZE);
    xWriteMutex     = xSemaphoreCreateMutex();
    xReadMutex      = xSemaphoreCreateMutex();

    configASSERT(xItemsAvailable != NULL);
    configASSERT(xSpaceAvailable != NULL);
    configASSERT(xWriteMutex     != NULL);
    configASSERT(xReadMutex      != NULL);

    printf("=== 다중 생산자-소비자 실습 ===\n\n");

    for(uint32_t i = 0; i < NUM_PRODUCERS; i++) {
        xTaskCreate(vProducerTask, "Producer",
                    256, (void *)(uintptr_t)i, 2, NULL);
    }

    for(uint32_t i = 0; i < NUM_CONSUMERS; i++) {
        xTaskCreate(vConsumerTask, "Consumer",
                    256, (void *)(uintptr_t)i, 2, NULL);
    }

    vTaskStartScheduler();
    while(1);
}

실습 3: Binary Semaphore를 이용한 ADC DMA 완료 동기화

ADC DMA 전송 완료 인터럽트를 Binary Semaphore로 Task에 전달하고, 수집된 데이터를 처리하는 구조를 구현합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>

#define ADC_SAMPLE_COUNT    64

static uint16_t adcBuffer[ADC_SAMPLE_COUNT];

SemaphoreHandle_t xAdcDoneSemaphore;   /* ADC DMA 완료 신호 */
SemaphoreHandle_t xResultMutex;        /* 처리 결과 보호 */

static float lastAverage = 0.0f;
static float lastPeakV   = 0.0f;

/* ADC DMA 완료 콜백 (HAL 인터럽트 컨텍스트) */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /* DMA 전송 완료: Task에 처리 신호 전달 */
    xSemaphoreGiveFromISR(xAdcDoneSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

/* ADC 데이터 처리 Task */
void vAdcProcessTask(void *pvParameters) {
    while(1) {
        /* DMA 완료 인터럽트 대기 */
        xSemaphoreTake(xAdcDoneSemaphore, portMAX_DELAY);

        /* 평균 및 피크 전압 계산 */
        uint32_t sum  = 0;
        uint16_t peak = 0;

        for(uint16_t i = 0; i < ADC_SAMPLE_COUNT; i++) {
            sum += adcBuffer[i];
            if(adcBuffer[i] > peak) peak = adcBuffer[i];
        }

        float average = (float)sum / ADC_SAMPLE_COUNT;
        float peakV   = peak * 3.3f / 4095.0f;
        float avgV    = average * 3.3f / 4095.0f;

        /* 결과 저장: Mutex 보호 */
        xSemaphoreTake(xResultMutex, portMAX_DELAY);
        lastAverage = avgV;
        lastPeakV   = peakV;
        xSemaphoreGive(xResultMutex);

        printf("[ADC] 평균: %.3fV, 피크: %.3fV\n", avgV, peakV);

        /* 다음 DMA 전송 시작 */
        /* HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer, ADC_SAMPLE_COUNT); */
    }
}

/* 결과 표시 Task */
void vDisplayTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(1000));

        xSemaphoreTake(xResultMutex, portMAX_DELAY);
        printf("[Display] 최신 평균: %.3fV, 피크: %.3fV\n",
               lastAverage, lastPeakV);
        xSemaphoreGive(xResultMutex);
    }
}

int main(void) {
    xAdcDoneSemaphore = xSemaphoreCreateBinary();
    xResultMutex      = xSemaphoreCreateMutex();

    configASSERT(xAdcDoneSemaphore != NULL);
    configASSERT(xResultMutex      != NULL);

    printf("=== ADC DMA Binary Semaphore 동기화 실습 ===\n\n");

    xTaskCreate(vAdcProcessTask, "AdcProc",  256, NULL, 3, NULL);
    xTaskCreate(vDisplayTask,    "Display",  256, NULL, 1, NULL);

    /* ADC DMA 첫 번째 전송 시작 */
    /* HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer, ADC_SAMPLE_COUNT); */

    vTaskStartScheduler();
    while(1);
}

실습 4: 종합 문제 - 데이터 수집 파이프라인

ADC 데이터 수집(Binary Semaphore), 처리 큐(Counting Semaphore), 결과 보호(Mutex)를 조합한 완전한 파이프라인을 구현합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <stdio.h>
#include <math.h>

#define RAW_BUF_SIZE        4
#define RESULT_BUF_SIZE     8

/* 동기화 객체 */
SemaphoreHandle_t xAdcTrigger;         /* Binary: ADC 트리거 */
SemaphoreHandle_t xRawDataAvailable;   /* Counting: 처리할 원시 데이터 수 */
SemaphoreHandle_t xRawDataSpace;       /* Counting: 원시 데이터 버퍼 빈 슬롯 */
SemaphoreHandle_t xResultMutex;        /* Mutex: 결과 버퍼 보호 */

/* 버퍼 */
static uint16_t rawBuffer[RAW_BUF_SIZE];
static float    resultBuffer[RESULT_BUF_SIZE];
static uint8_t  rawWriteIdx  = 0;
static uint8_t  rawReadIdx   = 0;
static uint8_t  resultIdx    = 0;

/* ADC 트리거 시뮬레이션 Task (실제: 타이머 ISR) */
void vAdcTriggerTask(void *pvParameters) {
    uint16_t adcVal = 0;

    while(1) {
        vTaskDelay(pdMS_TO_TICKS(100));

        adcVal = (uint16_t)(2048 + (xTaskGetTickCount() % 512));

        /* 원시 버퍼 빈 슬롯 확보 */
        if(xSemaphoreTake(xRawDataSpace, pdMS_TO_TICKS(10)) == pdTRUE) {
            rawBuffer[rawWriteIdx] = adcVal;
            rawWriteIdx = (rawWriteIdx + 1) % RAW_BUF_SIZE;
            xSemaphoreGive(xRawDataAvailable);

            /* 수집 완료 신호 */
            xSemaphoreGive(xAdcTrigger);
        }
    }
}

/* 데이터 처리 Task */
void vProcessTask(void *pvParameters) {
    while(1) {
        /* 원시 데이터 수신 대기 */
        xSemaphoreTake(xAdcTrigger, portMAX_DELAY);

        if(xSemaphoreTake(xRawDataAvailable, pdMS_TO_TICKS(50)) == pdTRUE) {
            uint16_t raw = rawBuffer[rawReadIdx];
            rawReadIdx = (rawReadIdx + 1) % RAW_BUF_SIZE;
            xSemaphoreGive(xRawDataSpace);

            /* 전압 변환 */
            float voltage = raw * 3.3f / 4095.0f;

            /* 결과 저장 */
            xSemaphoreTake(xResultMutex, portMAX_DELAY);
            resultBuffer[resultIdx % RESULT_BUF_SIZE] = voltage;
            resultIdx++;
            xSemaphoreGive(xResultMutex);

            printf("[Process] Raw: %u -> %.3fV\n", raw, voltage);
        }
    }
}

/* 통계 출력 Task */
void vStatsTask(void *pvParameters) {
    while(1) {
        vTaskDelay(pdMS_TO_TICKS(3000));

        xSemaphoreTake(xResultMutex, portMAX_DELAY);

        uint8_t count = (resultIdx < RESULT_BUF_SIZE)
                        ? resultIdx
                        : RESULT_BUF_SIZE;

        if(count == 0) {
            xSemaphoreGive(xResultMutex);
            continue;
        }

        float sum = 0.0f;
        float minV = 3.3f, maxV = 0.0f;

        for(uint8_t i = 0; i < count; i++) {
            sum += resultBuffer[i];
            if(resultBuffer[i] < minV) minV = resultBuffer[i];
            if(resultBuffer[i] > maxV) maxV = resultBuffer[i];
        }

        xSemaphoreGive(xResultMutex);

        printf("\n--- 통계 (최근 %u 샘플) ---\n", count);
        printf("  평균: %.3fV, 최솟값: %.3fV, 최댓값: %.3fV\n\n",
               sum / count, minV, maxV);
    }
}

int main(void) {
    xAdcTrigger       = xSemaphoreCreateBinary();
    xRawDataAvailable = xSemaphoreCreateCounting(RAW_BUF_SIZE, 0);
    xRawDataSpace     = xSemaphoreCreateCounting(RAW_BUF_SIZE, RAW_BUF_SIZE);
    xResultMutex      = xSemaphoreCreateMutex();

    configASSERT(xAdcTrigger       != NULL);
    configASSERT(xRawDataAvailable != NULL);
    configASSERT(xRawDataSpace     != NULL);
    configASSERT(xResultMutex      != NULL);

    printf("=== 데이터 수집 파이프라인 종합 실습 ===\n\n");

    xTaskCreate(vAdcTriggerTask, "AdcTrig",  256, NULL, 3, NULL);
    xTaskCreate(vProcessTask,    "Process",  256, NULL, 2, NULL);
    xTaskCreate(vStatsTask,      "Stats",    256, NULL, 1, NULL);

    vTaskStartScheduler();
    while(1);
}

5. 디버깅: Semaphore 관련 문제

5.1 Binary Semaphore 초기화 누락

/*
 * 증상: 생성 직후 xSemaphoreTake()가 즉시 성공함 (차단 없음)
 * 원인: xSemaphoreCreateBinary() 대신 xSemaphoreCreateMutex()를 사용하거나
 *       초기 상태를 잘못 이해한 경우
 *
 * Binary Semaphore: 초기 상태 = 0 (unavailable) -> 최초 Take는 차단됨
 * Mutex:           초기 상태 = 1 (available)    -> 최초 Take는 즉시 성공
 */

/* 이벤트 동기화에 Mutex를 잘못 사용한 예 */
SemaphoreHandle_t xWrongSem = xSemaphoreCreateMutex();
/* 생성 직후 Take가 차단 없이 성공 -> 의도하지 않은 동작 */
xSemaphoreTake(xWrongSem, portMAX_DELAY);

/* 올바른 Binary Semaphore 사용 */
SemaphoreHandle_t xCorrectSem = xSemaphoreCreateBinary();
/* 생성 직후 Take는 Give 호출 전까지 차단됨 */
xSemaphoreTake(xCorrectSem, portMAX_DELAY);   /* ISR Give 대기 */

5.2 Counting Semaphore 최대값 초과 Give

/*
 * 증상: xSemaphoreGive()가 pdFALSE를 반환
 * 원인: 세마포어 값이 이미 최대값에 도달한 상태에서 Give 호출
 *
 * 자원 풀 패턴에서 Take 없이 Give를 중복 호출하거나,
 * Take와 Give 횟수가 맞지 않을 때 발생
 */
#define MAX_SLOTS    4

SemaphoreHandle_t xSlotSem = xSemaphoreCreateCounting(MAX_SLOTS, MAX_SLOTS);

/* 오류: Take 없이 Give 호출 -> 최대값 초과 시도 */
BaseType_t result = xSemaphoreGive(xSlotSem);   /* pdFALSE 반환 */
if(result == pdFALSE) {
    printf("[오류] 세마포어 최대값 초과 Give 시도\n");
}

/*
 * 진단: Take/Give 호출 횟수를 전역 카운터로 추적하여
 *       불균형 여부를 로그로 확인
 */

5.3 ISR에서 일반 API 사용으로 인한 크래시

/*
 * 증상: 인터럽트 발생 시 HardFault 또는 시스템 재시작
 * 원인: ISR 내에서 FreeRTOS 일반 API 호출 (컨텍스트 전환 불가 컨텍스트에서 차단 시도)
 */

/* 오류: ISR에서 일반 API 사용 */
void TIM2_IRQHandler(void) {
    xSemaphoreGive(xTimerSemaphore);           /* 오류: ISR에서 사용 불가 */
}

/* 올바름: FromISR 접미사가 붙은 전용 API 사용 */
void TIM2_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xTimerSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

5.4 Counting Semaphore 설정 누락

/*
 * 증상: xSemaphoreCreateCounting() 호출 시 NULL 반환
 * 원인: FreeRTOSConfig.h에 configUSE_COUNTING_SEMAPHORES가 설정되지 않음
 */

/* FreeRTOSConfig.h 확인 */
#define configUSE_COUNTING_SEMAPHORES    1   /* 반드시 1로 설정 */

/*
 * 추가 확인: 힙 메모리 부족으로 NULL이 반환될 수도 있으므로
 * configTOTAL_HEAP_SIZE 값을 점검
 */
configASSERT(xCountingSemaphore != NULL);   /* 생성 직후 반드시 검증 */

학습 정리

오늘 배운 핵심 내용

  1. Binary Semaphore

    • 초기 상태 0(unavailable), ISR-Task 단방향 이벤트 동기화에 적합
    • ISR에서 Give 시 반드시 xSemaphoreGiveFromISR()portYIELD_FROM_ISR() 사용
    • 연속 이벤트가 빠르게 발생하면 신호 누락 가능성 존재
  2. Counting Semaphore

    • 0 ~ 최대값 범위의 정수를 관리, configUSE_COUNTING_SEMAPHORES=1 필수
    • 이벤트 카운팅: 초기값 0으로 생성, ISR 발생마다 Give
    • 자원 풀 관리: 초기값 = 최대값으로 생성, 자원 사용 시 Take, 반납 시 Give
  3. 사용 시나리오 구분

    • 공유 자원 보호 -> Mutex (Priority Inheritance 포함)
    • 단일 이벤트 동기화 -> Binary Semaphore
    • 이벤트 누적 또는 자원 수 제한 -> Counting Semaphore
  4. 버퍼 동기화 설계 원칙

    • xItemsAvailable(초기 0)과 xSpaceAvailable(초기 최대값)을 쌍으로 사용
    • 버퍼 인덱스 접근은 별도 Mutex로 보호
    • 생산 속도와 소비 속도 차이에 따른 타임아웃 설정 필요

핵심 개념 요약

개념설명
Binary Semaphore 초기 상태unavailable (0), 최초 Take는 Give 이후 성공
Counting Semaphore 초기값용도에 따라 0 또는 최대값으로 설정
xSemaphoreGiveFromISR()ISR에서 세마포어 Give 시 사용하는 전용 함수
portYIELD_FROM_ISR()ISR 종료 전 컨텍스트 전환 요청 함수
uxSemaphoreGetCount()현재 세마포어 값 조회 함수
이벤트 카운팅 패턴초기값 0, ISR에서 Give, Task에서 Take
자원 풀 패턴초기값 = 최대값, 자원 사용 전 Take, 반납 시 Give
생산자-소비자 패턴xItemsAvailable + xSpaceAvailable 쌍으로 구성

실습 과제

과제 1: 타이머 기반 이벤트 카운터

1ms 주기 타이머 인터럽트를 Binary Semaphore로 Task에 전달하고, Counting Semaphore로 1초 내 이벤트 누적 수를 측정하십시오.

요구사항:

  • 타이머 ISR에서 Binary Semaphore로 틱 신호 전달
  • 처리 Task에서 Counting Semaphore로 초당 처리 완료 수 집계
  • 1초마다 처리율(처리 완료 수 / 발생 수) 계산 및 출력
  • 처리율이 95% 미만으로 떨어지면 경고 로그 출력

과제 2: N-버퍼 자원 풀 관리자

고정 크기 메모리 블록 N개를 Counting Semaphore로 관리하는 간단한 메모리 풀을 구현하십시오.

요구사항:

  • pool_alloc(): Counting Semaphore Take 후 사용 가능한 블록 주소 반환
  • pool_free(): 블록 반납 후 Counting Semaphore Give
  • 여러 Task가 동시에 alloc/free 호출 시 안전성 보장
  • 풀 고갈 상태에서 타임아웃 처리 및 통계 로그 출력

과제 3: 이중 버퍼 ADC 동기화

ADC DMA를 핑퐁(Ping-Pong) 이중 버퍼로 운용하고, 각 버퍼 완료 시 Binary Semaphore를 통해 처리 Task에 신호를 전달하는 구조를 구현하십시오.

요구사항:

  • 버퍼 A 완료 -> 처리 Task에 신호 전달, 버퍼 B로 DMA 전환
  • 버퍼 B 완료 -> 처리 Task에 신호 전달, 버퍼 A로 DMA 전환
  • 처리 Task가 바쁜 동안 DMA가 다음 버퍼에 계속 수집하도록 구조 설계
  • 처리 지연으로 인한 버퍼 오버런 감지 시 오류 카운터 증가 및 로그 출력

디버깅 팁

문제 1: 생산자-소비자에서 데드락 발생

/*
 * 증상: 생산자와 소비자가 모두 차단된 상태로 진행 불가
 * 원인: xSpaceAvailable과 xItemsAvailable의 초기값 또는 최대값 설정 오류
 *
 * 점검 순서:
 * 1. xSpaceAvailable 초기값 = BUFFER_SIZE 인지 확인
 * 2. xItemsAvailable 초기값 = 0 인지 확인
 * 3. 생산 후 xItemsAvailable Give, 소비 후 xSpaceAvailable Give 순서 확인
 */

문제 2: 빠른 ISR에서 Counting Semaphore 최대값 초과

/*
 * 증상: ISR의 xSemaphoreGiveFromISR() 반환값이 pdFALSE
 * 원인: Task의 처리 속도보다 ISR 발생 빈도가 높아 세마포어가 포화
 * 해결:
 *   - 최대값을 더 크게 설정하거나
 *   - Task 우선순위를 높이거나
 *   - Queue를 사용하여 데이터 자체를 전달
 */
UBaseType_t uxMaxCount = 32;   /* 처리 지연을 감안하여 여유 있게 설정 */
xEventSemaphore = xSemaphoreCreateCounting(uxMaxCount, 0);

문제 3: portYIELD_FROM_ISR 누락

/*
 * 증상: 고우선순위 Task가 ISR 종료 후 즉시 실행되지 않고 다음 틱까지 지연
 * 원인: portYIELD_FROM_ISR() 호출 누락
 */

/* 오류: 컨텍스트 전환 요청 없음 */
void DMA1_IRQHandler(void) {
    BaseType_t xWoken = pdFALSE;
    xSemaphoreGiveFromISR(xDmaDoneSem, &xWoken);
    /* portYIELD_FROM_ISR(xWoken) 누락 */
}

/* 올바름 */
void DMA1_IRQHandler(void) {
    BaseType_t xWoken = pdFALSE;
    xSemaphoreGiveFromISR(xDmaDoneSem, &xWoken);
    portYIELD_FROM_ISR(xWoken);   /* 반드시 포함 */
}
profile
당신의 코딩 메이트

0개의 댓글