#include "low_level_display_st7796.h" #include #include #include #include // For abs() // RGB565 color definitions #define COLOR_BLACK 0x0000 #define COLOR_WHITE 0xFFFF LowLevelDisplayST7796::LowLevelDisplayST7796(const st7796_config* cfg, int w, int h, bool invert) : config(cfg), width(w), height(h), initialized(false), rgb_buffer(nullptr), invert_color(invert), prev_bit_buffer(nullptr), dirty_rect_enabled(true) { for (int i = 0; i < MAX_DIRTY_RECTS; i++) { dirty_rects[i].reset(); } } LowLevelDisplayST7796::~LowLevelDisplayST7796() { if (rgb_buffer) { free(rgb_buffer); rgb_buffer = nullptr; } if (prev_bit_buffer) { free(prev_bit_buffer); prev_bit_buffer = nullptr; } } bool LowLevelDisplayST7796::init() { if (initialized) { return true; } st7796_init(config, width, height); // Allocate RGB565 buffer once (reused for all draw operations) size_t buffer_size = width * height * sizeof(uint16_t); rgb_buffer = (uint16_t *)malloc(buffer_size); if (!rgb_buffer) { printf("Error: Failed to allocate %zu bytes for RGB buffer\n", buffer_size); return false; } printf("ST7796 display initialized: %dx%d (RGB buffer: %zu bytes)\n", width, height, buffer_size); initialized = true; return true; } void LowLevelDisplayST7796::clear(bool white) { bool out_white = invert_color ? !white : white; st7796_fill(out_white ? COLOR_WHITE : COLOR_BLACK); } void LowLevelDisplayST7796::draw_pixel(int x, int y, bool white) { bool out_white = invert_color ? !white : white; st7796_draw_pixel(x, y, out_white ? COLOR_WHITE : COLOR_BLACK); } void LowLevelDisplayST7796::draw_buffer(const uint8_t* bit_buffer) { if (!bit_buffer || !rgb_buffer) return; // Calculate buffer size size_t bit_buffer_size = (width * height + 7) / 8; // If dirty rectangle tracking is enabled and we have a previous buffer if (dirty_rect_enabled && prev_bit_buffer) { // Reset all dirty rectangles for (int i = 0; i < MAX_DIRTY_RECTS; i++) { dirty_rects[i].reset(); } // Split screen into 4 quadrants int mid_x = width / 2; int mid_y = height / 2; // Use bitwise XOR to quickly detect changed bytes for (size_t byte_idx = 0; byte_idx < bit_buffer_size; byte_idx++) { uint8_t diff = bit_buffer[byte_idx] ^ prev_bit_buffer[byte_idx]; // If this byte has changes if (diff != 0) { // Calculate pixel coordinates for this byte int pixel_idx = byte_idx * 8; int base_x = pixel_idx % width; int base_y = pixel_idx / width; // Check each changed bit/pixel in this byte for (int bit = 0; bit < 8 && (pixel_idx + bit) < (width * height); bit++) { if (diff & (0x80 >> bit)) { int x = base_x + bit; int y = base_y; // Adjust coordinates if we wrapped to next row if (x >= width) { x -= width; y++; } // Route to appropriate quadrant based on X and Y position // Quadrant 0: Top-left (x < mid_x, y < mid_y) // Quadrant 1: Top-right (x >= mid_x, y < mid_y) // Quadrant 2: Bottom-left (x < mid_x, y >= mid_y) // Quadrant 3: Bottom-right (x >= mid_x, y >= mid_y) int rect_idx = ((y >= mid_y) ? 2 : 0) + ((x >= mid_x) ? 1 : 0); dirty_rects[rect_idx].expand(x, y); } } } } // Check if we have any valid dirty rectangles int valid_rects = 0; for (int i = 0; i < MAX_DIRTY_RECTS; i++) { if (dirty_rects[i].is_valid) { valid_rects++; } } // If there are no changes, skip the update if (valid_rects == 0) { return; } // Optimization: Merge adjacent rectangles if beneficial // Check pairs of rectangles and merge if they overlap or are close if (valid_rects >= 2) { // Try merging adjacent quadrants // Check top row (0,1) merge if (dirty_rects[0].is_valid && dirty_rects[1].is_valid) { int gap_x = dirty_rects[1].x0 - dirty_rects[0].x1; int gap_y = abs(dirty_rects[0].y0 - dirty_rects[1].y0) + abs(dirty_rects[0].y1 - dirty_rects[1].y1); if (gap_x < 30 && gap_y < 20) { dirty_rects[0].merge(dirty_rects[1]); dirty_rects[1].reset(); valid_rects--; } } // Check bottom row (2,3) merge if (dirty_rects[2].is_valid && dirty_rects[3].is_valid) { int gap_x = dirty_rects[3].x0 - dirty_rects[2].x1; int gap_y = abs(dirty_rects[2].y0 - dirty_rects[3].y0) + abs(dirty_rects[2].y1 - dirty_rects[3].y1); if (gap_x < 30 && gap_y < 20) { dirty_rects[2].merge(dirty_rects[3]); dirty_rects[3].reset(); valid_rects--; } } // Check left column (0,2) merge if (dirty_rects[0].is_valid && dirty_rects[2].is_valid) { int gap_y = dirty_rects[2].y0 - dirty_rects[0].y1; int gap_x = abs(dirty_rects[0].x0 - dirty_rects[2].x0) + abs(dirty_rects[0].x1 - dirty_rects[2].x1); if (gap_y < 30 && gap_x < 20) { dirty_rects[0].merge(dirty_rects[2]); dirty_rects[2].reset(); valid_rects--; } } // Check right column (1,3) merge if (dirty_rects[1].is_valid && dirty_rects[3].is_valid) { int gap_y = dirty_rects[3].y0 - dirty_rects[1].y1; int gap_x = abs(dirty_rects[1].x0 - dirty_rects[3].x0) + abs(dirty_rects[1].x1 - dirty_rects[3].x1); if (gap_y < 30 && gap_x < 20) { dirty_rects[1].merge(dirty_rects[3]); dirty_rects[3].reset(); valid_rects--; } } // Final pass: merge any remaining valid rectangles if they're very close for (int i = 0; i < MAX_DIRTY_RECTS - 1; i++) { if (!dirty_rects[i].is_valid) continue; for (int j = i + 1; j < MAX_DIRTY_RECTS; j++) { if (!dirty_rects[j].is_valid) continue; DirtyRect merged = dirty_rects[i]; merged.merge(dirty_rects[j]); int combined_area = dirty_rects[i].get_area() + dirty_rects[j].get_area(); int merged_area = merged.get_area(); // Merge if the combined overhead is less than 40% if (merged_area < combined_area * 1.4f) { dirty_rects[i] = merged; dirty_rects[j].reset(); valid_rects--; break; // Move to next i } } } } // Copy current buffer to previous buffer for next frame comparison memcpy(prev_bit_buffer, bit_buffer, bit_buffer_size); // Process each valid dirty rectangle for (int rect_idx = 0; rect_idx < MAX_DIRTY_RECTS; rect_idx++) { if (!dirty_rects[rect_idx].is_valid) continue; DirtyRect& rect = dirty_rects[rect_idx]; // Convert only the dirty rectangle region to RGB565 for (int y = rect.y0; y <= rect.y1; y++) { for (int x = rect.x0; x <= rect.x1; x++) { int byte_index = (y * width + x) / 8; int bit_index = 7 - (x % 8); bool pixel_white = (bit_buffer[byte_index] >> bit_index) & 0x01; bool out_white = invert_color ? !pixel_white : pixel_white; rgb_buffer[y * width + x] = out_white ? COLOR_WHITE : COLOR_BLACK; } } // Draw only this dirty rectangle st7796_set_window(rect.x0, rect.y0, rect.x1, rect.y1); // Calculate size of dirty region int dirty_width = rect.get_width(); int dirty_height = rect.get_height(); // Write only the dirty rectangle pixels // We need to extract rows from the full rgb_buffer for (int row = 0; row < dirty_height; row++) { int buffer_offset = (rect.y0 + row) * width + rect.x0; st7796_write_raw((const uint8_t*)&rgb_buffer[buffer_offset], dirty_width * 2); } } } else { // Full screen update (original behavior) // Convert 1-bit buffer to RGB565 using persistent buffer for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int byte_index = (y * width + x) / 8; int bit_index = 7 - (x % 8); bool pixel_white = (bit_buffer[byte_index] >> bit_index) & 0x01; bool out_white = invert_color ? !pixel_white : pixel_white; rgb_buffer[y * width + x] = out_white ? COLOR_WHITE : COLOR_BLACK; } } // Draw entire buffer at once st7796_set_cursor(0, 0); // Use raw write for speed. // Since we only use 0x0000 (Black) and 0xFFFF (White), endianness doesn't matter. // 0x0000 -> 0x00, 0x00 (LE) -> Display sees 0x00, 0x00 (0x0000 correct) // 0xFFFF -> 0xFF, 0xFF (LE) -> Display sees 0xFF, 0xFF (0xFFFF correct) st7796_write_raw((const uint8_t*)rgb_buffer, width * height * 2); // If dirty rect is enabled, store this buffer for next comparison if (dirty_rect_enabled && prev_bit_buffer) { memcpy(prev_bit_buffer, bit_buffer, bit_buffer_size); } } } void LowLevelDisplayST7796::refresh() { // ST7796 updates immediately, no refresh needed } void LowLevelDisplayST7796::set_backlight(bool on) { // Use brightness control: on = 100%, off = 0% st7796_set_brightness(on ? 100 : 0); } void LowLevelDisplayST7796::set_brightness(uint8_t brightness) { st7796_set_brightness(brightness); } uint8_t LowLevelDisplayST7796::get_brightness() const { return st7796_get_brightness(); } void LowLevelDisplayST7796::sleep() { st7796_sleep(); } void LowLevelDisplayST7796::wake() { st7796_wake(); } void LowLevelDisplayST7796::set_rotation(uint8_t rotation) { // ST7796 driver doesn't have rotation control yet // TODO: Add MADCTL register manipulation for rotation (void)rotation; } void LowLevelDisplayST7796::enable_dirty_rect(bool enabled) { dirty_rect_enabled = enabled; if (enabled && !prev_bit_buffer) { // Allocate buffer to store previous frame for change detection size_t bit_buffer_size = (width * height + 7) / 8; // 1 bit per pixel prev_bit_buffer = (uint8_t *)malloc(bit_buffer_size); if (prev_bit_buffer) { // Initialize to all zeros (black screen) memset(prev_bit_buffer, 0, bit_buffer_size); printf("ST7796: Dirty rectangle tracking enabled (buffer: %zu bytes, max rects: %d)\n", bit_buffer_size, MAX_DIRTY_RECTS); } else { printf("Error: Failed to allocate %zu bytes for dirty rect buffer\n", bit_buffer_size); dirty_rect_enabled = false; } } else if (!enabled && prev_bit_buffer) { // Disable and free tracking buffer free(prev_bit_buffer); prev_bit_buffer = nullptr; for (int i = 0; i < MAX_DIRTY_RECTS; i++) { dirty_rects[i].reset(); } printf("ST7796: Dirty rectangle tracking disabled\n"); } } void LowLevelDisplayST7796::on_idle_2min() { if (!is_dimmed && !is_sleeping) { saved_brightness = get_brightness(); set_brightness(5); // Dim to 5% is_dimmed = true; printf("TFT: Dimmed to 5%%\n"); } } void LowLevelDisplayST7796::on_idle_10min() { if (!is_sleeping) { sleep(); is_sleeping = true; is_dimmed = true; // Sleep implies dimmed printf("TFT: Entered sleep mode\n"); } } void LowLevelDisplayST7796::on_user_interaction() { if (is_sleeping) { wake(); // Restore brightness if we have a saved value, or default to 100 set_brightness(saved_brightness > 0 ? saved_brightness : 100); is_sleeping = false; is_dimmed = false; printf("TFT: Woke from sleep\n"); } else if (is_dimmed) { set_brightness(saved_brightness > 0 ? saved_brightness : 100); is_dimmed = false; printf("TFT: Restored brightness\n"); } }