Abstraction Layer Architecture
이전 섹션에서 설명했듯이 TouchGFX AL은 특정한 책임을 맡고 있습니다. 책임은 AL(HAL)의 하드웨어 부분이나 RTOS(OSAL)를 통해 TouchGFX Engine과 동기화되는 AL 부분에서 구현됩니다. 아래 표에는 이전 섹션에서 설명한 이러한 책임들이 요약되어 있습니다.
책임 | 운영 체제 또는 하드웨어 |
---|---|
TouchGFX Engine 메인 루프를 디스플레이 전송과 동기화 | 운영 체제 및 하드웨어 |
터치 및 물리적 버튼 이벤트 보고 | 하드웨어 |
프레임 버퍼 액세스 동기화 | 운영 체제 |
차기 가용 프레임 버퍼 영역 보고 | 하드웨어 |
렌더링 작업 수행 | 하드웨어 |
디스플레이에 프레임 버퍼 전송 처리 | 하드웨어 |
아래의 각 하위 섹션에는 위의 책임들을 이행하기 위해 수행해야 하는 작업이 명시되어 있습니다. 커스텀 하드웨어 플랫폼의 경우, STM32CubeMX 내부의 TouchGFX Generator는 대부분의 AL과 이에 수반되는 TouchGFX 프로젝트를 생성할 수 있습니다. AL 개발자가 수동으로 구현해야 하는 나머지 부분은 TouchGFX Generator를 통한 코드 주석 및 알림을 통해 지정됩니다. 다음 섹션에서 TouchGFX Generator에 대해 자세히 알아보십시오.
AL(Abstraction Layer) 클래스
TouchGFX AL은 구체적인 하위 클래스와 TouchGFX Engine에 정의된 클래스의 멤버 함수가 구현되는 클래스 구현 파일(.cpp)을 통해 TouchGFX Engine이 액세스합니다. 이러한 하위 클래스 및 클래스 구현 파일은TouchGFX Generator에서 생성됩니다. TouchGFX Generator는 STM32CubeMX의 구성을 반영하는 HAL 부분과 사용된 RTOS에 대한 OSAL을 모두 생성할 수 있습니다. 자세한 내용은 TouchGFX Generator 섹션을 참조하시기 바랍니다. 일반적으로 HAL의 아키텍처는 아래 그림과 같습니다.
TouchGFX Engine 메인 루프를 디스플레이 전송과 동기화
이는 기본적으로 렌더링이 완료될 때 TouchGFX Engine 메인 루프를 차단하여 추가 프레임이 생성되지 않도록 하기 위한 단계입니다. 디스플레이가 준비되면 OSAL은 차단된 TouchGFX Engine 메인 루프를 시그널링 하여 프레임을 계속 생성합니다.
이러한 책임을 이행하기 위해 TouchGFX AL이 일반적으로 사용하는 방법은 Responsibilities of the Abstraction Layer에 설명되어 있듯이 엔진 후크 Rendering done과 인터럽트 Display Ready를 활용하는 것입니다. OSAL은 개발자가 호출 시 TouchGFX Engine이 대기하게 세마포어에 신호를 보낼 수 있도록 OSWrappers::signalVSync
함수를 정의하합니다.
Tip
Rendering Done
Rendering done 후크인 OSWrappers::waitForVSync
는 렌더링이 완료된 후에 TouchGFX Engine에서 호출됩니다.
이 OSAL 메서드를 구현할 때 추상화 계층은 그 다음 프레임을 렌더링할 때까지 그래픽 엔진을 차단해야 합니다. 이 블록을 구현하는 표준 메서드는 메시지 대기열에서 읽기 차단을 수행하는 것입니다. 이것이 가능하지 않은 경우, HAL 개발자는 블록을 구현하기 위해 어떤 메서드이든 자유롭게 사용할 수 있습니다.
Tip
OSWrappers::signalVSync
가 신호를 받으면(또는 OSWrappers::waitForVSync
에서 사용되는 세마포어/대기열이 신호를 받으면) TouchGFX는 그 다음 애플리케이션 프레임을 렌더링하기 시작합니다. CMSIS V2에 기반한 다음 코드는 요소가 시스템의 다른 부분(일반적으로 디스플레이에 동기화된 인터럽트)에 의해 대기열에 추가될 때까지 TouchGFX Engine을 차단하도록 합니다.
OSWrappers.cpp (CMSIS V2)
static osMessageQueueId_t vsync_queue = NULL; //Queue identifier is assigned elsewhere
void OSWrappers::waitForVSync()
{
uint32_t dummyGet;
// First make sure the queue is empty, by trying to remove an element with 0 timeout.
osMessageQueueGet(vsync_queue, &dummyGet, 0, 0);
// Then, wait for next VSYNC to occur.
osMessageQueueGet(vsync_queue, &dummyGet, 0, osWaitForever);
}
RTOS를 사용하지 않는 경우에는 TouchGFX Generator가 휘발성 변수를 사용해 waitForVSync
에 대해 다음과 같이 함수를 구현합니다.
OSWrappers.cpp (No OS)
static volatile uint32_t vsync_sem = 0;
void OSWrappers::waitForVSync()
{
if(vsync_sem)
{
vsync_sem = 0;
// Signal TouchGFX to start rendering next frame
...
}
}
Tip
Display ready
메인 루프의 차단을 해제하는 Display ready 신호가 디스플레이 컨트롤러, 디스플레이 자체, 심지어 하드웨어 타이머의 인터럽트에서 나옵니다. 신호 소스는 디스플레이 유형에 따라 다릅니다.
OSWrappers
클래스는 이 신호에 대해 함수 OSWrappers::signalVsync
를 정의합니다. 구현된 함수는 OSWrappers::waitForVSync
에 사용되는 대기 조건을 충족하여 메인 루프를 차단 해제해야 합니다.
위의 CMSIS V2 예제에서 계속하자면, 다음 코드는 TouchGFX Engine의 차단을 해제하는 메시지 대기열 vsync_queue
에 메시지를 저장합니다.
OSWrappers.cpp (CMSIS V2)
void OSWrappers::signalVSync()
{
osMessageQueuePut(vsync_queue, &dummy, 0, 0);
}
이 OSWrappers::signalVSync
메서드는 예를 들어 디스플레이나 하드웨어 타이머에서 나온 외부 신호인 LTDC를 위한 인터럽트로부터 하드웨어 수준에서 호출이 되어야 합니다.
RTOS를 사용하지 않는 경우에는 변수를 사용하고 0이 아닌 값을 할당해서 while 루프를 종료해야 합니다.
OSWrappers.cpp (No OS)
void OSWrappers::signalVSync()
{
vsync_sem = 1;
}
터치 및 물리적 버튼 이벤트 보고
TouchGFX Engine은 새 프레임을 렌더링하기 전에 TouchController
및 ButtonController
인터페이스에서 외부 입력을 수집합니다.
터치 좌표
터치 컨트롤러의 좌표는 TouchGFX Engine에서 클릭, 드래그 및 제스처 이벤트로 변환되어 애플리케이션에 전달됩니다.
TouchGFX Engine은 구현된 이벤트를 전달하는 방법으로 터치 컨트롤러에서 조정된 터치 좌표에 액세스합니다.
다음 코드는 TouchGFX Generator에서 생성됩니다.
TouchGFXConfiguration.cpp
static STM32TouchController tc;
...
static TouchGFXHAL hal(dma, display, tc, 390, 390);
TouchGFX Engine는 렌더링 주기 동안 입력을 수집할 때 tc 객체에서 sampleTouch()
함수를 호출합니다.
bool STM32TouchController::sampleTouch(int32_t& x, int32_t& y)
AL 개발자가 제공하는 함수는 읽기 터치 좌표 값을 x와 y로 할당하고 터치 감지 여부(true 또는 false)를 반환해야 합니다.
Tip
이러한 함수를 구현하는 방법에는 몇 가지가 있습니다.
- sampleTouch() 에서 폴링: 요청을 전송하고 결과에 대해 폴링을 수행함으로써 하드웨어 터치 컨트롤러(보통 I2C)에서 터치 상태를 읽어옵니다. 그래픽 엔진이 차단되는 동안 I2C 왕복에 최대 1ms의 시간이 소요되는 경우가 종종 있다는 점에서 이는 애플리케이션의 전체 렌더링 시간에 영향을 줍니다.
- 인터럽트 기반: 인터럽트를 사용하는 방법도 가능합니다. I2C 읽기 명령은 타이머에 의해, 또는 터치 하드웨어에서 나온 외부 인터럽트에 대한 응답으로서 주기적으로 시작됩니다. I2C 데이터가 가용 상태가 되면(또 다른 인터럽트) 이를 메시지 대기열이나 전역 변수를 통해 STM32TouchController에서 사용할 수 있습니다.
STM32TouchController.cpp
(TouchGFX Generator에서 생성됨)에서 나온 아래 코드는sampleTouch
가 RTOS를 이용해 시스템을 검색하는 방식을 보여줍니다.
STM32TouchController.cpp
bool STM32TouchController::sampleTouch(int32_t& x, int32_t& y)
{
if (osMessageQueueGet(mid_MsgQueue, &msg, NULL, 0U) == osOK)
{
x = msg.x;
y = msg.y;
return true;
}
return false;
}
이 파일의 위치는 TouchGFX Generator에 대한 다음 장에 나와 있습니다.
기타 외부 이벤트
버튼 컨트롤러 인터페이스인 touchgfx::ButtonController
는 하드웨어 신호(버튼 또는 기타)를 애플리케이션에 대한 이벤트로 매핑하는 데 사용할 수 있습니다. 이러한 이벤트에 대한 반응을 TouchGFX Designer 내에 구성할 수 있습니다. 이 인터페이스의 사용 방법은 ButtonController를 갖는 것이 필수가 아니라는 점을 제외하고 위의 터치 컨트롤러와 유사합니다.
이 인터페이스의 사용 방법은 ButtonController를 갖는 것이 필수가 아니라는 점을 제외하고 위의 터치 컨트롤러와 유사합니다. 사용을 위해 클래스의 인스턴스를 생성하여 ButtonController
인터페이스를 구현하고 HAL에 인스턴스에 대한 참조를 전달합니다.
MyButtonController.cpp
class MyButtonController : public touchgfx::ButtonController
{
bool sample(uint8_t& key)
{
... //Sample IO, set key, return true/false
}
};
TouchGFXConfiguration.cpp
static MyButtonController bc;
void touchgfx_init()
{
...
hal.initialize();
hal.setButtonController(&bc);
}
ButtonController 클래스의 sample 메서드는 각 프레임에 앞서 호출됩니다. True를 반환하면 이 값이 현재 화면의 handleKeyEvent 이벤트 핸들러에 전달됩니다.
Further reading
프레임 버퍼 액세스 동기화
다수의 동작들이 프레임 버퍼 메모리를 액세스하는 데 관심을 가질 수 있습니다.
1 | CPU | 렌더링 동안 픽셀 읽기 및 쓰기 |
2 | DMA2D* | 하드웨어 지원 렌더링 동안 픽셀 읽기 및 쓰기 |
3 | LTDC | 병렬 RGB 디스플레이에 전송하는 동안 픽셀 읽기 |
4 | DMA | SPI 디스플레이에 전송하는 동안 픽셀 읽기 |
TouchGFX Engine은 OSWrappers
인터페이스를 통해 프레임 버퍼 액세스를 동기화하며, 동일한 프레임 버퍼에 액세스하고 싶은 주변 장치(예: DMA2D)는 이와 똑같이 해야 합니다. 일반적인 설계에서는 세마포어를 사용하여 프레임 버퍼에 대한 액세스를 보호하지만, 다른 동기화 메커니즘을 사용할 수도 있습니다.
아래 표에는 TouchGFX Generator를 통해서 생성하거나 사용자가 직접 생성할 수 있는 OSWrappers
클래스(OSWrappers.cpp)의 함수 목록이 나와 있습니다.
메서드 | 설명 |
---|---|
takeFrameBufferSemaphore | 프레임 버퍼에 대한 독점 액세스 권한을 얻기 위해 TouchGFX Engine에서 호출됩니다. 이 메서드는 DMA2D가 수행될 때까지(실행되는 경우) TouchGFX Engine을 차단합니다. |
tryTakeFrameBufferSemaphore | 잠금이 실행되었는지 확인합니다. 이 메서드는 차단을 직접 하지 않고 takeFrameBufferSemaphore에 대한 다음 호출이 호출자를 차단하는지 확인합니다. |
giveFrameBufferSemaphore | 프레임 버퍼 잠금을 해제합니다. |
giveFrameBufferSemaphoreFromISR | 인터럽트 컨텍스트에서 프레임 버퍼 잠금을 해제합니다. |
Tip
차기 가용 프레임 버퍼 영역 보고
렌더링 전략에 관계없이 TouchGFX Engine은 각각의 틱에서 픽셀을 렌더링해야 하는 메모리 영역을 알아야 합니다. TouchGFX Engine은 단일 또는 이중 프레임 버퍼 전략을 사용하여 프레임 버퍼의 전체 너비, 높이 및 비트 심도에 따라 메모리 영역에 픽셀 데이터를 기록합니다. 또한 이중 버퍼 설정에서 두 버퍼 간의 스왑도 관리합니다.
프레임 버퍼에 대한 액세스 권한을 프레임 버퍼의 일부로 제한하는 것도 가능합니다. HAL::getTFTCurrentLine()
메소드는 HAL 하위 클래스에서 재구현이 가능합니다. TouchGFX Engine이 그리기를 위해 저장한 메소드 위의 라인 번호를 반환합니다.
개발자는 부분 프레임 버퍼 전략을 사용해 TouchGFX Engine이 렌더링 시 사용하게 될 메모리의 블록을 1개 이상 정의합니다. 이에 대한 자세한 내용은 여기를 참조하십시오.
Tip
렌더링 작업 수행
그래픽 렌더링 및 표시가 애플리케이션의 유일한 목적인 경우는 거의 없습니다. CPU를 사용하기 위해서는 다른 작업들도 필요합니다. TouchGFX의 목표 중 하나는 가능한 적은 CPU 주기를 사용해 사용자 인터페이스를 그리는 것입니다. HAL 클래스는 많은 STM32 마이크로컨틀로러(또는 기타 하드웨어 기능)에서 나타나는 DMA2D를 추상화하여 이를 TouchGFX Engine에서 사용할 수 있게 지원합니다.
TouchGFX Engine은 비트맵 같은 자산을 프레임 버퍼에 렌더링할 때 HAL에 비트맵의 일부 또는 전체를 프레임 버퍼로 '블릿(blit, block transfer)' 할 수 있는 기능이 있는지 확인합니다. 블릿 기능이 있는 경우에는 그리기 작업이 CPU에서 처리되는 대신 HAL에 위임됩니다.
TouchGFX Engine은 HAL::getBlitCaps()
메소드를 호출하여 하드웨어의 기능에 대한 설명을 가져옵니다. HAL 하위 클래스는 기능을 추가하기 위해 이를 다시 구현할 수 있습니다.
TouchGFX Engine은 사용자 인터페이스를 그리는 동안 HAL 클래스에서 연산(예: DMA에 대한 연산을 대기열에 저장하는 HAL::blitCopy)
을 사용합니다. HAL이 필요한 기능을 보고하지 않는 경우, TouchGFX Engine은 소프트웨어 렌더링 폴백(fallback)을 사용합니다.
Tip
디스플레이에 프레임 버퍼 전송 처리
프레임 버퍼를 디스플레이에 전달하기 위해 TouchGFX AL에서는 "Rendering of area complete" 후크가 주로 사용됩니다. TouchGFX Engine은 프레임 버퍼의 일부에 대한 렌더링이 완료되면 AL을 시그널링합니다. AL은 프레임 버퍼의 이 부분을 디스플레이에 전송하는 방법을 선택할 수 있습니다.
전체 영역 렌더링
코드에서 이러한 후크는 가상 함수 HAL::flushFrameBuffer(Rect& rect)
입니다.
LTDC 컨트롤러를 장착한 STM32 마이크로컨트롤러에서는 모든 렌더링 이후에 프레임 버퍼를 전송하기 위한 작업을 수행할 필요가 없습니다. LTDC가 개시된 이후에 일정한 주기로 전송이 계속 이루어지기 때문에 이 메서드를 빈 상태로 남겨둘 수 있습니다.
SPI나 8080 같은 다른 디스플레이 유형에서는 프레임 버퍼를 수동으로 전송해야 합니다.
이 함수를 구현하면 개발자가 GRAM에서 디스플레이에 프레임 버퍼의 해당 영역을 수동 전송하는 작업을 개시할 수 있습니다:
void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect& r)
{
HAL::flushFrameBuffer(rect); //call superclass
//start transfer if not running already!
if (!IsTransmittingData())
{
const uint8_t* pixels = ...; // Calculate pixel address
SendFrameBufferRect((uint8_t*)pixels, r.x, r.y, r.width, r.height);
}
else
{
... // Queue rect for later or wait here
}
}