/* * 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 #include #include #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); } }