지노랩 /JinoLab

FreeRTOS_Queues_n_Timers: UART 명령어로 LED와 RTC 제어하기 본문

임베디드 시스템/RTOS

FreeRTOS_Queues_n_Timers: UART 명령어로 LED와 RTC 제어하기

지노랩/JinoLab 2025. 7. 1. 09:55

이 예제에서는 FreeRTOS 큐(Queue), 소프트웨어 타이머(Software Timer), 그리고 **태스크(Task)**와 **태스크 알림(Task Notification)**을 결합하여,

  • UART로부터 사용자의 명령을 받아
  • LED 이펙트 제어(LED 플래싱 패턴)
  • 실시간 시계(RTC) 설정 / 조회
    기능을 수행하는 심플한 콘솔 애플리케이션을 완성해 봅니다.

1. 전체 구조 및 흐름 개요

이 애플리케이션은 크게 **4개의 주요 구성 요소(모듈)**으로 나눌 수 있습니다.

  1. UART 입출력 모듈
    • 사용자로부터 **명령어(Command)**를 받아 파싱
    • 응답(메뉴, 에러 메시지 등)을 다시 UART로 출력
  2. 명령어 처리(Task)
    • “사용자 입력 → 큐(Queue)로 전송 → 명령어 파싱 태스크(CommandParserTask)에서 꺼내 처리”
    • 파싱 결과(LED 제어 요청 / RTC 읽기·쓰기 요청)를
      • LED 제어 태스크(LEDTask)에 전달하거나
      • RTC 제어 태스크(RTCTask)에 전달
  3. LED 제어(Task + 소프트웨어 타이머)
    • “LED 이펙트”를 정의하고,
    • 소프트웨어 타이머를 통해 주기적으로 LED 패턴을 갱신
    • 사용자 명령에 따라 타이머를 생성·수정·삭제하여
      • “켜기 → 다양한 플래싱 모드(e1, e2, …) 적용 → 끄기(‘none’)”를 수행
  4. RTC 제어(Task)
    • 실시간 시계(RTC) 초기화 및 설정
    • 사용자 요청 시 시계 “읽기(Read)” 또는 “쓰기(Write)”
    • 잘못된 시간/날짜 입력 시 “Invalid Option” 메시지 전송

2. 하드웨어 및 보유 자원

  • MCU: STM32F4xx 계열 (Cortex-M4)
  • UART: 115200 bps, NVIC IRQ 핸들러에서 문자 단위로 수신
  • LED: 보드 상의 3개 이상의 LED 사용 가능 (Onboard LED)
  • RTC: STM32 내장 RTC 페리페럴 (보통 백업 레지스터로부터 클럭 설정 완료)

전체 시스템은 “인터럽트 → 큐 → 태스크” 구조로 설계됩니다.

  1. UART RX Interrupt
    • UART 하드웨어가 문자 하나를 수신하면,
    • ISR(USARTx_IRQHandler())에서 받은 문자를 큐로 전송(프로듀서)
    • (xQueueSendToBackFromISR())
  2. CommandParserTask (우선순위: 중간)
    • **메인 큐(xCmdQueue)**로부터 문자 하나씩 꺼내(xQueueReceive()),
    • **문장 단위(줄 끝 ‘\r\n’)**로 조립
    • “0 입력 → LED 메뉴, 1 입력 → RTC 메뉴, 그 외 → Invalid Option”
    • LED 메뉴/RTC 메뉴 선택 후 후속 입력(예: e1, hh:mm:ss, yyyy/mm/dd)을 다시 파싱
    • 파싱된 최종 명령을 LEDTaskQueue 또는 RTCTaskQueue로 전송
  3. LEDTask (우선순위: 낮음)
    • LEDTaskQueue를 xQueueReceive()로 블록 대기
    • “e1, e2, …, none” 패턴별로 소프트웨어 타이머 관리
      • e1, e2, …: 타이머 주기에 맞춰 LED를 번갈아 깜박이도록 콜백 실행
      • none: 현재 실행 중인 LED 타이머 삭제(멈춤)
  4. RTCTask (우선순위: 낮음)
    • RTCTaskQueue를 xQueueReceive()로 블록 대기
    • “현재 날짜/시간 출력 → 사용자로부터 ‘hh mm ss’ 입력 → RTC 레지스터 쓰기 → 다시 “현재 시간” 표시
    • 날짜 설정 시도 시, 입력 형식 오류 감지 후 “Invalid Option” 출력

3. 세부 구성 요소 및 API

3.1. 공통: 큐 정의 & 초기화

우선 FreeRTOSConfig.h에서 인터럽트 우선순위 설정(다음 강의에서 자세히 다룸)을 올바르게 해놓고,
FreeRTOS.h, task.h, queue.h, timers.h 등을 인클루드합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "timers.h"
#include <string.h>
#include <stdio.h>
#include "stm32f4xx_hal.h"
#include "main.h"

/* 큐 핸들 */
static QueueHandle_t xUartRxQueue;      // ISR → ParserTask 전달
static QueueHandle_t xCmdQueue;         // ParserTask → (LED/RTC Task) 전달
static QueueHandle_t xLedTaskQueue;     // ParserTask → LEDTask
static QueueHandle_t xRtcTaskQueue;     // ParserTask → RTCTask

/* 소프트웨어 타이머 핸들 */
// LED 패턴용
static TimerHandle_t xLedTimerHandle;   // LED 깜빡임 주기 관리를 위한 Timer

3.2. 1) UART ISR: 한 문자 수신 → 큐에 쓰기

/* UART1 IRQ Handler 예시 (HAL 라이브러리 사용) */
void USART1_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t ucRx;

    // HAL 라이브러리를 쓰는 상황이라면, HAL_UART_Receive_IT로 이미 인터럽트 설정됨
    // 여기서는 레지스터 직접 읽기 예시
    if(USART1->SR & USART_SR_RXNE) {  // 수신 데이터 레지스터 읽을 준비됨
        ucRx = (uint8_t)(USART1->DR & 0xFF); 
        // 한 문자 씩 큐로 보냄
        xQueueSendToBackFromISR(xUartRxQueue, &ucRx, &xHigherPriorityTaskWoken);
    }
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
  • 동작 요약
    1. UART 하드웨어가 문자 하나(ucRx)를 받으면, RXNE 플래그 세트 → ISR 진입
    2. xQueueSendToBackFromISR(xUartRxQueue, &ucRx, &xHigherPriorityTaskWoken)
      • ISR에서 블록 없음 → 즉시 큐에 쓰기 시도
      • 큐 풀(FULL) 상태면 errQUEUE_FULL 반환 → 문자 드롭
      • 큐가 비어 있거나 여유가 있으면 즉시 저장 후 xHigherPriorityTaskWoken에 값 세팅
    3. portYIELD_FROM_ISR(xHigherPriorityTaskWoken) → 만약 우선순위 높은 태스크가 “읽기 대기 중”이라면,
      ISR 종료 직전 태스크 컨텍스트 스위칭
  • 큐 크기 & 아이템 사이즈→ 32바이트 길이의 큐, 아이템 크기 1바이트
  • // 한 문자(1바이트) 저장. 대기량: ‘\r\n’ 개행 처리 + 여분의 버퍼 대역폭 확보 xUartRxQueue = xQueueCreate(32, sizeof(uint8_t));

3.3. 2) CommandParserTask: “문장(라인) 단위” 파싱

void CommandParserTask(void *pvParameters)
{
    uint8_t ucRxChar;
    static char lineBuf[64];
    static uint8_t linePos = 0;
    BaseType_t xStatus;

    for( ;; ) {
        // 1바이트 문자 읽기(무한 대기)
        xQueueReceive(xUartRxQueue, &ucRxChar, portMAX_DELAY);

        // 에코(터미널 화면 출력 용)
        char echo[2] = {ucRxChar, 0};
        HAL_UART_Transmit(&huart1, (uint8_t *)echo, 1, HAL_MAX_DELAY);

        // 개행(\r or \n) 감지 시 “한 줄 완성”으로 보고 처리
        if ((ucRxChar == '\r') || (ucRxChar == '\n')) {
            if (linePos > 0) {
                lineBuf[linePos] = '\0';  // 문자열 종료자 삽입
                // 2단계: “명령어 전송” 큐로 푸시
                // 예: 먼저 “메인 메뉴”인지, “LED 서브 메뉴”인지, “RTC 서브 메뉴”인지판별
                ProcessCompleteLine(lineBuf);
                linePos = 0;
            }
            // 개행 뒤, 메뉴 프롬프트 재표시
            HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n> ", 3, HAL_MAX_DELAY);
        }
        else {
            // 유효한 문자 → 버퍼에 저장
            if (linePos < sizeof(lineBuf) - 1) {
                lineBuf[linePos++] = (char)ucRxChar;
            }
            // 버퍼 오버플로우 시 무시 (혹은 에러 처리)
        }
    }
}

ProcessCompleteLine() 예시(메인/서브 메뉴 구분)

static enum { MAIN_MENU, LED_MENU, RTC_MENU } currentMenu = MAIN_MENU;

/* 완성된 lineBuf를 파싱하여, 해당 서브 메뉴/명령 태스크에 전달 */
void ProcessCompleteLine(const char *pszLine)
{
    BaseType_t xStatus;
    char cmd[16], arg1[16], arg2[16], arg3[16];
    memset(cmd, 0, sizeof(cmd));

    switch (currentMenu) {
        case MAIN_MENU:
            // 예: "0" → LED 메뉴, "1" → RTC 메뉴, 그 외 → Invalid
            if (sscanf(pszLine, "%15s", cmd) == 1) {
                if (strcmp(cmd, "0") == 0) {
                    currentMenu = LED_MENU;
                    HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n--- LED Menu ---\r\n"
                                        "e1: Effect 1\r\n"
                                        "e2: Effect 2\r\n"
                                        "none: Stop LED\r\n> ", 58, HAL_MAX_DELAY);
                }
                else if (strcmp(cmd, "1") == 0) {
                    currentMenu = RTC_MENU;
                    HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n--- RTC Menu ---\r\n"
                                        "0: Set Time (HH MM SS)\r\n"
                                        "1: Set Date (YYYY MM DD)\r\n"
                                        "2: Show Current\r\n> ", 68, HAL_MAX_DELAY);
                }
                else {
                    HAL_UART_Transmit(&huart1, (uint8_t *)"\r\nInvalid Option\r\n> ", 20, HAL_MAX_DELAY);
                }
            }
            break;

        case LED_MENU:
            // LED 메뉴 입력 처리: "e1", "e2", "none"
            xStatus = xQueueSendToBack(xLedTaskQueue, &pszLine, pdMS_TO_TICKS(100));
            if (xStatus != pdPASS) {
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\nLED Cmd Queue FULL\r\n> ", 23, HAL_MAX_DELAY);
            } else {
                // “메인 메뉴로 돌아가려면 'back' 입력” 안내
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n> ", 3, HAL_MAX_DELAY);
            }
            // “back” 입력 시 메인 메뉴 복귀
            if (strcmp(pszLine, "back") == 0) {
                currentMenu = MAIN_MENU;
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n--- Main Menu ---\r\n"
                                    "0: LED Menu\r\n"
                                    "1: RTC Menu\r\n> ", 42, HAL_MAX_DELAY);
            }
            break;

        case RTC_MENU:
            // RTC 메뉴 입력 처리: "0", "1", "2"
            xStatus = xQueueSendToBack(xRtcTaskQueue, &pszLine, pdMS_TO_TICKS(100));
            if (xStatus != pdPASS) {
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\nRTC Cmd Queue FULL\r\n> ", 23, HAL_MAX_DELAY);
            } else {
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n> ", 3, HAL_MAX_DELAY);
            }
            // “back” 입력 시 메인 메뉴 복귀
            if (strcmp(pszLine, "back") == 0) {
                currentMenu = MAIN_MENU;
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\n--- Main Menu ---\r\n"
                                    "0: LED Menu\r\n"
                                    "1: RTC Menu\r\n> ", 42, HAL_MAX_DELAY);
            }
            break;
    }
}
  • 동작 요약
    1. 메인 메뉴:
      • 입력 “0” → LED_MENU로 전환, LED 서브 메뉴 출력
      • 입력 “1” → RTC_MENU로 전환, RTC 서브 메뉴 출력
      • 그 외 → “Invalid Option”
    2. LED 서브 메뉴(LED_MENU):
      • e1, e2 또는 none → xLedTaskQueue에 문자열 주소 전송
      • “back” → MAIN_MENU로 복귀
    3. RTC 서브 메뉴(RTC_MENU):
      • 0, 1, 2 → xRtcTaskQueue에 문자열 주소 전송
      • “back” → MAIN_MENU로 복귀
  • 참고
    • xLedTaskQueue와 xRtcTaskQueue는 여백이 충분히 있는 길이로 미리 xQueueCreate() 해놓아야 합니다.
    • 반드시 “한 줄(‘\r\n’ 개행)”을 종료 문자로 감지하고, lineBuf에 저장된 전체 문자열을 큐로 전송해야 합니다.

3.4. 3) LEDTask: 소프트웨어 타이머로 LED 이펙트 구현

**LED 메뉴(LED_MENU)**에서 들어오는 명령(문자열)은 다음과 같이 가정합니다.

  • "e1": 이펙트 1 → 예: 500 ms 주기로 LED1 토글(깜빡임)
  • "e2": 이펙트 2 → 예: 200 ms 주기로 LED2 순차 점멸
  • "none": LED 정지 → 현재 동작 중인 소프트웨어 타이머 삭제
/* LEDTask 전역변수 */
static TimerHandle_t xLedTimerHandle = NULL;
static char xCurrentEffect[8] = {0};

/* LED 제어용 소프트웨어 타이머 콜백 */
void vLedTimerCallback(TimerHandle_t xTimer)
{
    if (strcmp(xCurrentEffect, "e1") == 0) {
        // Effect 1: LED1 토글
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    }
    else if (strcmp(xCurrentEffect, "e2") == 0) {
        // Effect 2: LED2 순차 점멸 (예시)
        static uint8_t state = 0;
        if (state == 0) {
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);
        } else {
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);
        }
        state ^= 1;
    }
    // “none”인 경우 타이머가 이미 삭제되어 콜백 불가능
}

/* LEDTask: LED 메뉴 명령 대기 & 처리 */
void LEDTask(void *pvParameters)
{
    char *pszCmd;  // 큐에서 수신된 문자열 주소

    for(;;) {
        // 큐에서 명령 수신(무한 대기)
        if (xQueueReceive(xLedTaskQueue, &pszCmd, portMAX_DELAY) == pdPASS) {
            if (strcmp(pszCmd, "e1") == 0) {
                // “e1” 이펙트: 500ms 주기 타이머 설정
                strcpy(xCurrentEffect, "e1");
                if (xLedTimerHandle != NULL) {
                    xTimerChangePeriod(xLedTimerHandle, pdMS_TO_TICKS(500), 0);
                } else {
                    xLedTimerHandle = xTimerCreate(
                        "LedTimer",                 // 이름(디버깅 용)
                        pdMS_TO_TICKS(500),         // 주기 500ms
                        pdTRUE,                     // 자동 리로드
                        (void *)0,                  // ID (미사용)
                        vLedTimerCallback           // 콜백
                    );
                    xTimerStart(xLedTimerHandle, 0);
                }
            }
            else if (strcmp(pszCmd, "e2") == 0) {
                // “e2” 이펙트: 200ms 주기 타이머 설정
                strcpy(xCurrentEffect, "e2");
                if (xLedTimerHandle != NULL) {
                    xTimerChangePeriod(xLedTimerHandle, pdMS_TO_TICKS(200), 0);
                } else {
                    xLedTimerHandle = xTimerCreate(
                        "LedTimer",
                        pdMS_TO_TICKS(200),
                        pdTRUE,
                        (void *)0,
                        vLedTimerCallback
                    );
                    xTimerStart(xLedTimerHandle, 0);
                }
            }
            else if (strcmp(pszCmd, "none") == 0) {
                // “none”: 타이머 정지 및 삭제
                strcpy(xCurrentEffect, "none");
                if (xLedTimerHandle != NULL) {
                    xTimerStop(xLedTimerHandle, 0);
                    xTimerDelete(xLedTimerHandle, 0);
                    xLedTimerHandle = NULL;
                }
                // LED도 모두 끔
                HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
                HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0 | GPIO_PIN_1, GPIO_PIN_RESET);
            }
            else if (strcmp(pszCmd, "back") == 0) {
                // 상위 메뉴(메인 메뉴)로 복귀 → LEDTask는 건너뛰고 처리 없음
            }
            else {
                // “e1,e2,none,back” 외 잘못된 값
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\nInvalid LED Cmd\r\n> ", 20, HAL_MAX_DELAY);
            }
            // 명령 문자열 동적 할당 해제
            vPortFree(pszCmd);
        }
    }
}

핵심 포인트

  1. **xLedTaskQueue**를 통해 문자열 포인터(char *)가 전달됨
  2. 소프트웨어 타이머(FreeRTOS Timer) 생성
    • xTimerCreate("LedTimer", periodTicks, pdTRUE, NULL, vLedTimerCallback)
    • “자동 리로드(pdTRUE)” 설정 → 주기마다 vLedTimerCallback() 호출
  3. 주기 변경(xTimerChangePeriod()) + 재시작/정지/삭제
    • e1 → 500 ms, e2 → 200 ms, none → 타이머 삭제
  4. 콜백 함수에서 LED 토글/점멸 제어

3.5. 4) RTCTask: RTC 읽기·쓰기

/* RTCTask: RTC 설정/조회 명령 대기 & 처리 */
void RTCTask(void *pvParameters)
{
    char *pszCmd;
    uint8_t hh, mm, ss;
    uint16_t yyyy, mo, day;

    for( ;; ) {
        // 큐에서 명령 수신(무한 대기)
        if (xQueueReceive(xRtcTaskQueue, &pszCmd, portMAX_DELAY) == pdPASS) {

            if (strcmp(pszCmd, "0") == 0) {
                // “0”: 시간 설정 모드 → 사용자로부터 “HH MM SS” 입력받기
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\nEnter Time (HH MM SS): ", 24, HAL_MAX_DELAY);
                ReadNumbersFromUART(&hh, &mm, &ss);  // 블록 대기 함수
                // RTC 구조체에 할당 후 반영
                RTC_TimeTypeDef sTime = {0};
                sTime.Hours = hh; sTime.Minutes = mm; sTime.Seconds = ss;
                HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\nTime Set OK\r\n> ", 17, HAL_MAX_DELAY);
            }
            else if (strcmp(pszCmd, "1") == 0) {
                // “1”: 날짜 설정 모드 → “YYYY MM DD” 입력받기
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\nEnter Date (YYYY MM DD): ", 25, HAL_MAX_DELAY);
                ReadNumbersFromUART(&yyyy, &mo, &day);
                RTC_DateTypeDef sDate = {0};
                sDate.Year = yyyy - 2000;   // HAL에서는 Year가 0~99
                sDate.Month = mo;
                sDate.Date = day;
                sDate.WeekDay = RTC_WEEKDAY_MONDAY; // (예시: 임의 값)
                HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\nDate Set OK\r\n> ", 17, HAL_MAX_DELAY);
            }
            else if (strcmp(pszCmd, "2") == 0) {
                // “2”: 현재 시간·날짜 조회
                RTC_TimeTypeDef readTime;
                RTC_DateTypeDef readDate;
                HAL_RTC_GetTime(&hrtc, &readTime, RTC_FORMAT_BIN);
                HAL_RTC_GetDate(&hrtc, &readDate, RTC_FORMAT_BIN);
                // 사용자에게 출력
                char buf[64];
                snprintf(buf, sizeof(buf),
                         "\r\nCurrent: %04d/%02d/%02d  %02d:%02d:%02d\r\n> ",
                         2000 + readDate.Year,
                         readDate.Month,
                         readDate.Date,
                         readTime.Hours,
                         readTime.Minutes,
                         readTime.Seconds);
                HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), HAL_MAX_DELAY);
            }
            else if (strcmp(pszCmd, "back") == 0) {
                // 상위 메뉴(메인 메뉴)로 복귀 → 별도 처리 없음
            }
            else {
                HAL_UART_Transmit(&huart1, (uint8_t *)"\r\nInvalid RTC Cmd\r\n> ", 19, HAL_MAX_DELAY);
            }
            vPortFree(pszCmd);
        }
    }
}

/* UART로부터 연속된 숫자(최대 3개)를 입력받아 포인터(3개 인자)로 리턴하는 헬퍼 함수 */
void ReadNumbersFromUART(uint8_t *p1, uint8_t *p2, uint8_t *p3)
{
    char buf[16];
    uint8_t idx = 0, cnt = 0;
    char c;

    while (cnt < 3) {
        // 한 문자씩 받아서 echo
        xQueueReceive(xUartRxQueue, (uint8_t *)&c, portMAX_DELAY);
        buf[idx++] = c;
        HAL_UART_Transmit(&huart1, (uint8_t *)&c, 1, HAL_MAX_DELAY);

        if (c == ' ' || c == '\r' || c == '\n') {
            buf[idx - 1] = '\0';  // 구분자 제거
            uint8_t val = (uint8_t)atoi(buf);
            if (cnt == 0)      *p1 = val;
            else if (cnt == 1) *p2 = val;
            else               *p3 = val;
            cnt++;
            idx = 0;           // 다음 숫자 받을 버퍼 재설정
            memset(buf, 0, sizeof(buf));
            if (c == '\r') {  // 개행 시 맨 끝 숫자 처리
                (*p3) = (uint8_t)atoi(buf);
                break;
            }
        }
    }
}
  • 핵심 포인트
    1. xQueueReceive(xRtcTaskQueue, &pszCmd, portMAX_DELAY) → 큐 블록 대기
    2. "0" 입력 → ReadNumbersFromUART(&hh, &mm, &ss)
      • 내부에서 **UART 수신 큐(xUartRxQueue)**를 통해 “스페이스(‘ ’) 또는 개행(‘\r\n’)” 단위로 숫자 파싱
    3. HAL RTC API (HAL_RTC_SetTime(), HAL_RTC_SetDate(), HAL_RTC_GetTime(), HAL_RTC_GetDate()) 호출
    4. 문자열 snprintf()으로 “현재 시간/날짜”를 형식화하여 UART로 전송

4. 초기화 코드: main() 함수 구성

int main(void)
{
    /* HAL 초기화 */
    HAL_Init();
    SystemClock_Config();      // 클럭 설정
    MX_GPIO_Init();            // LED, 버튼 GPIO 초기화
    MX_USART1_UART_Init();     // UART1 초기화: 115200, 8N1, 인터럽트 RX_ENABLE
    MX_RTC_Init();             // RTC 하드웨어 클럭 소스 + 동기

    /* 1) 큐 생성 */
    xUartRxQueue    = xQueueCreate( 32, sizeof(uint8_t) );
    xLedTaskQueue   = xQueueCreate( 8, sizeof(char*) );
    xRtcTaskQueue   = xQueueCreate( 8, sizeof(char*) );
    // Parser 큐: UART 수신 → 완성된 문자열 전송
    xCmdQueue       = xQueueCreate( 8, sizeof(char*) );

    if (!xUartRxQueue || !xLedTaskQueue || !xRtcTaskQueue || !xCmdQueue) {
        // 할당 실패 시 무한 루프
        for(;;);
    }

    /* 2) 태스크 생성 */
    xTaskCreate(CommandParserTask, "Parser", 256, NULL, 3, NULL);
    xTaskCreate(LEDTask,           "LED",    256, NULL, 2, NULL);
    xTaskCreate(RTCTask,          "RTC",    256, NULL, 2, NULL);

    /* 3) UART 인터럽트 활성화 */
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);

    /* 4) 기본 메뉴 출력 */
    HAL_UART_Transmit(&huart1, (uint8_t *)
        "\r\n--- Main Menu ---\r\n"
        "0: LED Menu\r\n"
        "1: RTC Menu\r\n> ", 36, HAL_MAX_DELAY);

    /* 5) 스케줄러 시작 */
    vTaskStartScheduler();

    // 컴파일러 경고 방지용 무한루프
    for(;;);
}
  1. 큐 생성
    • xUartRxQueue: ISR에서 받은 단일 문자를 저장(32개 여유)
    • xCmdQueue: ParserTask로 완성된 문자열(최대 8개까지) 전송
    • xLedTaskQueue, xRtcTaskQueue: 각각 “문자열 포인터”(char*) 전송용 큐(길이 8)
  2. 태스크 생성
    • CommandParserTask (우선순위 3): UART 문자 → 명령어 파싱 → “LED/RTC 큐”로 전송
    • LEDTask (우선순위 2): 소프트웨어 타이머 제어로 LED 패턴 수행
    • RTCTask (우선순위 2): RTC 설정/조회 처리
  3. UART 인터럽트 활성화
    • __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE) 호출로 RXNE(문자 수신) 인터럽트 발생
    • 이후 USART1_IRQHandler()가 실행
  4. 기본 메뉴 출력
    • 애플리케이션 시작 시 UART로 사용자에게 메뉴 안내
  5. 스케줄러 기동
    • vTaskStartScheduler() → Idle Task 생성 + PendSV/SysTick 설정 후 Task 스케줄링

5. 전반적 데이터 플로우 요약

   사용자(터미널)                                 STM32 보드 (FreeRTOS)
   ───────────────────▶ (0/1/e1/e2/none/...) ──▶ USART1_IRQHandler()   ──▶ xQueueSendToBackFromISR(xUartRxQueue, &문자)
                                                           │
                                                    ┌──────┴────────────────────┐
                                                    │ CommandParserTask (prio3)│
                                                    └──────┬────────────────────┘
                                                           │ xQueueSendToBack(xLedTaskQueue, &"e1")  or
                                                           │ xQueueSendToBack(xRtcTaskQueue, &"0")
                         ┌─────────────────────────────────┴──────────────────────────────────┐
                         │                                                                    │
            ┌────────────▼──────────────┐                                          ┌──────────▼───────────┐
            │         LEDTask (prio2)   │                                          │      RTCTask (prio2) │
            └────────────┬──────────────┘                                          └──────────┬───────────┘
                         │                                                              xQueueReceive()
                         │    소프트웨어 타이머(500ms, 200ms)                            │
                         ▼                                                              ▼
            ┌──────────────────────────────┐                                 ┌────────────────────────────────┐
            │    vLedTimerCallback()       │                                 │    RTC 읽기/쓰기 처리 로직    │
            └──────────────────────────────┘                                 └────────────────────────────────┘
  1. UART 인터럽트xUartRxQueue
  2. CommandParserTask
    • xUartRxQueue에서 한 문자씩 읽고
    • “줄 단위(RXNE→‘\r\n’)”로 완성된 문자열(char *)을 매번 커맨드 큐(xCmdQueue)에 삽입
    • 메인 메뉴 → LED/RTC 서브 메뉴로 분기
    • “LED 명령”은 xLedTaskQueue로, “RTC 명령”은 xRtcTaskQueue로 전달
  3. LEDTask
    • xQueueReceive(xLedTaskQueue, &pszCmd, portMAX_DELAY)로 명령 문자열을 블록 대기
    • "e1"/"e2"/"none"에 따라 소프트웨어 타이머 생성/제어
    • vLedTimerCallback() → 500ms / 200ms 주기로 LED 토글 또는 점멸
    • 사용자 입력 “back” 시 메인 메뉴 복귀
  4. RTCTask
    • xQueueReceive(xRtcTaskQueue, &pszCmd, portMAX_DELAY)로 명령 블록 대기
    • "0" → 시간 설정 모드 → ReadNumbersFromUART()(UART 문자를 큐로부터 블록 대기하며 파싱 → “HH MM SS” 분리)
    • "1" → 날짜 설정 모드 → “YYYY MM DD” 분리
    • "2" → RTC 하드웨어에서 시간/날짜 읽어 터미널로 출력
    • “back” 시 메인 메뉴 복귀

6. FreeRTOS 소프트웨어 타이머 사용 팁

  1. 타이머 생성 → xTimerCreate()
    • 인자:
      • 이름(디버깅 용)
      • 주기(틱 단위, pdMS_TO_TICKS( ms ))
      • 자동 리로드(pdTRUE or pdFALSE)
      • ID(任意 포인터, 콜백에서 쓰고 싶으면 남기기)
      • 콜백 함수(vLedTimerCallback)
  2. 타이머 주기 변경 → xTimerChangePeriod()
    • 이미 생성된 타이머를 즉시 “다른 주기”로 변경하고 싶을 때
  3. xTimerChangePeriod(xLedTimerHandle, pdMS_TO_TICKS( newPeriodMs ), 0);
  4. 타이머 시작/정지
    • xTimerStart(xLedTimerHandle, 0);
    • xTimerStop(xLedTimerHandle, 0);
  5. 타이머 삭제 → xTimerDelete()
    • **timerHandle**가 NULL이 아니면, 반드시 삭제 후 timerHandle = NULL로 초기화
  6. 커널 틱 수 조정
    • configTICK_RATE_HZ (FreeRTOSConfig.h) 값을 적절히 조절하면,
    • 소프트웨어 타이머의 최소 분해능이 달라짐(예: 1000Hz = 1ms 단위).

7. 팁 & 주의사항

  1. 인터럽트 우선순위
    • UART IRQ(Priority)는 반드시 configMAX_SYSCALL_INTERRUPT_PRIORITY (예: 0x50)
      요소 이하(숫자 더 큼)로 설정해야 xQueueSendToBackFromISR() 사용 가능
    • LED/RTC Task는 CPU 주기 내내 정상 실행 보장
  2. 동적 메모리 해제
    • CommandParserTask가 "완성된 문자열"을 pvPortMalloc()로 할당한 뒤 큐에 보냈다면,
    • 반드시 최종 수신 태스크(LEDTask, RTCTask)가 vPortFree()로 메모리 해제
  3. 큐 길이
    • UART RX 큐: 짧은 버퍼(예: 32)
    • cmdQueue/ledQueue/rtcQueue: “한 번에 최대 N개 명령” 정도 안전한 크기로 설정(예: 8~16)
  4. 태스크 우선순위 및 스택 크기
    • CommandParserTask(우선순위 3) > LEDTask/RTCTask(우선순위 2)
    • 스택: 최소 256 워드 정도 권장(파싱, HAL_UART_Transmit(), ReadNumbersFromUART() 내 atoi() 등으로 인해 스택 사용량 증가)
  5. 명령어 입력 시 에코(Echo)
    • 터미널 사용성 향상을 위해 CommandParserTask가 UART로 수신 문자를 그대로 재전송 → 유저가 입력 내용 확인
    • 단, 명령 완성 후 “\r\n> ” 프롬프트 출력
  6. 메뉴 전환 로직
    • currentMenu 전역 변수로 “상위/하위 메뉴” 상태 관리
    • “back” 명령어로 메인 메뉴 복귀

8. 빌드 & 실행 결과 예시

  1. 보드 리셋 → “메인 메뉴” 출력
  2. --- Main Menu --- 0: LED Menu 1: RTC Menu >
  3. LED 메뉴 진입
    • 0 → CommandParserTask가 xLedTaskQueue로 "e1" 푸시
    • 터미널 출력:
    • --- LED Menu --- e1: Effect 1 e2: Effect 2 none: Stop LED >
  4. LED 이펙트 설정
    • e1 → “500ms 주기 LED1 토글” 시작
    • 터미널: >
    • LED1이 500 ms 주기로 깜빡이기 시작
  5. “none”→LED 정지
    • none → LED 꺼짐, 타이머 삭제
    • 터미널: >
  6. 메인 메뉴 복귀
    • back →
    • --- Main Menu --- 0: LED Menu 1: RTC Menu >
  7. RTC 메뉴 진입
    • 1 →
    • --- RTC Menu --- 0: Set Time (HH MM SS) 1: Set Date (YYYY MM DD) 2: Show Current >
  8. 시간 설정
    • 0 입력 →
    • Enter Time (HH MM SS):
    • 11 59 50 입력 → RTC에 시각 설정 →
    • Time Set OK >
  9. 날짜 설정
    • 1 입력 →
    • Enter Date (YYYY MM DD):
    • 2023 06 05 입력 → RTC에 날짜 설정 →
    • Date Set OK >
  10. 현재 시간·날짜 조회
    • 2 입력 →
    • Current: 2023/06/05 11:59:55 >
  11. 잘못된 입력 처리
    • RTC 메뉴에서 3 입력 →
    • Invalid RTC Cmd >
    • LED 메뉴에서 xyz 입력 →
    • Invalid LED Cmd >

9. 마무리

위 예제를 통해, FreeRTOS 큐(xQueueCreate, xQueueSend…, xQueueReceive…),
ISR↔태스크 통신(*FromISR()),
소프트웨어 타이머(xTimerCreate, xTimerStart, xTimerChangePeriod, xTimerDelete),
그리고 태스크 간 문맥 전환/블록킹(vTaskDelay, portMAX_DELAY)을 결합하여
“UART 콘솔을 통한 실시간 제어 애플리케이션”을 구현했습니다.

  • 학습 포인트
    1. xQueueReceive() vs xQueuePeek()의 차이와 사용 시점
    2. ISR 환경에서 큐로 데이터 송신 → xQueueSend…FromISR()
    3. 소프트웨어 타이머를 이용해 “주기적 LED 패턴” 생성
    4. 태스크 우선순위 설계동기화(Blocking/Unblocking)
    5. HAL RTC API와 FreeRTOS 태스크를 결합한 “외부 입력으로 하드웨어 제어”

 


참고 자료