개요
이 글은 FreeRTOS 시리즈의 마지막 글로, Mutex(Mutual Exclusion) 를 이용하여 여러 Task가 하나의 공유 자원을 안전하게 사용하는 방법을 실습합니다.
Mutex는 FreeRTOS에서 제공하는 대표적인 동기화 기법 중 하나로, 여러 Task가 동시에 하나의 자원에 접근하는 상황에서 데이터 충돌을 방지하고 자원의 일관성을 유지하는 데 사용됩니다.
이번 실습에서는 직접 제작한 2채널 PT100 AFE(Analog Front End) 를 이용하여 두 개의 PT100 센서를 각각 독립적인 Task에서 읽고, 두 Task가 하나의 UART2를 공유하여 측정값을 PC로 전송하는 시스템을 구현합니다. 이 과정을 통해 Mutex가 공유 자원을 어떻게 보호하는지 실제 하드웨어를 이용하여 확인해 보겠습니다.
Mutex란 무엇인가?
Mutex(Mutual Exclusion) 는 여러 Task가 하나의 공유 자원(Shared Resource)을 동시에 사용하지 못하도록 보호하는 동기화 기법입니다. 이름 그대로 상호 배제(Mutual Exclusion) 를 의미하며, 하나의 Task가 자원을 사용하고 있는 동안에는 다른 Task의 접근을 제한합니다.
Mutex는 FreeRTOS에서 제공하는 대표적인 동기화 기법 중 하나로, 여러 Task가 동시에 하나의 자원에 접근하는 상황에서 데이터 충돌을 방지하고 자원의 일관성을 유지하는 데 사용됩니다.
예를 들어 두 개의 Task가 하나의 UART를 공유하여 문자열을 전송한다고 가정해 보겠습니다. 두 Task가 아무런 제어 없이 동시에 UART를 사용하면 공유 자원에 대한 접근 순서가 보장되지 않으므로 정상적인 동작을 기대할 수 없습니다.
여러 Task가 하나의 공유 자원에 동시에 접근하여 예측할 수 없는 결과가 발생하는 상황을 Race Condition(경쟁 상태) 이라고 합니다.
Race Condition으로 인해 발생하는 현상은 시스템의 구현 방식이나 드라이버의 동작 방식에 따라 다양하게 나타날 수 있습니다. 예를 들어 두 Task의 문자열이 서로 섞여 출력될 수도 있고, 일부 데이터가 손실되거나 특정 Task의 데이터가 정상적으로 전송되지 않는 현상이 발생할 수도 있습니다.
본 실험에서는 Mutex를 사용하지 않은 상태에서 두 개의 Task가 동시에 UART2를 사용하도록 구현한 결과, 문자열이 서로 섞이지는 않았지만 두 번째 Task의 데이터가 정상적으로 전송되지 않는 현상을 확인하였습니다. 이는 두 Task가 공유 자원인 UART2에 동시에 접근하면서 발생한 Race Condition의 한 사례라고 볼 수 있습니다.
이러한 문제를 해결하기 위해 Mutex를 사용합니다. 먼저 자원을 사용하려는 Task는 Mutex를 획득(Take) 하고, 작업이 완료되면 Mutex를 반환(Give) 합니다. 다른 Task는 Mutex가 반환될 때까지 대기하므로 항상 하나의 Task만 공유 자원을 사용할 수 있습니다.
Mutex는 Semaphore와 달리 Mutex를 획득한 Task만 Mutex를 반환할 수 있습니다. 이러한 특성은 공유 자원을 보다 안전하게 보호하는 데 중요한 역할을 합니다.
동작 순서는 다음과 같습니다.
- Task가 Mutex를 획득(Take)한다.
- 공유 자원(UART2)을 사용한다.
- 작업이 완료되면 Mutex를 반환(Give)한다.
- 대기 중인 다른 Task가 Mutex를 획득하여 동일한 작업을 수행한다.
이와 같이 Mutex는 여러 Task가 하나의 공유 자원을 순차적으로 사용하도록 제어하여 Race Condition을 방지하고 시스템의 안정성을 향상시키는 역할을 합니다.
아래는 Mutex에 대한 FreeRTOS 공식 사이트의 링크 입니다.
아래 그림은 FreeRTOS 사이트에서 제공하고 있는 Mutex 설명에 대한 애니메이션 그림 입니다.

위 그림에서는 Task A와 Task B가 xSemaphoreTake()와 xSemaphoreGive() API를 사용하는 것을 볼 수 있습니다.
이는 FreeRTOS에서 Mutex 역시 xSemaphoreTake()와 xSemaphoreGive() API를 사용하기 때문입니다. API 이름은 Semaphore이지만, 생성된 객체는 Mutex이므로 동작 방식은 Mutex의 특성을 따릅니다.
즉, Mutex를 획득한 Task만 Mutex를 반환할 수 있으며, Priority Inheritance 기능도 지원합니다. 이러한 특성 덕분에 Mutex는 여러 Task가 공유 자원을 안전하게 사용하는 데 적합합니다.
이번 실습에서는 직접 제작한 2채널 PT100 AFE(Analog Front End) 를 이용하여 두 개의 PT100 센서를 각각 독립적인 Task에서 읽고, 두 Task가 하나의 UART2를 공유하여 측정값을 PC로 전송합니다. 그리고 Mutex를 적용하기 전과 후의 동작을 실제 실험을 통해 비교하여, 공유 자원을 안전하게 보호하는 방법을 확인해 보겠습니다.
실험 하드웨어 구성
이번 실습에서는 두 개의 PT100 센서를 각각 독립적인 Task에서 읽고, 두 Task가 하나의 UART2를 공유하여 측정값을 PC로 전송하는 시스템을 구성하였습니다. 이를 통해 공유 자원인 UART2를 Mutex로 보호하는 방법을 실제 하드웨어를 이용하여 확인해 보겠습니다.
실험을 위해 아래 그림과 같이 하드웨어를 구성합니다.

그림의 각 구성 요소는 다음과 같습니다.
- PT100 ×2 : 두 개의 온도 센서입니다. 각각 별도의 FreeRTOS Task에서 온도 값을 읽습니다.
- PT100 AFE(Analog Front End) : PT100의 저항 변화를 STM32의 ADC에서 측정할 수 있는 전압으로 변환합니다.
- STM32C0316-DK : FreeRTOS가 실행되는 메인 제어 보드입니다. 두 개의 PT100 Task와 Mutex를 관리합니다.
- UART2 : 두 Task가 함께 사용하는 공유 자원입니다. Mutex를 이용하여 순차적으로 접근합니다.
- TTL to RS232C Converter : UART TTL 신호를 RS232C 레벨로 변환합니다.
- RS232C Extension Board : DB9 커넥터의 핀을 쉽게 연결하기 위한 브레이크아웃 보드입니다.
- USB to RS232C Cable : RS232C 신호를 USB로 변환하여 PC와 연결합니다.
아래 사진은 실제 하드웨어를 연결하여 구성한 실험 환경입니다.

프로그램 동작 구조
이번 실습에서는 두 개의 PT100 센서를 각각 독립적인 FreeRTOS Task에서 읽습니다. 각 Task는 ADC를 이용하여 센서 값을 측정하고, 공유 자원인 UART2를 통해 PC로 전송합니다.

먼저 각 Task는 ADC를 이용하여 PT100 AFE의 출력 전압을 읽고 ADC 값을 획득합니다. 다음은 공유 자원인 UART2를 사용하기 위해 Mutex를 획득(Take) 합니다.
Mutex를 획득한 Task만 UART2를 사용할 수 있으며, 다른 Task는 Mutex가 반환될 때까지 대기합니다. UART2를 통해 ADC 값을 PC로 전송한 후에는 Mutex를 반환(Give)하여 대기 중인 다른 Task가 UART2를 사용할 수 있도록 합니다.
이와 같이 두 Task는 하나의 UART2를 순차적으로 공유하면서 Race Condition을 방지하고, 안정적으로 ADC 데이터를 PC로 전송할 수 있습니다.
프로그램의 동작 순서는 다음과 같습니다.
- PT100 ADC 값을 읽습니다.
- Mutex를 획득(Take)합니다.
- UART2를 통해 데이터를 PC로 전송합니다.
- Mutex를 반환(Give)합니다.
- 다른 Task가 동일한 과정을 반복합니다.
이와 같이 Mutex는 여러 Task가 하나의 공유 자원을 순차적으로 사용하도록 제어하여 Race Condition을 방지하고 시스템의 안정성을 향상시키는 역할을 합니다.
프로젝트 생성 및 펌웨어 작성
프로젝트 생성 및 기본 Task 동작 확인
이번 실습 프로젝트는 앞서 FreeRTOS가 포팅되어 있는 C9SW-FreeRTOS-M-Queue 프로젝트를 복사하여 새 프로젝트인 C9SW-FreeRTOS-M-Mutex로 구성하였습니다.
그리고 이번 실습에서 사용할 사용자 코드 파일인 pt100_adc.c와 pt100_adc.h를 MyApp 폴더 아래에 추가하였습니다.
이 파일에서는 PT100 ADC 읽기, UART2를 통한 데이터 전송 기능을 구현하며, 각각의 PT100 채널은 독립적인 FreeRTOS Task에서 처리됩니다.
이번 실습에서는 UART2를 제어하기 위한 별도의 모듈은 작성하지 않았습니다. UART2는 STM32 HAL에서 제공하는 UART 드라이버를 직접 사용하였으며, 이는 Mutex를 이용한 공유 자원 보호 과정을 보다 직관적으로 설명하기 위한 것입니다.
아래는 프로젝트 구성이 완료된 화면입니다.

현재는 프로젝트의 기본 구조만 구성한 상태이며, 다음 단계에서는 두 개의 Task를 생성하여 FreeRTOS Scheduler가 정상적으로 동작하는지 먼저 확인해 보겠습니다.
우선 두 개의 Task를 먼저 생성하겠습니다. Task의 생성은 각각의 초기화 함수 내에 xTaskCreat()함수를 사용하여 생성합니다.
아래는 Task를 생성하는 초기화 함수 입니다.

Ch1ReadTask_Init()과 Ch2ReadTask_Init() 함수에서는 각각 StartCh1ReadTask()와 StartCh2ReadTask()를 실행하는 FreeRTOS Task를 생성합니다.
두 Task 모두 Priority 1로 생성하였으며, Stack Size는 128 Words로 설정하였습니다. 이번 실습에서는 두 Task의 우선순위를 동일하게 설정하여 Scheduler가 두 Task를 공평하게 실행하도록 하였습니다.
다음은 실제 Task의 동작을 구현하는 StartCh1ReadTask() 와 StartCh2ReadTask() 를 작성합니다.
먼저 FreeRTOS Scheduler가 정상적으로 동작하는지 확인하기 위해 아래와 같이 간단한 코드부터 작성하였습니다.

StartCh1ReadTask()에서는 200ms 주기로 LED1을 토글하도록 하여 Task가 정상적으로 실행되고 있는지 확인할 수 있도록 하였습니다.
반면 StartCh2ReadTask()에서는 아직 실제 기능을 구현하지 않고 500ms Delay만 수행하도록 하였습니다. 이후 단계에서 PT100 ADC 읽기와 UART2 전송 기능을 이 Task에 추가할 예정입니다.
작성된 Task 초기화 코드와 Task 시작 코드가 실행 되기 위해서는 main.c에서 초기화를 해 주고 FreeRTOS Scheduler를 시작 시켜야 합니다.
아래는 해당 코드 입니다.

실행 결과 LED1이 정상적으로 토글되는 것을 확인하였습니다. 이를 통해 FreeRTOS Scheduler가 정상적으로 동작하고 있으며, 두 개의 Task도 문제없이 생성되어 실행되고 있음을 확인할 수 있었습니다.
PT100 ADC Read
지금까지는 FreeRTOS Scheduler와 두 개의 Task가 정상적으로 동작하는지 확인하기 위해 LED 토글만 수행하였습니다.
이제 실제 PT100 센서를 읽는 기능을 추가하겠습니다. 각 Task는 자신에게 할당된 ADC 채널을 주기적으로 읽어 PT100 AFE에서 출력되는 전압을 ADC 값으로 변환합니다.
이번 실습에서는 PT100 CH1은 ADC1_IN0(PA0), PT100 CH2는 ADC1_IN1(PA1) 을 사용합니다. 각 Task는 자신에게 할당된 ADC 채널만 읽도록 구성하여 두 개의 PT100 센서를 독립적으로 측정합니다.
ADC 설정은 STM32CubeMX의 기본 설정을 사용하되, Sampling Time만 변경하였습니다. PT100 AFE의 출력은 안정적인 아날로그 신호이므로 변환 정확도를 높이기 위해 Sampling Time Common 1과 Sampling Time Common 2를 기본값인 1.5 Cycles에서 79.5 Cycles로 변경하였으며, 나머지 설정은 기본값을 그대로 사용하였습니다.
아래는 변경한 ADC Sampling Time 설정입니다.

이제 PT100 ADC 값을 읽는 함수를 작성하겠습니다.
ADC를 사용하기 위해서는 CubeMX에서 생성한 ADC Handle(hadc1)을 다른 소스 파일에서도 사용할 수 있도록 extern으로 선언해야 합니다.
또한 각 채널의 ADC 값을 저장하기 위한 변수도 함께 선언합니다.
아래는 해당 코드 입니다.

adc_ch1과 adc_ch2는 Live Expressions를 이용하여 실시간으로 ADC 값을 확인하기 위해 volatile로 선언하였습니다.
다음은 실제 ADC 값을 읽는 함수인 PT100_ReadADC() 입니다.

PT100_ReadADC() 함수는 인수로 전달받은 ADC 채널을 선택한 후 ADC 변환을 16회 수행합니다. 읽은 값을 평균하여 노이즈를 줄인 뒤 최종 ADC 값을 반환합니다.
PT100_ReadADC() 함수를 호출하는 두 개의 Task는 다음과 같습니다.

두 Task 모두 500ms 주기로 동작하도록 구성하였습니다. 따라서 각 Task는 500ms마다 한 번씩 자신에게 할당된 ADC 채널을 읽습니다.
ADC 값을 실시간으로 확인하기 위해 adc_ch1과 adc_ch2를 Live Expressions에 추가합니다.
프로젝트를 빌드하여 다운로드한 후 Live Expressions에서 두 채널의 ADC 값이 정상적으로 표시되는지 확인합니다.
아래는 Live Expressions의 화면입니다.

두 채널 모두 ADC 값이 정상적으로 표시되는 것을 확인할 수 있습니다.
ADC 값의 변화를 확인하기 위해 PT100 CH1 센서를 손으로 감싸 센서 온도를 상승시켜 보았습니다.

adc_ch1의 값이 2289에서 2325로 증가한 것을 확인할 수 있습니다. 반면 adc_ch2는 거의 변화하지 않아 두 채널이 독립적으로 동작하고 있음을 확인할 수 있었습니다.
이와 같이 두 개의 PT100 채널에서 ADC 값을 정상적으로 읽을 수 있음을 확인하였습니다. 다음 단계에서는 읽은 ADC 값을 UART2를 통해 PC로 전송하는 기능을 구현하겠습니다.
UART로 ADC 값 전송
이제 읽어 온 ADC 값을 UART2를 통해 PC로 전송하는 코드를 작성하겠습니다.
먼저 pt100_adc.c에서 UART2 Handle을 사용할 수 있도록 아래와 같이 extern으로 선언합니다.
![]()
각 Task에서는 snprintf() 함수를 사용하여 ADC 값을 문자열로 변환한 후, HAL_UART_Transmit() 함수를 이용하여 UART2로 전송합니다.
아래는 각 Task의 코드입니다.

Scheduler의 동작을 확인하는 단계에서는 Delay를 500ms로 설정하였으나, PC 터미널에서 출력 내용을 확인하기에는 너무 빠르게 표시되었습니다. 따라서 UART 출력 확인 단계에서는 두 Task 모두 Delay를 1000ms로 변경하였습니다.
실행 결과, PC의 Terminal 프로그램인 Docklight에서 CH1과 CH2의 ADC 값이 출력되는 것을 확인할 수 있었습니다.

그러나 출력 형태를 보면 조금 특이한 현상이 나타났습니다.
두 Task는 모두 1000ms 주기로 UART 전송을 수행하도록 작성되어 있습니다. 하지만 터미널에서는 CH1과 CH2가 번갈아 표시되면서, 각 채널 기준으로는 약 2초마다 한 번씩 출력되는 것을 확인할 수 있었습니다.
즉, 전체 UART 출력은 약 1초 간격으로 나타나지만, CH1만 보면 2초 간격, CH2만 보아도 2초 간격으로 출력됩니다.
이는 두 Task가 하나의 공유 자원인 UART2를 사용하고 있음에도, UART2 접근 구간이 별도로 보호되지 않았기 때문에 의도한 출력 주기가 보장되지 않은 결과로 볼 수 있습니다.
따라서 다음 단계에서는 Mutex를 적용하여 UART2 전송 구간을 보호하고, 두 Task가 안정적으로 UART2를 공유하도록 수정하겠습니다.
Mutex 적용
Mutex를 적용하는 코드를 작성해 보도록 하겠습니다.
Mutex는 아래와 같이 4가지 단계를 거쳐 적용을 할 수 있습니다.
- 전역 Mutex Handle 선언
- Mutex 생성 함수
- main.c에서 Scheduler 시작 전에 호출
- Task에서 UART 전송 구간 보호
먼저 pt100_adc.c에서 UART2를 보호하기 위한 Mutex Handle을 선언합니다.
![]()
다음으로 xSemaphoreCreateMutex() 함수를 사용하여 Mutex를 생성하는 초기화 함수를 작성합니다.

생성한 Mutex는 Scheduler가 시작되기 전에 한 번만 생성하면 되므로 main.c에서 UART_Mutex_Init() 함수를 호출합니다.

마지막으로 두 Task에서 UART2를 사용하는 구간을 xSemaphoreTake()와 xSemaphoreGive()로 감싸 공유 자원을 보호합니다.

Mutex 적용 결과 각 Task의 주기인 1초가 정확하게 적용되어 1초마다 2개의 값이 거의 동시에 표시가 되는 것이 확인 되었습니다.

실행 결과 정리
Mutex를 적용한 후 다시 실행 결과를 확인하였습니다.
이전에는 두 Task 모두 1000ms 주기로 동작하도록 작성하였음에도 CH1과 CH2가 번갈아 출력되어 각 채널의 출력 주기가 약 2초로 나타났습니다.
그러나 UART2 전송 구간에 Mutex를 적용한 후에는 CH1과 CH2 모두 원래 의도한 1초 주기를 유지하며 출력되는 것을 확인할 수 있었습니다.
Docklight에서도 CH1과 CH2의 ADC 값이 같은 주기 안에서 연속적으로 출력되어 두 Task가 UART2를 안정적으로 공유하고 있음을 확인할 수 있었습니다.
이를 통해 Mutex 적용 전후의 차이를 실제 실험으로 확인할 수 있었습니다.
결론
이번 실습에서는 FreeRTOS의 Mutex(Mutual Exclusion) 를 이용하여 두 개의 Task가 하나의 공유 자원인 UART2를 안전하게 사용하는 방법을 실습하였습니다.
실습에서는 PT100 센서를 이용하여 두 개의 FreeRTOS Task를 구성하고, 동일한 UART2를 공유하는 환경에서 Mutex 적용 전후의 동작을 비교해 보았습니다. 이를 통해 공유 자원에 대한 접근을 제어하는 Mutex의 역할과 필요성을 실제 하드웨어에서 확인할 수 있었습니다.
이번 글를 끝으로 STM32C0316-DK를 이용한 FreeRTOS 실습 시리즈를 마무리합니다.
지금까지의 시리즈에서는 FreeRTOS-Kernel을 직접 포팅하는 과정부터 Task, Queue, Binary Semaphore, Mutex까지 FreeRTOS의 핵심 기능을 실제 하드웨어를 이용하여 단계적으로 실습해 보았습니다.
이 시리즈가 FreeRTOS를 처음 시작하는 분들에게 내부 동작을 이해하고 직접 실습해 보는 데 도움이 되었기를 바랍니다.