touch & sd working
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user