/* * face_lcd.c — STM32 LCD Display Driver for Face Animations * * Implements low-level LCD framebuffer management and display control. * Supports 1-bit monochrome displays (SSD1306, etc.) via SPI. * * HARDWARE: * - SPI2 (PB13=SCK, PB14=MISO, PB15=MOSI) — already configured for OSD * - CS (GPIO) for LCD chip select * - DC (GPIO) for data/command mode select * - RES (GPIO) optional reset * * NOTE: SPI2 is currently used by OSD (MAX7456). For face LCD, we would * typically use a separate SPI or I2C. This implementation assumes * a dedicated I2C or separate SPI interface. Configure LCD_INTERFACE * in face_lcd.h to match your hardware. */ #include "face_lcd.h" #include /* === State Variables === */ static uint8_t lcd_framebuffer[LCD_FBSIZE]; static volatile uint32_t frame_counter = 0; static volatile bool flush_requested = false; static volatile bool transfer_busy = false; /* === Private Functions === */ /** * Initialize hardware (SPI/I2C) and LCD controller. * Sends initialization sequence to put display in active mode. */ static void lcd_hardware_init(void) { /* TODO: Implement hardware-specific initialization * - Configure SPI/I2C pins and clock * - Send controller init sequence (power on, set contrast, etc.) * - Clear display * * For SSD1306 (common monochrome): * - Send 0xAE (display off) * - Set contrast 0x81, 0x7F * - Set clock div ratio, precharge, comdesat * - Set address mode, column/page range * - Send 0xAF (display on) */ } /** * Push framebuffer to display via SPI/I2C DMA transfer. * Handles paging for monochrome displays (8 pixels per byte, horizontal pages). */ static void lcd_transfer_fb(void) { transfer_busy = true; /* TODO: Implement DMA/blocking transfer * For SSD1306 (8-pixel pages): * For each page (8 rows): * - Send page address command * - Send column address command * - DMA transfer 128 bytes (1 row of page data) * * Can use SPI DMA for async transfer or blocking transfer. * Set transfer_busy=false when complete (in ISR or blocking). */ transfer_busy = false; } /* === Public API Implementation === */ void face_lcd_init(void) { memset(lcd_framebuffer, 0, LCD_FBSIZE); frame_counter = 0; flush_requested = false; transfer_busy = false; lcd_hardware_init(); } void face_lcd_clear(void) { memset(lcd_framebuffer, 0, LCD_FBSIZE); } void face_lcd_pixel(uint16_t x, uint16_t y, lcd_color_t color) { /* Bounds check */ if (x >= LCD_WIDTH || y >= LCD_HEIGHT) return; #if LCD_BPP == 1 /* Monochrome: pack 8 pixels per byte, LSB = leftmost pixel */ uint16_t byte_idx = (y / 8) * LCD_WIDTH + x; uint8_t bit_pos = y % 8; if (color) lcd_framebuffer[byte_idx] |= (1 << bit_pos); else lcd_framebuffer[byte_idx] &= ~(1 << bit_pos); #else /* RGB565: 2 bytes per pixel */ uint16_t pixel_idx = y * LCD_WIDTH + x; ((uint16_t *)lcd_framebuffer)[pixel_idx] = color; #endif } void face_lcd_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, lcd_color_t color) { /* Bresenham line algorithm */ int16_t dx = (x1 > x0) ? (x1 - x0) : (x0 - x1); int16_t dy = (y1 > y0) ? (y1 - y0) : (y0 - y1); int16_t sx = (x0 < x1) ? 1 : -1; int16_t sy = (y0 < y1) ? 1 : -1; int16_t err = (dx > dy) ? (dx / 2) : -(dy / 2); int16_t x = x0, y = y0; while (1) { face_lcd_pixel(x, y, color); if (x == x1 && y == y1) break; int16_t e2 = err; if (e2 > -dx) { err -= dy; x += sx; } if (e2 < dy) { err += dx; y += sy; } } } void face_lcd_circle(uint16_t cx, uint16_t cy, uint16_t r, lcd_color_t color) { /* Midpoint circle algorithm */ int16_t x = r, y = 0; int16_t err = 0; while (x >= y) { face_lcd_pixel(cx + x, cy + y, color); face_lcd_pixel(cx + y, cy + x, color); face_lcd_pixel(cx - y, cy + x, color); face_lcd_pixel(cx - x, cy + y, color); face_lcd_pixel(cx - x, cy - y, color); face_lcd_pixel(cx - y, cy - x, color); face_lcd_pixel(cx + y, cy - x, color); face_lcd_pixel(cx + x, cy - y, color); if (err <= 0) { y++; err += 2 * y + 1; } else { x--; err -= 2 * x + 1; } } } void face_lcd_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, lcd_color_t color) { for (uint16_t row = y; row < y + h && row < LCD_HEIGHT; row++) { for (uint16_t col = x; col < x + w && col < LCD_WIDTH; col++) { face_lcd_pixel(col, row, color); } } } void face_lcd_flush(void) { flush_requested = true; } bool face_lcd_is_busy(void) { return transfer_busy; } void face_lcd_tick(void) { frame_counter++; /* Request flush every N frames to achieve LCD_REFRESH_HZ */ uint32_t frames_per_flush = (1000 / LCD_REFRESH_HZ) / 33; /* ~30ms per frame */ if (frame_counter % frames_per_flush == 0) { flush_requested = true; } /* Perform transfer if requested and not busy */ if (flush_requested && !transfer_busy) { flush_requested = false; lcd_transfer_fb(); } } uint8_t *face_lcd_get_fb(void) { return lcd_framebuffer; }