지노랩 /JinoLab
FreeRTOS_Queues_n_Timers: UART 명령어로 LED와 RTC 제어하기 본문
이 예제에서는 FreeRTOS 큐(Queue), 소프트웨어 타이머(Software Timer), 그리고 **태스크(Task)**와 **태스크 알림(Task Notification)**을 결합하여,
- UART로부터 사용자의 명령을 받아
- LED 이펙트 제어(LED 플래싱 패턴)
- 실시간 시계(RTC) 설정 / 조회
기능을 수행하는 심플한 콘솔 애플리케이션을 완성해 봅니다.
1. 전체 구조 및 흐름 개요
이 애플리케이션은 크게 **4개의 주요 구성 요소(모듈)**으로 나눌 수 있습니다.
- UART 입출력 모듈
- 사용자로부터 **명령어(Command)**를 받아 파싱
- 응답(메뉴, 에러 메시지 등)을 다시 UART로 출력
- 명령어 처리(Task)
- “사용자 입력 → 큐(Queue)로 전송 → 명령어 파싱 태스크(CommandParserTask)에서 꺼내 처리”
- 파싱 결과(LED 제어 요청 / RTC 읽기·쓰기 요청)를
- LED 제어 태스크(LEDTask)에 전달하거나
- RTC 제어 태스크(RTCTask)에 전달
- LED 제어(Task + 소프트웨어 타이머)
- “LED 이펙트”를 정의하고,
- 소프트웨어 타이머를 통해 주기적으로 LED 패턴을 갱신
- 사용자 명령에 따라 타이머를 생성·수정·삭제하여
- “켜기 → 다양한 플래싱 모드(e1, e2, …) 적용 → 끄기(‘none’)”를 수행
- 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 페리페럴 (보통 백업 레지스터로부터 클럭 설정 완료)
전체 시스템은 “인터럽트 → 큐 → 태스크” 구조로 설계됩니다.
- UART RX Interrupt
- UART 하드웨어가 문자 하나를 수신하면,
- ISR(USARTx_IRQHandler())에서 받은 문자를 큐로 전송(프로듀서)
- (xQueueSendToBackFromISR())
- CommandParserTask (우선순위: 중간)
- **메인 큐(xCmdQueue)**로부터 문자 하나씩 꺼내(xQueueReceive()),
- **문장 단위(줄 끝 ‘\r\n’)**로 조립
- “0 입력 → LED 메뉴, 1 입력 → RTC 메뉴, 그 외 → Invalid Option”
- LED 메뉴/RTC 메뉴 선택 후 후속 입력(예: e1, hh:mm:ss, yyyy/mm/dd)을 다시 파싱
- 파싱된 최종 명령을 LEDTaskQueue 또는 RTCTaskQueue로 전송
- LEDTask (우선순위: 낮음)
- LEDTaskQueue를 xQueueReceive()로 블록 대기
- “e1, e2, …, none” 패턴별로 소프트웨어 타이머 관리
- e1, e2, …: 타이머 주기에 맞춰 LED를 번갈아 깜박이도록 콜백 실행
- none: 현재 실행 중인 LED 타이머 삭제(멈춤)
- 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);
}
- 동작 요약
- UART 하드웨어가 문자 하나(ucRx)를 받으면, RXNE 플래그 세트 → ISR 진입
- xQueueSendToBackFromISR(xUartRxQueue, &ucRx, &xHigherPriorityTaskWoken)
- ISR에서 블록 없음 → 즉시 큐에 쓰기 시도
- 큐 풀(FULL) 상태면 errQUEUE_FULL 반환 → 문자 드롭
- 큐가 비어 있거나 여유가 있으면 즉시 저장 후 xHigherPriorityTaskWoken에 값 세팅
- 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;
}
}
- 동작 요약
- 메인 메뉴:
- 입력 “0” → LED_MENU로 전환, LED 서브 메뉴 출력
- 입력 “1” → RTC_MENU로 전환, RTC 서브 메뉴 출력
- 그 외 → “Invalid Option”
- LED 서브 메뉴(LED_MENU):
- e1, e2 또는 none → xLedTaskQueue에 문자열 주소 전송
- “back” → MAIN_MENU로 복귀
- 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);
}
}
}
핵심 포인트
- **xLedTaskQueue**를 통해 문자열 포인터(char *)가 전달됨
- 소프트웨어 타이머(FreeRTOS Timer) 생성
- xTimerCreate("LedTimer", periodTicks, pdTRUE, NULL, vLedTimerCallback)
- “자동 리로드(pdTRUE)” 설정 → 주기마다 vLedTimerCallback() 호출
- 주기 변경(xTimerChangePeriod()) + 재시작/정지/삭제
- e1 → 500 ms, e2 → 200 ms, none → 타이머 삭제
- 콜백 함수에서 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;
}
}
}
}
- 핵심 포인트
- xQueueReceive(xRtcTaskQueue, &pszCmd, portMAX_DELAY) → 큐 블록 대기
- "0" 입력 → ReadNumbersFromUART(&hh, &mm, &ss)
- 내부에서 **UART 수신 큐(xUartRxQueue)**를 통해 “스페이스(‘ ’) 또는 개행(‘\r\n’)” 단위로 숫자 파싱
- HAL RTC API (HAL_RTC_SetTime(), HAL_RTC_SetDate(), HAL_RTC_GetTime(), HAL_RTC_GetDate()) 호출
- 문자열 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(;;);
}
- 큐 생성
- xUartRxQueue: ISR에서 받은 단일 문자를 저장(32개 여유)
- xCmdQueue: ParserTask로 완성된 문자열(최대 8개까지) 전송
- xLedTaskQueue, xRtcTaskQueue: 각각 “문자열 포인터”(char*) 전송용 큐(길이 8)
- 태스크 생성
- CommandParserTask (우선순위 3): UART 문자 → 명령어 파싱 → “LED/RTC 큐”로 전송
- LEDTask (우선순위 2): 소프트웨어 타이머 제어로 LED 패턴 수행
- RTCTask (우선순위 2): RTC 설정/조회 처리
- UART 인터럽트 활성화
- __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE) 호출로 RXNE(문자 수신) 인터럽트 발생
- 이후 USART1_IRQHandler()가 실행
- 기본 메뉴 출력
- 애플리케이션 시작 시 UART로 사용자에게 메뉴 안내
- 스케줄러 기동
- 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 읽기/쓰기 처리 로직 │
└──────────────────────────────┘ └────────────────────────────────┘
- UART 인터럽트 → xUartRxQueue
- CommandParserTask
- xUartRxQueue에서 한 문자씩 읽고
- “줄 단위(RXNE→‘\r\n’)”로 완성된 문자열(char *)을 매번 커맨드 큐(xCmdQueue)에 삽입
- 메인 메뉴 → LED/RTC 서브 메뉴로 분기
- “LED 명령”은 xLedTaskQueue로, “RTC 명령”은 xRtcTaskQueue로 전달
- LEDTask
- xQueueReceive(xLedTaskQueue, &pszCmd, portMAX_DELAY)로 명령 문자열을 블록 대기
- "e1"/"e2"/"none"에 따라 소프트웨어 타이머 생성/제어
- vLedTimerCallback() → 500ms / 200ms 주기로 LED 토글 또는 점멸
- 사용자 입력 “back” 시 메인 메뉴 복귀
- RTCTask
- xQueueReceive(xRtcTaskQueue, &pszCmd, portMAX_DELAY)로 명령 블록 대기
- "0" → 시간 설정 모드 → ReadNumbersFromUART()(UART 문자를 큐로부터 블록 대기하며 파싱 → “HH MM SS” 분리)
- "1" → 날짜 설정 모드 → “YYYY MM DD” 분리
- "2" → RTC 하드웨어에서 시간/날짜 읽어 터미널로 출력
- “back” 시 메인 메뉴 복귀
6. FreeRTOS 소프트웨어 타이머 사용 팁
- 타이머 생성 → xTimerCreate()
- 인자:
- 이름(디버깅 용)
- 주기(틱 단위, pdMS_TO_TICKS( ms ))
- 자동 리로드(pdTRUE or pdFALSE)
- ID(任意 포인터, 콜백에서 쓰고 싶으면 남기기)
- 콜백 함수(vLedTimerCallback)
- 인자:
- 타이머 주기 변경 → xTimerChangePeriod()
- 이미 생성된 타이머를 즉시 “다른 주기”로 변경하고 싶을 때
- xTimerChangePeriod(xLedTimerHandle, pdMS_TO_TICKS( newPeriodMs ), 0);
- 타이머 시작/정지
- xTimerStart(xLedTimerHandle, 0);
- xTimerStop(xLedTimerHandle, 0);
- 타이머 삭제 → xTimerDelete()
- **timerHandle**가 NULL이 아니면, 반드시 삭제 후 timerHandle = NULL로 초기화
- 커널 틱 수 조정
- configTICK_RATE_HZ (FreeRTOSConfig.h) 값을 적절히 조절하면,
- 소프트웨어 타이머의 최소 분해능이 달라짐(예: 1000Hz = 1ms 단위).
7. 팁 & 주의사항
- 인터럽트 우선순위
- UART IRQ(Priority)는 반드시 configMAX_SYSCALL_INTERRUPT_PRIORITY (예: 0x50)
요소 이하(숫자 더 큼)로 설정해야 xQueueSendToBackFromISR() 사용 가능 - LED/RTC Task는 CPU 주기 내내 정상 실행 보장
- UART IRQ(Priority)는 반드시 configMAX_SYSCALL_INTERRUPT_PRIORITY (예: 0x50)
- 동적 메모리 해제
- CommandParserTask가 "완성된 문자열"을 pvPortMalloc()로 할당한 뒤 큐에 보냈다면,
- 반드시 최종 수신 태스크(LEDTask, RTCTask)가 vPortFree()로 메모리 해제
- 큐 길이
- UART RX 큐: 짧은 버퍼(예: 32)
- cmdQueue/ledQueue/rtcQueue: “한 번에 최대 N개 명령” 정도 안전한 크기로 설정(예: 8~16)
- 태스크 우선순위 및 스택 크기
- CommandParserTask(우선순위 3) > LEDTask/RTCTask(우선순위 2)
- 스택: 최소 256 워드 정도 권장(파싱, HAL_UART_Transmit(), ReadNumbersFromUART() 내 atoi() 등으로 인해 스택 사용량 증가)
- 명령어 입력 시 에코(Echo)
- 터미널 사용성 향상을 위해 CommandParserTask가 UART로 수신 문자를 그대로 재전송 → 유저가 입력 내용 확인
- 단, 명령 완성 후 “\r\n> ” 프롬프트 출력
- 메뉴 전환 로직
- currentMenu 전역 변수로 “상위/하위 메뉴” 상태 관리
- “back” 명령어로 메인 메뉴 복귀
8. 빌드 & 실행 결과 예시
- 보드 리셋 → “메인 메뉴” 출력
- --- Main Menu --- 0: LED Menu 1: RTC Menu >
- LED 메뉴 진입
- 0 → CommandParserTask가 xLedTaskQueue로 "e1" 푸시
- 터미널 출력:
- --- LED Menu --- e1: Effect 1 e2: Effect 2 none: Stop LED >
- LED 이펙트 설정
- e1 → “500ms 주기 LED1 토글” 시작
- 터미널: >
- LED1이 500 ms 주기로 깜빡이기 시작
- “none”→LED 정지
- none → LED 꺼짐, 타이머 삭제
- 터미널: >
- 메인 메뉴 복귀
- back →
- --- Main Menu --- 0: LED Menu 1: RTC Menu >
- RTC 메뉴 진입
- 1 →
- --- RTC Menu --- 0: Set Time (HH MM SS) 1: Set Date (YYYY MM DD) 2: Show Current >
- 시간 설정
- 0 입력 →
- Enter Time (HH MM SS):
- 11 59 50 입력 → RTC에 시각 설정 →
- Time Set OK >
- 날짜 설정
- 1 입력 →
- Enter Date (YYYY MM DD):
- 2023 06 05 입력 → RTC에 날짜 설정 →
- Date Set OK >
- 현재 시간·날짜 조회
- 2 입력 →
- Current: 2023/06/05 11:59:55 >
- 잘못된 입력 처리
- 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 콘솔을 통한 실시간 제어 애플리케이션”을 구현했습니다.
- 학습 포인트
- xQueueReceive() vs xQueuePeek()의 차이와 사용 시점
- ISR 환경에서 큐로 데이터 송신 → xQueueSend…FromISR()
- 소프트웨어 타이머를 이용해 “주기적 LED 패턴” 생성
- 태스크 우선순위 설계와 동기화(Blocking/Unblocking)
- HAL RTC API와 FreeRTOS 태스크를 결합한 “외부 입력으로 하드웨어 제어”
참고 자료
- FreeRTOS 공식 문서:
- HAL 라이브러리:
- HAL_RTC_SetTime(), HAL_RTC_SetDate(), HAL_RTC_GetTime(), HAL_RTC_GetDate()
- HAL_UART_Transmit(), __HAL_UART_ENABLE_IT()
'임베디드 시스템 > RTOS' 카테고리의 다른 글
동기화(Synchronization)와 상호 배제(Mutual Exclusion): 일상 예시부터 FreeRTOS 활용까지 (1) | 2025.07.02 |
---|---|
FreeRTOS 소프트웨어 타이머(Software Timer) 완벽 정리 (1) | 2025.07.01 |
FreeRTOS 큐(Queue)에서 데이터 읽기: xQueueReceive() & xQueuePeek() (1) | 2025.06.30 |
FreeRTOS 큐에 데이터 보내기: xQueueSendToFront() & xQueueSendToBack() (1) | 2025.06.30 |
FreeRTOS 큐(Queue) 생성하기: xQueueCreate API (1) | 2025.06.29 |