FMC 디스플레이 인터페이스
이 시나리오는 FMC 병렬 인터페이스와 GRAM이 있는 디스플레이를 사용할 때 STM32 FMC(Flexible Memory Controller) 및 TouchGFX Generator를 구성하는 방법을 설명합니다.
Note
구성
FMC 구성
STM32CubeMX 카테고리 목록의 Connectivity 그룹에서 FMC를 활성화합니다. 여기에서 사용 가능한 FMC Bank 중 하나를 LCD Interface로 구성해야 합니다. 이는 STM32CubeMX에서 Memory type을 LCD Interface로 설정하여 수행할 수 있습니다. 픽셀을 디스플레이로 전송하는 데 사용되는 병렬 데이터 라인의 개수도 여기에서 선택됩니다(예: 16비트 또는 8비트 병렬 인터페이스).
사용되는 디스플레이에 맞춰 타이밍을 올바르게 설정해야 합니다. MCU에 대한 FMC의 기본 주소와 디스플레이에 연결하는 데 사용된 뱅크를 확인하세요.
TouchGFX Generator
디스플레이 인터페이스: 병렬 RGB(FMC)
16비트 인터페이스를 사용할 경우 TouchGFX Generator가 HAL을 생성할 수 있습니다. 이 HAL은 LCD에 데이터 및 레지스터를 읽고 쓰는 기본 구현이 포함된 간단한 API를 제공합니다:
TouchGFXGeneratedHAL.cpp
...
/**
* @brief Initialize the LCD IO.
* @param None
* @retval None
*/
__weak void LCD_IO_Init(void)
{
// Already Done by MX_FMC_Init()
}
/**
* @brief Writes data on LCD data register.
* @param Data: Data to be written
* @retval None
*/
__weak void LCD_IO_WriteData(uint16_t RegValue)
{
/* Write 16-bits Reg */
FMC_BANK1_WriteData(RegValue);
}
...
생성된 전체 코드에 대해서는 TouchGFXGeneratedHAL.cpp
파일을 참조하세요. 이러한 함수는 개발자가 필요할 경우 자신의 HAL 구현에서 재정의할 수 있도록 __weak
로 정의되어 있습니다.
FMC 뱅크가 올바르게 구성되었다면 TouchGFX Generator에서 선택할 수 있습니다.
Display Interface: Custom
8비트 FMC 인터페이스(또는 SPI나 DSI Command 모드 등의 다른 직렬 인터페이스)를 사용할 경우 TouchGFX Generator에서 Custom Display Interface를 선택해야 합니다. 이는 전체 HAL은 자동으로 생성될 수 없으므로 개발자가 프레임 버퍼 메모리에서 픽셀을 구성하여 수동으로 디스플레이로 전송해 기능을 구현해야 한다는 뜻입니다. 이를 달성하는 데 필요한 모든 핸들은 TouchGFX Generator에 의해 생성됩니다. 아래 내용은 Custom을 선택하는 방법을 보여줍니다.
Tip
DMA 구성
CPU 부하를 최소화하기 위해 픽셀 데이터가 프레임 버퍼에서 FMC로 전송되도록 DMA 채널을 구성할 수 있습니다. 16비트 병렬 인터페이스를 사용할 경우 생성된 LCD API를 대신 DMA 전송을 수행하도록 재정의할 수 있습니다. 또는, TouchGFXHAL.cpp
에서 DMA 전송 기능을 포함하도록 API를 확장할 수도 있습니다. 아래 내용은 FMC에 대한 GPDMA 채널 구성의 예시입니다:
프레임 버퍼를 디스플레이로 전송하기 위해 HAL 계층에서 요청을 프로그래밍해야 하므로 요청은 소프트웨어로 설정됩니다. 프레임 버퍼 포인터가 이동되도록 소스 주소 증분을 활성화하고 항상 FMC 뱅크 메모리 주소에 쓰도록 대상 주소 증분을 비활성화합니다.
사용자 코드
일반적으로 임베디드 GRAM이 탑재된 디스플레이의 경우 TouchGFXHAL.cpp
에서 생성된 TouchGFX HAL 핸들을 구현하려면 다음 단계를 수행해 픽셀을 디스플레이로 전송하고 디스플레이를 TouchGFX Engine과 동기화해야 합니다:
- "VSYNC"(TE(Tearing Effect) 신호라고도 불림)가 TouchGFX Engine에 신호를 보낼 때까지 대기합니다.
- 다시 그려질 프레임 버퍼의 영역을 기준으로 "디스플레이 커서"와 "활성 창"(업데이트되는 디스플레이의 영역)을 이 영역과 일치하는 GRAM의 위치로 이동시킵니다.
- GRAM에 입력되는 픽셀 데이터를 기록할 준비를 합니다. 사용되는 프레임 버퍼 전략과 디스플레이 인터페이스에 따라 이는 프레임 버퍼 포인터를 바꾸거나, TouchGFX Engine에 신호를 보내거나, 이전 전송이 완료될 때까지 기다릴 수도 있습니다.
- 픽셀 데이터를 전송합니다.
어떤 디스플레이를 사용하고 프레임 버퍼 전략이 무엇인지에 따라 위 단계의 구현이 달라질 수 있습니다. FMC 병렬 인터페이스가 탑재된 GRAM 디스플레이를 사용할 때 이러한 단계를 구현하는 방법은 다음 섹션에서 설명합니다.
지원되는 프레임 버퍼 전략
- 단일
- 이중
- 부분 - GRAM 디스플레이
Further reading
단일
FMC 병렬 인터페이스를 사용하여 픽셀 데이터를 디스플레이로 전송하면 매우 높은 대역폭을 제공하는 경우가 많습니다. 이 때문에 다수의 애플리케이션은 나이브(naive) 방식으로 프레임 버퍼를 업데이트할 수 있습니다. 나이브 방식은 이전 프레임이 디스플레이로 전송된 후에 다음 프레임의 렌더링이 시작된다는 뜻입니다. 보다 최적의 접근 방식은 이전 프레임을 작은 조각으로 전송하고 지금까지 전송된 프레임 버퍼 영역에서 다음 프레임의 렌더링을 시작하라고 TouchGFX Engine에 신호를 보내는 방식입니다. 이는 LTDC 기반 디스플레이의 주사선을 따르는 개념을 에뮬레이션하는 것이지만 여기에서는 대신 전송선을 따릅니다.
최적의 디스플레이 전송과 렌더링을 달성하려면 TouchGFX Engine이 올바른 프레임 버퍼 전략을 사용하도록 설정되어야 하며 지연 함수가 제공되어야 합니다:
TouchGFXHAL.cpp
void TouchGFXHAL::initialize()
{
// Other initialization code
registerTaskDelayFunction(&OSWrappers::taskDelay);
setFrameRefreshStrategy(REFRESH_STRATEGY_OPTIM_SINGLE_BUFFER_TFT_CTRL);
TouchGFXGeneratedHAL::initialize();
}
디스플레이 전송을 시작하기 위해, 디스플레이의 TE 신호에 의해 생성된 외부 인터럽트가 TouchGFX 엔진에 신호를 보내 다음 프레임 렌더링을 시작하고, 이전에 렌더링된 프레임을 디스플레이로 전송하도록 합니다. 일반적으로 TE 신호는 디스플레이 주사선이 상승 엣지의 활성 영역을 떠나 하강 엣지로 다시 진입한다고 신호를 보내는 펄스입니다. 디스플레이 전송 대역폭이 디스플레이가 픽셀을 스캔하는 대역폭보다 클 경우(FMC에서 종종 발생함), 깨짐 현상이 발생할 위험 없이 TE 신호의 상승 엣지에서 안전하게 전송을 시작할 수 있습니다. 아래 코드가 바로 이런 경우에 해당합니다.
TouchGFXHAL.cpp
void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == LCD_TE_Pin)
{
// VSync has occurred, increment TouchGFX engine vsync counter
HAL::getInstance()->vSync();
// VSync has occurred, signal TouchGFX engine
OSWrappers::signalVSync();
GPIO::set(GPIO::VSYNC_FREQ);
if (refreshRequested)
{
refreshRequested = false;
nextSendToDisplayLine = refreshMinLine;
maxSendToDisplayLine = refreshMaxLine;
sendNextFrameBufferBlockToDisplay();
}
}
}
Note
위의 코드에서 헬퍼 변수 nextSendToDisplayLine
및 maxSendToDisplayLine
은 디스플레이로 전송되는 이전 프레임의 영역을 정의합니다. 이러한 변수의 값은 TouchGFXHAL::flushFrameBuffer
에서 TouchGFX Engine에 의해 업데이트되며 이는 프레임 버퍼가 업데이트될 때마다 호출됩니다. 프레임 버퍼는 다음 TE 신호에서 데이터를 전송해야 한다는 것을 알리기 위해 더티(dirty)로 표시됩니다.
TouchGFXHAL.cpp
void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
// Calling parent implementation of flushFrameBuffer(const touchgfx::Rect& rect).
TouchGFXGeneratedHAL::flushFrameBuffer(rect);
// In the below code, the refreshMinLine and refreshMaxLine variables are updated to span the
// smallest possible range, which covers all changes to the framebuffer.
int rectMin = rect.y;
int rectMax = rect.bottom();
refreshMinLine = MIN(rectMin, refreshMinLine);
refreshMaxLine = MAX(rectMax, refreshMaxLine);
refreshRequested = true; // Signal that the framebuffer has been updated this tick
}
대량의 프레임 버퍼 픽셀을 디스플레이로 전송하는 헬퍼 함수는 FMC 함수를 이용해 구현되어야 합니다:
TouchGFXHAL.cpp
void sendNextFrameBufferBlockToDisplay()
{
maxDrawLine = nextSendToDisplayLine; // Update the area that TouchGFX framework is allow to draw to based on the progress of the transfer
const int32_t remainingLines = maxSendToDisplayLine - nextSendToDisplayLine;
if (remainingLines > 0)
{
// The display transfer is not done. Start transfer of the next block
const uint32_t sendBlockHeight = MIN(MAX_BLOCK_HEIGHT, remainingLines);
setDisplayWindow(0, nextSendToDisplayLine, DISPLAY_WIDTH, sendBlockHeight);
uint16_t* dataPtr = framebuffer + nextSendToDisplayLine * DISPLAY_WIDTH;
uint32_t dataSize = DISPLAY_WIDTH * sendBlockHeight * 2;
nextSendToDisplayLine += sendBlockHeight;
LCD_IO_SendDataDMA((uint8_t*)dataPtr, dataSize);
}
else
{
// The display transfer is done. Allow drawing to the entire framebuffer
maxDrawLine = DISPLAY_HEIGHT;
}
}
위의 코드에서 remainingLines
는 디스플레이로 전송해야 하는 남은 라인의 수입니다. 남은 라인이 있다면 sendBlockHeight
를 전송되는 라인의 수로 정의합니다. 상수 MAX_BLOCK_HEIGHT
는 전송 크기를 제한합니다. 변수 maxDrawLine
은 전송된 영역에 대한 하이 워터마크로, TouchGFX Engine이 다음 프레임에 그릴 수 있는 현재 라인을 결정합니다. 이 값은 전송이 진행됨에 따라 증가하며 더티 영역의 전송이 완료되면 DISPLAY_HEIGHT
로 설정되어 TouchGFX Engine이 다음 프레임을 완료할 수 있도록 합니다. TouchGFX Engine은 얼마나 많은 프레임 버퍼를 업데이트할 수 있는지 알아보기 위해 maxDrawLine
을 쿼리합니다:
TouchGFXHAL.cpp
uint16_t TouchGFXHAL::getTFTCurrentLine()
{
return maxDrawLine;
}
블록이 전송되면 다음 전송을 시작할 수 있도록 전송 완료 콜백이 사용됩니다:
TouchGFXHAL.cpp
static void DMA_TxCpltCallback(DMA_HandleTypeDef* hdma)
{
if (hdma == DISPLAY_DMA)
{
sendNextFrameBufferBlockToDisplay();
}
}
프레임 버퍼의 모든 더티 영역이 디스플레이로 전송될 때까지 이 과정이 반복됩니다.
다음 TE 신호가 수신되면 위에 설명된 사이클이 반복됩니다.
참조 구현
TouchGFX Board Setup NUCLEO-U575ZI + RVA35HI에는 FMC를 사용해 렌더링/전송이 최적화된 단일 프레임 버퍼 전략의 참조 구현이 포함됩니다:
이중
단일 프레임 버퍼와 비교하자면, 이중 버퍼링은 TouchGFX 엔진이 하나의 프레임 버퍼에 다음 프레임을 자유롭게 쓰는 동안, 다른 프레임 버퍼에서 이전 프레임을 디스플레이로 전송할 수 있게 해줍니다. 이는 디스플레이 전송을 기다릴 필요가 없으므로 최적의 렌더링 시간을 제공합니다.
디스플레이 전송을 시작하기 위해, 디스플레이의 TE 신호에 의해 생성된 외부 인터럽트가 TouchGFX 엔진에 신호를 보내 프레임 버퍼를 변경하고 다음 프레임 렌더링을 시작합니다. 디스플레이는 이전에 렌더링한 프레임을 디스플레이로 전송하도록 설정됩니다:
TouchGFXHAL.cpp
void LCD_SignalTearingEffectEvent(void)
{
// VSync has occurred, increment TouchGFX engine vsync counter
HAL::getInstance()->vSync();
// VSync has occurred, signal TouchGFX engine
OSWrappers::signalVSync();
if (refreshRequested)
{
// Swap frame buffers immediately instead of waiting for the task to be scheduled in.
// Note: task will also swap when it wakes up, but that operation is guarded and will not have
// any effect if already swapped.
touchgfx::HAL::getInstance()->swapFrameBuffers();
// Set window, enable display reading to GRAM, transmit buffer using DMA
setWindow(0, 0, 240, 240);
LCD_IO_WriteReg(ST7789H2_WRITE_RAM);
dmaCompleted = false;
LCD_IO_SendDataDMA((uint8_t*)TFTframebuffer, (240 * 240 * 2));
}
}
위의 코드에서 디스플레이 해상도는 240x240픽셀(16비트 RGB565 프레임 버퍼)이므로 창은 전체 디스플레이 영역으로 설정됩니다. 대역폭이 제한된 경우 창은 더티 영역만 포함하도록 더 작게 줄일 수 있습니다. TouchGFX HAL 함수 TouchGFXHAL::flushFrameBuffer
를 이 목적으로 사용할 수도 있습니다.
TouchGFX HAL 함수 TouchGFXHAL::beginFrame()
및 TouchGFXHAL::endFrame()
은 전송이 수행되어야 한다고 요청하는 함수입니다. 즉, 프레임 버퍼가 더티(dirty)로 표시됩니다.
TouchGFXHAL.cpp
bool TouchGFXHAL::beginFrame()
{
refreshRequested = false;
return TouchGFXGeneratedHAL::beginFrame();
}
void TouchGFXHAL::endFrame()
{
TouchGFXGeneratedHAL::endFrame();
if (frameBufferUpdatedThisFrame)
{
refreshRequested = true;
}
}
디스플레이로 전송되는 프레임 버퍼의 주소는 메소드 TouchGFXHAL::setTFTFrameBuffer()
및 TouchGFXHAL::getTFTFrameBuffer()
로 유지됩니다:
TouchGFXHAL.cpp
uint16_t* TouchGFXHAL::getTFTFrameBuffer() const
{
return TFTframebuffer;
}
void TouchGFXHAL::setTFTFrameBuffer(uint16_t* address)
{
TFTframebuffer = address;
TouchGFXGeneratedHAL::setTFTFrameBuffer(address);
}
포인터 TFTframebuffer
는 TE 인터럽트 핸들러가 디스플레이로 전송되는 창의 소스 주소를 설정하는 데 사용됩니다.
참조 구현
TouchGFX Board Setup STM32H573-DK Int. 플래시에는 FMC를 사용하는 이중 프레임 버퍼 전략의 참조 구현이 포함됩니다:
부분 - GRAM 디스플레이
TocuhGFX Generator에서 부분 - GRAM 디스플레이 프레임 버퍼 전략을 선택하는 경우 사용자는 부분 프레임 버퍼 블록의 수와 그 크기를 구성해야 합니다. TouchGFX Generator에서 생성된 코드는 FrameBufferAllocator
를 인스턴스화하여 부분 프레임 버퍼 블록의 상태를 추적하고 프레임 버퍼 전략을 구성합니다:
TouchGFXGeneratedHAL.cpp
// Block Allocator for Partial Framebuffer strategy
static ManyBlockAllocator<2048, /* block size */
3, /* number of blocks */
2 /* bytes per pixel */
> blockAllocator;
void TouchGFXGeneratedHAL::initialize()
{
HAL::initialize();
registerEventListener(*(Application::getInstance()));
// Partial framebuffer strategy
setFrameBufferAllocator(&blockAllocator);
setFrameRefreshStrategy(HAL::REFRESH_STRATEGY_PARTIAL_FRAMEBUFFER);
}
또 다른 클래스 PartialFrameBufferManager
는 부분 프레임 버퍼 블록 간의 동기화, 블록 전송, 디스플레이 주사율의 타이밍을 처리합니다. 생성된 코드는 PartialFrameBufferManager
에 의해 노출된 API를 사용하여 TouchGFXGeneratedHAL::flushFrameBuffer
의 프레임 버퍼 블록을 전송합니다:
TouchGFXGeneratedHAL.cpp
void TouchGFXGeneratedHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
HAL::flushFrameBuffer(rect);
// Try transmitting a block
PartialFrameBufferManager::tryTransmitBlock();
}
PartialFrameBufferManager
는 3개의 함수를 사용해 디스플레이 드라이버 코드와 상호작용합니다. 이는 TouchGFX Board Setup에서 구현되어야 합니다:
TouchGFXGeneratedHAL.cpp
/**
* Check if a Frame Buffer Block is beeing transmitted.
*/
int transmitActive()
{
return touchgfxDisplayDriverTransmitActive();
}
/**
* Check if a Frame Buffer Block ending at bottom may be sent.
*/
int shouldTransferBlock(uint16_t bottom)
{
return touchgfxDisplayDriverShouldTransferBlock(bottom);
}
/**
* Transmit a Frame Buffer Block.
*/
void transmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
touchgfxDisplayDriverTransmitBlock(pixels, x, y, w, h);
}
위의 코드는 TouchGFXHAL.cpp
의 디스플레이 드라이버에서 구현될 수 있는 C 함수로 호출을 전달합니다.
아래 예제에서 구현은 PartialFrameBufferManager
의 매개변수를 이용해 FMC API 함수를 호출합니다:
TouchGFXHAL.cpp
extern "C" void touchgfxDisplayDriverTransmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
isTransmittingBlock = true;
setDisplayWindow(x, y, w, h);
LCD_IO_SendDataDMA((uint8_t*)pixels, w * h * 2);
}
touchgfxDisplayDriverTransmitActive
함수는 단순히 전역 변수 isTransmittingBlock
의 값을 반환합니다:
TouchGFXHAL.cpp
extern "C" int touchgfxDisplayDriverTransmitActive()
{
return isTransmittingBlock ? 1 : 0;
}
이 값은 전송 완료 콜백에서 재설정됩니다.
touchgfxDisplayDriverShouldTransferBlock
함수는 디스플레이가 새 블록을 받을 준비가 되었는지 확인합니다. 부분 프레임 버퍼 블록은 디스플레이 주사선 뒤에서 전송됩니다. 예를 들어 디스플레이 주사선이 50번째 라인까지 스캔되었고 전송되는 블록의 하단이 70번째 라인에 있다면 TouchGFX Engine은 디스플레이가 70번째 라인 아래로 진행될 때까지 기다려야 블록 전송을 시작할 수 있습니다. 이는 디스플레이의 깨짐을 방지하기 위한 것입니다.
다수의 디스플레이는 이 기능을 지원하지 않으므로 라인마다 카운터를 늘려서 라인을 예측하는 하드웨어 타이머를 사용할 수 있습니다. 카운터가 증가해야 하는 속도는 모든 프레임 시간(ms)/디스플레이 높이(픽셀)여야 이상적입니다(예: 16.67ms / 320pixels = 라인당 52.1us). 라인 타이머를 사용하여 touchgfxDisplayDriverShouldTransferBlock
을 구현한 예시는 아래에 나와 있습니다:
TouchGFXHAL.cpp
extern "C" int touchgfxDisplayDriverShouldTransferBlock(uint16_t bottom)
{
// Only allow block transfer if the display has drawn past the bottom of the requested block (plus a margin of two lines)
// A timer is used to estimate how many lines have been drawn by setting the prescaler so the tick rate matches the line draw rate
uint16_t lastLineDrawn = LL_TIM_GetCounter(LINE_TIMER);
return bottom + 2 < lastLineDrawn || tearingEffectCount > 0;
}
위의 코드에서 하단(약간의 여백을 더함)이 디스플레이 라인 카운터보다 작거나 tearingEffectCount
가 0보다 크면 함수가 true를 반환합니다. tearingEffectCount
가 0보다 크다면 이는 디스플레이가 TE 신호를 방출했고 이제 전송될 블록과 동일한 프레임에 있다는 뜻이며(하지만 디스플레이 Y축의 뒤), 블록을 전송해야 한다는 의미입니다.
Tip
tearingEffectCount
는 TE 인터럽트 핸들러에서 증가하며 TouchGFX Engine은 다음 프레임을 렌더링하기 시작하라는 신호를 받습니다:
TouchGFXHAL.cpp
void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == LCD_TE_Pin)
{
// Code to handle TE signal. Could be fine-tuning the display line timer frequency
tearingEffectCount++;
// VSync has occurred, increment TouchGFX engine vsync counter
HAL::getInstance()->vSync();
// VSync has occurred, signal TouchGFX engine
OSWrappers::signalVSync();
GPIO::set(GPIO::VSYNC_FREQ);
startRenderingImmediately = true;
}
}
다음 프레임을 시작할 때 tearingEffectCount
를 재설정합니다:
TouchGFXHAL.cpp
bool TouchGFXHAL::beginFrame()
{
tearingEffectCount = 0;
return TouchGFXGeneratedHAL::beginFrame();
}
참조 구현
TouchGFX Board Setup NUCLEO-H563ZI + RVA35HI에는 FMC 및 적응형 디스플레이 라인 타이머 구성을 사용하는 부분 프레임 버퍼 전략의 참조 구현이 포함됩니다: