Implements expressive face animations with 5 core emotions (happy/sad/curious/angry/sleeping) and smooth transitions on small LCD displays. Features: - State machine with smooth 0.5s emotion transitions (ease-in-out cubic easing) - Automatic idle blinking (4-6s intervals, 100-150ms duration per blink) - UART command interface via USART3 @ 115200 (text-based protocol) - 30Hz target refresh rate via systick integration - Low-level LCD abstraction supporting monochrome and RGB565 - Rendering primitives: pixel, line (Bresenham), circle (midpoint), filled rect Architecture: - face_lcd.h/c: Hardware-agnostic framebuffer & display driver - face_animation.h/c: Emotion state machine & parameterized face rendering - face_uart.h/c: UART command parser (HAPPY/SAD/CURIOUS/ANGRY/SLEEP/NEUTRAL/BLINK/STATUS) - Unit tests (14 test cases): emotion transitions, blinking, rendering, all emotions Integration: - main.c: Added includes, initialization (servo_init), systick tick, main loop processing - Pending: LCD hardware initialization (SPI/I2C config, display controller setup) Files: 9 new (headers, source, tests, docs), 1 modified (main.c) Lines: ~1450 total (345 headers, 650 source, 350 tests, 900 docs) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
192 lines
5.3 KiB
C
192 lines
5.3 KiB
C
/*
|
|
* 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 <string.h>
|
|
|
|
/* === 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;
|
|
}
|