/* * USBvalve - Display Driver * * Two implementations selected at compile time: * - Default: SSD1306 OLED via I2C (128x32 or 128x64) * - PIWATCH: GC9A01 round TFT via SPI (240x240) */ #include #include "pico/stdlib.h" #include "hardware/gpio.h" #include "display.h" #include "serial_output.h" #include "usb_config.h" #include "font6x8.h" #define FONT_W 6 #define FONT_H 8 #ifdef PIWATCH //==================================================================== // PIWATCH: GC9A01 round TFT (240x240) via SPI //==================================================================== #include "hardware/spi.h" #include "background.h" // GC9A01 pin definitions #define GFX_DC 8 #define GFX_CS 9 #define GFX_CLK 10 #define GFX_MOSI 11 #define GFX_RST 12 #define GFX_BL 25 // Backlight (shared with LED_PIN on non-PIWATCH) #define TFT_WIDTH 240 #define TFT_HEIGHT 240 // Text area for event messages #define TEXT_X 70 #define TEXT_Y_START 80 #define TEXT_Y_MAX 120 #define TEXT_AREA_X 60 #define TEXT_AREA_Y 70 #define TEXT_AREA_W 140 #define TEXT_AREA_H 75 // Cursor state static uint16_t cursor_x = TEXT_X; static uint16_t cursor_y = TEXT_Y_START; // Text color (magenta in RGB565) #define COLOR_MAGENTA 0xF81F #define COLOR_BLACK 0x0000 //-------------------------------------------------------------------- // Low-level SPI helpers //-------------------------------------------------------------------- static void gc9a01_cmd(uint8_t cmd) { gpio_put(GFX_DC, 0); // Command mode gpio_put(GFX_CS, 0); spi_write_blocking(spi1, &cmd, 1); gpio_put(GFX_CS, 1); } static void gc9a01_data(const uint8_t *data, size_t len) { gpio_put(GFX_DC, 1); // Data mode gpio_put(GFX_CS, 0); spi_write_blocking(spi1, data, len); gpio_put(GFX_CS, 1); } static void gc9a01_data8(uint8_t val) { gc9a01_data(&val, 1); } static void gc9a01_data16(uint16_t val) { uint8_t buf[2] = {val >> 8, val & 0xFF}; gc9a01_data(buf, 2); } static void gc9a01_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { gc9a01_cmd(0x2A); // Column address set gc9a01_data16(x0); gc9a01_data16(x1); gc9a01_cmd(0x2B); // Row address set gc9a01_data16(y0); gc9a01_data16(y1); gc9a01_cmd(0x2C); // Memory write } //-------------------------------------------------------------------- // GC9A01 Init //-------------------------------------------------------------------- void display_init(void) { // Init SPI at 40MHz spi_init(spi1, 40000000); gpio_set_function(GFX_CLK, GPIO_FUNC_SPI); gpio_set_function(GFX_MOSI, GPIO_FUNC_SPI); // Init control pins gpio_init(GFX_CS); gpio_set_dir(GFX_CS, GPIO_OUT); gpio_put(GFX_CS, 1); gpio_init(GFX_DC); gpio_set_dir(GFX_DC, GPIO_OUT); gpio_init(GFX_RST); gpio_set_dir(GFX_RST, GPIO_OUT); gpio_init(GFX_BL); gpio_set_dir(GFX_BL, GPIO_OUT); // Hardware reset gpio_put(GFX_RST, 1); sleep_ms(10); gpio_put(GFX_RST, 0); sleep_ms(10); gpio_put(GFX_RST, 1); sleep_ms(120); // GC9A01 initialization sequence gc9a01_cmd(0xEF); gc9a01_cmd(0xEB); gc9a01_data8(0x14); gc9a01_cmd(0xFE); // Inter register enable 1 gc9a01_cmd(0xEF); // Inter register enable 2 gc9a01_cmd(0xEB); gc9a01_data8(0x14); gc9a01_cmd(0x84); gc9a01_data8(0x40); gc9a01_cmd(0x85); gc9a01_data8(0xFF); gc9a01_cmd(0x86); gc9a01_data8(0xFF); gc9a01_cmd(0x87); gc9a01_data8(0xFF); gc9a01_cmd(0x88); gc9a01_data8(0x0A); gc9a01_cmd(0x89); gc9a01_data8(0x21); gc9a01_cmd(0x8A); gc9a01_data8(0x00); gc9a01_cmd(0x8B); gc9a01_data8(0x80); gc9a01_cmd(0x8C); gc9a01_data8(0x01); gc9a01_cmd(0x8D); gc9a01_data8(0x01); gc9a01_cmd(0x8E); gc9a01_data8(0xFF); gc9a01_cmd(0x8F); gc9a01_data8(0xFF); gc9a01_cmd(0xB6); // Display function control uint8_t b6[] = {0x00, 0x00}; gc9a01_data(b6, 2); gc9a01_cmd(0x36); // Memory access control: MV+BGR (rotation=1) gc9a01_data8(0x28); gc9a01_cmd(0x3A); // Pixel format: 16-bit RGB565 gc9a01_data8(0x05); gc9a01_cmd(0x90); uint8_t d90[] = {0x08, 0x08, 0x08, 0x08}; gc9a01_data(d90, 4); gc9a01_cmd(0xBD); gc9a01_data8(0x06); gc9a01_cmd(0xBC); gc9a01_data8(0x00); gc9a01_cmd(0xFF); uint8_t dff[] = {0x60, 0x01, 0x04}; gc9a01_data(dff, 3); gc9a01_cmd(0xC3); gc9a01_data8(0x13); // Voltage reg 1a gc9a01_cmd(0xC4); gc9a01_data8(0x13); // Voltage reg 1b gc9a01_cmd(0xC9); gc9a01_data8(0x22); gc9a01_cmd(0xBE); gc9a01_data8(0x11); gc9a01_cmd(0xE1); gc9a01_data8(0x10); gc9a01_cmd(0xDF); uint8_t ddf[] = {0x21, 0x0C, 0x02}; gc9a01_data(ddf, 3); // Gamma gc9a01_cmd(0xF0); uint8_t gp[] = {0x45, 0x09, 0x08, 0x08, 0x26, 0x2A}; gc9a01_data(gp, 6); gc9a01_cmd(0xF1); uint8_t gn[] = {0x43, 0x70, 0x72, 0x36, 0x37, 0x6F}; gc9a01_data(gn, 6); gc9a01_cmd(0xF2); uint8_t g2p[] = {0x45, 0x09, 0x08, 0x08, 0x26, 0x2A}; gc9a01_data(g2p, 6); gc9a01_cmd(0xF3); uint8_t g2n[] = {0x43, 0x70, 0x72, 0x36, 0x37, 0x6F}; gc9a01_data(g2n, 6); gc9a01_cmd(0xED); uint8_t ded[] = {0x1B, 0x0B}; gc9a01_data(ded, 2); gc9a01_cmd(0xAE); gc9a01_data8(0x77); gc9a01_cmd(0xCD); gc9a01_data8(0x63); gc9a01_cmd(0x70); uint8_t d70[] = {0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03}; gc9a01_data(d70, 9); gc9a01_cmd(0xE8); gc9a01_data8(0x34); gc9a01_cmd(0x62); uint8_t d62[] = {0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70}; gc9a01_data(d62, 12); gc9a01_cmd(0x63); uint8_t d63[] = {0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70}; gc9a01_data(d63, 12); gc9a01_cmd(0x64); uint8_t d64[] = {0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07}; gc9a01_data(d64, 7); gc9a01_cmd(0x66); uint8_t d66[] = {0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00}; gc9a01_data(d66, 10); gc9a01_cmd(0x67); uint8_t d67[] = {0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98}; gc9a01_data(d67, 10); gc9a01_cmd(0x74); uint8_t d74[] = {0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00}; gc9a01_data(d74, 7); gc9a01_cmd(0x98); uint8_t d98[] = {0x3E, 0x07}; gc9a01_data(d98, 2); gc9a01_cmd(0x35); // Tearing effect line on gc9a01_cmd(0x21); // Display inversion on (IPS) gc9a01_cmd(0x11); // Sleep out sleep_ms(120); gc9a01_cmd(0x29); // Display on sleep_ms(20); // Fill screen black gc9a01_set_window(0, 0, TFT_WIDTH - 1, TFT_HEIGHT - 1); gpio_put(GFX_DC, 1); gpio_put(GFX_CS, 0); for (uint32_t i = 0; i < TFT_WIDTH * TFT_HEIGHT; i++) { uint8_t buf[2] = {0, 0}; spi_write_blocking(spi1, buf, 2); } gpio_put(GFX_CS, 1); // Draw background bitmap (210x210 at offset 10,0) gc9a01_set_window(10, 0, 10 + 210 - 1, 210 - 1); gpio_put(GFX_DC, 1); gpio_put(GFX_CS, 0); for (uint32_t i = 0; i < 44100; i++) { uint16_t pixel = background[i]; uint8_t buf[2] = {pixel >> 8, pixel & 0xFF}; spi_write_blocking(spi1, buf, 2); } gpio_put(GFX_CS, 1); sleep_ms(2000); // Backlight on gpio_put(GFX_BL, 1); } //-------------------------------------------------------------------- // TFT drawing helpers //-------------------------------------------------------------------- static void tft_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { if (x >= TFT_WIDTH || y >= TFT_HEIGHT) return; if (x + w > TFT_WIDTH) w = TFT_WIDTH - x; if (y + h > TFT_HEIGHT) h = TFT_HEIGHT - y; gc9a01_set_window(x, y, x + w - 1, y + h - 1); gpio_put(GFX_DC, 1); gpio_put(GFX_CS, 0); uint8_t hi = color >> 8; uint8_t lo = color & 0xFF; for (uint32_t i = 0; i < (uint32_t)w * h; i++) { uint8_t buf[2] = {hi, lo}; spi_write_blocking(spi1, buf, 2); } gpio_put(GFX_CS, 1); } static void tft_draw_char(uint16_t x, uint16_t y, char c, uint16_t color) { if (c < 0x20 || c > 0x7E) return; uint8_t idx = c - 0x20; // Draw character pixel by pixel using individual 1x1 rects for (uint8_t col = 0; col < FONT_W; col++) { uint8_t line = font6x8[idx][col]; for (uint8_t row = 0; row < FONT_H; row++) { if (line & (1 << row)) { gc9a01_set_window(x + col, y + row, x + col, y + row); gc9a01_data16(color); } } } } //-------------------------------------------------------------------- // Display interface implementation //-------------------------------------------------------------------- void display_clear(void) { cursor_x = TEXT_X; cursor_y = TEXT_Y_START; } void display_update(void) { // TFT updates are immediate, no framebuffer to flush } void display_set_cursor(uint8_t x, uint8_t y) { cursor_x = x; cursor_y = y; } uint8_t display_get_cursor_y(void) { return (uint8_t)cursor_y; } uint8_t display_width(void) { return TFT_WIDTH; } uint8_t display_height(void) { return TFT_HEIGHT; } void display_print(const char* str) { while (*str) { if (*str == '\n') { cursor_x = TEXT_X; cursor_y += FONT_H + 2; } else if (*str == '\r') { cursor_x = TEXT_X; } else { tft_draw_char(cursor_x, cursor_y, *str, COLOR_MAGENTA); cursor_x += FONT_W; } str++; } } uint8_t display_get_pixel(uint8_t x, uint8_t y) { (void)x; (void)y; return 0; // Not used for TFT } void display_draw_pixel(uint8_t x, uint8_t y, uint8_t color) { gc9a01_set_window(x, y, x, y); gc9a01_data16(color ? COLOR_MAGENTA : COLOR_BLACK); } void display_fill_rect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color) { tft_fill_rect(x, y, w, h, color ? COLOR_MAGENTA : COLOR_BLACK); } void display_scroll_up(uint8_t pixels) { (void)pixels; // TFT: no pixel scroll, just clear text area on overflow } void display_check_and_scroll(void) { if (cursor_y > TEXT_Y_MAX) { cls(); } } void printout(const char* str) { display_check_and_scroll(); // Skip leading newline for TFT if (str[0] == '\n') { cursor_x = TEXT_X; cursor_y += FONT_H + 2; display_print(str + 1); } else { display_print(str); } // Also output on CDC serial serial_println(str); } void cls(void) { // Clear the text area with a black rounded rect tft_fill_rect(TEXT_AREA_X, TEXT_AREA_Y, TEXT_AREA_W, TEXT_AREA_H, COLOR_BLACK); cursor_x = TEXT_X; cursor_y = TEXT_Y_START; display_print(VERSION); cursor_x = TEXT_X; cursor_y = TEXT_Y_START + FONT_H + 2; display_print("-----------------"); } #else // !PIWATCH //==================================================================== // Default: SSD1306 OLED via I2C (128x32 or 128x64) //==================================================================== #include "hardware/i2c.h" // Framebuffer #define FB_SIZE (OLED_WIDTH * OLED_HEIGHT / 8) static uint8_t framebuffer[FB_SIZE]; // Cursor state static uint8_t cursor_x = 0; static uint8_t cursor_y = 0; //-------------------------------------------------------------------- // Low-level I2C helpers //-------------------------------------------------------------------- static void ssd1306_cmd(uint8_t cmd) { uint8_t buf[2] = {0x00, cmd}; // Co=0, D/C#=0 (command) i2c_write_blocking(i2c0, I2C_ADDRESS, buf, 2, false); } //-------------------------------------------------------------------- // Init //-------------------------------------------------------------------- void display_init(void) { // Init I2C at 400kHz i2c_init(i2c0, 400000); gpio_set_function(I2C_SDA_PIN, GPIO_FUNC_I2C); gpio_set_function(I2C_SCL_PIN, GPIO_FUNC_I2C); gpio_pull_up(I2C_SDA_PIN); gpio_pull_up(I2C_SCL_PIN); // Wait for display to power up sleep_ms(100); // SSD1306 initialization sequence (works for 128x32 and 128x64) ssd1306_cmd(0xAE); // Display off ssd1306_cmd(0xD5); // Set display clock divide ratio ssd1306_cmd(0x80); // Suggested ratio ssd1306_cmd(0xA8); // Set multiplex ratio ssd1306_cmd(OLED_HEIGHT - 1); ssd1306_cmd(0xD3); // Set display offset ssd1306_cmd(0x00); // No offset ssd1306_cmd(0x40); // Set start line to 0 ssd1306_cmd(0x8D); // Charge pump ssd1306_cmd(0x14); // Enable charge pump ssd1306_cmd(0x20); // Memory addressing mode ssd1306_cmd(0x00); // Horizontal addressing mode ssd1306_cmd(0xA1); // Segment re-map (column 127 mapped to SEG0) ssd1306_cmd(0xC8); // COM output scan direction (remapped) ssd1306_cmd(0xDA); // Set COM pins hardware configuration #if OLED_HEIGHT == 64 ssd1306_cmd(0x12); // Alternate COM pin config for 128x64 #else ssd1306_cmd(0x02); // Sequential COM pin config for 128x32 #endif ssd1306_cmd(0x81); // Set contrast ssd1306_cmd(0x8F); // Medium contrast ssd1306_cmd(0xD9); // Set pre-charge period ssd1306_cmd(0xF1); ssd1306_cmd(0xDB); // Set VCOMH deselect level ssd1306_cmd(0x40); ssd1306_cmd(0xA4); // Entire display ON (follow RAM) ssd1306_cmd(0xA6); // Normal display (not inverted) ssd1306_cmd(0xAF); // Display on // Clear framebuffer and update memset(framebuffer, 0, FB_SIZE); display_update(); } //-------------------------------------------------------------------- // Framebuffer operations //-------------------------------------------------------------------- void display_clear(void) { memset(framebuffer, 0, FB_SIZE); cursor_x = 0; cursor_y = 0; } void display_update(void) { // Set column address range ssd1306_cmd(0x21); // Column address ssd1306_cmd(0); // Start ssd1306_cmd(OLED_WIDTH - 1); // End // Set page address range ssd1306_cmd(0x22); // Page address ssd1306_cmd(0); // Start ssd1306_cmd((OLED_HEIGHT / 8) - 1); // End // Send framebuffer data for (uint16_t i = 0; i < FB_SIZE; i += 128) { uint8_t buf[129]; buf[0] = 0x40; // Co=0, D/C#=1 (data) uint16_t chunk = (FB_SIZE - i < 128) ? (FB_SIZE - i) : 128; memcpy(&buf[1], &framebuffer[i], chunk); i2c_write_blocking(i2c0, I2C_ADDRESS, buf, chunk + 1, false); } } void display_set_cursor(uint8_t x, uint8_t y) { cursor_x = x; cursor_y = y; } uint8_t display_get_cursor_y(void) { return cursor_y; } uint8_t display_width(void) { return OLED_WIDTH; } uint8_t display_height(void) { return OLED_HEIGHT; } uint8_t display_get_pixel(uint8_t x, uint8_t y) { if (x >= OLED_WIDTH || y >= OLED_HEIGHT) return 0; uint16_t byte_idx = x + (y / 8) * OLED_WIDTH; uint8_t bit_idx = y & 7; return (framebuffer[byte_idx] >> bit_idx) & 1; } void display_draw_pixel(uint8_t x, uint8_t y, uint8_t color) { if (x >= OLED_WIDTH || y >= OLED_HEIGHT) return; uint16_t byte_idx = x + (y / 8) * OLED_WIDTH; uint8_t bit_idx = y & 7; if (color) { framebuffer[byte_idx] |= (1 << bit_idx); } else { framebuffer[byte_idx] &= ~(1 << bit_idx); } } void display_fill_rect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color) { for (uint8_t j = y; j < y + h && j < OLED_HEIGHT; j++) { for (uint8_t i = x; i < x + w && i < OLED_WIDTH; i++) { display_draw_pixel(i, j, color); } } } //-------------------------------------------------------------------- // Text rendering //-------------------------------------------------------------------- static void draw_char(uint8_t x, uint8_t y, char c) { if (c < 0x20 || c > 0x7E) return; uint8_t idx = c - 0x20; for (uint8_t col = 0; col < FONT_W; col++) { uint8_t line = font6x8[idx][col]; for (uint8_t row = 0; row < FONT_H; row++) { if (line & (1 << row)) { display_draw_pixel(x + col, y + row, 1); } } } } void display_print(const char* str) { while (*str) { if (*str == '\n') { cursor_x = 0; cursor_y += FONT_H; } else if (*str == '\r') { cursor_x = 0; } else { if (cursor_x + FONT_W > OLED_WIDTH) { cursor_x = 0; cursor_y += FONT_H; } draw_char(cursor_x, cursor_y, *str); cursor_x += FONT_W; } str++; } } //-------------------------------------------------------------------- // High-level display functions //-------------------------------------------------------------------- void display_scroll_up(uint8_t pixels) { for (uint8_t y = 0; y < OLED_HEIGHT - pixels; y++) { for (uint8_t x = 0; x < OLED_WIDTH; x++) { uint8_t color = display_get_pixel(x, y + pixels); display_draw_pixel(x, y, color); } } display_fill_rect(0, OLED_HEIGHT - pixels, OLED_WIDTH, pixels, 0); display_update(); } void display_check_and_scroll(void) { if ((cursor_y + 16) > OLED_HEIGHT) { display_scroll_up(8); cursor_y -= 8; } } void printout(const char* str) { display_check_and_scroll(); display_print(str); display_update(); // Also output on CDC serial serial_println(str); } void cls(void) { display_clear(); display_set_cursor(0, 0); printout(VERSION); printout("\n-----------------"); } #endif // PIWATCH