2026-02-18 15:53:48 -05:00
2026-01-28 17:22:15 -05:00
2026-01-28 17:22:15 -05:00
2026-01-31 22:23:49 -05:00
2026-01-29 14:33:54 -05:00
2026-01-30 22:52:25 -05:00
2026-01-28 20:12:41 -05:00
2026-01-28 17:22:15 -05:00
2026-01-29 15:58:58 -05:00

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

  1. Modify GameState - Define your game variables
  2. Implement game_init() - Set initial values
  3. Implement game_update() - Handle input and update state
  4. 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

  1. 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
  2. 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
  3. 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 configuration
  • board_pico2_tft.h - Pico 2 + TFT configuration
  • board_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:

  1. Detect which board is selected in board_config.h
  2. Configure CMake if needed
  3. Build the project using Ninja
  4. 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:

  1. Backup your current board_config.h
  2. Build each board configuration automatically
  3. Generate board-specific output files:
    • basic1_feather_tft.uf2
    • basic1_pico2_tft.uf2
    • basic1_pico2_eink.uf2
  4. 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

  1. Hold the BOOTSEL button while connecting USB
  2. The board appears as a USB drive (e.g., RPI-RP2)
  3. Copy the appropriate .uf2 file to the drive:
    cp basic1.uf2 /Volumes/RPI-RP2/
    
  4. 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

  1. Create a new config file in board_configs/:

    cp board_configs/board_pico2_tft.h board_configs/board_myboard.h
    
  2. Edit the new file and update all pin definitions for your hardware

  3. 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
    // --------------------------------
    
  4. 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
    
  5. (Optional) Add to build_all_boards.sh for 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 support
  • TOUCH_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 lines
  • DISPLAY_CS_PIN - Chip select (active LOW)
  • DISPLAY_DC_PIN - Data/Command select
  • DISPLAY_RST_PIN - Hardware reset
  • DISPLAY_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 lines
  • TOUCH_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

  1. Create implementation files:

    display/low_level_display_mynewdisplay.h
    display/low_level_display_mynewdisplay.cpp
    
  2. Inherit from LowLevelDisplay base class and implement all virtual methods

  3. Add a new constant in board_config.h:

    #define DISPLAY_TYPE_MYNEWDISPLAY_VAL  3
    
  4. Update the factory in display/low_level_display.cpp:

    case 3:  // DISPLAY_TYPE_MYNEWDISPLAY
        display = new LowLevelDisplayMyNewDisplay(width, height);
        break;
    
  5. Update CMakeLists.txt to compile the new files

Adding a New Touch Controller

  1. Create implementation files:

    display/low_level_touch_mynewtouch.h
    display/low_level_touch_mynewtouch.cpp
    
  2. Inherit from LowLevelTouch base class

  3. Add constant and factory case similar to display drivers

  4. Update CMakeLists.txt

Workflow Summary

The typical development workflow:

  1. Select target board: Edit board_config.h, uncomment your board
  2. Build: Run ./build_and_flash.sh (builds and flashes current board)
  3. Test: Connect to USB, monitor via serial terminal
  4. Switch boards: Edit board_config.h, rebuild
  5. Release: Run ./build_all_boards.sh to 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_xxx define in board_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_SELECTED matches 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_Y in board config
  • These depend on physical mounting orientation

Interrupt not triggering:

  • Verify TOUCH_INT_PIN is 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_PIN is 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:

  1. Follow the abstraction layer pattern
  2. Update board_config.h for hardware settings
  3. Keep application code hardware-agnostic
  4. Test on multiple boards if possible
  5. Update this README
Description
No description provided
Readme 6.5 MiB
Languages
C 84.5%
Python 7.1%
C++ 3.8%
HTML 3.2%
Lua 1.1%
Other 0.2%