STM32 Display Buffering
Introduction
Display buffering is a fundamental concept in graphics programming for embedded systems like the STM32 microcontrollers. When working with displays such as LCDs or OLEDs, understanding how to properly manage display data is crucial for creating smooth and responsive user interfaces.
In this tutorial, we'll explore different buffering techniques for STM32 displays, their advantages and disadvantages, and how to implement them in your projects. Whether you're creating a simple gauge, a complex UI, or even a basic game, mastering display buffering will significantly improve your application's performance and user experience.
What is Display Buffering?
Display buffering refers to the technique of storing pixel data in memory before sending it to the physical display. Instead of updating the display pixel-by-pixel (which can be slow and cause visual artifacts), we create a representation of the display in RAM first, manipulate it as needed, and then transfer the entire buffer to the display at once.
Types of Display Buffering
1. Direct Mode (No Buffering)
The simplest approach is to write directly to the display without any buffering. While this method uses minimal RAM, it often results in flickering and slow updates.
// Example of direct mode (no buffering)
void drawPixel(uint16_t x, uint16_t y, uint16_t color) {
// Set cursor position
LCD_SetCursor(x, y);
// Write directly to display
LCD_WriteData(color);
}
Advantages:
- Minimal RAM usage
- Simple implementation
Disadvantages:
- Slow performance
- Visible flickering
- Can't perform complex drawing operations easily
2. Single Buffering
With single buffering, we maintain one complete copy of the display in RAM. We modify this buffer and then send it to the display all at once.
// Example of single buffering
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
uint16_t displayBuffer[DISPLAY_WIDTH * DISPLAY_HEIGHT];
void drawPixel(uint16_t x, uint16_t y, uint16_t color) {
if (x < DISPLAY_WIDTH && y < DISPLAY_HEIGHT) {
displayBuffer[y * DISPLAY_WIDTH + x] = color;
}
}
void updateDisplay(void) {
// Send entire buffer to display
LCD_SetCursor(0, 0);
LCD_WriteMultipleData(displayBuffer, DISPLAY_WIDTH * DISPLAY_HEIGHT);
}
Advantages:
- Reduced flickering
- Ability to perform complex drawing operations
- Only updates the display when needed
Disadvantages:
- Requires significant RAM
- May still show tearing during updates
3. Double Buffering
Double buffering uses two complete frame buffers. While one buffer is being displayed, the other is being modified. Once the modification is complete, we swap the buffers.
// Example of double buffering
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
uint16_t frontBuffer[DISPLAY_WIDTH * DISPLAY_HEIGHT];
uint16_t backBuffer[DISPLAY_WIDTH * DISPLAY_HEIGHT];
uint16_t* currentDrawBuffer = backBuffer;
uint16_t* currentDisplayBuffer = frontBuffer;
void drawPixel(uint16_t x, uint16_t y, uint16_t color) {
if (x < DISPLAY_WIDTH && y < DISPLAY_HEIGHT) {
currentDrawBuffer[y * DISPLAY_WIDTH + x] = color;
}
}
void swapBuffers(void) {
uint16_t* temp = currentDisplayBuffer;
currentDisplayBuffer = currentDrawBuffer;
currentDrawBuffer = temp;
// Send new display buffer to screen
LCD_SetCursor(0, 0);
LCD_WriteMultipleData(currentDisplayBuffer, DISPLAY_WIDTH * DISPLAY_HEIGHT);
}
Advantages:
- Eliminates flickering
- Smooth animations
- No visual artifacts during drawing
Disadvantages:
- Doubles the RAM requirement
- More complex to implement
4. Partial Buffering
For STM32 devices with limited RAM, you can implement partial buffering, where you buffer only a portion of the screen at a time.
// Example of partial buffering
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define BUFFER_HEIGHT 40 // Buffer only 40 rows at a time
uint16_t partialBuffer[DISPLAY_WIDTH * BUFFER_HEIGHT];
void updateDisplaySection(uint16_t startY, uint16_t endY) {
uint16_t rows = endY - startY;
if (rows > BUFFER_HEIGHT) rows = BUFFER_HEIGHT;
// Set cursor to the starting position
LCD_SetCursor(0, startY);
// Send the partial buffer to display
LCD_WriteMultipleData(partialBuffer, DISPLAY_WIDTH * rows);
}
Advantages:
- Works with limited RAM
- Better than direct mode
- Can be optimized for specific applications
Disadvantages:
- More complex to manage
- May still show some visual artifacts
Memory Considerations
STM32 microcontrollers have varying amounts of RAM, so it's important to calculate your buffer size needs:
Buffer Size (bytes) = Width × Height × (Bits Per Pixel ÷ 8)
For example, a 320×240 display with 16 bits per pixel (65,536 colors) requires:
320 × 240 × (16 ÷ 8) = 320 × 240 × 2 = 153,600 bytes
This is significant for many STM32 devices, especially if double buffering (which would require 307,200 bytes).
Some strategies to reduce memory usage:
- Use a lower color depth (e.g., 8-bit instead of 16-bit)
- Buffer only a portion of the screen
- Use smaller displays
- Leverage DMA for transfers to free up CPU time
Practical Implementation: Animated Gauge
Let's implement a simple animated gauge using single buffering:
#include "stm32f4xx_hal.h"
#include "lcd_driver.h" // Your LCD driver
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define CENTER_X (DISPLAY_WIDTH / 2)
#define CENTER_Y (DISPLAY_HEIGHT / 2)
#define RADIUS 100
uint16_t displayBuffer[DISPLAY_WIDTH * DISPLAY_HEIGHT];
// Draw a single pixel in the buffer
void drawPixel(uint16_t x, uint16_t y, uint16_t color) {
if (x < DISPLAY_WIDTH && y < DISPLAY_HEIGHT) {
displayBuffer[y * DISPLAY_WIDTH + x] = color;
}
}
// Draw a line using Bresenham's algorithm
void drawLine(int x0, int y0, int x1, int y1, uint16_t color) {
int dx = abs(x1 - x0);
int dy = -abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx + dy;
int e2;
while (1) {
drawPixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if (e2 >= dy) {
if (x0 == x1) break;
err += dy;
x0 += sx;
}
if (e2 <= dx) {
if (y0 == y1) break;
err += dx;
y0 += sy;
}
}
}
// Clear the buffer
void clearBuffer(uint16_t color) {
for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) {
displayBuffer[i] = color;
}
}
// Draw the gauge with needle at specified angle
void drawGauge(float angle) {
// Clear buffer with black background
clearBuffer(0x0000);
// Draw gauge outline
for (int i = 0; i < 270; i += 5) {
float rad = (i - 45) * 3.14159 / 180.0;
int x = CENTER_X + (int)(RADIUS * cos(rad));
int y = CENTER_Y + (int)(RADIUS * sin(rad));
if (i % 45 == 0) {
// Draw major tick
int x2 = CENTER_X + (int)((RADIUS - 15) * cos(rad));
int y2 = CENTER_Y + (int)((RADIUS - 15) * sin(rad));
drawLine(x, y, x2, y2, 0xFFFF); // White
} else {
// Draw minor tick
int x2 = CENTER_X + (int)((RADIUS - 5) * cos(rad));
int y2 = CENTER_Y + (int)((RADIUS - 5) * sin(rad));
drawLine(x, y, x2, y2, 0xFFFF); // White
}
}
// Draw needle
float rad = (angle - 45) * 3.14159 / 180.0;
int needleX = CENTER_X + (int)((RADIUS - 20) * cos(rad));
int needleY = CENTER_Y + (int)((RADIUS - 20) * sin(rad));
drawLine(CENTER_X, CENTER_Y, needleX, needleY, 0xF800); // Red
// Draw center hub
for (int y = CENTER_Y - 5; y <= CENTER_Y + 5; y++) {
for (int x = CENTER_X - 5; x <= CENTER_X + 5; x++) {
if ((x - CENTER_X) * (x - CENTER_X) + (y - CENTER_Y) * (y - CENTER_Y) <= 25) {
drawPixel(x, y, 0xFFFF); // White
}
}
}
// Update the display
LCD_SetCursor(0, 0);
LCD_WriteMultipleData(displayBuffer, DISPLAY_WIDTH * DISPLAY_HEIGHT);
}
// Main function
int main(void) {
// Initialize system, clocks, LCD, etc.
SystemInit();
LCD_Init();
float angle = 0;
while (1) {
// Update gauge angle
angle += 1.0;
if (angle >= 270) angle = 0;
// Draw gauge with current angle
drawGauge(angle);
// Wait a short time before next update
HAL_Delay(50);
}
}
This example creates a gauge with a sweeping needle animation. The entire display is buffered in RAM, and the complete frame is sent to the display with each update.
Optimizing Performance
DMA Transfers
For STM32 devices with DMA (Direct Memory Access), you can significantly improve performance by offloading data transfers to the DMA controller:
// Setup DMA for display transfer
void setupDisplayDMA(void) {
// Configure DMA (specific to your STM32 model)
__HAL_RCC_DMA2_CLK_ENABLE();
hdma_spi.Instance = DMA2_Stream3;
hdma_spi.Init.Channel = DMA_CHANNEL_3;
hdma_spi.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_spi.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_spi.Init.Mode = DMA_NORMAL;
hdma_spi.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_spi);
__HAL_LINKDMA(&hspi, hdmatx, hdma_spi);
HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
}
// Function to update display using DMA
void updateDisplayDMA(void) {
LCD_SetCursor(0, 0);
// Start DMA transfer
HAL_SPI_Transmit_DMA(&hspi, (uint8_t*)displayBuffer, DISPLAY_WIDTH * DISPLAY_HEIGHT * 2);
// CPU is free to do other tasks while DMA is transferring
}
Using Hardware Acceleration
Some STM32 models include hardware graphics acceleration. For example, the STM32F4 and STM32F7 series include a Chrom-ART Accelerator (DMA2D) for 2D graphics operations:
// Example of using DMA2D to fill a rectangle
void fillRect(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t color) {
// Configure DMA2D
hdma2d.Init.Mode = DMA2D_R2M; // Register to memory mode
hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB565;
hdma2d.Init.OutputOffset = DISPLAY_WIDTH - width;
hdma2d.Instance = DMA2D;
// Start transfer
HAL_DMA2D_Init(&hdma2d);
HAL_DMA2D_ConfigLayer(&hdma2d, 0);
HAL_DMA2D_Start(&hdma2d, color, (uint32_t)&displayBuffer[y * DISPLAY_WIDTH + x], width, height);
HAL_DMA2D_PollForTransfer(&hdma2d, 100);
}
Practical Example: Menu System with Double Buffering
Here's a more advanced example that implements a simple menu system using double buffering:
#include "stm32f4xx_hal.h"
#include "lcd_driver.h"
#include "font.h" // Include a font library
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
uint16_t frontBuffer[DISPLAY_WIDTH * DISPLAY_HEIGHT];
uint16_t backBuffer[DISPLAY_WIDTH * DISPLAY_HEIGHT];
uint16_t* currentDrawBuffer = backBuffer;
uint16_t* currentDisplayBuffer = frontBuffer;
typedef struct {
char title[20];
uint8_t selected;
} MenuItem;
MenuItem menuItems[5] = {
{"Settings", 0},
{"Status", 0},
{"Calibration", 0},
{"Data Log", 0},
{"About", 0}
};
uint8_t currentMenuIndex = 0;
uint8_t menuItemCount = 5;
// Draw a single pixel
void drawPixel(uint16_t x, uint16_t y, uint16_t color) {
if (x < DISPLAY_WIDTH && y < DISPLAY_HEIGHT) {
currentDrawBuffer[y * DISPLAY_WIDTH + x] = color;
}
}
// Draw a filled rectangle
void fillRect(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint16_t color) {
for (uint16_t j = 0; j < height; j++) {
for (uint16_t i = 0; i < width; i++) {
if ((x + i) < DISPLAY_WIDTH && (y + j) < DISPLAY_HEIGHT) {
currentDrawBuffer[(y + j) * DISPLAY_WIDTH + (x + i)] = color;
}
}
}
}
// Draw text (simplified - you'd need a proper font implementation)
void drawText(uint16_t x, uint16_t y, char* text, uint16_t color) {
// This is a placeholder - implement with your font library
// For each character, draw it at the position using font data
uint16_t xPos = x;
for (int i = 0; text[i] != '\0'; i++) {
// Draw character from font data
// This is simplified - actual implementation depends on your font format
char c = text[i];
// ... draw character pixels ...
xPos += 8; // Advance by character width
}
}
// Draw the menu
void drawMenu(void) {
// Clear back buffer
fillRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, 0x0000); // Black background
// Draw title bar
fillRect(0, 0, DISPLAY_WIDTH, 30, 0x001F); // Blue title bar
drawText(10, 8, "Main Menu", 0xFFFF); // White text
// Draw menu items
for (int i = 0; i < menuItemCount; i++) {
uint16_t itemY = 40 + i * 35;
if (i == currentMenuIndex) {
// Selected item
fillRect(10, itemY, DISPLAY_WIDTH - 20, 30, 0x07E0); // Green
drawText(20, itemY + 8, menuItems[i].title, 0x0000); // Black text
} else {
// Unselected item
fillRect(10, itemY, DISPLAY_WIDTH - 20, 30, 0xCE59); // Light gray
drawText(20, itemY + 8, menuItems[i].title, 0x0000); // Black text
}
}
// Draw footer
fillRect(0, DISPLAY_HEIGHT - 30, DISPLAY_WIDTH, 30, 0x001F); // Blue footer
drawText(10, DISPLAY_HEIGHT - 22, "Press UP/DOWN to navigate", 0xFFFF);
}
// Swap buffers and update display
void swapBuffers(void) {
uint16_t* temp = currentDisplayBuffer;
currentDisplayBuffer = currentDrawBuffer;
currentDrawBuffer = temp;
// Send the display buffer to the screen
LCD_SetCursor(0, 0);
LCD_WriteMultipleData(currentDisplayBuffer, DISPLAY_WIDTH * DISPLAY_HEIGHT);
}
void handleKeyPress(uint8_t key) {
switch (key) {
case KEY_UP:
if (currentMenuIndex > 0) currentMenuIndex--;
break;
case KEY_DOWN:
if (currentMenuIndex < menuItemCount - 1) currentMenuIndex++;
break;
case KEY_SELECT:
menuItems[currentMenuIndex].selected = 1;
// Handle menu selection
break;
}
// Redraw menu with updated selection
drawMenu();
// Swap buffers to update display
swapBuffers();
}
// Main function
int main(void) {
// Initialize system, clocks, LCD, etc.
SystemInit();
LCD_Init();
// Initialize both buffers to black
for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) {
frontBuffer[i] = 0x0000;
backBuffer[i] = 0x0000;
}
// Draw initial menu
drawMenu();
swapBuffers();
while (1) {
// Check for key presses
uint8_t key = readKey(); // Implement this function based on your hardware
if (key != KEY_NONE) {
handleKeyPress(key);
// Add debounce delay
HAL_Delay(200);
}
}
}
When to Use Each Buffering Method
Choose the appropriate buffering method based on your application's needs:
-
Direct Mode (No Buffering):
- Very simple displays with minimal graphics
- Extremely memory-constrained devices
- Static content that rarely changes
-
Single Buffering:
- Most general-purpose applications
- Good balance of performance and memory usage
- When display updates are not frequent
-
Double Buffering:
- Animation-heavy applications
- Games or video playback
- When smooth transitions are critical
- User interfaces that need to be highly responsive
-
Partial Buffering:
- When memory is constrained but better performance than direct mode is needed
- Applications where only specific areas of the screen are updated frequently
- Segmented displays where different areas have different update requirements
Summary
Display buffering is a critical technique for creating smooth and responsive graphical interfaces on STM32 microcontrollers. By understanding the different buffering strategies and their trade-offs, you can choose the most appropriate approach for your specific application.
Key takeaways:
- Direct mode is simple but provides poor performance
- Single buffering offers a good balance of performance and memory usage
- Double buffering provides the smoothest visuals but consumes the most memory
- Partial buffering can be used when memory is limited
- DMA and hardware acceleration can significantly improve performance
Remember to always consider your STM32 device's memory constraints when designing your graphics system, and choose the appropriate buffering strategy accordingly.
Exercise: Implement a Progress Bar
Try implementing a progress bar that updates smoothly using one of the buffering techniques described. The progress bar should:
- Fill from left to right as progress increases
- Change color based on progress (e.g., red to green)
- Show percentage text in the center
- Update smoothly without flickering
Additional Resources
- STM32 Graphics Library (STemWin)
- AN4861: LCD-TFT display controller (LTDC) on STM32 MCUs
- TouchGFX - Advanced graphics framework for STM32
- STM32CubeF4 - Contains examples of LCD interfacing
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)