eacc03a38c
Add intelligent partial screen update system using bitwise XOR change detection and 4-quadrant tracking (top-left, top-right, bottom-left, bottom-right). Each changed pixel is routed to its quadrant, with sophisticated merge logic that combines adjacent rectangles when beneficial (<40% overhead). This dramatically reduces SPI bandwidth for UIs with scattered updates (e.g., corners, sidebars). Key changes: - 4-quadrant dirty rectangle tracking with automatic merging - XOR-based change detection for fast byte-level comparison - Expose st7796_set_window() for partial region updates - 30 FPS frame rate limiter (33ms per frame) to prevent excessive refreshes - Smart sleep timing when frame rate limit is active Performance: Up to 99% reduction in SPI traffic for corner-based UIs (e.g., 4 small regions vs full 480x320 screen updates). Co-Authored-By: Claude <noreply@anthropic.com>
1109 lines
38 KiB
C
1109 lines
38 KiB
C
/*
|
|
* 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 "hardware/dma.h"
|
|
#include "hardware/pwm.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 struct st7796_config config_storage; // Static storage for config copy
|
|
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)
|
|
static uint8_t current_brightness = 100; // Current brightness level (0-100)
|
|
static uint pwm_slice; // PWM slice number for backlight control
|
|
static uint pwm_channel; // PWM channel number for backlight control
|
|
|
|
// DMA Channel for fast transfers
|
|
static int dma_tx_channel = -1;
|
|
static dma_channel_config dma_tx_config;
|
|
|
|
/**
|
|
* @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).
|
|
*/
|
|
void st7796_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) {
|
|
// Copy config to static storage to avoid dangling pointer
|
|
memcpy(&config_storage, c, sizeof(struct st7796_config));
|
|
config = &config_storage;
|
|
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 DMA for SPI
|
|
dma_tx_channel = dma_claim_unused_channel(true);
|
|
dma_tx_config = dma_channel_get_default_config(dma_tx_channel);
|
|
channel_config_set_transfer_data_size(&dma_tx_config, DMA_SIZE_8); // 8-bit transfers
|
|
channel_config_set_dreq(&dma_tx_config, spi_get_dreq(config->spi, true)); // Sync with SPI TX
|
|
|
|
// 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 with PWM for brightness control
|
|
// Most TFT displays have LED backlights that need power
|
|
if (config->gpio_bl >= 0) {
|
|
// Configure GPIO for PWM function
|
|
gpio_set_function(config->gpio_bl, GPIO_FUNC_PWM);
|
|
|
|
// Find PWM slice and channel for this GPIO
|
|
pwm_slice = pwm_gpio_to_slice_num(config->gpio_bl);
|
|
pwm_channel = pwm_gpio_to_channel(config->gpio_bl);
|
|
|
|
// Configure PWM
|
|
// PWM frequency = clock_freq / (wrap + 1)
|
|
// We want ~1 kHz to avoid flicker: 125 MHz / 125000 = 1000 Hz
|
|
pwm_set_wrap(pwm_slice, 65535); // 16-bit resolution
|
|
pwm_set_clkdiv(pwm_slice, 1.907f); // 125 MHz / 1.907 / 65536 ≈ 1 kHz
|
|
|
|
// Start at full brightness
|
|
pwm_set_chan_level(pwm_slice, pwm_channel, 65535);
|
|
pwm_set_enabled(pwm_slice, true);
|
|
|
|
current_brightness = 100;
|
|
}
|
|
|
|
// 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 ON for displays that need it
|
|
// Some displays need INVON, others need INVOFF
|
|
// If this doesn't work, try: write_command(ST7796_INVOFF);
|
|
write_command(ST7796_INVON);
|
|
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) {
|
|
st7796_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) {
|
|
st7796_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 Write raw buffer data directly to display
|
|
*/
|
|
void st7796_write_raw(const uint8_t *data, size_t len) {
|
|
dc_data();
|
|
cs_select();
|
|
|
|
// DMA Implementation (Blocking)
|
|
// 1. Configure DMA channel to write from buffer to SPI TX register
|
|
dma_channel_configure(
|
|
dma_tx_channel,
|
|
&dma_tx_config,
|
|
&spi_get_hw(config->spi)->dr, // Write to SPI TX register
|
|
data, // Read from buffer
|
|
len, // Element count (bytes)
|
|
true // Start immediately
|
|
);
|
|
|
|
// 2. Wait for DMA to finish
|
|
dma_channel_wait_for_finish_blocking(dma_tx_channel);
|
|
|
|
// 3. Wait for SPI Transfer to complete (DMA only fills the FIFO)
|
|
// We need to wait for the FIFO to drain and the bus to be idle before raising CS
|
|
while (spi_is_busy(config->spi)) {
|
|
// tight_loop_contents() is empty on some platforms,
|
|
// using a simple volatile read ensures the compiler doesn't optimize this away
|
|
// efficiently.
|
|
__asm volatile ("nop");
|
|
}
|
|
|
|
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
|
|
|
|
st7796_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;
|
|
|
|
st7796_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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Set display brightness using PWM on backlight pin
|
|
*
|
|
* Controls the backlight LED brightness by adjusting the PWM duty cycle.
|
|
* The brightness parameter is a percentage that's converted to the
|
|
* appropriate PWM level (0-65535 for 16-bit PWM).
|
|
*
|
|
* Implementation:
|
|
* - brightness = 0: PWM level = 0 (backlight off)
|
|
* - brightness = 100: PWM level = 65535 (backlight fully on)
|
|
* - brightness = 50: PWM level = 32767 (50% duty cycle)
|
|
*
|
|
* The conversion formula: PWM_level = (brightness * 65535) / 100
|
|
*
|
|
* Human perception of brightness is logarithmic, but we use linear
|
|
* PWM for simplicity. For perceived linear brightness, a gamma
|
|
* correction curve could be applied.
|
|
*
|
|
* @param brightness Brightness level (0-100 percent)
|
|
*/
|
|
void st7796_set_brightness(uint8_t brightness) {
|
|
// Clamp brightness to valid range
|
|
if (brightness > 100) {
|
|
brightness = 100;
|
|
}
|
|
|
|
// Store current brightness
|
|
current_brightness = brightness;
|
|
|
|
// Convert percentage to 16-bit PWM level (0-65535)
|
|
// Using 32-bit intermediate to avoid overflow
|
|
uint32_t pwm_level = ((uint32_t)brightness * 65535) / 100;
|
|
|
|
// Set PWM duty cycle
|
|
if (config->gpio_bl >= 0) {
|
|
pwm_set_chan_level(pwm_slice, pwm_channel, (uint16_t)pwm_level);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Get current display brightness level
|
|
*
|
|
* Returns the brightness level that was last set via st7796_set_brightness()
|
|
* or the default value (100) if brightness was never changed.
|
|
*
|
|
* @return Current brightness level (0-100 percent)
|
|
*/
|
|
uint8_t st7796_get_brightness(void) {
|
|
return current_brightness;
|
|
}
|
|
|
|
/**
|
|
* @brief Put display into sleep mode (low power)
|
|
*
|
|
* Enters deep sleep mode to minimize power consumption while maintaining
|
|
* initialization state. The framebuffer contents and all settings are
|
|
* preserved, so waking up is fast.
|
|
*
|
|
* Sleep sequence:
|
|
* 1. Turn off display (DISPOFF) - stops showing framebuffer
|
|
* 2. Enter sleep mode (SLPIN) - stops oscillator, minimal power
|
|
* 3. Turn off backlight - set PWM to 0
|
|
*
|
|
* Total power savings: Display draws ~10μA in sleep vs ~150mA active
|
|
* Touch controller on separate I2C bus remains fully functional
|
|
*/
|
|
void st7796_sleep(void) {
|
|
// Turn off display output first
|
|
write_command(ST7796_DISPOFF);
|
|
sleep_ms(10);
|
|
|
|
// Enter sleep mode (stops internal oscillator)
|
|
write_command(ST7796_SLPIN);
|
|
sleep_ms(120); // Wait for sleep mode to take effect (spec: 120ms)
|
|
|
|
// Turn off backlight to save power
|
|
if (config->gpio_bl >= 0) {
|
|
pwm_set_chan_level(pwm_slice, pwm_channel, 0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Wake display from sleep mode
|
|
*
|
|
* Exits sleep mode and restores display to full operation.
|
|
* All framebuffer contents and settings are preserved.
|
|
*
|
|
* Wake sequence:
|
|
* 1. Exit sleep mode (SLPOUT) - restarts oscillator
|
|
* 2. Wait for oscillator to stabilize (120ms)
|
|
* 3. Turn on display (DISPON) - starts showing framebuffer
|
|
* 4. Restore backlight to previous brightness
|
|
*/
|
|
void st7796_wake(void) {
|
|
// Exit sleep mode (restart oscillator)
|
|
write_command(ST7796_SLPOUT);
|
|
sleep_ms(120); // Wait for oscillator to stabilize (spec: 120ms)
|
|
|
|
// Turn on display output
|
|
write_command(ST7796_DISPON);
|
|
sleep_ms(10);
|
|
|
|
// Restore backlight to previous brightness level
|
|
if (config->gpio_bl >= 0) {
|
|
uint32_t pwm_level = ((uint32_t)current_brightness * 65535) / 100;
|
|
pwm_set_chan_level(pwm_slice, pwm_channel, (uint16_t)pwm_level);
|
|
}
|
|
}
|