STM32C0316-DK FreeRTOS 실습 #1 – 두 개의 Task가 동시에 실행되는 것처럼 보이는 이유

개요

이전 글에서는 STM32C0316-DK 개발보드에 FreeRTOS-Kernel V11.3.0을 수동으로 포팅하고 하나의 LED Task가 정상적으로 동작하는 것을 확인하였습니다.

이전 글은 아래 링크에서 확인할 수 있습니다.
STM32C0316-DK에 FreeRTOS-Kernel V11.3.0 수동 설치하기

이번 글은 시리즈의 두 번째 글이자 FreeRTOS 실습 시리즈의 첫 번째 예제입니다. 두 개의 Task를 생성하여 서로 다른 주기로 LED를 점멸시키고, 단일 코어 MCU에서 여러 Task가 동시에 실행되는 것처럼 보이는 이유를 살펴보겠습니다.

이를 통해 FreeRTOS의 핵심 기능인 Task Scheduling의 기본 동작 원리를 이해해 보겠습니다.

물론 STM32C031의 CPU 코어는 하나이므로 실제로는 한 순간에 하나의 Task만 실행할 수 있습니다. 그럼에도 불구하고 여러 Task가 동시에 동작하는 것처럼 보이는 이유는 FreeRTOS 스케줄러가 매우 짧은 시간 간격으로 실행 중인 Task를 전환하기 때문입니다. 이번 실습에서는 두 개의 LED를 서로 다른 주기로 점멸시켜 이러한 동작을 직접 확인해 보겠습니다.

실습은 빠른 진행을 위해 매번 새로운 프로젝트를 생성하기보다는 이전 프로젝트를 복사하여 이를 기반으로 진행하겠습니다. 실제 개발 현장에서도 기존 프로젝트를 기반으로 기능을 추가하거나 확장하는 경우가 많기 때문에 이러한 방식이 더욱 실용적입니다.

또한 앞으로 실습이 진행될수록 사용자 코드의 양이 점차 늘어날 것을 고려하여 MyApp이라는 별도의 폴더를 생성하고 응용 프로그램 코드는 이 폴더에서 관리하도록 하겠습니다. 이를 통해 STM32CubeIDE가 생성한 코드, FreeRTOS-Kernel 코드, 그리고 사용자가 작성한 코드를 명확하게 구분할 수 있습니다.

따라서 이번 글에서는 프로젝트를 복사하여 새로운 실습 환경을 만드는 방법과 MyApp 폴더를 이용하여 사용자 코드를 체계적으로 관리하는 방법도 함께 살펴보겠습니다.

먼저 이전 프로젝트를 복사하여 새로운 실습 프로젝트를 만드는 것부터 시작해 보겠습니다.

프로그램 환경 구성

프로젝트 복사

개요에서 언급한 것처럼 이번 실습부터는 매번 새로운 프로젝트를 생성하지 않고 기존 프로젝트를 복사하여 사용하도록 하겠습니다. 실습이 진행될수록 프로젝트 설정과 소스 코드가 점차 늘어나기 때문에 매번 처음부터 설정하는 것보다 기존 프로젝트를 기반으로 기능을 확장하는 것이 훨씬 효율적입니다.

다만 프로젝트를 단순히 복사하는 것만으로는 정상적으로 사용할 수 없으며 몇 가지 추가 작업이 필요합니다. 이번 기회에 프로젝트를 복사하여 새로운 실습 프로젝트를 만드는 방법도 함께 살펴보겠습니다.

아래는 프로젝트를 복사한 후 수행할 작업 순서입니다.

  1. 프로젝트 복사
  2. 프로젝트 이름 변경
  3. Debug 폴더 및 Debug.launcg 파일 삭제
  4. IOC 파일 이름 변경
  5. Refresh Policy 변경
  6. Debug Configuration 설정

STM32CubeIDE에서 프로젝트를 복사하여 붙여넣기하면 자동으로 버전 번호가 증가된 새로운 프로젝트 이름을 제안합니다. 예를 들어 기존 프로젝트가 FreeRTOS_LED_V1이라면 복사 후에는 FreeRTOS_LED_V2와 같은 이름으로 생성할 수 있습니다.

이제 프로젝트 복사부터 진행해 보겠습니다.

프로젝트 복사

Project Exploler 창에서 기존 프로젝트인 C9SW-FreeRTOS-Manual-v1.0 에 커서를 위치 시키고 마우스 오른쪽을 클릭하여 Copy를 하고 같은 창에 Paste를 하면 아래와 같은 화면이 표시 됩니다.

버전이 자동으로 1.0에서 1.1로 변경되어 표시되는 것을 볼 수 있습니다.

프로젝트 이름 변경

위 창에서 자동으로 변경된 이름을 그대로 사용해도 되고 원하는 이름으로 변경해도 됩니다.

이번 실습은 앞으로 이어질 FreeRTOS 실습 시리즈의 첫 번째 예제이므로 각 프로젝트를 쉽게 구분할 수 있도록 기능 중심의 이름을 사용하겠습니다.

본 글에서는 두 개의 LED Task를 이용한 멀티태스킹 실습을 진행할 예정이므로 프로젝트 이름을 C9SW-FreeRTOS-M-LedTask 로 변경하겠습니다.

프로젝트 이름만 보아도 실험 내용을 쉽게 파악할 수 있으므로 별도의 버전 번호는 사용하지 않겠습니다.

따라서 새로 생성할 프로젝트의 이름은 C9SW-FreeRTOS-M-LedTask 입니다.

여기서 M은 Manual을 의미하며, STM32CubeMX의 FreeRTOS 미들웨어를 사용하지 않고 직접 포팅한 프로젝트임을 나타냅니다.

아래는 복사를 하여 새로 생성된 프로젝트 입니다.

Debug 폴더 및 Debug.launch 파일 삭제

새로 생성된 프로젝트를 보면 Debug 폴더가 존재하는 것을 확인할 수 있습니다.

이 폴더에는 이전 프로젝트에서 생성된 빌드 결과물과 관련 파일들이 포함되어 있습니다. 프로젝트를 복사한 후에도 해당 파일들이 그대로 유지되므로 새로운 프로젝트 환경을 깨끗하게 정리하기 위해 삭제하도록 하겠습니다.

Debug 폴더를 삭제하더라도 소스 코드는 영향을 받지 않으며, 이후 Build를 수행하면 STM32CubeIDE가 필요한 파일들을 자동으로 다시 생성합니다.

또한 이전 프로젝트의 Debug 설정 파일인 C9SW-FreeRTOS-Manual-v1.0 Debug.launch 파일도 함께 복사되므로 삭제하겠습니다.

따라서 Debug 폴더와 Debug.launch 파일을 삭제한 후 Build를 진행하겠습니다.

IOC 파일 이름 변경

프로젝트 이름을 변경하였지만 IOC 파일은 여전히 이전 프로젝트 이름을 사용하고 있습니다. 프로젝트를 관리하는 데에는 큰 문제가 없지만, 프로젝트 이름과 IOC 파일 이름이 서로 다르면 혼동이 발생할 수 있습니다.

따라서 IOC 파일 이름도 현재 프로젝트 이름에 맞추어 변경하도록 하겠습니다.

기존 파일:

C9SW-FreeRTOS-Manual-v1.0.ioc
변경 후:
C9SW-FreeRTOS-M-LedTask.ioc
아래는 IOC 파일 이름을 변경한 후의 화면입니다.

IOC 파일 이름을 변경한 후에는 STM32CubeIDE를 한 번 Refresh 하거나 프로젝트를 다시 열어 정상적으로 인식되는지 확인합니다.

Refreh Policy 변경

프로젝트 이름과 IOC 파일 이름을 변경한 후에는 Refresh Policy 설정도 함께 확인해 주는 것이 좋습니다.

프로젝트를 복사한 경우 Refresh Policy에 이전 프로젝트 정보가 남아 있을 수 있습니다. 대부분의 경우 큰 문제는 발생하지 않지만 프로젝트 설정을 정리하는 차원에서 현재 프로젝트 이름에 맞게 수정하도록 하겠습니다.

따라서 현재 프로젝트 이름에 맞도록 Refresh Policy를 수정하겠습니다.

STM32CubeIDE에서 프로젝트를 선택한 후 Properties → C/C++ Build → Refresh Policy 메뉴로 이동합니다.

사실 이름을 바꾼다음 Refresh 명령을 수행하면 Refresh Policy도 자동으로 바뀌어 있습니다. 혹시 Refresh를 하지 않았을 경우에는 이것이 이전 프로젝트 이름으로 되어 있을 수 있습니다. 확인을 하고 이전 프로그램일 경우에는 삭제를 하고 Add Resource를 하여 추가를 해 주면 됩니다.

아래 화면과 같이 표시 됩니다.

설정을 변경한 후 Apply and Close 버튼을 눌러 저장합니다.

이 작업을 통해 프로젝트 이름 변경 이후에도 STM32CubeIDE가 프로젝트 파일을 올바르게 인식하고 관리할 수 있도록 정리할 수 있습니다.

프로젝트 정리가 완료되었으므로 이제 Build를 수행하여 Debug 폴더를 다시 생성하고 프로젝트가 정상적으로 동작하는지 확인하겠습니다.

Build 수행 및 Debug Configuration 확인

지금까지 프로젝트 이름 변경, Debug 폴더 정리, IOC 파일 이름 변경, Refresh Policy 수정 등의 작업을 진행하였습니다.

먼저 Build를 수행하여 프로젝트가 정상적으로 빌드되는지 확인합니다.

이번 프로젝트는 이전에 정상 동작하던 프로젝트를 복사하여 생성한 것이므로 특별한 문제가 없다면 오류 없이 Build가 완료되어야 합니다.

Build가 성공적으로 완료되었다면 다음으로 Debug Configuration을 확인하겠습니다.

메뉴에서 Debug As → Debug Configurations… 를 선택합니다.

아래와 같은 화면이 표시됩니다.

Debug Configurations를 클릭하면 아래와 같은 화면이 나타납니다.

좌측 목록에서 STM32 C/C++ Application 을 선택합니다.

그러면 아래와 같은 화면이 표시됩니다.

붉은색 박스로 표시한 항목을 확인합니다.

  • Project : C9SW-FreeRTOS-M-LedTask
  • C/C++ Application : Debug/C9SW-FreeRTOS-M-LedTask.elf

두 항목 모두 현재 프로젝트 이름과 일치하는지 확인합니다.

만약 이전 프로젝트 이름이 표시된다면 올바른 프로젝트를 다시 선택하거나 새로운 Debug Configuration을 생성해야 합니다.

설정이 올바른 것을 확인하였다면 화면 우측 하단의 Debug 버튼을 클릭합니다.

프로그램이 STM32C0316-DK 보드에 다운로드되고 실행이 시작됩니다. 이전 실습과 마찬가지로 LED가 정상적으로 점멸하는 것을 확인할 수 있습니다.

이를 통해 프로젝트 복사 및 환경 설정이 정상적으로 완료되었음을 확인할 수 있습니다.

사용자 코드 관리를 위한 MyApp 폴더 만들기

개발이 진행되다 보면 프로그램의 규모가 점점 커지게 됩니다. 초기에는 main.c 파일에 한두 개의 함수만 추가하여 실험할 수 있지만, 기능이 늘어날수록 소스 코드가 복잡해지고 관리가 어려워집니다.

따라서 앞으로 진행할 실습에서는 사용자 프로그램을 체계적으로 관리하기 위해 MyApp이라는 별도의 폴더를 만들어 사용하겠습니다. 이후 추가되는 응용 프로그램 코드는 모두 이 폴더에서 작성하고 관리할 예정입니다.

폴더 이름은 반드시 MyApp일 필요는 없습니다. 중요한 것은 STM32CubeIDE가 생성한 코드와 사용자가 작성한 코드를 구분하여 관리할 수 있도록 하는 것입니다. 본 글에서는 설명의 편의를 위해 MyApp이라는 이름을 사용하겠습니다.

아래 그림과 같이 메뉴를 선택하여 프로젝트 밑에 MyApp 폴더를 만듦니다.

동일한 방법으로 MyApp 폴더 밑에 inc폴더와 src 폴더를 만듦니다.

아래는 완성된 폴더 구조 입니다.

앞으로 작성하는 사용자 코드는 모두 MyApp 폴더에서 관리할 예정입니다. 일반적으로 헤더 파일은 inc 폴더에, 소스 파일은 src 폴더에 저장합니다. STM32CubeIDE가 생성한 Core 폴더를 수정하지 않고 사용자 코드를 별도로 관리하면 프로젝트 규모가 커지더라도 유지보수가 훨씬 수월해집니다.

마지막으로 MyApp/inc 폴더를 Include Path에 추가하고 MyApp/src 폴더를 Source Location에 포함시켜 사용자 코드를 빌드할 수 있도록 설정합니다.

LED 하드웨어 구성

실험을 위해서는 실제로 Led가 보드에 부착이 되어 있어야 하고 프로그램에서 해당 Led가 어떤 것인지 알아야 합니다.

우리는 두 개의 Led가 필요한데 하나는 보드에 기본적으로 부착되어 있는 Led를 사용하고 나머지 하나는 외부에 회로를 구성하여 Led를 연결하는 방식으로 진행 하겠습니다.

보드에 장착된 Led는 PA5에 연결되어 있고 라벨 이름을 Led1이라고 정하였습니다. Led2는 PA6핀을 사용하고 Led2라고 정하겠습니다.

아래는 Led1과 Led2를 설정한 CubeMX 화면입니다.

외부에 연결되는 Led2는 브레드보드를 이용하여 회로를 구성하였고 20Pin CN1 Connector에 아래 그림과 같이 연결하였습니다.

두 개의 Led를 지정하는 하드웨어 구성을 마지막으로 모든 시험환경 설정이 완료 되었습니다.

다음 장에서는 실제로 Led Task를 작성하여 두 개의 Task가 동시에 동작하는 지 실험을 해 보겠습니다.

LED Task 작성

지금까지 프로젝트 환경을 구성하고 사용자 코드를 관리하기 위한 MyApp 폴더를 생성하였으며 Led 점멸을 위한 하드웨어 구성까지 마쳤습니다.

이제 실제로 LED를 점멸시키기 위한 LED Task를 작성해 보겠습니다.

위에서 생성한 MyApp 폴더의 src 폴더에는 led_task.c 파일을 생성하고, inc 폴더에는 led_task.h 파일을 생성합니다.

앞으로 LED Task와 관련된 코드는 모두 이 두 파일에서 관리하겠습니다.

LED Task 헤더 파일 작성

헤더 파일에는 Task 초기화 프로시저인 LedTask_Init() 함수를 정의 합니다.

LedTask_Init() 함수에서는 FreeRTOS의 xTaskCreate() 함수를 이용하여 두 개의 LED Task를 생성합니다.

LED Task 소스파일 작성

먼저 작성한 led_task.h 헤더 파일을 include 시키고 main.h, FreeRTOS.h 그리고 task.h파일을 include 합니다.

다음은 StartLed1Task()와 StartLed2Task()를 선언합니다. 이 두 함수는 FreeRTOS에서 생성되는 Task의 진입 함수(Entry Function)이며 실제로 LED를 점멸시키는 역할을 담당합니다.

두 함수는 led_task.c 파일 내부에서만 사용되므로 static으로 선언하였습니다. 이렇게 하면 다른 소스 파일에서는 접근할 수 없으며, LED Task 모듈 내부에서만 사용할 수 있습니다.

반면 LedTask_Init() 함수는 main.c에서 호출해야 하므로 헤더 파일에 선언하였으며 static을 사용하지 않았습니다.

LedTask_Init() 함수에서는 FreeRTOS의 xTaskCreate() 함수를 사용하여 두 개의 LED Task를 생성합니다.

첫 번째 인자는 Task의 진입 함수이며, 두 번째 인자는 Task 이름입니다. 세 번째 인자는 Task Stack 크기이고, 다섯 번째 인자는 Task 우선순위를 의미합니다.

아래는 LedTask_Init()함수의 구현한 코드 입니다.

다음은 위에서 static으로 정의한 두 개의 Led를 점멸 시키는 task를 구현하는 코드 입니다.

두 Task의 우선순위는 동일하게 설정하였습니다. 따라서 두 Task가 실행 가능한 상태(Ready State)에 있을 때 FreeRTOS 스케줄러는 각 Task에 CPU 실행 시간을 번갈아 할당하게 됩니다.

StartLed1Task() 함수는 무한 루프 안에서 LED1의 상태를 반전시킨 후 500ms 동안 대기합니다. LED의 상태를 반전시키는 동작은 HAL_GPIO_TogglePin() 함수를 이용하여 수행하였으며, vTaskDelay(pdMS_TO_TICKS(500))를 사용하여 500ms 동안 Task를 대기 상태로 전환하였습니다.

StartLed2Task() 함수도 동일한 구조로 작성하였으며 대기 시간을 100ms로 설정하였습니다. 따라서 LED2는 LED1보다 더 빠른 주기로 점멸하게 됩니다.

두 Task는 서로 다른 주기로 동작하지만 동시에 생성되어 실행됩니다. 실제 실행 결과를 확인하면 두 개의 LED가 독립적으로 동작하는 것을 볼 수 있으며, 이를 통해 FreeRTOS의 멀티태스킹 동작을 확인할 수 있습니다.

Task 실행

led_task.c와 led_task.h를 통해서 두 개의 Led를 점멸하는 코드를 작성 하였습니다.

이제는 두 개의 Task를 실행 시킬 차례 입니다.

main.c에서 led_task.h를 include 합니다.

다음으로 main() 함수 내에서 주변기기들을 초기화 하는 코드 뒤에서 LedTask_Init() 함수를 호출하고 FreeRTOS의 스케줄러를 실행 시키는 함수인 vTaskStartScheduler() 함수를 호출하여 task를 실행 시킵니다.

위 코드에서 보는 바와 같이 지금까지 일반적인 MCU 프로그램에서 사용하던 while(1) 루프에는 어떠한 코드도 작성되어 있지 않습니다.

LedTask_Init() 함수에서 두 개의 LED Task를 생성한 후 vTaskStartScheduler() 함수를 호출하면 FreeRTOS 스케줄러가 실행됩니다. 이후부터는 FreeRTOS가 각 Task의 실행을 관리하게 되며, 프로그램의 주 실행 흐름은 더 이상 while(1) 루프가 아니라 각각의 Task 함수 내부에서 동작하게 됩니다.

즉, StartLed1Task()와 StartLed2Task() 함수가 실제 프로그램의 동작을 수행하며, FreeRTOS 스케줄러는 각 Task의 상태를 관리하면서 적절한 시점에 CPU 실행 시간을 할당하게 됩니다.

따라서 FreeRTOS 환경에서는 기존의 while(1) 루프 중심 구조에서 Task 중심 구조로 프로그램이 변경된다고 볼 수 있습니다.

프로그램을 보드에 다운로드하여 실행한 결과 LED1은 500ms 주기로, LED2는 100ms 주기로 점멸하는 것을 확인할 수 있었습니다.

두 LED가 서로 다른 주기로 독립적으로 동작하는 것을 통해 두 개의 Task가 정상적으로 생성되고 실행되고 있음을 확인할 수 있었습니다.

두 개의 Task가 동시에 실행되는 것처럼 보이는 이유

STM32C031은 단일 코어 MCU입니다. 그러나 실행 결과에서 확인한 바와 같이 두 개의 Task가 동시에 실행되는 것처럼 보이고 있습니다.

실제로는 한 순간에 하나의 Task만 실행할 수 있는데 왜 이렇게 보이는 것일까요?

이번 장에서는 FreeRTOS 스케줄러의 동작 원리를 통해 그 이유를 알아보겠습니다.

아래 그림은 사용자의 관점에서 바라본 Task의 실행 모습입니다. CPU가 단일 코어임에도 불구하고 각 Task가 동시에 실행되는 것처럼 보이는 것을 확인할 수 있습니다.

그러나 실제로는 한 순간에 하나의 Task만 실행됩니다. FreeRTOS 스케줄러는 매우 짧은 시간 간격으로 실행 중인 Task를 전환(Context Switching)하며, 이 과정이 반복되면서 여러 Task가 동시에 실행되는 것처럼 보이게 됩니다.

아래 그림은 CPU가 실제로 Task를 실행하는 모습을 나타낸 것입니다.

그림에서 볼 수 있듯이 CPU는 한 순간에 하나의 Task만 실행하고 있습니다. Task 1이 실행되다가 일정 시간이 지나면 Task 2로 전환되고, 다시 Task 3으로 전환되는 방식으로 동작합니다.

이러한 전환이 매우 빠른 속도로 반복되기 때문에 사용자는 여러 Task가 동시에 실행되는 것처럼 느끼게 됩니다. 이를 멀티태스킹(Multitasking)이라고 하며, FreeRTOS의 핵심 기능 중 하나입니다.

상세한 내용은 FreeRTOS 공식 사이트에서 확인 할 수 있습니다.

RTOS Fundamentals

결론

이번 글에서는 STM32C0316-DK에서 FreeRTOS를 이용하여 두 개의 LED Task를 생성하고 실행해 보았습니다.

프로젝트를 복사하여 새로운 실습 환경을 구성하는 방법을 살펴보았고, 사용자 코드를 관리하기 위한 MyApp 폴더도 만들어 보았습니다.

또한 두 개의 LED Task를 생성하여 각각 500ms와 100ms 주기로 동작하도록 구현하였으며, 단일 코어 MCU에서도 여러 Task가 동시에 실행되는 것처럼 보이는 이유를 확인하였습니다.

실제로는 CPU가 한 순간에 하나의 Task만 실행하지만 FreeRTOS 스케줄러가 매우 짧은 시간 간격으로 Task를 전환하기 때문에 여러 Task가 동시에 실행되는 것처럼 보이게 됩니다.

이번 실습을 통해 FreeRTOS 멀티태스킹의 가장 기본적인 동작 원리를 확인할 수 있었습니다.

다음 글에서는 Queue를 이용하여 Task 간에 데이터를 전달하는 방법을 실습해 보겠습니다.

댓글 남기기