STM32C0316-DK FreeRTOS Queue 실습 #3 – ADC 조이스틱 이벤트를 다른 Task로 전달하기

개요

지금까지 우리는 FreeRTOS를 수동으로 설치하는 방법과 두 개의 LED Task를 이용한 멀티태스킹의 개념을 살펴보았으며, Semaphore를 이용한 Task 간 이벤트 전달도 실습해 보았습니다.

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

Semaphore가 이벤트의 발생 여부를 전달하는 데 사용되었다면, Queue는 이벤트와 함께 실제 데이터를 전달할 수 있다는 점이 가장 큰 차이점입니다.

실습에는 STM32C0316-DK 보드에 내장된 ADC 조이스틱을 사용합니다. 이 조이스틱은 방향에 따라 서로 다른 ADC 값을 출력하며, Button Task는 이 값을 읽어 LEFT, RIGHT, UP, DOWN과 같은 버튼 이벤트로 변환합니다.

이렇게 생성된 버튼 이벤트는 Queue를 통해 LED Task로 전달되며, LED Task는 수신한 이벤트에 따라 각각의 LED를 제어하게 됩니다.

이를 통해 Queue를 이용한 Producer-Consumer 구조와 Task 간 데이터 전달 방식을 자연스럽게 이해할 수 있을 것입니다.

FreeRTOS Queue에 대한 자세한 사항은 FreeRTOS 공식 사이트에서 확인 할 수 있습니다.

FreeRTOS Queue

하드웨어 구성

FreeRTOS Queue 실습을 위해 아래 그림과 같이 하드웨어를 구성 하겠습니다.

4개의 LED는 개발보드의 28pin Dip Socket의 헤더 핀에 연결되며 실제 회로도는 아래 그림과 같습니다.

STM32C0316-DK 보드에는 방향 입력이 가능한 ADC 조이스틱이 내장되어 있습니다. 조이스틱의 LEFT, RIGHT, UP, DOWN 버튼을 누르면 서로 다른 ADC 값이 출력되며, 본 실습에서는 이 값을 이용하여 버튼 이벤트를 생성합니다.

아래는 Joystic 부분의 회로도 입니다.

조이스틱의 각 방향 버튼은 서로 다른 ADC 값을 출력하므로, Button Task는 ADC 값을 읽어 어떤 버튼이 눌렸는지 판별할 수 있습니다. 판별된 결과는 Queue를 통해 LED Task로 전달됩니다.

또한 Queue를 통해 전달된 버튼 이벤트가 정상적으로 처리되는지 확인하기 위해 브레드보드에 LED 4개를 추가하였습니다.

각각의 LED는 GPIO 출력에 연결되어 있으며, Queue를 통해 전달된 버튼 이벤트에 따라 점등 또는 소등됩니다.

실습에 사용한 GPIO는 다음과 같습니다.

기능 GPIO
ADC Joystick PA4
LED1 PA5
LED2 PA10
LED3 PA9
LED4 PB11

동작 구조

이번 예제는 Joystick Task와 LED Task가 Queue를 통해 서로 데이터를 전달하는 구조입니다. 아래는 그림으로 그 구조를 설명 하고 있습니다.

Joystick Task는 일정 주기로 조이스틱의 방향을 읽습니다. 입력된 방향은 단순한 ADC 값이 아니라 ButtonEvent_t 형식의 이벤트(BTN_UP, BTN_DOWN, BTN_LEFT, BTN_RIGHT, BTN_CENTER)로 변환됩니다.

생성된 이벤트는 xQueueSend()를 이용하여 Queue에 저장됩니다. Queue는 FIFO(First In, First Out) 구조이므로 먼저 저장된 이벤트가 먼저 처리됩니다.

LED Task는 xQueueReceive()를 이용하여 Queue에서 이벤트가 들어오기를 기다립니다. 새로운 이벤트가 도착하면 해당 이벤트를 꺼내어 어떤 버튼이 눌렸는지 판단한 후 LED를 제어합니다.

이번 예제에서는 다음과 같은 방식으로 동작합니다.

  • UP : 해당 LED를 Toggle
  • DOWN : 해당 LED를 Toggle
  • LEFT : 해당 LED를 Toggle
  • RIGHT : 해당 LED를 Toggle
  • CENTER : 모든 LED를 OFF

이와 같이 두 Task는 서로 직접 함수를 호출하지 않습니다. 중간에 Queue를 사용하여 이벤트만 전달하므로 입력 처리와 출력 처리가 완전히 분리됩니다. 이러한 구조는 FreeRTOS에서 가장 많이 사용하는 Task 간 통신 방식 중 하나이며, 기능이 추가되더라도 각 Task를 독립적으로 수정할 수 있다는 장점이 있습니다.

이번 예제의 핵심은 LED를 제어하는 것이 아니라, Queue를 이용하여 한 Task가 생성한 이벤트를 다른 Task가 순서대로 처리하는 구조를 이해하는 것입니다.

ADC 및 GPIO 설정

STM32C0316-DK의 조이스틱은 각각의 버튼이 GPIO에 연결되어 있는 것이 아니라, 저항 분배 회로를 이용한 아날로그 입력 방식입니다.

따라서 조이스틱을 읽기 위해서는 GPIO 입력이 아닌 ADC를 사용해야 합니다.

Joystick은 위 해당 회로에 나와 있는 것과 같이 저항 연결을 통해 각 버튼의 전압 값이 PA4로 인가되는 형태 입니다.

각 방향 버튼은 서로 다른 전압을 발생시키며, ADC는 이 전압을 디지털 값으로 변환합니다.

Select버튼은 Center Button으로 0V가 출력되고 Left: 0.67V, Down: 1.32V, Up: 2.01V, Right: 2.65V가 각각 출력됩니다.

버튼을 눌렀을 때 어떤 버튼이 눌러졌는 지에 대한 확인은 4개의 LED를 사용하므로 이 포트들을 GPIO_Output으로 설정을 하여야 합니다.

해당 GPIO 포트는 다음과 같습니다.

PA5 LED1
PA4 LED2
PA10 LED3
PA9 LED4 

이제 조이스틱과 LED를 사용하기 위한 STM32의 주변장치를 설정해 보겠습니다.

이번 예제에서는 STM32CubeMX를 이용하여 ADC와 GPIO를 설정합니다. 조이스틱은 PA4의 ADC 입력을 사용하여 방향을 판별하고, LED는 4개의 GPIO 출력으로 제어합니다.

먼저 ADC를 설정한 후 LED 출력에 사용할 GPIO를 차례대로 설정하겠습니다.

아래 그림은 PA4를 ADC1_IN4로 설정한 화면입니다.

Parameter Settings는 아래 화면과 같이 설정합니다. CubeMX에서 제시하는 기본 사항을 변경 하지 않고 사용합니다.

LED 제어를 위해 PA5, PA4, PA10, PA9를 출력(Output)으로 설정합니다.

GPIO는 모두 Output Push-Pull 모드로 설정하고, 나머지 옵션은 기본값을 사용하였습니다.

설정이 완료되면 Generate Code를 실행하여 프로젝트를 생성합니다.

이렇게 생성된 초기화 코드는 main()에서 자동으로 호출되므로 별도의 초기화 코드를 작성할 필요는 없습니다.

ADC의 동작 원리와 각 설정 항목에 대한 자세한 내용은 별도의 글에서 다룰 예정이며, 이번 예제에서는 Queue 실습에 필요한 최소한의 설정만 사용하겠습니다.

Joystick Task 생성

이제 조이스틱의 입력을 읽어 Queue로 전달하는 Joystick Task를 작성해 보겠습니다.

Joystick Task는 사용자가 조이스틱을 어느 방향으로 움직였는지 주기적으로 확인하는 역할을 수행합니다. 조이스틱의 방향은 ADC를 통해 읽은 아날로그 값을 이용하여 판단하며, 해당 값을 그대로 사용하는 것이 아니라 프로그램에서 사용할 수 있는 ButtonEvent_t 형태의 이벤트로 변환합니다.

이처럼 하드웨어에서 읽은 ADC 값을 추상화된 이벤트로 변환하면, 다른 Task는 ADC의 동작 방식이나 전압 값을 전혀 알 필요 없이 이벤트만 받아 처리할 수 있습니다. 따라서 입력 처리와 출력 처리가 명확하게 분리되어 프로그램의 구조가 더욱 단순해집니다.

joystick.c 및 joystick.h 파일 생성

먼저 joystick.cjoystick.h 파일을 생성해야 합니다.

이전 프로젝트인 C9SW-FreeRTOS-M-Semaphore를 복사하여 C9SW-FreeRTOS-M-Queue 프로젝트를 만듦니다.

MyApp > src 에 joystick.c를 MyApp > inc 폴더에는 joystick.h를 각각 만듦니다.

아래는 만들어진 화면입니다.

joystick.c 및 joystick.h 코드 구현

joystick.h의 코드는 다음과 같습니다.

joystick.h에는 조이스틱에서 발생하는 이벤트를 정의하는 ButtonEvent_t 열거형(enum)과 Joystick Task에서 사용할 함수의 원형을 선언합니다.

ButtonEvent_t는 조이스틱의 각 방향을 의미하는 이벤트를 정의한 것으로, ADC 값을 직접 전달하는 대신 BTN_LEFT, BTN_RIGHT와 같은 의미 있는 이벤트로 변환하여 다른 Task에 전달하기 위해 사용합니다.

또한 JoystickTask_Init()은 조이스틱 초기화에 사용되며, StartJoystickTask()는 FreeRTOS에서 실행되는 Joystick Task의 진입 함수입니다. 실제 조이스틱 입력을 읽고 이벤트를 생성하는 기능은 이 Task 함수에서 수행하게 됩니다.

joystick.c에서는 FreeRTOS의 Queue 기능과 ADC를 사용하기 위해 아래와 같은 헤더 파일을 include합니다.

조이스틱의 방향을 읽기 위해서는 ADC를 제어하는 ADC_HandleTypeDef가 필요하며, 생성한 이벤트를 Queue로 전달하기 위해 QueueHandle_t도 필요합니다.

이 두 핸들은 main.c에서 전역 변수로 선언되어 있으므로 joystick.c에서는 extern 키워드를 사용하여 참조합니다.

아래는 main.c에 선언된 hadc1buttonQueue입니다.

joystick.c에서는 다음과 같이 extern으로 선언하여 main.c에서 생성된 핸들을 사용할 수 있습니다.

extern은 다른 소스 파일에 정의된 전역 변수를 현재 파일에서도 사용할 수 있도록 선언하는 키워드입니다. 따라서 별도의 객체를 새로 생성하는 것이 아니라 main.c에 생성된 동일한 hadc1buttonQueue를 공유하여 사용하게 됩니다.

다음은 Joystick Task를 생성하는 부분입니다. JoystickTask_Init() 함수에서 FreeRTOS의 xTaskCreate()를 호출하여 StartJoystickTask()를 생성합니다.

xTaskCreate()의 각 인자는 다음과 같은 의미를 가집니다.

  • StartJoystickTask : Task가 시작되면 실행될 함수입니다.
  • “Joystick” : 디버깅 시 표시되는 Task의 이름입니다.
  • 128 : Task에 할당할 Stack 크기입니다.
  • NULL : Task에 전달할 매개변수로, 이번 예제에서는 사용하지 않습니다.
  • 1 : Task의 우선순위입니다.
  • NULL : 생성된 Task Handle을 저장하지 않으므로 NULL을 사용합니다.

이 함수는 실제로 Task를 실행하는 것이 아니라 Joystick Task를 생성하여 FreeRTOS Scheduler에 등록하는 역할을 합니다. Scheduler가 시작되면 StartJoystickTask() 함수가 독립적인 Task로 실행됩니다.

다음으로 FreeRTOS Scheduler에 등록한 StartJoystickTask() 함수를 작성해 보겠습니다. 이 함수에서는 조이스틱의 ADC 값을 읽고 ButtonEvent_t 이벤트로 변환한 후 Queue로 전달하는 실제 동작을 구현합니다.

Joystick Task는 무한 루프 안에서 일정한 주기로 다음과 같은 작업을 반복합니다.

  • ADC 변환 시작
  • ADC 값 읽기
  • ADC 값을 ButtonEvent_t 이벤트로 변환
  • Queue에 이벤트 저장
  • 일정 시간 대기

아래는 전체적인 동작 흐름입니다.

아래는 구현된 코드 입니다.

ADC 값 읽기

무한 루프의 첫 번째 작업은 조이스틱의 ADC 값을 읽는 것입니다.

Read_Joystick_ADC() 함수는 PA4에 입력된 아날로그 전압을 ADC로 변환하여 디지털 값으로 반환합니다. 조이스틱의 방향에 따라 서로 다른 ADC 값이 반환되며, 이 값은 다음 단계에서 어떤 버튼이 눌렸는지 판단하는 데 사용됩니다.

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

ButtonEvent_t 생성

ADC 값을 읽은 후에는 버튼의 방향을 판단하여 ButtonEvent_t 이벤트를 생성합니다.

Get_Button_Event() 함수는 ADC 값을 미리 정의한 전압 범위와 비교하여 BTN_LEFT, BTN_RIGHT, BTN_UP, BTN_DOWN, BTN_CENTER 중 하나의 이벤트를 반환합니다.

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

동일한 버튼 입력 제거

조이스틱을 한 방향으로 계속 누르고 있으면 동일한 ADC 값이 반복해서 읽힙니다. 만약 이 값을 모두 Queue에 저장하면 같은 이벤트가 계속 전달되어 LED가 반복적으로 동작하게 됩니다.

이를 방지하기 위해 이전 이벤트(prevEvent)와 현재 이벤트(currentEvent)를 비교합니다.

버튼이 눌려 있지 않으면 prevEventBTN_NONE으로 초기화하고, 이전 이벤트와 현재 이벤트가 서로 다른 경우에만 다음 단계로 진행합니다.

즉, 버튼이 처음 눌린 순간에만 이벤트를 발생시키고, 같은 버튼을 계속 누르고 있는 동안에는 추가 이벤트를 생성하지 않습니다.

Queue에 이벤트 저장

새로운 버튼 이벤트가 발생하면 Queue를 이용하여 LED Task로 전달합니다.

buttonQueueButtonEvent_t 이벤트를 저장하는 Queue이며, currentEvent를 Queue의 마지막에 저장합니다.

이번 예제에서는 대기 시간을 0으로 설정하여 Queue가 가득 찬 경우에는 기다리지 않고 즉시 함수를 반환하도록 하였습니다. 일반적인 버튼 입력에서는 Queue가 가득 찰 가능성이 거의 없으므로 이러한 방식으로도 충분합니다.

일정 주기로 반복 실행

마지막으로 일정 시간 동안 대기한 후 다시 조이스틱을 읽습니다.

50ms 동안 대기한 후 다시 ADC를 읽기 때문에 조이스틱 입력은 약 20Hz의 주기로 확인됩니다. 이 정도의 주기는 버튼 입력을 처리하기에 충분하며, CPU를 불필요하게 점유하지 않는 장점도 있습니다.

정리

StartJoystickTask()는 조이스틱 입력을 담당하는 Task로, ADC 값을 읽고 버튼 이벤트를 생성한 후 Queue에 저장하는 역할을 수행합니다. 또한 이전 버튼 상태를 비교하여 같은 이벤트가 반복해서 Queue에 저장되지 않도록 처리하였습니다.

이와 같이 입력을 이벤트 형태로 변환하여 Queue에 전달하면 LED Task는 입력 장치에 대한 정보를 알 필요 없이 이벤트만 받아 처리할 수 있습니다. 이러한 입력과 출력의 분리는 FreeRTOS에서 Task를 설계할 때 자주 사용하는 구조입니다.

LED Task 생성

이제 Queue에 저장된 버튼 이벤트를 읽어 LED를 제어하는 LED Task를 작성해 보겠습니다.

앞에서 구현한 Joystick Task는 조이스틱의 방향을 읽어 ButtonEvent_t 이벤트를 생성한 후 Queue에 저장하는 역할을 수행하였습니다. 하지만 Queue에 저장된 이벤트는 다른 Task가 꺼내어 처리하기 전까지는 아무런 동작도 하지 않습니다.

이번에는 Queue에 저장된 이벤트를 xQueueReceive()를 이용하여 순서대로 읽고, 이벤트의 종류에 따라 해당 LED를 제어하는 LED Task를 구현해 보겠습니다.

LED Task는 Queue에서 새로운 이벤트가 전달되기를 기다리며, 이벤트를 수신하면 해당 버튼에 맞는 LED를 제어합니다. 즉, Joystick Task는 이벤트를 생성하는 역할을 담당하고, LED Task는 이벤트를 처리하는 역할을 담당합니다.

이와 같이 입력을 담당하는 Task와 출력을 담당하는 Task를 분리하면 각 Task가 하나의 역할만 수행하게 되어 프로그램의 구조가 단순해지고 유지보수도 쉬워집니다. 또한 두 Task는 Queue를 통해서만 데이터를 주고받기 때문에 서로 직접 의존하지 않는 구조를 만들 수 있습니다.

먼저 led_task.cled_task.h 파일을 생성한 후, LED Task를 구현해 보겠습니다.

led_task.c 및 led_task.h 파일 생성

C9SE-FreeRTOS-M-Queue 프로젝트에서 MyApp > src폴더에 led_task.c, MyApp > inc 폴더에 led_task.h 파일을 각각 만듧니다.

아래는 생성된 모습을 보여주는 Project Explorer의 C9SW-FreeRTOS-M-Queue 프로젝트 폴더 그림 입니다.

LED Task 구현

LED Task를 구현해야 할 차례 입니다.

가장 먼저 해야 할 일을 헤더파일을 만들어서 필요한 함수들을 정의해야 합니다.

아래는 led_task.h파일의 코드 입니다.

LedTask_Init()함수에서는 LED Task를 생성합니다.

led_task.c에서는 위 해더 파일에서 정의한 LedTask_Init() 함수의 구현과 실제 task 함수인 StartLedTask() 함수를 구현합니다.

LedTask_Init() 함수에서는 FreeRTOS의 xTaskCreate()를 호출하여 StartLedTask()를 생성합니다.

아래는 구현된 코드입니다.

xTaskCreate()의 각 인자는 Joystick Task를 생성할 때와 동일한 의미를 갖습니다.

  • StartLedTask : Scheduler가 시작되면 실행될 Task 함수입니다.
  • “LED” : 디버깅 시 표시되는 Task의 이름입니다.
  • 128 : Task에 할당할 Stack 크기입니다.
  • NULL : Task에 전달할 매개변수로, 이번 예제에서는 사용하지 않습니다.
  • 1 : Task의 우선순위입니다.
  • NULL : 생성된 Task Handle을 저장하지 않으므로 NULL을 사용합니다.

이 함수는 LED를 제어하는 것이 아니라 LED Task를 생성하여 FreeRTOS Scheduler에 등록하는 역할을 수행합니다. Scheduler가 시작되면 StartLedTask() 함수가 독립적인 Task로 실행됩니다.

다음으로 Scheduler에 등록한 StartLedTask() 함수를 구현해 보겠습니다. 이 함수에서는 Queue에서 버튼 이벤트를 읽어 이벤트에 맞는 LED를 제어하는 실제 동작을 구현합니다.

StartLedTask()는 Queue에 새로운 버튼 이벤트가 전달되기를 기다립니다. 이벤트가 도착하면 Queue에서 이벤트를 읽은 후, 해당 이벤트에 따라 LED를 제어합니다.

LED Task는 무한 루프 안에서 다음과 같은 작업을 반복합니다.

  • Queue에서 버튼 이벤트 수신
  • 이벤트에 따라 LED 제어
  • 다시 Queue에서 이벤트 대기

아래는 전체적인 동작 흐름입니다.

아래는 구현된 코드 입니다.

StartLedTask()는 LED Task에서 실제로 실행되는 함수입니다. 이 함수는 Queue에서 ButtonEvent_t 이벤트가 들어오기를 기다리다가, 이벤트가 수신되면 해당 이벤트에 맞는 LED를 제어합니다.

먼저 Queue에서 이벤트를 수신합니다.

xQueueReceive()buttonQueue에 저장된 이벤트를 하나 꺼내어 event 변수에 저장합니다. 세 번째 인자인 portMAX_DELAY는 Queue에 데이터가 없을 경우 계속 기다리겠다는 의미입니다. 따라서 LED Task는 불필요하게 반복 실행되지 않고, 새로운 이벤트가 들어올 때까지 대기 상태에 있게 됩니다.

이벤트가 수신되면 switch(event)문을 이용하여 어떤 버튼 이벤트인지 판단하여 해당 LED를 Toggle합니다.

여기서 GPIO_PIN_SET을 사용했는데, 이는 STM32C0316-DK 보드의 LED가 Active Low 방식으로 동작하기 때문입니다. 즉, GPIO 출력이 LOW가 되면 LED가 켜지고, HIGH가 되면 LED가 꺼집니다. 따라서 모든 LED를 끄기 위해 GPIO_PIN_SET을 사용합니다.

이와 같이 LED Task는 조이스틱이나 ADC에 대해 전혀 알 필요가 없습니다. 단지 Queue에서 전달된 ButtonEvent_t 이벤트만 보고 LED를 제어합니다. 이것이 Queue를 이용한 Task 분리 구조의 핵심입니다.

결론

이번 실습에서는 FreeRTOS Queue를 이용하여 두 개의 Task가 서로 데이터를 주고받는 방법을 구현해 보았습니다.

Joystick Task는 조이스틱의 방향을 읽어 ButtonEvent_t 이벤트를 생성하고 이를 Queue에 저장합니다. LED Task는 Queue에서 이벤트를 수신하여 해당 LED를 제어합니다.

이처럼 Queue를 사용하면 두 Task가 서로 직접 함수를 호출하지 않고도 안전하게 데이터를 전달할 수 있습니다. 또한 입력을 담당하는 Task와 출력을 담당하는 Task를 분리함으로써 프로그램의 구조가 단순해지고 유지보수도 쉬워집니다.

이번 예제에서는 조이스틱 이벤트를 전달하는 간단한 구조를 구현하였지만, Queue는 버튼 이벤트뿐만 아니라 센서 데이터, UART 수신 데이터, 측정 결과 등 다양한 정보를 Task 간에 전달하는 데 활용할 수 있습니다. 따라서 FreeRTOS에서 가장 많이 사용되는 Task 간 통신 방법 중 하나라고 할 수 있습니다.

아래는 FreeRTOS 관련 이전 글에 대한 링크 입니다.

STM32C0316-DK에 FreeRTOS-Kernel V11.3.0 수동 설치하기

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

STM32C0316-DK FreeRTOS 실습 #2 – Semaphore를 이용한 Task 간 이벤트 전달

댓글 남기기