From e1101e6672cfdbd8768aa2193d0a78ce1ffd4857 Mon Sep 17 00:00:00 2001 From: Eugene Krashtan Date: Fri, 31 Oct 2025 09:15:46 +0200 Subject: [PATCH] Convert to palette --- klippy/extras/display/gc9a01.py | 130 ++++++++++++++++++-------------- src/Kconfig | 3 + src/lcd_gc9a01.c | 80 ++++++++++++++++++-- 3 files changed, 150 insertions(+), 63 deletions(-) diff --git a/klippy/extras/display/gc9a01.py b/klippy/extras/display/gc9a01.py index 58032054c..e14f09c7f 100644 --- a/klippy/extras/display/gc9a01.py +++ b/klippy/extras/display/gc9a01.py @@ -16,13 +16,6 @@ GC9A01_CMD_DELAY = .000020 # 20us between commands DISPLAY_WIDTH = 240 DISPLAY_HEIGHT = 240 -# RGB565 color definitions -COLOR_BLACK = 0x0000 -COLOR_WHITE = 0xFFFF -COLOR_RED = 0xF800 -COLOR_GREEN = 0x07E0 -COLOR_BLUE = 0x001F - class GC9A01: def __init__(self, config): self.printer = config.get_printer() @@ -63,14 +56,14 @@ class GC9A01: self.width = base_width self.height = base_height - # Display colors - self.fg_color = config.getint('fg_color', COLOR_WHITE, - minval=0, maxval=65535) - self.bg_color = config.getint('bg_color', COLOR_BLACK, - minval=0, maxval=65535) + # Display colors (palette indices 0-15) + self.fg_color = config.getint('fg_color', 15, + minval=0, maxval=15) + self.bg_color = config.getint('bg_color', 0, + minval=0, maxval=15) - # Framebuffer for RGB565 format (2 bytes per pixel) - fb_size = self.width * self.height * 2 + # Framebuffer for 4-bit color (2 pixels per byte) + fb_size = (self.width * self.height + 1) // 2 self.framebuffer = bytearray(fb_size) self.glyphs = {} @@ -92,6 +85,7 @@ class GC9A01: self.start_write_cmd = None self.send_data_cmd = None self.fill_rect_cmd = None + self.set_palette_cmd = None # Register with printer self.printer.register_event_handler("klippy:ready", self._handle_ready) @@ -123,8 +117,12 @@ class GC9A01: self.send_data_cmd = self.mcu.lookup_command( "gc9a01_send_data oid=%c data=%*s", cq=cmd_queue) self.fill_rect_cmd = self.mcu.lookup_command( - "gc9a01_fill_rect oid=%c x0=%hu y0=%hu x1=%hu y1=%hu color=%hu", + "gc9a01_fill_rect oid=%c x0=%hu y0=%hu x1=%hu y1=%hu color=%c", cq=cmd_queue) + self.set_palette_cmd = self.mcu.lookup_command( + "gc9a01_set_palette oid=%c c1=%hu c2=%hu c3=%hu c4=%hu " + "c5=%hu c6=%hu c7=%hu c8=%hu c9=%hu c10=%hu c11=%hu " + "c12=%hu c13=%hu c14=%hu", cq=cmd_queue) logging.debug("GC9A01 MCU commands configured") except Exception as e: @@ -156,30 +154,42 @@ class GC9A01: self.set_window_cmd.send([self.oid, x0, y0, x1, y1], reqclock=BACKGROUND_PRIORITY_CLOCK) - def fill_rectangle(self, x0, y0, x1, y1, color): - """Fill a rectangle on the display with a color""" + def fill_rectangle(self, x0, y0, x1, y1, color_idx): + """Fill a rectangle on the display with a palette color index""" if self.fill_rect_cmd is None: return - logging.debug("GC9A01 fill_rect %d,%d to %d,%d color=0x%04X", - x0, y0, x1, y1, color) - self.fill_rect_cmd.send([self.oid, x0, y0, x1, y1, color], + color_idx &= 0x0F + logging.debug("GC9A01 fill_rect %d,%d to %d,%d color_idx=%d", + x0, y0, x1, y1, color_idx) + self.fill_rect_cmd.send([self.oid, x0, y0, x1, y1, color_idx], reqclock=BACKGROUND_PRIORITY_CLOCK) self.window_x0 = x0 self.window_y0 = y0 self.window_x1 = x1 self.window_y1 = y1 + def set_palette(self, colors): + """Upload custom palette colors (indices 1-14) + colors: list of 14 RGB565 values + """ + if self.set_palette_cmd is None: + return + if len(colors) != 14: + logging.error("GC9A01 set_palette: need exactly 14 colors") + return + self.set_palette_cmd.send([self.oid] + colors, + reqclock=BACKGROUND_PRIORITY_CLOCK) + def init(self): """Initialize the display""" self.clear() logging.debug("GC9A01 init called") def clear(self): - """Clear the framebuffer (fill with background color)""" - color_bytes = bytes([(self.bg_color >> 8) & 0xFF, - self.bg_color & 0xFF]) - for i in range(0, len(self.framebuffer), 2): - self.framebuffer[i:i+2] = color_bytes + """Clear the framebuffer (fill with background color index)""" + fill_byte = (self.bg_color << 4) | self.bg_color + for i in range(len(self.framebuffer)): + self.framebuffer[i] = fill_byte self.dirty_x0 = self.width self.dirty_y0 = self.height self.dirty_x1 = -1 @@ -195,10 +205,10 @@ class GC9A01: logging.debug("GC9A01 flush: no dirty regions, skipping") return - # Send only the dirty region - x0 = self.dirty_x0 + # Align dirty region to even pixel boundaries (2 pixels per byte) + x0 = self.dirty_x0 & ~1 # Round down to even y0 = self.dirty_y0 - x1 = self.dirty_x1 + x1 = self.dirty_x1 | 1 # Round up to odd y1 = self.dirty_y1 self.set_window(x0, y0, x1, y1) @@ -208,15 +218,18 @@ class GC9A01: reqclock=BACKGROUND_PRIORITY_CLOCK) # Send framebuffer data for dirty region - max_chunk = 32 - bytes_per_pixel = 2 - row_width = self.width * bytes_per_pixel - dirty_width = (x1 - x0 + 1) * bytes_per_pixel + max_chunk = 32 # Max bytes per send + dirty_width = x1 - x0 + 1 for y in range(y0, y1 + 1): - row_start = y * row_width + x0 * bytes_per_pixel - row_end = row_start + dirty_width - row_data = self.framebuffer[row_start:row_end] + # Calculate byte positions for this row + row_start_pixel = y * self.width + x0 + row_end_pixel = row_start_pixel + dirty_width + + byte_start = row_start_pixel // 2 + byte_end = (row_end_pixel + 1) // 2 + + row_data = self.framebuffer[byte_start:byte_end] # Send row in chunks for i in range(0, len(row_data), max_chunk): @@ -230,22 +243,26 @@ class GC9A01: self.dirty_x1 = -1 self.dirty_y1 = -1 - def draw_pixel(self, x, y, color): - """Draw a single pixel (RGB565 format)""" + def draw_pixel(self, x, y, color_idx): + """Draw a single pixel using 4-bit color index (0-15)""" if x >= self.width or y >= self.height or x < 0 or y < 0: return - pos = (y * self.width + x) * 2 - color_high = (color >> 8) & 0xFF - color_low = color & 0xFF + color_idx &= 0x0F + pixel_num = y * self.width + x + byte_pos = pixel_num // 2 + is_high = (pixel_num % 2) == 0 - # Only update if color changed - if (self.framebuffer[pos] == color_high - and self.framebuffer[pos + 1] == color_low): + old_byte = self.framebuffer[byte_pos] + if is_high: + new_byte = (color_idx << 4) | (old_byte & 0x0F) + else: + new_byte = (old_byte & 0xF0) | color_idx + + if old_byte == new_byte: return - self.framebuffer[pos] = color_high - self.framebuffer[pos + 1] = color_low + self.framebuffer[byte_pos] = new_byte # Update dirty region self.dirty_x0 = min(self.dirty_x0, x) @@ -253,9 +270,9 @@ class GC9A01: self.dirty_x1 = max(self.dirty_x1, x) self.dirty_y1 = max(self.dirty_y1, y) - def fill_rect(self, x, y, w, h, color): - """Fill a rectangle with a color""" - self.fill_rectangle(x, y, x + w - 1, y + h - 1, color) + def fill_rect(self, x, y, w, h, color_idx): + """Fill a rectangle with a palette color index""" + self.fill_rectangle(x, y, x + w - 1, y + h - 1, color_idx) # Display interface compatibility methods # For testing purposes only @@ -321,8 +338,9 @@ class GC9A01: for col in range(width): pixel_x = char_x + col pixel_y = y * height + row + centered_y - color = self.fg_color if (bits & (0x8000 >> col)) else self.bg_color - self.draw_pixel(pixel_x, pixel_y, color) + color_idx = (self.fg_color if (bits & (0x8000 >> col)) + else self.bg_color) + self.draw_pixel(pixel_x, pixel_y, color_idx) return 2 # Handle icon16x16 format @@ -344,12 +362,14 @@ class GC9A01: for col in range(width): pixel_x = char_x + col pixel_y = y * height + row + centered_y - color = self.fg_color if (byte1 & (0x80 >> col)) else self.bg_color - self.draw_pixel(pixel_x, pixel_y, color) + color_idx = (self.fg_color if (byte1 & (0x80 >> col)) + else self.bg_color) + self.draw_pixel(pixel_x, pixel_y, color_idx) pixel_x = char_x + col + width - color = self.fg_color if (byte2 & (0x80 >> col)) else self.bg_color - self.draw_pixel(pixel_x, pixel_y, color) + color_idx = (self.fg_color if (byte2 & (0x80 >> col)) + else self.bg_color) + self.draw_pixel(pixel_x, pixel_y, color_idx) return 2 return 0 @@ -360,4 +380,4 @@ class GC9A01: def load_config(config): """Klipper config loader - this is the entry point""" - return GC9A01(config) \ No newline at end of file + return GC9A01(config) diff --git a/src/Kconfig b/src/Kconfig index 525aa638f..56b749424 100644 --- a/src/Kconfig +++ b/src/Kconfig @@ -228,6 +228,9 @@ config WANT_ST7920 config WANT_HD44780 bool "Support HD44780 LCD display" depends on HAVE_GPIO +config WANT_GC9A01 + bool "Support GC9A01 LCD display" + depends on WANT_SPI comment "Accelerometer chips" depends on WANT_SPI || WANT_I2C config WANT_ADXL345 diff --git a/src/lcd_gc9a01.c b/src/lcd_gc9a01.c index 41a23feba..922eb238b 100644 --- a/src/lcd_gc9a01.c +++ b/src/lcd_gc9a01.c @@ -72,6 +72,24 @@ struct gc9a01 { #define GC9A01_IREG_0x8E 0x8E #define GC9A01_IREG_0x8F 0x8F +static uint16_t palette[16] = { + 0x0000, // 0: RGB( 0, 0, 0) #000000 Black + 0xF800, // 1: RGB(255, 0, 0) #FF0000 Red + 0x07E0, // 2: RGB( 0,255, 0) #00FF00 Green + 0xFFE0, // 3: RGB(255,255, 0) #FFFF00 Yellow + 0x001F, // 4: RGB( 0, 0,255) #0000FF Blue + 0xF81F, // 5: RGB(255, 0,255) #FF00FF Magenta + 0x07FF, // 6: RGB( 0,255,255) #00FFFF Cyan + 0xFFFF, // 7: RGB(255,255,255) #FFFFFF White + 0x8410, // 8: RGB(132,130,132) #848284 Dark Gray + 0x4208, // 9: RGB( 66, 65, 66) #424142 Very Dark Gray + 0xA514, // 10: RGB(165,162,165) #A5A2A5 Medium Gray + 0x6318, // 11: RGB( 99, 97,198) #6361C6 Blue-Gray + 0xC618, // 12: RGB(198,195,198) #C6C3C6 Light Gray + 0x39E7, // 13: RGB( 57, 60, 57) #393C39 Dark Green-Gray + 0xE73C, // 14: RGB(231,231,231) #E7E7E7 Very Light Gray + 0xFFFF // 15: RGB(255,255,255) #FFFFFF White +}; /**************************************************************** * Timing functions @@ -207,13 +225,13 @@ command_config_gc9a01(uint32_t *args) gc9a01_cmd_with_data(g, GC9A01_COLMOD, 1, colmod_data); // Memory Data Access Control - apply rotation - uint8_t madctl_value = 0x00; + uint8_t madctl_value = 0x08; // BGR bit if (g->rotation == 90) - madctl_value = 0x60; // MV=1, MX=1 + madctl_value |= 0x60; else if (g->rotation == 180) - madctl_value = 0xC0; // MY=1, MX=1 + madctl_value |= 0xC0; else if (g->rotation == 270) - madctl_value = 0xA0; // MV=1, MY=1 + madctl_value |= 0xA0; uint8_t madctl_data[] = {madctl_value}; gc9a01_cmd_with_data(g, GC9A01_MADCTL, 1, madctl_data); @@ -325,7 +343,8 @@ command_gc9a01_fill_rect(uint32_t *args) uint16_t y0 = args[2]; uint16_t x1 = args[3]; uint16_t y1 = args[4]; - uint16_t color = args[5]; + uint8_t color_idx = args[5] & 0x0F; + uint16_t color = palette[color_idx]; uint32_t pixels = (x1 - x0 + 1) * (y1 - y0 + 1) * 2; uint8_t color_high = (color >> 8) & 0xFF; uint8_t color_low = color & 0xFF; @@ -361,15 +380,36 @@ command_gc9a01_fill_rect(uint32_t *args) } } DECL_COMMAND(command_gc9a01_fill_rect, - "gc9a01_fill_rect oid=%c x0=%hu y0=%hu x1=%hu y1=%hu color=%hu"); + "gc9a01_fill_rect oid=%c x0=%hu y0=%hu x1=%hu y1=%hu color=%c"); void command_gc9a01_send_data(uint32_t *args) { struct gc9a01 *g = oid_lookup(args[0], command_config_gc9a01); uint8_t len = args[1], *data = command_decode_ptr(args[2]); - if (len > 0) - gc9a01_send_data(g, len, data); + uint8_t out_buf[GC9A01_LINE_SIZE]; + uint16_t out_idx = 0; + + for (uint8_t i = 0; i < len; i++) { + uint8_t byte = data[i]; + uint8_t idx1 = (byte >> 4) & 0x0F; + uint8_t idx2 = byte & 0x0F; + uint16_t color1 = palette[idx1]; + uint16_t color2 = palette[idx2]; + + out_buf[out_idx++] = (color1 >> 8) & 0xFF; + out_buf[out_idx++] = color1 & 0xFF; + out_buf[out_idx++] = (color2 >> 8) & 0xFF; + out_buf[out_idx++] = color2 & 0xFF; + + if (out_idx >= GC9A01_LINE_SIZE) { + gc9a01_send_data(g, out_idx, out_buf); + out_idx = 0; + } + } + + if (out_idx > 0) + gc9a01_send_data(g, out_idx, out_buf); } DECL_COMMAND(command_gc9a01_send_data, "gc9a01_send_data oid=%c data=%*s"); @@ -407,6 +447,30 @@ command_gc9a01_set_window(uint32_t *args) DECL_COMMAND(command_gc9a01_set_window, "gc9a01_set_window oid=%c x0=%hu y0=%hu x1=%hu y1=%hu"); +void +command_gc9a01_set_palette(uint32_t *args) +{ + oid_lookup(args[0], command_config_gc9a01); + palette[1] = args[1]; + palette[2] = args[2]; + palette[3] = args[3]; + palette[4] = args[4]; + palette[5] = args[5]; + palette[6] = args[6]; + palette[7] = args[7]; + palette[8] = args[8]; + palette[9] = args[9]; + palette[10] = args[10]; + palette[11] = args[11]; + palette[12] = args[12]; + palette[13] = args[13]; + palette[14] = args[14]; +} +DECL_COMMAND(command_gc9a01_set_palette, + "gc9a01_set_palette oid=%c c1=%hu c2=%hu c3=%hu c4=%hu " + "c5=%hu c6=%hu c7=%hu c8=%hu c9=%hu c10=%hu c11=%hu " + "c12=%hu c13=%hu c14=%hu"); + void gc9a01_shutdown(void) {