07. Framebuffer Graphics - Drawing to the Screen¶
Moving beyond serial console, let's display graphics directly on screen using the Raspberry Pi's framebuffer. This enables rich visual output for games, GUIs, and animations.
What is a Framebuffer?¶
A framebuffer is a region of video memory that represents the screen. Each pixel's color is stored as a value in this buffer, and the GPU continuously reads from it to display on the monitor.
How it Works¶
flowchart LR
CPU["CPU"] -->|"Writes pixels"| FB["Framebuffer<br/>(RAM)"]
FB -->|"Reads continuously"| GPU["GPU"]
GPU -->|"HDMI Signal"| Display["Monitor/Display"]
style FB fill:#ffeb3b
- CPU: Writes pixel colors to framebuffer memory
- GPU: Reads framebuffer and outputs to display
- Display: Shows the pixels on screen
Mailbox Protocol¶
On Raspberry Pi, the CPU communicates with the GPU via a mailbox interface. This is a message-passing system where:
- CPU sends requests to GPU
- GPU processes and responds
- Used for: allocating framebuffers, setting resolution, getting hardware info
Mailbox Registers (RPi 4)¶
| Register | Offset | Purpose |
|---|---|---|
MAILBOX_READ |
0x00 | Read messages from GPU |
MAILBOX_STATUS |
0x18 | Check if mailbox is full/empty |
MAILBOX_WRITE |
0x20 | Write messages to GPU |
Base address: 0xFE00B880 (RPi 4)
Mailbox Channels¶
| Channel | Use |
|---|---|
| 0 | Power management |
| 1 | Framebuffer (deprecated) |
| 8 | Property tags (we use this) |
Property Tag Format¶
Messages are structured as property tags:
| Index | Field | Description |
|---|---|---|
| buffer[0] | Total Size | Total message size in bytes |
| buffer[1] | Request Code | 0 for request |
| buffer[2] | TAG_ID | Property tag identifier |
| buffer[3] | Value Size | Size of value buffer |
| buffer[4] | Request/Response | 0 for request |
| buffer[5...N-2] | Values | Tag-specific values |
| buffer[N-1] | End Tag | 0 to mark end |
Implementing the Mailbox Driver¶
Header: include/mailbox.h¶
16-Byte Alignment
The mailbox buffer must be 16-byte aligned. Use __attribute__((aligned(16))).
Driver: drivers/mailbox.c¶
Framebuffer Initialization¶
To get a framebuffer, we send multiple property tags to the GPU:
- Set physical dimensions (width × height)
- Set virtual dimensions (same as physical for now)
- Set color depth (32 bits = ARGB)
- Get pitch (bytes per line)
- Allocate framebuffer
Code: drivers/framebuffer.c¶
Virtual Offset Tag Required
The MBOX_TAG_FB_SET_VIRT_OFF tag is essential for proper framebuffer initialization on Raspberry Pi 4. Without it, the screen will remain black even if other tags succeed.
4096-Byte Alignment
Use 4096 for alignment instead of 16. This ensures proper memory alignment for the GPU.
Framebuffer Address¶
The GPU returns a bus address. We must convert it to a CPU address:
Drawing Pixels¶
Pixel Format (32-bit ARGB)¶
Each pixel is 4 bytes:
| Byte 3 | Byte 2 | Byte 1 | Byte 0 |
|---|---|---|---|
| Alpha (0xFF) | Red (0x00-0xFF) | Green (0x00-0xFF) | Blue (0x00-0xFF) |
32-bit value: 0xAARRGGBB
Combine as: 0xAARRGGBB
Example colors:
- Red: 0xFFFF0000
- Green: 0xFF00FF00
- Blue: 0xFF0000FF
- White: 0xFFFFFFFF
Framebuffer Memory Layout¶
graph TB
subgraph "Screen (Width × Height)"
Row0["Row 0: pixel[0] pixel[1] ... pixel[width-1]"]
Row1["Row 1: pixel[0] pixel[1] ... pixel[width-1]"]
RowN["Row N: pixel[0] pixel[1] ... pixel[width-1]"]
Row0 -.->|"pitch bytes"| Row1
Row1 -.->|"pitch bytes"| RowN
end
Note["Pitch may include padding\npitch ≥ width × 4 bytes"]
style Row0 fill:#e3f2fd
style Row1 fill:#e3f2fd
style RowN fill:#e3f2fd
style Note fill:#fff9c4
Setting a Pixel¶
Why pitch / 4?
- Pitch is in bytes (total bytes per row)
- Each pixel is 4 bytes (32 bits)
- So one row has pitch / 4 pixels
- Pitch may be larger than width × 4 due to alignment padding
Filling a Rectangle¶
Drawing Lines (Bresenham's Algorithm)¶
Bresenham's algorithm draws lines efficiently without floating-point math:
Color Helpers¶
Define colors as macros:
Building and Testing¶
1. Build¶
2. Deploy¶
Copy framebuffer_demo.img to SD card (rename to kernel8.img).
Important: Add the following to config.txt on the SD card:
Without this setting, the resolution requested in framebuffer_init() may not be applied correctly, and the display may use default or unexpected dimensions.
3. Connect Display¶
Connect HDMI cable to monitor.
4. Expected Output¶
On screen: - 3 large rectangles (red, green, blue) at top - 2 rectangles (yellow, cyan) at bottom - White diagonal lines - Bouncing white square animation
On serial:
Performance Considerations¶
Double Buffering¶
For smooth animation without flicker, use double buffering:
- Draw to back buffer
- Swap back and front buffers
- Repeat
Implement by setting virtual height = 2 × physical height.
DMA for Fast Copy¶
For large fills, use DMA instead of CPU loops:
Complete Source Code¶
- drivers/mailbox.c - GPU communication
- drivers/framebuffer.c - Graphics API
- include/mailbox.h
- include/framebuffer.h
- examples/07_framebuffer_demo.c - Demo
Troubleshooting¶
| Problem | Solution |
|---|---|
| Black screen | Check HDMI connection, verify mailbox response |
| Wrong colors | Check byte order (ARGB vs RGBA) |
| Garbled display | Incorrect pitch calculation |
| Slow drawing | Use DMA, reduce resolution, or optimize loops |
Important Notes¶
Framebuffer Address
The framebuffer address from GPU is a bus address, not a CPU address. Mask with 0x3FFFFFFF to convert.
Resolution Limits
Maximum resolution depends on GPU memory. 1920×1080 @ 32bpp works reliably.
What's Next?¶
With graphics working, we can now: - Implement text rendering (fonts and console) - Create a graphical shell or desktop environment - Build games with sprites and animations
The next article covers Text Rendering, which will allow us to print text on screen instead of just colored rectangles!