Files
basic1/lib/st7796/st7796.c
2026-01-28 23:23:49 -05:00

937 lines
32 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 "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)
/**
* @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) {
// 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 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 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) {
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);
}
}