지노랩 /JinoLab

FreeRTOS 태스크 시작과 SWO 출력 설정 (ITM_SendChar 적용) 본문

임베디드 시스템/RTOS

FreeRTOS 태스크 시작과 SWO 출력 설정 (ITM_SendChar 적용)

지노랩/JinoLab 2025. 6. 14. 09:55

이 강의에서는 이전에 생성한 두 개의 태스크(Task-1, Task-2)를 실제로 스케줄러에 올려 실행하고, SWO(Single Wire Output)를 통해 printf() 메시지를 출력하도록 설정하는 과정을 단계별로 자세히 설명합니다.


목차

  1. 스케줄러 시작
  2. 태스크 핸들러 구현
  3. SWO 출력을 위한 ITM_SendChar 설정
    1. syscalls.c 수정
    2. ITM_SendChar 함수 코드
    3. write() 함수 오버라이드
  4. 전체 코드 정리 및 컴파일 확인
  5. 하드웨어에서 테스트

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로 문자를 내보내도록 변경해야 합니다.

  1. SWO 관련 함수 추가(ITM_SendChar)
    • ST 공식 애플리케이션 노트에 나온 코드를 복사해 syscalls.c 상단에 붙여넣습니다.
    • 이 코드는 Cortex-M3/M4/M7 공통 ITM 레지스터를 직접 제어해 문자를 출력하는 함수입니다.
  2. 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 기준)

  1. 프로젝트 속성 → Debug Configurations…
  2. “Debugger” 탭에서 ”ST-Link GDB Server” 옵션을 선택
  3. “Port”나 “SWO” 탭에서:
    • SWO 활성화 체크
    • Baud Rate: 2 MHz (STM32F4 SWO 속도)
    • CPU 클럭: (보드 설정대로, 예: 168000000 Hz)
  4. SWO Viewer 열기:
    • STM32CubeIDE의 상단 툴바 → “Open Perspective” → “Other…” → “SWV” (혹은 “SWO Trace”)
    • SWV Console이 뜨면, SWO Channel 0 활성화

5.2. 디버그 세션 시작

  1. 디버그 모드(Debug) 로 빌드된 펌웨어를 업로드
  2. SWV Console 창에와 같이 태스크들이 500ms 주기로 번갈아가며 출력되는지 확인
    • 선점형 모드:
      • 우선순위(둘 다 2)가 같으므로, 라운드로빈에 따라 1ms 틱마다 선점
      • 하지만 vTaskDelay(500) 내부에서 “블록(500ms 동안)” → 실제로 500ms마다 번갈아가며 출력
    • 협력형 모드(configUSE_PREEMPTION = 0):
      • vTaskDelay 호출 시에만 “다음 태스크”가 실행 → 결과적으로 비슷한 출력이 나오나,
      • 이론적으로는 500ms Delay 후 자발적 양도 시에만 교대
  3. 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. 정리

  1. 스케줄러 시작
    • vTaskStartScheduler()를 호출하면 FreeRTOS 커널 변경 → 프로그램 흐름이 태스크로 이양됨
    • 정상 동작 중에는 절대 리턴되지 않으므로, 이 함수 아래 코드는 “스케줄러 미시작(메모리 부족)” 예외 처리용
  2. 태스크 핸들러 (Task Handler)
    • 반드시 for (;;) 무한 루프 안에서 작업
    • 작업 완료 시점에 태스크를 삭제하려면 vTaskDelete(NULL) 사용
    • “각 태스크마다 독립 스택” → 지역 변수를 많이 쓰면 스택 오버플로우 주의
  3. printf → SWO 출력
    • STM32 Cortex-M4/M7에 내장된 ITM → SWO 핀을 통해 메시지 전송 가능
    • syscalls.c에서 write()를 오버라이드 → ITM_SendChar() 함수 호출로 SWO에 문자 출력
    • SWO Viewer(또는 SWV Console)로 메시지 확인
  4. 스케줄링 정책
    • configUSE_PREEMPTION = 1 → 선점형(Preemptive)
      • 우선순위 높은 태스크가 즉시 실행 → 동일 우선순위는 라운드로빈
    • configUSE_PREEMPTION = 0 → 협력형(Co-operative)
      • 태스크 스스로 vTaskDelay(), taskYIELD() 등을 호출하여 CPU를 양도할 때만 스케줄러 동작
  5. 빌드 & 디버깅 체크포인트
    • configUSE_IDLE_HOOK, configUSE_TICK_HOOK, configCHECK_FOR_STACK_OVERFLOW 등을 “0”으로 설정하여 불필요한 훅 함수 호출을 비활성화
    • configMAX_PRIORITIES 값이 태스크 생성 시 지정한 우선순위 값보다 항상 높아야 함
    • syscalls.c에서 ITM_SendChar 부분을 올바르게 붙여넣었는지, 주소(0xE0000000, 0xE0000FB0) 및 레지스터 접근이 맞는지 확인

이제 여러분의 STM32 하드웨어에서 FreeRTOS 태스크들이 순차적으로 실행되며, SWO로 printf 메시지를 확인할 수 있습니다. 스케줄러의 동작 원리를 익히고, 협력형과 선점형 모드를 전환해보며 실제 변화를 눈으로 확인하는 것까지 마친다면, FreeRTOS 기반 실시간 애플리케이션 개발의 기초를 탄탄히 다진 것입니다.

끝까지 읽어주셔서 감사합니다!