주요 내용으로 건너뛰기

운영 체제

서론

이 섹션에서는 그래픽 사용자 인터페이스 애플리케이션의 운영 체제 사용에 대해 알아보겠습니다.

임베디드 장치는 갈수록 진화하고 있습니다. 대부분의 운영 체제가 그래픽 사용자 인터페이스뿐만 아니라 복잡한 제어 알고리즘과 작업까지 처리하기 때문입니다.

예를 들면 모터 제어, 데이터 수집, 보안 관련 작업 등이 있습니다. 오늘날 대부분의 장치에는 데이터 센터와의 통신을 위한 TCP/IP 같은 통신 프로토콜 스택이, 또는 다른 로컬 장치와의 통신을 위한 블루투스 같은 무선 스택이 포함되어 있습니다.

다른 작업과 사용자 인터페이스의 교차 실행

에그 타이머와 같이 그래픽 사용자 인터페이스와 몇 가지 단순한 지원 작업이 포함된 장치에서는 사용자 인터페이스 코드를 중심으로 전체 애플리케이션을 구성할 수 있습니다. 이러한 경우에는 애플리케이션이 정기적인 사용자 인터페이스 업데이트를 제외하고 다른 작업을 거의 수행하지 않기 때문에 다른 실행 작업을 사용자 인터페이스 코드에 삽입하는 데 별 문제가 없습니다.

하지만 모터 조절과 같은 타이밍 요건과 함께 “백그라운드에서 실행”되는 고급 기능을 추가하게 되면 타이밍 요건을 지원하는 동시에 두 가지 작업을 사용자 인터페이스에 통합하기가 어려워집니다.

이전 섹션에서도 언급했지만 그래픽 엔진은 새로운 프레임을 끊임없이 그려서 자연스러운 사용자 인터페이스를 지원해야 합니다. 그래픽 엔진이 다른 작업을 실행하느라 그리기를 멈춘다면 프레임 속도가 떨어지게 됩니다. 마찬가지로 다른 작업들이 프레임 사이에, 즉 유휴 시간에만 실행된다고 가정할 경우 사용자 인터페이스가 복잡한 장면을 렌더링하여 유휴 시간이 비교적 적을 때는 다른 작업들이 어렵습니다. 이러한 이유들로 인해 UI 작업을 다른 복잡한 작업과 번갈아 가며 수동으로 실행하기가 매우 까다롭습니다.

예시

이제부터 디스플레이가 장착된 블루투스 스피커를 개발한다고 가정하겠습니다. 여기서 크게 세 가지 작업을 수행해야 하는데, 바로 그래픽 사용자 인터페이스 실행, 스피커에 음악 전송, 다른 장치와 통신할 수 있는 블루투스 스택 처리입니다.

사용자 인터페이스 기반 애플리케이션 아키텍처의 성능 저하 여부를 알아보는 것은 어렵지 않습니다. 예를 들어 음악 코드를 사용자 인터페이스와 블렌딩한 후 사용자 인터페이스의 버튼 이벤트 핸들러로 재생을 시작할 수 있도록 코드를 삽입합니다. 이제 음악을 시작하는 동안 사용자 인터페이스가 잠깁니다. 애니메이션 실행도 중단됩니다.

일반적으로 사용자 인터페이스의 응답은 음악 작업(시작, 정지, 다음 등)이 실행되는 시간에 따라 달라집니다. 이는 일반적인 문제로, 나중에 자세히 살펴보겠습니다.

그렇다면 블루투스에서도 음악을 시작하고 싶다면 어떻게 될까요? 사용자 인터페이스도 어떤 식으로든 여기에 연관되어야 할까요?

또한 음악이 멈추지 않게 하려면 음악 작업에 우선순위를 어떻게 부여해야 할까요? 이와 동시에 실행할 음악 작업이 없을 때는 사용자 인터페이스의 실행 성능을 극대화하려고 합니다.

작업, 통신 수단 및 동기화를 모두 갖춘 운영 체제를 사용하면 이러한 문제를 모두 해결할 수 있습니다.

RTOS

실시간 운영 체제(RTOS)는 간단한 소프트웨어로서 다양한 서비스로 애플리케이션을 지원하는 동시에 컴퓨팅 리소스를 애플리케이션 작업에 할당합니다.

RTOS를 사용하면 독립적이지만 서로 연계된 작업들로 애플리케이션을 구성할 수 있습니다. 구조화를 마친 작업들은 필요할 때 우선순위에 따라 RTOS를 통해 함께 실행됩니다.

작업을 높은 우선순위와 낮은 우선순위로 나눌 수도 있습니다. 블루투스 데이터가 수신되면 데이터를 버퍼에서 매우 빠르게 읽어와서 용량이 더 큰 애플리케이션 버퍼에 저장해야 한다고 가정하겠습니다. 이때 데이터 처리가 잠시 지연될 수 있지만 이러한 방식으로 결국 두 가지 블루투스 작업을 마치게 됩니다.

예를 들어 아래와 같이 main에서 네 가지 작업을 시작합니다.

int main() {
...
os_start_task(gui_task, medium_priority);
os_start_task(music_task, low_priority);
os_start_task(bt_comm_task, high_priority);
os_start_task(bt_appl_task, low_priority);
os_start_scheduler();
}

음악 작업에서도 비슷한 분할이 가능하여 데이터를 스피커에 전송하는 작업에 높은 우선순위를, 그리고 재생하는 노래를 제어하는 작업이나 사용자 인터페이스에 알림을 전송하는 작업에 낮은 우선순위를 부여할 수 있습니다.

위와 같이 우선순위를 다르게 사용하면 처리할 데이터가 있을 때 bt_comm_task가 실행되고, 그렇지 않을 때 사용자 인터페이스 작업이 실행됩니다. 사용자 인터페이스 작업이 디스플레이 신호를 기다릴 때는 우선순위가 낮은 작업 2개가 실행됩니다. 운영 체제 스케줄러가 이러한 시간 분배를 자동으로 처리합니다.

일반적인 TouchGFX 애플리케이션에서는 사용자 인터페이스가 모든 프레임에서 디스플레이 신호를 기다릴 뿐만 아니라 그래픽 가속기인 ChromArt가 요소 그리기를 마칠 때까지 주기적으로 기다립니다. 이는 잠깐씩 정지되는 일이 많고, 이때 우선순위가 낮은 작업들이 실행된다는 것을 의미합니다. 우선순위가 높은 작업들이 기다릴 때 운영 체제 스케줄러가 MCU를 자동으로 변경하여 우선순위가 낮은 작업들을 실행합니다.

작업 통신

다수의 작업들을 사용할 때는 작업 사이에서 안전하게 통신할 수 있는 방법도 필요합니다. 간단한 예로, 사용자 인터페이스에서 음악 작업으로 통신하는 경우를 들어보겠습니다. 여기에서는 다른 무엇보다 gui_task가 음악 재생을 시작하도록 요청할 때까지 음악 작업이 기다려야 합니다. 이때는 메시지 대기열을 사용하면 간단하게 해결할 수 있습니다. 그러면 대기열에 메시지가 있을 때까지 음악 작업이 대기합니다. 이후 메시지가 대기열로 전송되고 우선순위가 더욱 높은 작업이 없으면 스케줄러가 음악 작업을 실행합니다.

   ...
music_task_input_queue = os_create_queue(10); //10 element queue
...

사용자 인터페이스에서 "Play"를 누르면 메시지가 음악 작업의 대기열로 전송됩니다.

void ScreenMusic::handlePlayPressed()
{
os_send_message(music_task_input_queue, play_message);
}

음악 작업은 대기열을 읽으면서 메시지를 기다립니다. 이를 통해 메시지가 수신될 때까지 음악 작업이 차단됩니다.

...
Message message;
os_receive_message(music_task_input_queue, &message);

메시지가 음악 작업 대기열에 추가된 후에도 사용자 인터페이스는 계속 실행되어 프레임을 최대한 빠르게 렌더링합니다. 재생 메시지를 바로 처리하는 데 시간을 소비하지 않습니다. 오히려 렌더링이 완료되어 UI 작업이 다음 프레임을 렌더링할 때까지 기다리는 동안 스케줄러가 음악 작업으로 바꿔 실행하여 수신되는 메시지를 처리합니다.

마찬가지로 사용자 인터페이스에도 입력 대기열을 부여할 수 있습니다. 그러면 예를 들어 노래가 끝났을 때 음악 작업이 알림 메시지를 전송할 수 있습니다. 사용자 인터페이스가 메시지를 기다릴 필요는 없지만 차단 없이 메시지 유무를 빠르게 확인하여 메시지를 읽습니다.

이러한 설정은 시스템에서 작업 사이에 매우 느슨한 연결을 구성합니다. 실제로 사용자 인터페이스를 사용하지 않고 음악 작업을 테스트하거나, 블루투스 작업에서도 음악을 손쉽게 재생할 수 있습니다.

인터럽트 처리

작업 중에는 인터럽트에 대한 응답으로 실행되는 작업도 있는데, 블루투스 통신 작업도 여기에 해당합니다. 예를 들어 블루투스 칩에 새로운 패키지가 있을 때 블루투스 통신 작업을 실행하려고 합니다. 이러한 경우에 인터럽트가 발생한다고 가정하면 인터럽트 핸들러에서 메시지를 보낼 수 있습니다.

void BT_DataAvailable_Handler(void)
{
os_send_message(bt_data_queue, data_available_message);
}

대기열 외에 다른 동기화 서비스도 사용할 수 있습니다. 예를 들어 세마포어와 뮤텍스도 여러 운영 체제에서 발견됩니다.

FreeRTOS

TouchGFX는 개발 과정에서 FreeRTOS 운영 체제로 테스트됩니다. TouchGFX는 요구 사항이 거의 없어서 여러 운영 체제에서 실행이 가능하지만 구체적인 요건이 없다면 FreeRTOS가 좋은 출발점이 될 수 있습니다.

FreeRTOS는 상용 애플리케이션에서 무료로 사용할 수 있는 간단한 운영 체제입니다. 이 운영 체제는 STM32Cube 펌웨어와 함께 소스 코드로 제공되며, 이 펌웨어에는 언제든지 사용할 수 있는 STM32 마이크로컨트롤러 예제 코드도 포함되어 있습니다.

FreeRTOS에 대한 자세한 내용과 라이선스 조건은 freertos.org를 참조하십시오.

TouchGFX OS Wrappers

TouchGFX는 기본적으로 FreeRTOS에서 실행되며, 단일 메시지 대기열을 사용해 디스플레이 컨트롤러와 동기화하고, 세마포어를 사용해 프레임버퍼에 대한 액세스를 보호합니다.

이것을 처리하는 것은 touchgfx/os/OSWrappers.cpp에서 정의한 OSWrappers 클래스입니다. 이 클래스에서 사용하는 메소드는 다음과 같습니다.

메소드표현
signalVSync()이 메소드는 디스플레이가 다음 프레임을 위한 준비를 마쳤을 때 디스플레이 드라이버에서 호출되어야 합니다.
waitForVSync()대기하기 위해 그래픽 엔진에서 호출되며, signalVSync가 호출될 때까지 반환해서는 안 됩니다.
isVSyncAvailable()(선택 사항) VSync가 발생하면 true를 반환합니다. waitForVSync에서의 차단을 방지하는 데 사용됩니다.
signalRenderingDone()(선택 사항) 대기 중인 VSync 신호를 모두 제거합니다.
takeFrameBufferSemaphore()그래픽 엔진과 가속기에서 프레임버퍼에 대한 직접 액세스 권한을 얻을 목적으로 호출됩니다.
giveFrameBufferSemaphore()직접 액세스 권한을 다시 해제할 목적으로 호출됩니다.

기본 구현체는 메시지 대기열을 사용해 VSync(프레임) 동기화를 구현합니다. 그래픽 엔진 작업은 다음 VSync가 수신될 때까지 대기합니다.

이 OSWrapper 클래스는 TouchGFX Generator에서 생성됩니다. TouchGFX Generator에 대한 자세한 내용은 여기를 참조하십시오.

RTOS 미사용

TouchGFX는 운영 체제 없이도 실행이 가능합니다. 다만, 이때는 main에서 직접 그래픽 엔진 메인 루프를 시작해야 합니다.

int main()
{
...
touchgfx::HAL::getInstance()->taskEntry();

//never returns
}

RTOS를 사용하지 않는다고 해서 TouchGFX의 성능이 떨어지는 것은 아닙니다. 다만 MCU 부하가 증가하여 TouchGFX와 함께 다른 작업을 실행하기가 더욱 어려워질 수 있습니다.

위에서도 설명했지만 이때는 main에서 사용자 인터페이스가 실행 중일 때 다른 작업을 수동으로 실행해야 합니다.

Model::tick

한 가지 방법은 모든 프레임마다 한 번씩 Model 클래스에서 작업 확인을 수행하는 것입니다.

Model.cpp
void Model::tick()
{
//run other tasks here
music_task_tick();
bluetooth_task_tick();
}

이 메소드를 사용하면 모든 작업이 프레임마다 한 번씩 실행됩니다. 그러면 작업 소요 시간이 사용자 인터페이스의 렌더링 시간에 추가됩니다. 간단한 방법이기 때문에 모든 작업이 빠르게 종료될 수 있는 단순 시스템이라면 수용할 만한 해결책입니다.

OSWrappers

이 외에 OSWrappers 클래스에서 후크를 사용하는 방법도 있습니다. 위에서 설명했듯이 그래픽 엔진은 이벤트를 기다려야 할 때 이 클래스에서 메소드를 호출합니다. 이벤트를 기다릴 때 다음과 같은 메소드로 다른 작업을 실행할 수 있습니다.

OSWrappers.cpp
static volatile uint8_t vsync_sem = 0;

void OSWrappers::signalVSync()
{
vsync_sem = 1;
}

void OSWrappers::waitForVSync()
{
vsync_sem = 0; //clear the flag, so we wait for the next vsync
do {
// Perform other work while waiting
music_task_tick();
bluetooth_task_tick();
} while(!vsync_sem);
}

이 메소드를 사용하면 다른 작업에서 프레임 간 유휴 작업을 모두 사용할 수 있지만 작업에 소요되는 시간에는 차이가 있습니다.

그 밖에 OSWrappers::isVSyncAvailable 함수와 OSWrappers::signalRenderingDone 함수를 사용하는 방법도 있습니다. 이 경우 애플리케이션이 다수의 while-루프를 피할 수 있습니다. 두 함수는 운영 체제가 없는 구성을 선택했을 때 TouchGFX Generator에서 사용됩니다.

이때는 작업을 1밀리초 정도의 단계로 세분화하여 나눌 수 있다는 점이 중요합니다. 그렇지 않으면 사용자 인터페이스의 성능이 떨어지기 때문입니다.