Store game names and descriptions in persistent LuaGameFactoryData structure instead of local stack variables to prevent dangling pointers. Same fix as emulator version.
RP2350 TFT Display with Touch and SD Card Demo
A modular embedded application for RP2350 microcontrollers featuring display, touch, and SD card support with hardware abstraction layers. Now includes a reactive game template architecture!
Features
- Reactive Game Template - Event-driven architecture optimized for e-ink displays and power efficiency
- Display Abstraction Layer - Support for multiple display types (ST7796, ST7789, E-Paper)
- Touch Abstraction Layer - Extensible touch controller support (FT6336U)
- SD Card with FatFS - File system support with board-aware initialization
- Multi-Board Support - Single-file board configuration system for easy board switching
- 1-bit Rendering - Memory-efficient monochrome graphics with GUI widgets
- Automated Build Scripts - Build for one board or all boards with single commands
- Hardware Abstraction - Factory pattern for displays and touch controllers
🎮 Reactive Game Template
The project now uses a clean, event-driven architecture perfect for building games and interactive applications:
Key Features
- ⚡ Event-Driven: Display only updates when input is received
- 🔋 Power Efficient: Uses
__wfi()to sleep between inputs (< 1mA idle) - 📄 E-ink Optimized: Minimizes screen refreshes for e-paper displays
- 🎯 Interrupt-Driven: Touch and button handling via hardware interrupts
- 🧩 Modular: Clear separation of input processing, game logic, and rendering
Architecture Highlights
Interrupt → Set Flag → Wake CPU → Process Input → Update Game → Draw → Refresh → Sleep
Before (Polling Loop):
while(1) {
if (touch_interrupt_flag) {
// Read touch data
// Process coordinates
// Draw directly
// Handle gestures inline
refresh_screen();
}
}
After (Reactive Template):
while(1) {
__wfi(); // Sleep until interrupt
InputEvent input = process_button_input(config);
if (!input.valid) {
input = process_touch_input(config, &last_touch_time);
}
if (input.valid && game_update(&game_state, input, config, &renderer)) {
game_draw(&game_state, &renderer, &gui);
refresh_screen(bit_buffer, display);
}
}
Creating Your Own Game
- Modify GameState - Define your game variables
- Implement game_init() - Set initial values
- Implement game_update() - Handle input and update state
- Implement game_draw() - Render your game graphics
The reactive loop and input system work automatically!
📖 Read the Full Template Usage Guide for detailed examples and patterns.
Example Game (Included)
The template includes a Button Navigation Game demonstrating:
- Hardware button input handling (KEY0 switches focus, KEY1 clicks)
- GUI component usage (buttons, radio buttons, status bars)
- State management (click counters, focus tracking)
- Visual feedback (filled buttons show focus)
- E-ink optimized refreshes
Perfect starting point for your own game!
Supported Hardware Configurations
Available Board Configurations
-
BOARD_FEATHER_TFT - Adafruit Feather RP2350 with 4.0" TFT ST7796
- Display: ST7796 (480x320 RGB TFT)
- Touch: FT6336U capacitive touch
- Features: High-speed refresh, backlight control, SD card support
-
BOARD_PICO2_TFT - Raspberry Pi Pico 2 with 4.0" TFT ST7796
- Display: ST7796 (480x320 RGB TFT)
- Touch: FT6336U capacitive touch
- Features: High-speed refresh, backlight control, SD card support
-
BOARD_PICO2_EINK - Raspberry Pi Pico 2 with E-Ink Display
- Display: E-Paper (400x300 monochrome)
- Touch: None (uses hardware buttons KEY0/KEY1)
- Features: Ultra-low power, partial/full refresh modes
Supported Displays
- ST7796 - 480x320 RGB TFT LCD (Fully implemented)
- E-Paper - Various sizes, monochrome (Fully implemented)
- ST7789 - Ready for driver implementation
Supported Touch Controllers
- FT6336U - I2C capacitive touch with gesture support (Fully implemented)
- Extensible architecture for additional controllers
Board Configuration System
Switching Between Boards
The project uses a single-file configuration system. To switch boards, simply edit board_config.h and uncomment your target board:
// ---- SELECT YOUR BOARD HERE ----
// #define BOARD_FEATHER_TFT // Feather RP2350 + 4.0" TFT ST7796
// #define BOARD_PICO2_TFT // Pico 2 + 4.0" TFT ST7796
#define BOARD_PICO2_EINK // Pico 2 + E-Ink Display
// --------------------------------
Important: Only one board should be uncommented at a time!
What Gets Configured
Each board configuration includes:
- Display type and resolution
- All pin assignments (SPI, I2C, GPIO)
- Touch controller configuration
- Coordinate transformation (rotation/inversion)
- SD card pins
- Hardware button pins (e-ink boards)
- Communication speeds (SPI/I2C baud rates)
Board-Specific Files
Board configurations are located in board_configs/:
board_feather_tft.h- Feather RP2350 + TFT configurationboard_pico2_tft.h- Pico 2 + TFT configurationboard_pico2_eink.h- Pico 2 + E-Ink configuration
Building and Flashing
Method 1: Build for Currently Selected Board
The simplest method - builds for whatever board is selected in board_config.h:
./build_and_flash.sh
This script will:
- Detect which board is selected in
board_config.h - Configure CMake if needed
- Build the project using Ninja
- Flash to the board using picotool (if board is in BOOTSEL mode)
To build without flashing:
mkdir -p build
cd build
cmake -G Ninja ..
ninja
The output file will be build/basic1.uf2.
Method 2: Build for All Boards
To create UF2 files for all supported boards in one command:
./build_all_boards.sh
This script will:
- Backup your current
board_config.h - Build each board configuration automatically
- Generate board-specific output files:
basic1_feather_tft.uf2basic1_pico2_tft.uf2basic1_pico2_eink.uf2
- Restore your original
board_config.h
This is useful for:
- Creating releases for multiple hardware variants
- Testing code on all board configurations
- Batch building without manual configuration changes
Manual Flashing
- Hold the BOOTSEL button while connecting USB
- The board appears as a USB drive (e.g.,
RPI-RP2) - Copy the appropriate
.uf2file to the drive:cp basic1.uf2 /Volumes/RPI-RP2/ - The board automatically reboots and runs the program
Using picotool
If picotool is installed at ~/.pico-sdk/picotool/:
# Load and auto-reboot
picotool load build/basic1.uf2 -fx
# Or load without auto-reboot
picotool load build/basic1.uf2
Project Structure
basic1/
├── basic1.cpp # Main application
├── board_config.h # Board selector (edit this to switch boards!)
├── board_configs/ # Board-specific configurations
│ ├── board_feather_tft.h # Feather RP2350 + TFT pin config
│ ├── board_pico2_tft.h # Pico 2 + TFT pin config
│ ├── board_pico2_eink.h # Pico 2 + E-Ink pin config
│ └── README.md # Board config documentation
│
├── CMakeLists.txt # Build configuration
├── build_all_boards.sh # Build all boards automatically
├── build_and_flash.sh # Build and flash current board
│
├── display/ # Display and GUI abstraction layer
│ ├── low_level_display.h # Display interface (factory pattern)
│ ├── low_level_display_st7796.cpp # ST7796 TFT implementation
│ ├── low_level_display_epaper.cpp # E-Paper implementation
│ ├── low_level_touch.h # Touch interface (factory pattern)
│ ├── low_level_touch_ft6336u.cpp # FT6336U touch implementation
│ ├── low_level_render.h/cpp # 1-bit rendering engine
│ └── low_level_gui.h/cpp # GUI widgets (buttons, gauges, etc.)
│
├── lib/ # Hardware drivers
│ ├── ft6336u/ # FT6336U capacitive touch driver
│ ├── st7796/ # ST7796 TFT display driver
│ ├── epaper/ # E-Paper display driver
│ ├── sd_card/ # SD card driver with FatFS test
│ └── fatfs/ # FatFS filesystem
│
├── fonts/ # 25+ bitmap font definitions (5x5 to 8x8)
└── fatfs_time.c # FatFS timestamp support
Adding a New Board Configuration
-
Create a new config file in
board_configs/:cp board_configs/board_pico2_tft.h board_configs/board_myboard.h -
Edit the new file and update all pin definitions for your hardware
-
Add your board to
board_config.h:// ---- SELECT YOUR BOARD HERE ---- // #define BOARD_FEATHER_TFT // #define BOARD_PICO2_TFT // #define BOARD_PICO2_EINK #define BOARD_MYBOARD // Your new board // -------------------------------- -
Add the include section:
#ifdef BOARD_FEATHER_TFT #include "board_configs/board_feather_tft.h" // ... other boards ... #elif defined(BOARD_MYBOARD) #include "board_configs/board_myboard.h" #endif -
(Optional) Add to
build_all_boards.shfor automated builds
See board_configs/README.md for detailed configuration structure.
Hardware Configuration Details
Display Types
Each board configuration specifies a display type via DISPLAY_TYPE_SELECTED:
DISPLAY_TYPE_ST7796_VAL(0) - ST7796 TFT (480x320)DISPLAY_TYPE_ST7789_VAL(1) - ST7789 TFT (ready for implementation)DISPLAY_TYPE_EPAPER_VAL(2) - E-Paper displays (various sizes)
Touch Controller Types
Each board configuration specifies a touch type via TOUCH_TYPE_SELECTED:
TOUCH_TYPE_FT6336U_VAL(0) - FT6336U capacitive touch with gesture supportTOUCH_TYPE_NONE_VAL(1) - No touch controller (use hardware buttons)
Touch Coordinate Transformation
Touch coordinates can be adjusted for different mounting orientations:
#define TOUCH_SWAP_XY true // Swap X/Y coordinates (for 90° rotation)
#define TOUCH_INVERT_X true // Invert X axis (mirror horizontally)
#define TOUCH_INVERT_Y false // Invert Y axis (mirror vertically)
These settings are in each board's config file under board_configs/.
Pin Assignments
Each board config file defines all hardware pin connections:
Display Pins:
DISPLAY_SPI_PORT- SPI bus (spi0 or spi1)DISPLAY_SCK_PIN,DISPLAY_MOSI_PIN,DISPLAY_MISO_PIN- SPI data linesDISPLAY_CS_PIN- Chip select (active LOW)DISPLAY_DC_PIN- Data/Command selectDISPLAY_RST_PIN- Hardware resetDISPLAY_BL_PIN- Backlight control (TFT only, -1 for e-ink)DISPLAY_BUSY_PIN- Busy signal (E-Paper only, -1 for TFT)
Touch Pins:
TOUCH_I2C_PORT- I2C bus (i2c0 or i2c1)TOUCH_SDA_PIN,TOUCH_SCL_PIN- I2C data linesTOUCH_INT_PIN- Interrupt pin (active LOW when touch detected)TOUCH_RST_PIN- Hardware reset
Button Pins (E-Ink boards):
BUTTON_KEY0_PIN- First hardware button (active LOW)BUTTON_KEY1_PIN- Second hardware button (active LOW)
SD Card Pins (optional):
SD_SPI_PORT,SD_CS_PIN,SD_MISO_PIN,SD_MOSI_PIN,SD_SCK_PIN
Communication Speeds
Configured per board based on display type:
SPI_BAUDRATE- Display SPI speed (32MHz for TFT, 4MHz for e-ink)I2C_BAUDRATE- Touch controller I2C speed (typically 400kHz)
Architecture
Display Abstraction Layer
Provides a unified interface for different display types:
class LowLevelDisplay {
public:
virtual bool init() = 0;
virtual void clear(bool white = true) = 0;
virtual void draw_buffer(const uint8_t* bit_buffer) = 0;
virtual void refresh() = 0;
// ... more methods
static LowLevelDisplay* create(DisplayType type, int width, int height);
};
Usage:
LowLevelDisplay* display = LowLevelDisplay::create(
(DisplayType)DISPLAY_TYPE_SELECTED, V_WIDTH, V_HEIGHT);
display->init();
display->draw_buffer(buffer);
display->refresh();
Touch Abstraction Layer
Unified interface for touch controllers:
class LowLevelTouch {
public:
virtual bool init() = 0;
virtual bool read_touch(TouchData* data) = 0;
virtual bool is_touched() = 0;
// ... more methods
static LowLevelTouch* create(TouchType type, int width, int height,
bool swap_xy, bool invert_x, bool invert_y);
};
Usage:
LowLevelTouch* touch = LowLevelTouch::create(
(TouchType)TOUCH_TYPE_SELECTED, V_WIDTH, V_HEIGHT,
TOUCH_SWAP_XY, TOUCH_INVERT_X, TOUCH_INVERT_Y);
TouchData touch_data;
if (touch->read_touch(&touch_data)) {
int x = touch_data.points[0].x;
int y = touch_data.points[0].y;
// Handle touch
}
SD Card Abstraction
Board-aware initialization:
// Initialize with board configuration
if (sd_card_init_with_board_config()) {
// Test FatFS functionality
sd_card_test_fatfs();
}
The test function:
- Mounts FatFS
- Lists directory contents
- Creates and reads test file
- Safely unmounts filesystem
Pin Configurations Summary
Actual pin assignments are defined in board_configs/*.h files. Here's a quick reference:
BOARD_FEATHER_TFT (Adafruit Feather RP2350)
- Display (ST7796, SPI1): SCK=18, MOSI=19, MISO=20, CS=17, DC=16, RST=15, BL=14
- Touch (FT6336U, I2C0): SDA=4, SCL=5, INT=6, RST=7
- SD Card (SPI1): CS=10 (shares SPI with display)
BOARD_PICO2_TFT (Raspberry Pi Pico 2)
- Display (ST7796, SPI0): SCK=2, MOSI=3, MISO=4, CS=5, DC=6, RST=7, BL=8
- Touch (FT6336U, I2C0): SDA=12, SCL=13, INT=14, RST=15
- SD Card (SPI1): CS=17
BOARD_PICO2_EINK (Raspberry Pi Pico 2 + E-Ink)
- Display (E-Paper, SPI0): SCK=10, MOSI=11, MISO=12, CS=9, DC=8, RST=12, BUSY=13
- Touch: None (uses hardware buttons instead)
- Buttons: KEY0=GP15 (active LOW), KEY1=GP17 (active LOW)
See individual board config files in board_configs/ for complete details.
Extending the System
Adding a New Display Driver
-
Create implementation files:
display/low_level_display_mynewdisplay.h display/low_level_display_mynewdisplay.cpp -
Inherit from
LowLevelDisplaybase class and implement all virtual methods -
Add a new constant in
board_config.h:#define DISPLAY_TYPE_MYNEWDISPLAY_VAL 3 -
Update the factory in
display/low_level_display.cpp:case 3: // DISPLAY_TYPE_MYNEWDISPLAY display = new LowLevelDisplayMyNewDisplay(width, height); break; -
Update
CMakeLists.txtto compile the new files
Adding a New Touch Controller
-
Create implementation files:
display/low_level_touch_mynewtouch.h display/low_level_touch_mynewtouch.cpp -
Inherit from
LowLevelTouchbase class -
Add constant and factory case similar to display drivers
-
Update CMakeLists.txt
Workflow Summary
The typical development workflow:
- Select target board: Edit
board_config.h, uncomment your board - Build: Run
./build_and_flash.sh(builds and flashes current board) - Test: Connect to USB, monitor via serial terminal
- Switch boards: Edit
board_config.h, rebuild - Release: Run
./build_all_boards.shto create UF2s for all boards
Memory Usage
- 1-bit framebuffer: Width×Height÷8 bytes
- 480×320: 19.2 KB
- 400×300: 15.0 KB
- Display conversion: Automatic 1-bit → RGB565 (TFT) or native (E-Paper)
- Stack/heap: Minimal, uses static buffers where possible
- Code size: ~100-150 KB depending on features enabled
Troubleshooting
Build Issues
Error: "No board selected!"
- You must uncomment exactly one
BOARD_xxxdefine inboard_config.h
CMake configuration errors:
- Delete
build/directory and reconfigure:rm -rf build && mkdir build
Display Issues
Display stays blank:
- Check SPI wiring (especially CS, DC, SCK, MOSI pins)
- Verify power supply (some displays need 5V logic level shifters)
- Check
DISPLAY_TYPE_SELECTEDmatches your hardware
Wrong colors or garbled display:
- Verify display driver type (ST7796 vs ST7789)
- Check SPI baud rate (try reducing from 32MHz to 16MHz)
Touch Issues
Touch not responding:
- Verify I2C wiring (SDA, SCL, INT, RST pins)
- Check for pull-up resistors on I2C lines (typically 4.7kΩ)
- Confirm touch controller type and I2C address
- Enable debug output to see if touch data is being read
Touch coordinates inverted or swapped:
- Adjust
TOUCH_SWAP_XY,TOUCH_INVERT_X,TOUCH_INVERT_Yin board config - These depend on physical mounting orientation
Interrupt not triggering:
- Verify
TOUCH_INT_PINis correctly defined - Check INT pin is pulled high (internal pull-up or external resistor)
- Confirm touch controller is configured for interrupt mode
SD Card Issues
SD Card not detected:
- Verify card is inserted and formatted (FAT32)
- Check SPI wiring and CS pin
- Ensure SD card shares SPI correctly with display (different CS pins)
- Try different SD card (some old/large cards have compatibility issues)
E-Paper Display Issues
Ghosting or partial images:
- Use
full_refresh()periodically to clear ghosting - E-Paper retains previous image until fully refreshed
Slow refresh:
- Normal for e-paper (several seconds for full refresh)
- Use partial refresh for faster updates (may cause ghosting)
BUSY pin timeout:
- Verify
DISPLAY_BUSY_PINis correctly connected - Increase timeout in e-paper driver if needed
Development Tips
Serial Debugging
Connect via USB and monitor serial output:
# macOS
screen /dev/tty.usbmodem* 115200
# Linux
screen /dev/ttyACM0 115200
# Exit screen: Ctrl+A, then K
The code includes printf() statements for debugging touch, display, and SD card operations.
Power Optimization
For battery-powered projects:
- E-Paper displays use almost no power when idle
- Use
__wfi()(Wait For Interrupt) to sleep between inputs - Disable TFT backlight when idle
- Lower SPI/I2C baud rates to reduce power consumption
Performance Tips
TFT Displays:
- Increase SPI baud rate up to 62.5MHz if display supports it
- Minimize full-screen refreshes (use partial updates if possible)
- 1-bit rendering is much faster than RGB565
E-Paper Displays:
- Use partial refresh for responsive UI (accepts some ghosting)
- Full refresh only when needed (menu changes, game over, etc.)
- Consider caching frequently used graphics
Features by Component
Display Features
- 1-bit monochrome rendering
- RGB565 color support (ST7796)
- Drawing primitives (lines, rectangles, circles)
- GUI widgets (windows, gauges, status bars)
- Multiple font support
Touch Features
- Multi-touch support (up to 2 points)
- Coordinate transformation
- Touch debouncing
- Event types (press, lift, contact)
SD Card Features
- SPI mode support
- FatFS integration
- Directory listing
- File read/write
- Automatic timestamps from compile time
License
Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. SPDX-License-Identifier: Apache-2.0
Dependencies
- Raspberry Pi Pico SDK 2.2.0+
- FatFS (included)
- TinyUSB (via SDK)
- Hardware drivers (included in
lib/)
Contributing
When adding new features:
- Follow the abstraction layer pattern
- Update
board_config.hfor hardware settings - Keep application code hardware-agnostic
- Test on multiple boards if possible
- Update this README