지노랩 /JinoLab
FreeRTOS 태스크 시작과 SWO 출력 설정 (ITM_SendChar 적용) 본문
이 강의에서는 이전에 생성한 두 개의 태스크(Task-1, Task-2)를 실제로 스케줄러에 올려 실행하고, SWO(Single Wire Output)를 통해 printf() 메시지를 출력하도록 설정하는 과정을 단계별로 자세히 설명합니다.
목차
- 스케줄러 시작
- 태스크 핸들러 구현
- SWO 출력을 위한 ITM_SendChar 설정
- syscalls.c 수정
- ITM_SendChar 함수 코드
- write() 함수 오버라이드
- 전체 코드 정리 및 컴파일 확인
- 하드웨어에서 테스트
1. 스케줄러 시작
FreeRTOS를 실제로 실행하려면, 애플리케이션이 생성한 모든 태스크를 READY 상태로 올린 뒤에 스케줄러를 시작해야 합니다. 이를 위해 애플리케이션의 main() 함수 마지막 부분에 다음 API를 호출합니다.
vTaskStartScheduler();
- vTaskStartScheduler()
- FreeRTOS 커널이 스케줄링을 시작하도록 하는 핵심 함수
- 내부에서 Idle Task(우선순위 최하위)와, 필요하다면 Timer Service Task 등을 생성
- SysTick(또는 HAL에서 설정한 대체 타이머)를 활성화하여 틱 인터럽트를 매밀리초마다 발생시킴
- “힙에 남은 메모리가 부족해 Idle/Timer Task를 만들 수 없는 경우”에만 반환(ret)하며, 정상 구동 중에는 절대 리턴하지 않음
주의:
- 만약 vTaskStartScheduler()가 리턴된다면 → “힙 메모리 부족” 등으로 커널 시작에 실패한 것.
- 이때는 이후 코드가 실행되므로, 무한 루프나 오류 메시지 처리 등을 넣어야 함.
/* main.c (일부). 태스크 생성 코드 뒤에 추가 */
// 모든 태스크가 정상 생성되었다면 스케줄러 시작
vTaskStartScheduler();
// 이 아래 코드는, 스케줄러 시작에 실패했을 때만 도달한다.
// 일반적으로는 무한 루프를 걸어주거나 오류 표시를 한다.
for (;;)
{
// 디버그용 - 스케줄러 시작 실패
}
2. 태스크 핸들러 구현
xTaskCreate()로 태스크를 등록할 때, 2번째 인자는 “태스크를 실행할 함수(핸들러)”의 포인터입니다. 즉, 스케줄러가 해당 태스크를 CPU에 실어서 실행할 때 호출할 함수입니다.
2.1. 함수 구조
FreeRTOS의 태스크 핸들러(예: Task1_Handler)는 반드시 무한 루프 (for(;;)) 안에서 동작해야 합니다.
- 함수가 끝나면(리턴하면) 안 되고, 끝까지 for 루프 안에서 동작하다가
- 만약 정말 “이 태스크를 끝내고 싶다면” vTaskDelete(NULL); 를 호출하고 리턴
- 하지만 보통은 무한 루프 형태로 동작
static void Task1_Handler(void *pvParameters)
{
/* pvParameters에는 xTaskCreate() 호출 시 전달했던 포인터(예: 문자열)가 들어온다. */
char *msg = (char *)pvParameters;
for (;;)
{
/* 1) 메시지 출력 */
printf("%s\r\n", msg);
/* 2) 500ms 블록 → 이 동안 CPU 사용권 양보(다른 태스크 실행 가능) */
vTaskDelay(pdMS_TO_TICKS(500));
}
/* 만약 루프를 깨고 나오려면, 반드시 vTaskDelete(NULL) 호출할 것 */
// vTaskDelete(NULL);
}
static void Task2_Handler(void *pvParameters)
{
char *msg = (char *)pvParameters;
for (;;)
{
printf("%s\r\n", msg);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
2.2. main()에서 태스크 생성 및 스케줄러 시작
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h> // printf 용
/* 태스크 핸들러 프로토타입 */
static void Task1_Handler(void *pvParameters);
static void Task2_Handler(void *pvParameters);
int main(void)
{
/* HAL 초기화 등 STM32CubeMX가 생성한 초기화 코드 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init(); // 나중에 printf를 UART로 바꿀 수도 있지만, 우리는 SWO 사용 예정
/* 태스크1 생성 */
BaseType_t xReturn1;
xReturn1 = xTaskCreate(
Task1_Handler, // 태스크 함수
"Task-1", // 태스크 이름(디버깅용)
200, // 스택 깊이(워드 단위)
(void *)"Hello from Task-1", // pvParameters: 출력할 메시지 주소
2, // 우선순위 (0~configMAX_PRIORITIES-1)
NULL // TaskHandle_t (필요시 핸들 저장, 여기는 NULL)
);
configASSERT(xReturn1 == pdPASS);
/* 태스크2 생성 */
BaseType_t xReturn2;
xReturn2 = xTaskCreate(
Task2_Handler,
"Task-2",
200,
(void *)"Hello from Task-2",
2,
NULL
);
configASSERT(xReturn2 == pdPASS);
/* 2개의 태스크가 READY 상태가 되었으므로, 스케줄러 시작 */
vTaskStartScheduler();
/* 절대 도달하지 않을 부분(힙 부족 등 문제 시) */
for (;;);
}
- 우선순위(2) 동일이므로, 두 태스크는 라운드로빈(Round-Robin) 선점형으로 매 1ms 틱마다 교대됨
- 각 printf() 호출 후 vTaskDelay(500) → 500ms 동안 블록(Delay) 상태 → CPU 사용권을 자발적으로 반납
Tip: UART로 직접 printf() 하고 싶다면, MX_USART2_UART_Init() 이후에 printf retarget 코드를 넣어야 합니다. 본 예제에서는 SWO를 이용하므로, UART retarget은 불필요합니다.
3. SWO 출력을 위한 ITM_SendChar 설정
STM32F4 코어(Cortex-M4)에는 ITM(In-trumentation Trace Macrocell) 라는 디버깅용 하드웨어 블록이 내장되어 있습니다.
이 ITM을 통해 SWO(Single Wire Output) 핀으로 데이터를 전송하면, ST-Link → PC를 거쳐 터미널(예: STM32CubeIDE의 SWO Viewer, 또는 다른 SWO 터미널) 에서 출력 내용을 실시간으로 볼 수 있습니다.
3.1. syscalls.c 수정
STM32CubeMX로 생성된 기본 프로젝트에는 syscalls.c가 포함되어 있고, 내부에 write() 함수가 “반드시 구현”되어 있습니다. C 라이브러리의 printf()는 내부적으로 write()를 호출하므로, 이 함수 안에서 “ITM_SendChar()”를 호출하여 SWO로 문자를 내보내도록 변경해야 합니다.
- SWO 관련 함수 추가(ITM_SendChar)
- ST 공식 애플리케이션 노트에 나온 코드를 복사해 syscalls.c 상단에 붙여넣습니다.
- 이 코드는 Cortex-M3/M4/M7 공통 ITM 레지스터를 직접 제어해 문자를 출력하는 함수입니다.
- write() 함수 오버라이드
- 기존 syscalls.c에 있던 “stdin/stdout/stderr” 처리 코드를 주석 처리하고, 대신 ITM_SendChar()를 호출하도록 수정
3.1.1. syscalls.c 편집 위치
/* syscalls.c */
#include "stm32f4xx_hal.h"
#include <stdio.h>
#include <sys/stat.h>
#include <sys/errno.h>
#include <stdint.h>
// ==================== 1) ITM_SendChar 함수 코드 추가 ====================
/*
ITM_SendChar : ARM Cortex-M3/M4/M7 프로세서의 ITM 포트를 통해 단일 문자를 출력
SWO 터미널(예: STM32CubeIDE SWO Viewer)로 메시지 전송 가능
*/
#define ITM_PORT0 (*(volatile uint32_t*)0xE0000000) // ITM Stimulus Port 0
int ITM_SendChar (int ch)
{
if (((*(volatile uint32_t*)0xE0000FB0) & 1) == 0) {
return 0; // ITM이 활성화되어 있지 않으면 아무것도 하지 않음
}
while ((*(volatile uint32_t*)0xE0000FB0 & 0x1) == 0); // 포트가 사용 가능해질 때까지 대기
ITM_PORT0 = (uint32_t)ch;
return ch;
}
// ====================================================================
/* 구현된 다른 syscalls 함수들… */
/*------------------------------------------------------------------------------
* write()
* - libc printf() → 이 write()를 호출해서 'stdout'으로 보낼 바이트를 여기에 전달
*----------------------------------------------------------------------------*/
int _write(int fd, char *ptr, int len)
{
/* fd (file descriptor):
0: stdin, 1: stdout, 2: stderr
우리는 stdout/stderr 모두 SWO로 보내도록 처리
*/
int DataIdx;
for (DataIdx = 0; DataIdx < len; DataIdx++)
{
ITM_SendChar(*ptr++);
}
return len;
}
/* 나머지 함수들(예: _read, _fstat, _isatty 등…)은 기본 STM32CubeMX에서 생성된 대로 둡니다. */
- 0xE0000FB0: “ITM 포트 활성화 레지스터”(ITM_TCR)로, 최하위 비트(0x1)가 1이면 ITM 활성화 여부 확인 가능
- 0xE0000000: “ITM Stimulus Port 0” 주소. 이곳에 쓰면 SWO로 바이트가 출력됨
3.1.2. 주의 사항
- 이 코드는 Cortex-M4/M7/M3 전용.
- Cortex-M0/M0+ 코어는 ITM 모듈이 없으므로 이 방법 사용 불가.
4. 전체 코드 정리 및 컴파일 확인
4.1. FreeRTOSConfig.h 확인
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
/* 스케줄링 유형: 선점형(1) vs 협력형(0) */
#define configUSE_PREEMPTION 1
/* 동일 우선순위 간 라운드로빈 */
#define configUSE_TIME_SLICING 1
/* Tick 주기 설정: 1000Hz → 1ms */
#define configTICK_RATE_HZ 1000
/* 태스크 우선순위 개수(0~4) */
#define configMAX_PRIORITIES 5
/* Idle 태스크 훅 사용 여부 (우리는 사용 안하므로 0으로) */
#define configUSE_IDLE_HOOK 0
/* Stack overflow 훅 사용 여부 (우리는 사용 안하므로 0으로) */
#define configCHECK_FOR_STACK_OVERFLOW 0
/* malloc/free 등 힙 관리 방식: heap_4.c 사용 (동적 할당) */
/* … 나머지 설정은 STM32CubeMX에서 생성된 대로 둡니다 … */
#endif /* FREERTOS_CONFIG_H */
특이사항
- configUSE_IDLE_HOOK를 0으로 두면, 커널이 “Idle Hook” 함수를 호출하지 않으므로, IDLE_HOOK 정의 관련 빌드 오류 해소
- configCHECK_FOR_STACK_OVERFLOW를 0으로 두어 “Stack Overflow Hook” 호출 끔
4.2. main.c 전체 예시
/* main.c */
#include "stm32f4xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
/* 태스크 핸들러 프로토타입 */
static void Task1_Handler(void *pvParameters);
static void Task2_Handler(void *pvParameters);
/* System Clock 및 Peripherals 초기화 함수 */
extern void SystemClock_Config(void);
extern void MX_GPIO_Init(void);
extern void MX_USART2_UART_Init(void);
int main(void)
{
/* HAL 및 MCU 초기화 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
/* 1) Task-1 생성 */
BaseType_t xReturn1 = xTaskCreate(
Task1_Handler,
"Task-1",
200,
(void *)"Hello from Task-1",
2,
NULL
);
configASSERT(xReturn1 == pdPASS);
/* 2) Task-2 생성 */
BaseType_t xReturn2 = xTaskCreate(
Task2_Handler,
"Task-2",
200,
(void *)"Hello from Task-2",
2,
NULL
);
configASSERT(xReturn2 == pdPASS);
/* 3) 스케줄러 시작 → 절대 리턴되지 않음(실패 시 리턴) */
vTaskStartScheduler();
/* 스케줄러 시작에 실패한 경우만 도달 */
while (1)
{
/* 힙 부족 등으로 스케줄러를 시작하지 못했다는 의미 */
}
}
/*---------------------------------------------------------------------------*/
/* Task1: 500ms마다 메시지 출력 */
static void Task1_Handler(void *pvParameters)
{
char *msg = (char *)pvParameters;
for (;;)
{
printf("%s\r\n", msg);
vTaskDelay(pdMS_TO_TICKS(500)); // 500ms 블록
}
}
/*---------------------------------------------------------------------------*/
/* Task2: 500ms마다 메시지 출력 */
static void Task2_Handler(void *pvParameters)
{
char *msg = (char *)pvParameters;
for (;;)
{
printf("%s\r\n", msg);
vTaskDelay(pdMS_TO_TICKS(500)); // 500ms 블록
}
}
4.3. syscalls.c 전체 예시
/* syscalls.c */
#include "stm32f4xx_hal.h"
#include <stdio.h>
#include <sys/stat.h>
#include <sys/errno.h>
#include <stdint.h>
/* ============================ ITM_SendChar 함수 ============================ */
#define ITM_PORT0 (*(volatile uint32_t*)0xE0000000)
int ITM_SendChar (int ch)
{
// ITM 활성화 여부 확인
if (((*(volatile uint32_t*)0xE0000FB0) & 1) == 0) {
return 0;
}
// ITM 포트가 비어질 때까지 대기
while ((*(volatile uint32_t*)0xE0000FB0 & 0x1) == 0);
ITM_PORT0 = (uint32_t)ch;
return ch;
}
/* =========================================================================== */
extern int _read(int file, char *ptr, int len);
extern int _write(int file, char *ptr, int len);
extern int _close(int handle);
extern int _lseek(int handle, int offset, int whence);
extern int _fstat(int handle, struct stat *st);
extern int _isatty(int handle);
extern void _exit(int status);
/*------------------------------------------------------------------------------
* write()
* - libc printf() → 이 write()가 호출되어, buf 문자열을 순차적으로 ITM으로 출력
*----------------------------------------------------------------------------*/
int _write(int file, char *ptr, int len)
{
/* file == 1 (stdout) 또는 file == 2 (stderr) */
for (int DataIdx = 0; DataIdx < len; DataIdx++)
{
ITM_SendChar(*ptr++);
}
return len;
}
/*------------------------------------------------------------------------------
* _read(), _close(), _lseek(), _fstat(), _isatty(), _exit() 등은
* 기본 STM32CubeMX 생성 코드 그대로 남겨두면 됩니다.
*----------------------------------------------------------------------------*/
/* 예시:
int _read(int file, char *ptr, int len) { return 0; }
int _close(int handle) { return -1; }
int _lseek(int handle, int offset, int whence) { return 0; }
int _fstat(int handle, struct stat *st) {
st->st_mode = S_IFCHR;
return 0;
}
int _isatty(int handle) { return 1; }
void _exit(int status) { while (1); }
*/
4.4. 컴파일 확인
이 상태에서 빌드(Build) 버튼을 눌러 프로젝트를 컴파일합니다.
- 에러1: ApplicationIdleHook 미정의 → configUSE_IDLE_HOOK = 0 로 바꾸면 해결
- 에러2: portYIELD_FROM_ISR, vApplicationTickHook 등 다른 Hook 함수 관련 → configUSE_TICK_HOOK = 0, configCHECK_FOR_STACK_OVERFLOW = 0 등으로 해소
컴파일이 **“성공”**했다면, Task-1과 Task-2가 정상적으로 생성되고,
vTaskStartScheduler() 호출 뒤 스케줄러가 실행되어 SWO를 통해 “Hello from Task-X” 메시지를 반복 출력할 준비가 된 것입니다.
5. 하드웨어에서 테스트
마지막으로, STM32F4 발견(Discovery) 보드나 Nucleo-64(F446RE) 보드 등에 ST-Link가 내장되어 있다고 가정하고, SWO 모니터링을 통해 메시지가 실제로 나오는지 검증합니다.
5.1. SWO 및 디버거 설정 (STM32CubeIDE 기준)
- 프로젝트 속성 → Debug Configurations…
- “Debugger” 탭에서 ”ST-Link GDB Server” 옵션을 선택
- “Port”나 “SWO” 탭에서:
- SWO 활성화 체크
- Baud Rate: 2 MHz (STM32F4 SWO 속도)
- CPU 클럭: (보드 설정대로, 예: 168000000 Hz)
- SWO Viewer 열기:
- STM32CubeIDE의 상단 툴바 → “Open Perspective” → “Other…” → “SWV” (혹은 “SWO Trace”)
- SWV Console이 뜨면, SWO Channel 0 활성화
5.2. 디버그 세션 시작
- 디버그 모드(Debug) 로 빌드된 펌웨어를 업로드
- SWV Console 창에와 같이 태스크들이 500ms 주기로 번갈아가며 출력되는지 확인
- 선점형 모드:
- 우선순위(둘 다 2)가 같으므로, 라운드로빈에 따라 1ms 틱마다 선점
- 하지만 vTaskDelay(500) 내부에서 “블록(500ms 동안)” → 실제로 500ms마다 번갈아가며 출력
- 협력형 모드(configUSE_PREEMPTION = 0):
- vTaskDelay 호출 시에만 “다음 태스크”가 실행 → 결과적으로 비슷한 출력이 나오나,
- 이론적으로는 500ms Delay 후 자발적 양도 시에만 교대
- 선점형 모드:
- Hello from Task-1 Hello from Task-2 Hello from Task-1 Hello from Task-2 …
TIP:
- SWV Console → “Variable Trace Enable” → ITM Port 0(채널 0)을 반드시 활성화해야 제대로 보임
- 보드마다 SWO 핀 연결(SWO = PA13, TRACESWO 핀)을 점검
6. 정리
- 스케줄러 시작
- vTaskStartScheduler()를 호출하면 FreeRTOS 커널 변경 → 프로그램 흐름이 태스크로 이양됨
- 정상 동작 중에는 절대 리턴되지 않으므로, 이 함수 아래 코드는 “스케줄러 미시작(메모리 부족)” 예외 처리용
- 태스크 핸들러 (Task Handler)
- 반드시 for (;;) 무한 루프 안에서 작업
- 작업 완료 시점에 태스크를 삭제하려면 vTaskDelete(NULL) 사용
- “각 태스크마다 독립 스택” → 지역 변수를 많이 쓰면 스택 오버플로우 주의
- printf → SWO 출력
- STM32 Cortex-M4/M7에 내장된 ITM → SWO 핀을 통해 메시지 전송 가능
- syscalls.c에서 write()를 오버라이드 → ITM_SendChar() 함수 호출로 SWO에 문자 출력
- SWO Viewer(또는 SWV Console)로 메시지 확인
- 스케줄링 정책
- configUSE_PREEMPTION = 1 → 선점형(Preemptive)
- 우선순위 높은 태스크가 즉시 실행 → 동일 우선순위는 라운드로빈
- configUSE_PREEMPTION = 0 → 협력형(Co-operative)
- 태스크 스스로 vTaskDelay(), taskYIELD() 등을 호출하여 CPU를 양도할 때만 스케줄러 동작
- configUSE_PREEMPTION = 1 → 선점형(Preemptive)
- 빌드 & 디버깅 체크포인트
- configUSE_IDLE_HOOK, configUSE_TICK_HOOK, configCHECK_FOR_STACK_OVERFLOW 등을 “0”으로 설정하여 불필요한 훅 함수 호출을 비활성화
- configMAX_PRIORITIES 값이 태스크 생성 시 지정한 우선순위 값보다 항상 높아야 함
- syscalls.c에서 ITM_SendChar 부분을 올바르게 붙여넣었는지, 주소(0xE0000000, 0xE0000FB0) 및 레지스터 접근이 맞는지 확인
이제 여러분의 STM32 하드웨어에서 FreeRTOS 태스크들이 순차적으로 실행되며, SWO로 printf 메시지를 확인할 수 있습니다. 스케줄러의 동작 원리를 익히고, 협력형과 선점형 모드를 전환해보며 실제 변화를 눈으로 확인하는 것까지 마친다면, FreeRTOS 기반 실시간 애플리케이션 개발의 기초를 탄탄히 다진 것입니다.
끝까지 읽어주셔서 감사합니다!
'임베디드 시스템 > RTOS' 카테고리의 다른 글
| FreeRTOS의 힙(Heap) 메모리 구조와 태스크 생성 과정 (0) | 2025.06.15 |
|---|---|
| FreeRTOS 태스크 Example (0) | 2025.06.14 |
| FreeRTOS의 스케줄링 심화 설명: 선점형 vs 협력형 (0) | 2025.06.13 |
| FreeRTOS 태스크 우선순위 (0) | 2025.06.13 |
| FreeRTOS Task Priority 완전 정복 (0) | 2025.06.12 |