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
- 🎯 Hybrid Input: IRQ wake-up plus active-touch sampling for smoother drag/move handling
- 🧩 Modular: Clear separation of input processing, game logic, and rendering
Architecture Highlights
IRQ/Timer → 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) {
bool stay_awake = pending_refresh || touch_event_down || (last_touch_time != 0);
if (!stay_awake) {
__wfi(); // Sleep until interrupt
if (!has_pending_wake_source()) continue; // Ignore unrelated wake-ups
}
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 trigger/interrupt mode
- Touch processing uses hybrid mode: IRQ wake-up plus active-session sampling while touch is down
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 event bursts - 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
- Hybrid IRQ + active-session sampling
- Touch debouncing with release hysteresis
- 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