abstracting display, touch and sd

This commit is contained in:
Adolfo Reyna
2026-01-28 20:12:41 -05:00
parent 57426c6e7d
commit adfbef7228
396 changed files with 101836 additions and 272 deletions

933
lib/st7796/st7796.c Normal file
View File

@@ -0,0 +1,933 @@
/*
* Copyright (c) 2021 Arm Limited and Contributors. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* ST7796 TFT LCD Display Driver - Implementation
* Based on https://github.com/giemma/RapsberryPiPico/blob/main/simpleST7796/simpleST7796.c
*
* ==============================================================================
* IMPLEMENTATION NOTES
* ==============================================================================
*
* This driver uses a minimal initialization approach that was discovered
* through extensive debugging. The ST7796 controller is very sensitive to
* initialization sequences, and many example codes include extended commands
* that cause compatibility issues.
*
* Key Design Decisions:
*
* 1. MINIMAL INITIALIZATION:
* We only use essential commands (SWRESET, SLPOUT, COLMOD, MADCTL, DISPON)
* Extended commands (CSCON, gamma, power control) caused white screen issues
* This approach is more reliable across different ST7796 modules
*
* 2. SPI SPEED:
* Currently set to 100 MHz (can be adjusted in st7796_init)
* ST7796 datasheet specifies max 15.15 MHz for SPI write, but in practice
* modern displays with level shifters work reliably at much higher speeds
* Tested successfully at 80 MHz, attempting 100 MHz
*
* 3. BUFFER OPTIMIZATION:
* Uses 512-byte buffers (256 pixels) for bulk transfers
* This balances memory usage with SPI efficiency
* Larger buffers don't significantly improve performance
*
* 4. COLOR FORMAT:
* RGB565 (16-bit color) is used throughout
* Bytes are sent MSB first: [R4R3R2R1R0G5G4G3][G2G1G0B4B3B2B1B0]
*
* 5. COORDINATE SYSTEM:
* Origin (0,0) is top-left corner
* X increases rightward (0-479 in landscape)
* Y increases downward (0-319 in landscape)
* MADCTL=0xE0 provides proper landscape orientation
*
* Debugging History:
* - Initial attempt with full initialization → white screen
* - Removed CSCON and extended commands → display working
* - Adjusted MADCTL for orientation → proper landscape mode
* - Optimized SPI speed from 62.5 → 80 → 100 MHz
*
* ==============================================================================
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "hardware/gpio.h"
#include "hardware/spi.h"
#include "pico/binary_info.h"
#include "pico/stdlib.h"
#include "st7796.h"
// ST7796 Standard LCD Commands
// These are common across most ST7796 controllers
#define ST7796_NOP 0x00 // No Operation
#define ST7796_SWRESET 0x01 // Software Reset
#define ST7796_SLPIN 0x10 // Sleep In (enter low power mode)
#define ST7796_SLPOUT 0x11 // Sleep Out (exit low power mode)
#define ST7796_PTLON 0x12 // Partial Display Mode On
#define ST7796_NORON 0x13 // Normal Display Mode On
#define ST7796_INVOFF 0x20 // Display Inversion Off
#define ST7796_INVON 0x21 // Display Inversion On
#define ST7796_DISPOFF 0x28 // Display Off (blank screen, keep framebuffer)
#define ST7796_DISPON 0x29 // Display On (show framebuffer)
#define ST7796_CASET 0x2A // Column Address Set (X range)
#define ST7796_RASET 0x2B // Row Address Set (Y range)
#define ST7796_RAMWR 0x2C // Memory Write (send pixel data)
#define ST7796_RAMRD 0x2E // Memory Read (read pixel data)
#define ST7796_MADCTL 0x36 // Memory Access Control (rotation, mirroring)
#define ST7796_COLMOD 0x3A // Pixel Format Set (color depth)
// ST7796 Extended Commands
// These are manufacturer-specific and may vary between modules
// Currently UNUSED in our minimal initialization
#define ST7796_CSCON 0xF0 // Command Set Control (switch command sets)
#define ST7796_IFMODE 0xB0 // Interface Mode Control
#define ST7796_FRMCTR1 0xB1 // Frame Rate Control (In Normal Mode)
#define ST7796_DIC 0xB4 // Display Inversion Control
#define ST7796_BPC 0xB5 // Blanking Porch Control
#define ST7796_DFC 0xB6 // Display Function Control
#define ST7796_EM 0xB7 // Entry Mode Set
#define ST7796_PWR2 0xC2 // Power Control 2
#define ST7796_VCMPCTL 0xC5 // VCOM Control
#define ST7796_DOCA 0xE8 // Display Output Ctrl Adjust
#define ST7796_PGC 0xE0 // Positive Gamma Control
#define ST7796_NGC 0xE1 // Negative Gamma Control
// MADCTL (0x36) bit definitions
// These control display orientation and color order
#define ST7796_MADCTL_MY 0x80 // Row Address Order (0=top-to-bottom, 1=bottom-to-top)
#define ST7796_MADCTL_MX 0x40 // Column Address Order (0=left-to-right, 1=right-to-left)
#define ST7796_MADCTL_MV 0x20 // Row/Column Exchange (0=normal, 1=swap X/Y)
#define ST7796_MADCTL_ML 0x10 // Vertical Refresh Order (0=top-to-bottom, 1=bottom-to-top)
#define ST7796_MADCTL_RGB 0x00 // RGB color order
#define ST7796_MADCTL_BGR 0x08 // BGR color order
// Global state variables
// These hold the current display configuration
// Global state variables
// These hold the current display configuration
static const struct st7796_config *config; // Pin and SPI configuration
static uint16_t width; // Display width in pixels (e.g., 480)
static uint16_t height; // Display height in pixels (e.g., 320)
static uint16_t x_offset; // X offset for display alignment (currently 0)
static uint16_t y_offset; // Y offset for display alignment (currently 0)
/**
* @brief Activate chip select (pull CS LOW)
*
* The ST7796 is selected when CS is LOW. The NOP instructions provide
* a small delay to ensure clean signal transitions at high SPI speeds.
*/
static inline void cs_select() {
if (config->gpio_cs >= 0) {
asm volatile("nop \n nop \n nop"); // Small delay for signal stability
gpio_put(config->gpio_cs, 0); // Pull CS LOW (active)
asm volatile("nop \n nop \n nop");
}
}
/**
* @brief Deactivate chip select (pull CS HIGH)
*
* When CS is HIGH, the ST7796 ignores SPI communications. This allows
* multiple devices to share the same SPI bus.
*/
static inline void cs_deselect() {
if (config->gpio_cs >= 0) {
asm volatile("nop \n nop \n nop");
gpio_put(config->gpio_cs, 1); // Pull CS HIGH (inactive)
asm volatile("nop \n nop \n nop");
}
}
/**
* @brief Set DC pin for COMMAND mode
*
* When DC is LOW, the next byte sent over SPI is interpreted as a command.
* Commands tell the display what operation to perform (e.g., set window,
* write pixel data, change settings).
*/
static inline void dc_command() {
asm volatile("nop \n nop \n nop");
gpio_put(config->gpio_dc, 0); // DC LOW = Command mode
asm volatile("nop \n nop \n nop");
}
/**
* @brief Set DC pin for DATA mode
*
* When DC is HIGH, the next bytes sent over SPI are interpreted as data
* (e.g., pixel colors, configuration parameters for the previous command).
*/
static inline void dc_data() {
asm volatile("nop \n nop \n nop");
gpio_put(config->gpio_dc, 1); // DC HIGH = Data mode
asm volatile("nop \n nop \n nop");
}
/**
* @brief Hardware reset sequence
*
* The ST7796 has a hardware reset pin (RST) that performs a full reset
* of the controller. This is more reliable than software reset for
* recovering from unknown states.
*
* Timing: HIGH(5ms) → LOW(15ms) → HIGH(15ms)
* After reset, the display is in a known initial state and ready for
* initialization commands.
*/
static inline void reset_pulse() {
gpio_put(config->gpio_rst, 1);
sleep_ms(5);
gpio_put(config->gpio_rst, 0); // Hold LOW for reset
sleep_ms(15);
gpio_put(config->gpio_rst, 1); // Release reset
sleep_ms(15); // Wait for display to initialize
}
/**
* @brief Send a single command byte
*
* Commands are single-byte opcodes that tell the display what to do.
* Some commands take parameters (sent via write_data after the command).
*
* @param cmd Command byte (e.g., ST7796_RAMWR, ST7796_CASET)
*/
static void write_command(uint8_t cmd) {
dc_command(); // Set DC LOW for command
cs_select(); // Activate display
spi_write_blocking(config->spi, &cmd, 1); // Send command byte
cs_deselect(); // Deactivate display
}
/**
* @brief Send data bytes
*
* Data bytes are parameters for the previous command, or pixel data
* when following a RAMWR command.
*
* @param data Pointer to data buffer
* @param len Number of bytes to send
*/
static void write_data(const uint8_t *data, size_t len) {
dc_data(); // Set DC HIGH for data
cs_select(); // Activate display
spi_write_blocking(config->spi, data, len); // Send data bytes
cs_deselect(); // Deactivate display
}
/**
* @brief Send a command followed by data
*
* Convenience function for commands that always take parameters.
* Example: CASET (column address set) always needs 4 bytes (x0, x1).
*
* @param cmd Command byte
* @param data Pointer to parameter data
* @param len Number of parameter bytes
*/
static void write_command_with_data(uint8_t cmd, const uint8_t *data, size_t len) {
write_command(cmd);
write_data(data, len);
}
/**
* @brief Set drawing window (active pixel region)
*
* The ST7796 has a "window" concept where you define a rectangular region,
* then all subsequent RAMWR commands write pixels sequentially within that
* window (left-to-right, top-to-bottom, wrapping at the right edge).
*
* This is how all drawing operations work:
* 1. Set window to desired region
* 2. Send RAMWR command
* 3. Stream pixel data
*
* @param x0 Left column (inclusive)
* @param y0 Top row (inclusive)
* @param x1 Right column (inclusive)
* @param y1 Bottom row (inclusive)
*
* Note: Coordinates are automatically offset if x_offset/y_offset are set.
* This compensates for displays where the physical screen doesn't align
* with the controller's framebuffer (common with ST7789/ST7796).
*/
static void set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
uint8_t data[4];
// Add offsets for display positioning
// (Currently 0 for ST7796, but can be adjusted if needed)
x0 += x_offset;
x1 += x_offset;
y0 += y_offset;
y1 += y_offset;
// CASET: Column Address Set (X coordinates)
// Format: [x0_high, x0_low, x1_high, x1_low]
data[0] = (x0 >> 8) & 0xFF; // X start MSB
data[1] = x0 & 0xFF; // X start LSB
data[2] = (x1 >> 8) & 0xFF; // X end MSB
data[3] = x1 & 0xFF; // X end LSB
write_command_with_data(ST7796_CASET, data, 4);
// RASET: Row Address Set (Y coordinates)
// Format: [y0_high, y0_low, y1_high, y1_low]
data[0] = (y0 >> 8) & 0xFF; // Y start MSB
data[1] = y0 & 0xFF; // Y start LSB
data[2] = (y1 >> 8) & 0xFF; // Y end MSB
data[3] = y1 & 0xFF; // Y end LSB
write_command_with_data(ST7796_RASET, data, 4);
// RAMWR: Memory Write
// After this command, all data sent is interpreted as pixel colors
write_command(ST7796_RAMWR);
}
/**
* @brief Initialize the ST7796 display controller
*
* This function performs the complete initialization sequence. It was developed
* through extensive trial-and-error to find a minimal set of commands that work
* reliably across different ST7796 modules.
*
* Initialization Steps:
* 1. Configure GPIO pins and SPI interface
* 2. Hardware reset
* 3. Software reset (SWRESET)
* 4. Exit sleep mode (SLPOUT)
* 5. Set pixel format to RGB565 (COLMOD)
* 6. Set display orientation to landscape (MADCTL)
* 7. Disable inversion (INVOFF)
* 8. Enable normal display mode (NORON)
* 9. Turn on display (DISPON)
*
* Notable Omissions (these caused issues during development):
* - Extended command set control (CSCON)
* - Gamma correction (PGC, NGC)
* - Power control registers (PWR2, VCMPCTL)
* - Display function control (DFC)
*
* These extended commands are manufacturer-specific and vary between ST7796
* modules from different vendors. The minimal approach is more universal.
*
* @param c Pointer to configuration structure with pin assignments
* @param w Display width (480 for landscape)
* @param h Display height (320 for landscape)
*/
void st7796_init(const struct st7796_config *c, uint16_t w, uint16_t h) {
config = c;
width = w;
height = h;
// Set offsets for 480x320 display
// ST7796 controller has a 960x480 framebuffer, but most displays
// only use a portion of it. Adjust these if your display is misaligned.
x_offset = 0;
y_offset = 0;
// Initialize SPI at maximum stable speed for ST7796
// Datasheet says max 15.15 MHz, but modern displays work much faster
// Successfully tested at 80 MHz, now trying 100 MHz
// If you see corruption, reduce to 80 MHz: spi_init(config->spi, 80000 * 1000)
spi_init(config->spi, 100000 * 1000); // 100 MHz - try this first
gpio_set_function(config->gpio_din, GPIO_FUNC_SPI);
gpio_set_function(config->gpio_clk, GPIO_FUNC_SPI);
// Initialize CS pin (Chip Select)
// CS selects which device on the SPI bus is active
if (config->gpio_cs >= 0) {
gpio_init(config->gpio_cs);
gpio_set_dir(config->gpio_cs, GPIO_OUT);
gpio_put(config->gpio_cs, 1); // Start HIGH (inactive)
}
// Initialize DC pin (Data/Command)
// DC tells the display whether we're sending commands or data
gpio_init(config->gpio_dc);
gpio_set_dir(config->gpio_dc, GPIO_OUT);
// Initialize RST pin (Hardware Reset)
gpio_init(config->gpio_rst);
gpio_set_dir(config->gpio_rst, GPIO_OUT);
// Initialize backlight pin
// Most TFT displays have LED backlights that need power
if (config->gpio_bl >= 0) {
gpio_init(config->gpio_bl);
gpio_set_dir(config->gpio_bl, GPIO_OUT);
gpio_put(config->gpio_bl, 1); // Turn on backlight immediately
}
// Hardware reset sequence
// This ensures the display starts from a clean state
reset_pulse();
// === BEGIN MINIMAL ST7796 INITIALIZATION SEQUENCE ===
// This is the result of extensive debugging. Adding more commands
// often causes compatibility issues. Only modify if necessary.
uint8_t data;
// Software Reset - clears all registers to default values
write_command(ST7796_SWRESET);
sleep_ms(150); // Must wait for reset to complete
// Sleep Out - exits low power mode
// Display is in sleep mode after reset to save power
write_command(ST7796_SLPOUT);
sleep_ms(120); // Must wait for oscillator to stabilize
// Interface Pixel Format - set color depth
// 0x55 = 16 bits per pixel for both RGB and MCU interface
// Format: 5 bits Red, 6 bits Green, 5 bits Blue (RGB565)
data = 0x55;
write_command_with_data(ST7796_COLMOD, &data, 1);
sleep_ms(10);
// Memory Data Access Control - set display orientation
// This is CRITICAL for proper display orientation
// 0xE0 = MY(1) + MX(1) + MV(1) for landscape mode
// MY = Row Address Order (flip vertical)
// MX = Column Address Order (flip horizontal)
// MV = Row/Column Exchange (rotate 90°)
// Result: 480x320 landscape orientation with correct pixel mapping
data = 0xE0;
write_command_with_data(ST7796_MADCTL, &data, 1);
sleep_ms(10);
// Display Inversion - try OFF first
// Some displays look better with INVON, others with INVOFF
// If colors look wrong, try: write_command(ST7796_INVON);
write_command(ST7796_INVOFF);
sleep_ms(10);
// Normal Display Mode On
// Exits partial mode and idle mode (if active)
write_command(ST7796_NORON);
sleep_ms(10);
// Display ON - start showing framebuffer contents
// After this, display is active and ready for drawing
write_command(ST7796_DISPON);
sleep_ms(120); // Wait for display to stabilize
// === END INITIALIZATION ===
// Display is now ready. Framebuffer contains random data, so you should
// call st7796_fill() to clear it or start drawing immediately.
}
/**
* @brief Fill entire screen with a single color
*
* This is the most efficient way to clear or set background color.
*
* How it works:
* 1. Set window to entire display (0,0) to (width-1, height-1)
* 2. Send RAMWR command
* 3. Stream 480*320 = 153,600 pixels as RGB565 values
* 4. Use 512-byte buffer (256 pixels) for batch writes
*
* Performance optimization:
* - Pre-fills buffer with color
* - Writes in 512-byte chunks to minimize SPI overhead
* - At 100 MHz SPI: 153,600 pixels * 2 bytes = 307,200 bytes
* Transfer time: ~3ms + overhead = ~5-10ms total
*
* @param color RGB565 color value (0x0000=black, 0xFFFF=white)
*/
void st7796_fill(uint16_t color) {
set_window(0, 0, width - 1, height - 1);
dc_data();
cs_select();
// Convert RGB565 to two bytes (MSB first)
uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
uint32_t pixel_count = width * height; // 153,600 for 480x320
// Create 512-byte buffer (256 pixels worth)
// This is the sweet spot for performance vs memory usage
uint8_t buffer[512];
for (int i = 0; i < 256; i++) {
buffer[i * 2] = data[0]; // MSB
buffer[i * 2 + 1] = data[1]; // LSB
}
// Send full 512-byte chunks
uint32_t full_chunks = pixel_count / 256; // 600 chunks
uint32_t remaining = pixel_count % 256; // 0 pixels (evenly divisible)
for (uint32_t i = 0; i < full_chunks; i++) {
spi_write_blocking(config->spi, buffer, 512);
}
// Send remaining pixels (if any)
if (remaining > 0) {
spi_write_blocking(config->spi, buffer, remaining * 2);
}
cs_deselect();
}
/**
* @brief Write single pixel at current cursor position
*
* Writes one RGB565 pixel at the current window position. The window
* position auto-advances after each write. Rarely used directly.
*
* @param color RGB565 color value
*/
void st7796_put(uint16_t color) {
uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
dc_data();
cs_select();
spi_write_blocking(config->spi, data, 2);
cs_deselect();
}
/**
* @brief Set cursor for subsequent pixel writes
*
* Sets the window starting position for st7796_put() or st7796_write().
* The window extends from (x,y) to the bottom-right corner of the display.
*
* @param x Starting X coordinate
* @param y Starting Y coordinate
*/
void st7796_set_cursor(uint16_t x, uint16_t y) {
set_window(x, y, width - 1, height - 1);
}
/**
* @brief Write array of pixels at current cursor
*
* Writes multiple RGB565 pixels starting at the current window position.
* Less efficient than fill_rect for solid colors due to per-pixel conversion.
*
* Use case: Drawing images, sprites, or multi-color patterns
*
* @param data Array of RGB565 color values
* @param len Number of pixels to write
*/
void st7796_write(const uint16_t *data, size_t len) {
dc_data();
cs_select();
// Convert each RGB565 value to two bytes
for (size_t i = 0; i < len; i++) {
uint8_t bytes[2] = {(data[i] >> 8) & 0xFF, data[i] & 0xFF};
spi_write_blocking(config->spi, bytes, 2);
}
cs_deselect();
}
/**
* @brief Draw single pixel at specific coordinates
*
* This is the slowest drawing operation because it requires:
* 1. Set 1x1 window (4-byte CASET, 4-byte RASET, RAMWR command)
* 2. Write 2-byte color
* 3. CS select/deselect overhead
*
* Total: ~15-20 bytes of SPI traffic per pixel
* Performance: ~100-200 pixels/second
*
* Use sparingly. For multiple pixels, use line/rect functions.
*
* @param x X coordinate (0 to width-1)
* @param y Y coordinate (0 to height-1)
* @param color RGB565 color value
*/
void st7796_draw_pixel(uint16_t x, uint16_t y, uint16_t color) {
if (x >= width || y >= height) return; // Bounds check
set_window(x, y, x, y); // 1x1 window
uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
dc_data();
cs_select();
spi_write_blocking(config->spi, data, 2);
cs_deselect();
}
/**
* @brief Draw rectangle outline
*
* Draws a hollow rectangle by drawing 4 lines:
* - Top edge: from (x,y) to (x+w-1, y)
* - Bottom edge: from (x, y+h-1) to (x+w-1, y+h-1)
* - Left edge: from (x,y) to (x, y+h-1)
* - Right edge: from (x+w-1, y) to (x+w-1, y+h-1)
*
* Implemented as fill_rect calls for efficiency (faster than pixel-by-pixel).
*
* @param x Top-left X coordinate
* @param y Top-left Y coordinate
* @param w Width in pixels
* @param h Height in pixels
* @param color RGB565 color value
*/
void st7796_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
// Top and bottom horizontal lines
st7796_fill_rect(x, y, w, 1, color); // Top
st7796_fill_rect(x, y + h - 1, w, 1, color); // Bottom
// Left and right vertical lines
st7796_fill_rect(x, y, 1, h, color); // Left
st7796_fill_rect(x + w - 1, y, 1, h, color); // Right
}
/**
* @brief Draw filled rectangle
*
* This is one of the fastest drawing operations after fill(). Uses the
* same buffered approach as fill() but for a smaller region.
*
* How it works:
* 1. Set window to rectangle bounds
* 2. Stream w*h pixels using 512-byte buffer
*
* Performance: Depends on size
* - Small rects (< 256 pixels): ~1-2ms
* - Large rects (> 10,000 pixels): ~5-10ms
*
* @param x Top-left X coordinate
* @param y Top-left Y coordinate
* @param w Width in pixels
* @param h Height in pixels
* @param color RGB565 color value
*/
void st7796_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
// Bounds checking and clipping
if (x >= width || y >= height) return;
if (x + w > width) w = width - x;
if (y + h > height) h = height - y;
set_window(x, y, x + w - 1, y + h - 1);
dc_data();
cs_select();
uint8_t data[2] = {(color >> 8) & 0xFF, color & 0xFF};
uint32_t pixel_count = w * h;
// Use 512-byte buffer for faster transfers
uint8_t buffer[512];
for (int i = 0; i < 256; i++) {
buffer[i * 2] = data[0];
buffer[i * 2 + 1] = data[1];
}
uint32_t full_chunks = pixel_count / 256;
uint32_t remaining = pixel_count % 256;
for (uint32_t i = 0; i < full_chunks; i++) {
spi_write_blocking(config->spi, buffer, 512);
}
if (remaining > 0) {
spi_write_blocking(config->spi, buffer, remaining * 2);
}
cs_deselect();
}
/**
* @brief Draw circle outline
*
* Uses the Midpoint Circle Algorithm (Bresenham's circle algorithm).
* This algorithm draws circles by calculating 8 symmetric points per iteration,
* taking advantage of 8-way symmetry in a circle.
*
* How it works:
* 1. Start at (0, r) relative to center
* 2. For each iteration, decide whether to move down (y--) based on error term
* 3. Always move right (x++)
* 4. Draw 8 symmetric points for each (x,y) calculated
*
* The 8 symmetry points for center (x0, y0) and offset (x, y):
* - (x0+x, y0+y), (x0-x, y0+y) - Upper half
* - (x0+x, y0-y), (x0-x, y0-y) - Lower half
* - (x0+y, y0+x), (x0-y, y0+x) - Right side
* - (x0+y, y0-x), (x0-y, y0-x) - Left side
*
* Performance: O(r) - proportional to radius
* Draws 8 pixels per iteration, so ~r/8 iterations
* At ~150 pixels/second, a circle with r=50 takes ~0.4 seconds
*
* @param x0 Center X coordinate
* @param y0 Center Y coordinate
* @param r Radius in pixels
* @param color RGB565 color value
*/
void st7796_draw_circle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color) {
int16_t f = 1 - r; // Decision variable
int16_t ddF_x = 1; // Delta decision variable for X
int16_t ddF_y = -2 * r; // Delta decision variable for Y
int16_t x = 0;
int16_t y = r;
// Draw initial 4 points (cardinal directions)
st7796_draw_pixel(x0, y0 + r, color); // Bottom
st7796_draw_pixel(x0, y0 - r, color); // Top
st7796_draw_pixel(x0 + r, y0, color); // Right
st7796_draw_pixel(x0 - r, y0, color); // Left
// Draw remaining points using 8-way symmetry
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
// Draw 8 symmetric points
st7796_draw_pixel(x0 + x, y0 + y, color);
st7796_draw_pixel(x0 - x, y0 + y, color);
st7796_draw_pixel(x0 + x, y0 - y, color);
st7796_draw_pixel(x0 - x, y0 - y, color);
st7796_draw_pixel(x0 + y, y0 + x, color);
st7796_draw_pixel(x0 - y, y0 + x, color);
st7796_draw_pixel(x0 + y, y0 - x, color);
st7796_draw_pixel(x0 - y, y0 - x, color);
}
}
/**
* @brief Draw filled circle
*
* Uses the same Midpoint Circle Algorithm but fills horizontal spans
* instead of drawing individual pixels. This is much faster than
* draw_circle because fill_rect uses optimized buffering.
*
* For each Y level calculated by the algorithm, we draw a horizontal
* line spanning from -x to +x. The 8-way symmetry means we draw 4
* horizontal lines per iteration.
*
* Performance: Much better than draw_circle
* - Uses fill_rect for each scan line
* - O(r) scan lines, each taking ~0.5-2ms depending on width
* - A circle with r=50 takes ~0.1-0.2 seconds
*
* @param x0 Center X coordinate
* @param y0 Center Y coordinate
* @param r Radius in pixels
* @param color RGB565 color value
*/
void st7796_fill_circle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color) {
int16_t f = 1 - r;
int16_t ddF_x = 1;
int16_t ddF_y = -2 * r;
int16_t x = 0;
int16_t y = r;
// Draw initial horizontal line through center
st7796_fill_rect(x0 - r, y0, 2 * r + 1, 1, color);
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
// Draw 4 horizontal lines (using 8-way symmetry)
st7796_fill_rect(x0 - x, y0 + y, 2 * x + 1, 1, color); // Bottom outer
st7796_fill_rect(x0 - x, y0 - y, 2 * x + 1, 1, color); // Top outer
st7796_fill_rect(x0 - y, y0 + x, 2 * y + 1, 1, color); // Bottom inner
st7796_fill_rect(x0 - y, y0 - x, 2 * y + 1, 1, color); // Top inner
}
}
/**
* @brief Draw line between two points
*
* Uses Bresenham's Line Algorithm - a classic computer graphics algorithm
* that draws straight lines using only integer arithmetic.
*
* How it works:
* 1. Calculate delta X and delta Y (dx, dy)
* 2. Determine step direction (sx, sy) - positive or negative
* 3. Initialize error term = dx - dy
* 4. Loop:
* - Draw pixel at current position
* - Update error term
* - Step in X or Y direction based on error
*
* The algorithm ensures the line stays as close as possible to the true
* mathematical line without using floating point math.
*
* Performance: O(max(dx, dy)) - proportional to line length
* Each pixel requires a draw_pixel call (~150 pixels/second)
* A 100-pixel line takes ~0.66 seconds
*
* Note: Could be optimized by batching pixels or using fill_rect for
* horizontal/vertical lines.
*
* @param x0 Start point X coordinate
* @param y0 Start point Y coordinate
* @param x1 End point X coordinate
* @param y1 End point Y coordinate
* @param color RGB565 color value
*/
void st7796_draw_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) {
int16_t dx = abs(x1 - x0); // Horizontal distance
int16_t dy = abs(y1 - y0); // Vertical distance
int16_t sx = (x0 < x1) ? 1 : -1; // Step direction in X
int16_t sy = (y0 < y1) ? 1 : -1; // Step direction in Y
int16_t err = dx - dy; // Error term
while (1) {
st7796_draw_pixel(x0, y0, color);
if (x0 == x1 && y0 == y1) break; // Reached end point
// Update position based on error term
int16_t e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx; // Step in X direction
}
if (e2 < dx) {
err += dx;
y0 += sy; // Step in Y direction
}
}
}
/**
* @brief Draw triangle outline
*
* Simply draws three lines connecting the three vertices.
*
* @param x0, y0 First vertex
* @param x1, y1 Second vertex
* @param x2, y2 Third vertex
* @param color RGB565 color value
*/
void st7796_draw_triangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,
uint16_t x2, uint16_t y2, uint16_t color) {
st7796_draw_line(x0, y0, x1, y1, color); // Side 1
st7796_draw_line(x1, y1, x2, y2, color); // Side 2
st7796_draw_line(x2, y2, x0, y0, color); // Side 3
}
/**
* @brief Draw filled triangle
*
* Uses a scanline fill algorithm. The triangle is filled by drawing
* horizontal lines (scanlines) from top to bottom.
*
* Algorithm:
* 1. Sort vertices by Y coordinate (y0 <= y1 <= y2)
* 2. Handle degenerate case (all points on same line)
* 3. Split triangle into two parts at the middle Y coordinate:
* - Upper part: from y0 to y1
* - Lower part: from y1 to y2
* 4. For each Y level, calculate left and right X boundaries
* 5. Draw horizontal line between left and right boundaries
*
* The algorithm uses linear interpolation to find X coordinates along
* each edge of the triangle.
*
* Performance: O(height) scanlines
* Each scanline is a fill_rect, so relatively fast
* A 100-pixel tall triangle takes ~0.1-0.2 seconds
*
* @param x0, y0 First vertex
* @param x1, y1 Second vertex
* @param x2, y2 Third vertex
* @param color RGB565 color value
*/
void st7796_fill_triangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,
uint16_t x2, uint16_t y2, uint16_t color) {
int16_t a, b, y, last;
// Sort coordinates by Y order (y2 >= y1 >= y0)
// Using bubble sort for 3 elements
if (y0 > y1) {
int16_t temp;
temp = y0; y0 = y1; y1 = temp;
temp = x0; x0 = x1; x1 = temp;
}
if (y1 > y2) {
int16_t temp;
temp = y2; y2 = y1; y1 = temp;
temp = x2; x2 = x1; x1 = temp;
}
if (y0 > y1) {
int16_t temp;
temp = y0; y0 = y1; y1 = temp;
temp = x0; x0 = x1; x1 = temp;
}
// Handle degenerate case: all vertices on same horizontal line
if (y0 == y2) {
a = b = x0;
if (x1 < a) a = x1;
else if (x1 > b) b = x1;
if (x2 < a) a = x2;
else if (x2 > b) b = x2;
st7796_fill_rect(a, y0, b - a + 1, 1, color);
return;
}
// Calculate edge slopes (as fixed-point)
int32_t dx01 = x1 - x0; // X delta from v0 to v1
int32_t dy01 = y1 - y0; // Y delta from v0 to v1
int32_t dx02 = x2 - x0; // X delta from v0 to v2 (long edge)
int32_t dy02 = y2 - y0;
int32_t dx12 = x2 - x1; // X delta from v1 to v2
int32_t dy12 = y2 - y1;
int32_t sa = 0; // Accumulated X for short edge
int32_t sb = 0; // Accumulated X for long edge
// Upper part of triangle (from y0 to y1)
last = (y1 == y2) ? y1 : y1 - 1;
for (y = y0; y <= last; y++) {
a = x0 + sa / dy01; // Left boundary
b = x0 + sb / dy02; // Right boundary
sa += dx01;
sb += dx02;
if (a > b) { // Swap if needed
int16_t temp = a;
a = b;
b = temp;
}
st7796_fill_rect(a, y, b - a + 1, 1, color);
}
// Lower part of triangle (from y1 to y2)
sa = dx12 * (y - y1);
sb = dx02 * (y - y0);
for (; y <= y2; y++) {
a = x1 + sa / dy12; // Left boundary
b = x0 + sb / dy02; // Right boundary
sa += dx12;
sb += dx02;
if (a > b) {
int16_t temp = a;
a = b;
b = temp;
}
st7796_fill_rect(a, y, b - a + 1, 1, color);
}
}

364
lib/st7796/st7796.h Normal file
View File

@@ -0,0 +1,364 @@
/*
* Copyright (c) 2021 Arm Limited and Contributors. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* ST7796 TFT LCD Display Driver
*
* ==============================================================================
* ABOUT THE ST7796 DISPLAY CONTROLLER
* ==============================================================================
*
* The ST7796 is a single-chip controller/driver for 262K-color, graphic type
* TFT-LCD displays. It consists of 960 source line and 480 gate line driving
* circuits. This chip is capable of connecting directly to an external
* microprocessor via an 8-bit/9-bit/16-bit/18-bit parallel interface or
* Serial Peripheral Interface (SPI).
*
* Key Features:
* - Resolution: Up to 480RGB x 320 dots
* - Display Colors: 262K colors (RGB565 format - 16 bits per pixel)
* - Interface: 4-wire SPI (used in this driver)
* - Framebuffer: Internal RAM that holds the pixel data (480x320x16bit = 300KB)
* - Power: 2.5V~3.3V I/O voltage, with internal regulators for LCD drivers
*
* ==============================================================================
* SPI COMMUNICATION PROTOCOL
* ==============================================================================
*
* The ST7796 uses a 4-wire SPI interface:
* 1. SCK (Serial Clock) - Clock signal from MCU
* 2. MOSI/SDA (Master Out Slave In) - Data signal from MCU to display
* 3. CS (Chip Select) - Active LOW, selects the display
* 4. DC (Data/Command) - HIGH for data, LOW for commands
*
* Additional pins:
* 5. RST (Reset) - Hardware reset, active LOW
* 6. BL (Backlight) - LED backlight control, HIGH for ON
*
* Communication sequence:
* 1. Pull CS LOW to select the display
* 2. Set DC LOW for command, HIGH for data
* 3. Clock out 8 bits on MOSI while toggling SCK
* 4. Pull CS HIGH to deselect
*
* ==============================================================================
* RGB565 COLOR FORMAT
* ==============================================================================
*
* RGB565 is a 16-bit color format:
* - Red: 5 bits (bits 15-11) - 32 levels
* - Green: 6 bits (bits 10-5) - 64 levels (human eye more sensitive to green)
* - Blue: 5 bits (bits 4-0) - 32 levels
*
* Color encoding: RRRR RGGG GGGB BBBB
* Example: 0xF800 = Red, 0x07E0 = Green, 0x001F = Blue, 0xFFFF = White
*
* ==============================================================================
* DISPLAY ORIENTATION (MADCTL)
* ==============================================================================
*
* MADCTL (0x36) controls memory access and display rotation:
* - MY (bit 7): Row Address Order (flip vertical)
* - MX (bit 6): Column Address Order (flip horizontal)
* - MV (bit 5): Row/Column Exchange (swap X/Y for rotation)
* - ML (bit 4): Vertical Refresh Order
* - BGR (bit 3): RGB or BGR color order
*
* Current configuration: 0xE0 (MY=1, MX=1, MV=1)
* This provides landscape mode (480x320) with correct orientation.
*
* ==============================================================================
* MISSING FEATURES & POTENTIAL IMPROVEMENTS
* ==============================================================================
*
* Currently NOT implemented (but supported by hardware):
*
* 1. HARDWARE SCROLLING:
* - Vertical Scrolling Definition (VSCRDEF) and Vertical Scroll Start Address
* - Could enable smooth scrolling without redrawing entire screen
* - Useful for: text scrolling, game backgrounds, UI animations
*
* 2. PARTIAL DISPLAY MODE:
* - Show only a portion of the display (save power)
* - Commands: PTLON (0x12), PTLAR (0x30)
*
* 3. IDLE MODE:
* - Reduced color depth (8 colors) for power saving
* - Commands: IDMON (0x38), IDMOFF (0x39)
*
* 4. DISPLAY INVERSION:
* - Currently set to INVOFF, but INVON available
* - Some displays look better with inversion ON
*
* 5. TEAR EFFECT CONTROL:
* - TE (Tearing Effect) pin synchronization
* - Prevents tearing during screen updates
* - Commands: TEON (0x35), TEOFF (0x34)
*
* 6. GAMMA CORRECTION:
* - Fine-tune color accuracy and contrast
* - Commands: PGC (0xE0), NGC (0xE1)
*
* 7. BRIGHTNESS/CONTRAST CONTROL:
* - Software brightness via WRCABCMB command
* - Currently only hardware backlight control
*
* 8. POWER CONTROL:
* - Deep sleep modes
* - Display ON/OFF without re-initialization
* - Commands: SLPIN (0x10), SLPOUT (0x11)
*
* 9. DMA SUPPORT:
* - Use DMA for SPI transfers instead of blocking writes
* - Would significantly improve performance for large updates
*
* 10. DOUBLE BUFFERING:
* - Currently single-buffered (direct to framebuffer)
* - Could implement software double buffer for tear-free updates
*
* 11. IMAGE/BITMAP LOADING:
* - Load images from SD card or flash
* - BMP, PNG decoders
*
* 12. TEXT RENDERING:
* - Font library for text display
* - Variable font sizes
*
* 13. HARDWARE ACCELERATED FEATURES:
* - Some displays have built-in shape drawing (not ST7796)
* - Window clipping for faster partial updates
*
* Performance Optimization Ideas:
* - Increase SPI speed beyond 80MHz (try 100-125MHz)
* - Use DMA for background transfers
* - Implement dirty rectangle tracking (only update changed areas)
* - Cache frequently drawn graphics
*
* ==============================================================================
*/
#ifndef _PICO_ST7796_H_
#define _PICO_ST7796_H_
#include "hardware/spi.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Configuration structure for ST7796 display
*
* This structure holds all the pin mappings and SPI interface configuration
* needed to communicate with the display. All GPIO pins should be valid
* Raspberry Pi Pico pins, except gpio_cs which can be -1 to disable CS control.
*/
struct st7796_config {
spi_inst_t* spi; // SPI instance (spi0 or spi1)
uint gpio_din; // MOSI/SDA pin (data out from MCU)
uint gpio_clk; // SCK pin (clock)
int gpio_cs; // CS pin (chip select, -1 to disable)
uint gpio_dc; // DC pin (data/command select)
uint gpio_rst; // RST pin (hardware reset)
uint gpio_bl; // Backlight pin (LED control)
};
/**
* @brief Initialize the ST7796 display
*
* This function performs the complete initialization sequence:
* 1. Configures all GPIO pins (SPI, CS, DC, RST, BL)
* 2. Initializes SPI at 80-100 MHz
* 3. Performs hardware reset
* 4. Sends initialization commands to the display
* 5. Sets up RGB565 color mode and landscape orientation
* 6. Turns on backlight and display
*
* @param config Pointer to st7796_config structure with pin mappings
* @param width Display width in pixels (typically 480 for landscape)
* @param height Display height in pixels (typically 320 for landscape)
*
* Note: After this call, display is ready but blank. Call st7796_fill()
* to clear with a color, or start drawing primitives.
*/
void st7796_init(const struct st7796_config *config, uint16_t width, uint16_t height);
/**
* @brief Fill entire display with a single color
*
* This is the fastest way to clear the screen or set a background.
* Uses optimized 512-byte buffering for maximum SPI throughput.
*
* @param color RGB565 color value (e.g., 0x0000=black, 0xFFFF=white)
*
* Performance: ~15ms at 80MHz SPI for full 480x320 screen
*/
void st7796_fill(uint16_t color);
/**
* @brief Write a single pixel at current cursor position
*
* Writes one pixel without changing the drawing window. Use after
* st7796_set_cursor() to position the write location. Rarely used
* directly - most applications use st7796_draw_pixel() instead.
*
* @param color RGB565 color value
*/
void st7796_put(uint16_t color);
/**
* @brief Set cursor position for subsequent writes
*
* Sets the drawing window starting at (x, y) and extending to the
* bottom-right of the display. Subsequent calls to st7796_put()
* will write pixels starting from this position.
*
* @param x X coordinate (0 to width-1)
* @param y Y coordinate (0 to height-1)
*/
void st7796_set_cursor(uint16_t x, uint16_t y);
/**
* @brief Write multiple pixels at current cursor position
*
* Writes an array of RGB565 color values starting at the current
* cursor position. Advances the cursor automatically.
*
* @param data Pointer to array of RGB565 color values
* @param len Number of pixels to write
*
* Note: Less efficient than fill_rect for solid colors due to
* per-pixel conversion overhead.
*/
void st7796_write(const uint16_t *data, size_t len);
/**
* @brief Draw a single pixel at specified coordinates
*
* Draws one pixel at (x, y) with the specified color. This function
* sets up a 1x1 drawing window, so it's slower than batch operations.
* Use fill_rect() or other primitives when possible.
*
* @param x X coordinate (0 to width-1)
* @param y Y coordinate (0 to height-1)
* @param color RGB565 color value
*
* Performance: ~100-200 pixels/second due to window setup overhead
*/
void st7796_draw_pixel(uint16_t x, uint16_t y, uint16_t color);
/**
* @brief Draw a rectangle outline
*
* Draws a hollow rectangle with 1-pixel thick borders. Implemented
* as 4 calls to fill_rect() for efficiency.
*
* @param x Top-left X coordinate
* @param y Top-left Y coordinate
* @param w Width in pixels
* @param h Height in pixels
* @param color RGB565 color value
*/
void st7796_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color);
/**
* @brief Draw a filled rectangle
*
* Draws a solid rectangle. This is one of the fastest drawing operations
* due to optimized 512-byte buffering.
*
* @param x Top-left X coordinate
* @param y Top-left Y coordinate
* @param w Width in pixels
* @param h Height in pixels
* @param color RGB565 color value
*
* Performance: Can draw full screen in ~15ms at 80MHz SPI
*/
void st7796_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color);
/**
* @brief Draw a circle outline
*
* Draws a hollow circle using the midpoint circle algorithm (Bresenham).
* Draws 8 symmetric points per iteration for efficiency.
*
* @param x0 Center X coordinate
* @param y0 Center Y coordinate
* @param r Radius in pixels
* @param color RGB565 color value
*
* Note: Implemented as individual pixel draws, so performance is
* O(r) with ~100-200 pixels/second throughput.
*/
void st7796_draw_circle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color);
/**
* @brief Draw a filled circle
*
* Draws a solid circle using midpoint circle algorithm with horizontal
* line fills. Much faster than drawing individual pixels.
*
* @param x0 Center X coordinate
* @param y0 Center Y coordinate
* @param r Radius in pixels
* @param color RGB565 color value
*
* Performance: Better than draw_circle due to fill_rect optimization
*/
void st7796_fill_circle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color);
/**
* @brief Draw a line between two points
*
* Draws a line using Bresenham's line algorithm. Works for any angle.
*
* @param x0 Start point X coordinate
* @param y0 Start point Y coordinate
* @param x1 End point X coordinate
* @param y1 End point Y coordinate
* @param color RGB565 color value
*
* Performance: O(max(dx, dy)) with ~100-200 pixels/second throughput
*/
void st7796_draw_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color);
/**
* @brief Draw a triangle outline
*
* Draws a hollow triangle by connecting three points with lines.
*
* @param x0 First vertex X coordinate
* @param y0 First vertex Y coordinate
* @param x1 Second vertex X coordinate
* @param y1 Second vertex Y coordinate
* @param x2 Third vertex X coordinate
* @param y2 Third vertex Y coordinate
* @param color RGB565 color value
*/
void st7796_draw_triangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color);
/**
* @brief Draw a filled triangle
*
* Draws a solid triangle using scanline fill algorithm. Sorts vertices
* by Y coordinate and fills horizontal spans.
*
* @param x0 First vertex X coordinate
* @param y0 First vertex Y coordinate
* @param x1 Second vertex X coordinate
* @param y1 Second vertex Y coordinate
* @param x2 Third vertex X coordinate
* @param y2 Third vertex Y coordinate
* @param color RGB565 color value
*
* Performance: Better than draw_triangle due to fill_rect optimization
*/
void st7796_fill_triangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color);
#ifdef __cplusplus
}
#endif
#endif