From d53c3a8816e907a38c4077688cfdd78fc7fb34c8 Mon Sep 17 00:00:00 2001 From: elpekenin Date: Mon, 6 Mar 2023 11:07:14 +0100 Subject: [PATCH] Initial code --- drivers/painter/eink_panel/qp_eink_panel.c | 215 +++++++++++++++++++++ drivers/painter/eink_panel/qp_eink_panel.h | 86 +++++++++ 2 files changed, 301 insertions(+) create mode 100644 drivers/painter/eink_panel/qp_eink_panel.c create mode 100644 drivers/painter/eink_panel/qp_eink_panel.h diff --git a/drivers/painter/eink_panel/qp_eink_panel.c b/drivers/painter/eink_panel/qp_eink_panel.c new file mode 100644 index 00000000000..b33f84d0fc0 --- /dev/null +++ b/drivers/painter/eink_panel/qp_eink_panel.c @@ -0,0 +1,215 @@ +// Copyright 2023 Pablo Martinez (@elpekenin) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "qp_comms.h" +#include "qp_draw.h" +#include "qp_eink_panel.h" +#include "qp_internal.h" +#include "qp_surface.h" +#include "qp_surface_internal.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// NOTE: The way in which data is stored may break if QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE is not an even number of bytes + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Helper functions + +// Reset can_flush flag back to true after timeout +uint32_t can_flush_callback(uint32_t trigger_time, void *cb_arg) { + eink_panel_dc_reset_painter_device_t *driver = (eink_panel_dc_reset_painter_device_t *)cb_arg; + driver->can_flush = true; + return 0; +} + +// Set can_flush to false and schedule its cleanup +void qp_eink_update_can_flush(painter_device_t device) { + eink_panel_dc_reset_painter_device_t *driver = (eink_panel_dc_reset_painter_device_t *)device; + driver->can_flush = false; + defer_exec(driver->timeout, can_flush_callback, (void *)device); +} + +static inline void decode_masked_pixels(uint8_t *pixels, uint32_t byte, uint8_t *black, uint8_t *color) { + /* Convert pixel data into convenient representation, buffer holds data as: + * B0C0 B1C1 B2C2 B3C3 || B4C4 B5C5 B6C6 B7C7 ... + * + * This function shuffles it so we get + * black_data: B7B6B5B4B3B2B1B0 + * color_data: C7C6C5C4C3C2C1C0 + */ + uint16_t raw_data = (pixels[byte] << 8) | (pixels[byte + 1]); + + // clear data so we can simply |= + *black = 0; + *color = 0; + + uint16_t black_mask = 1 << 15; + uint16_t color_mask = 1 << 14; + + for (uint8_t i = 0; i < 8; ++i) { + bool black_bit = raw_data & black_mask; + bool color_bit = raw_data & color_mask; + + black_mask >>= 2; + color_mask >>= 2; + + *black |= black_bit << i; + *color |= color_bit << i; + } +} + +// Interpolate any HSV888 to the nearest of the three +static inline uint32_t hsv_distance(HSV hsv1, HSV hsv2) { + return (hsv1.h - hsv2.h) * (hsv1.h - hsv2.h) + (hsv1.s - hsv2.s) * (hsv1.s - hsv2.s) + (hsv1.v - hsv2.v) * (hsv1.v - hsv2.v); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Quantum Painter API implementations + +// Power control +bool qp_eink_panel_power(painter_device_t device, bool power_on) { + painter_driver_t * driver = (painter_driver_t *)device; + eink_panel_dc_reset_painter_driver_vtable_t *vtable = (eink_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable; + + qp_comms_command(device, power_on ? vtable->opcodes.display_on : vtable->opcodes.display_off); + + return true; +} + +// Screen clear +bool qp_eink_panel_clear(painter_device_t device) { + painter_driver_t *driver = (painter_driver_t *)device; + qp_init(device, driver->rotation); + return true; +} + +// Screen flush +bool qp_eink_panel_flush(painter_device_t device) { + eink_panel_dc_reset_painter_device_t * driver = (eink_panel_dc_reset_painter_device_t *)device; + eink_panel_dc_reset_painter_driver_vtable_t *vtable = (eink_panel_dc_reset_painter_driver_vtable_t *)driver->base.driver_vtable; + surface_painter_device_t * black = (surface_painter_device_t *)driver->black_surface; + surface_painter_device_t * color = (surface_painter_device_t *)driver->color_surface; + uint32_t n_bytes = SURFACE_REQUIRED_BUFFER_BYTE_SIZE(driver->base.panel_width, driver->base.panel_height, 1); + + if (!black->dirty.is_dirty && !color->dirty.is_dirty) { + qp_dprintf("qp_eink_panel_flush: done (no changes to be sent)\n"); + return true; + } + + if (!driver->can_flush) { + qp_dprintf("qp_eink_panel_flush: fail (can_flush == false)\n"); + return false; + } + + qp_comms_command(device, vtable->opcodes.send_black_data); + qp_comms_send(device, black->buffer, n_bytes); + + qp_comms_command(device, vtable->opcodes.send_color_data); + qp_comms_send(device, color->buffer, n_bytes); + + qp_comms_command(device, vtable->opcodes.refresh); + + qp_eink_update_can_flush(device); + + return true; +} + +// Viewport to draw to +bool qp_eink_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) { + eink_panel_dc_reset_painter_device_t *driver = (eink_panel_dc_reset_painter_device_t *)device; + + qp_viewport(driver->black_surface, left, top, right, bottom); + qp_viewport(driver->color_surface, left, top, right, bottom); + + return true; +} + +// Stream pixel data to the current write position +bool qp_eink_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) { + eink_panel_dc_reset_painter_device_t *driver = (eink_panel_dc_reset_painter_device_t *)device; + uint8_t * pixels = (uint8_t *)pixel_data; + + uint32_t i = 0; + uint8_t black_data, color_data; + while (i < native_pixel_count) { + // at most, 8 pixels per cycle + uint8_t pixels_this_loop = QP_MIN(native_pixel_count - i, 8); + uint32_t byte = i / 4; + + // stream data to display + decode_masked_pixels(pixels, byte, &black_data, &color_data); + // this is very slow with debugging on, call the vtable's function manually instead + qp_pixdata(driver->black_surface, (const void *)&black_data, pixels_this_loop); + qp_pixdata(driver->color_surface, (const void *)&color_data, pixels_this_loop); + + // update position + i += pixels_this_loop; + } + + return true; +} + +// Convert supplied palette entries into their native equivalents +bool qp_eink_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette) { + eink_panel_dc_reset_painter_device_t *driver = (eink_panel_dc_reset_painter_device_t *)device; + + for (int16_t i = 0; i < palette_size; ++i) { + HSV hsv = (HSV){palette[i].hsv888.h, palette[i].hsv888.s, palette[i].hsv888.v}; + uint32_t white_distance = hsv_distance(hsv, (HSV){ HSV_WHITE }); + uint32_t black_distance = hsv_distance(hsv, (HSV){ HSV_BLACK }); + uint32_t color_distance = hsv_distance(hsv, driver->color); + + // Default to white + bool black = false; + bool color = false; + + uint32_t min_distance = QP_MIN(white_distance, QP_MIN(black_distance, color_distance)); + if (min_distance == black_distance) + black = true; + else if (min_distance == color_distance) + color = true; + + uint8_t converted = (black << 1) | (color << 0); + + palette[i].mono = converted ^ driver->invert_mask; + } + + return true; +} + +// Append pixels to the target location, keyed by the pixel index +bool qp_eink_panel_append_pixels(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices) { + for (uint32_t i = 0; i < pixel_count; ++i) { + uint32_t pixel_num = pixel_offset + i; + + // each pixel takes 2 bits, aka each byte holds 4 pixels, offset based on that + uint32_t byte_offset = pixel_num / 4; + uint8_t bit_offset = 3 - (pixel_num % 4); + + // check each color bit from palette + bool black_bit = palette[palette_indices[i]].mono & 0b10; + bool color_bit = palette[palette_indices[i]].mono & 0b01; + + // compute where data goes + // Data ends up arranged: B0R0B1R1B2R2B3R3 | ... + uint8_t black_mask = 1 << (2 * bit_offset + 1); + uint8_t color_mask = 1 << (2 * bit_offset + 0); + + // add it + if (black_bit) + target_buffer[byte_offset] |= black_mask; + else + target_buffer[byte_offset] &= ~black_mask; + + if (color_bit) + target_buffer[byte_offset] |= color_mask; + else + target_buffer[byte_offset] &= ~color_mask; + } + + return true; +} + +bool qp_eink_panel_append_pixdata(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte) { + qp_dprintf("Shouldn't call append_pixdata on e-Ink displays\n"); + return false; +} diff --git a/drivers/painter/eink_panel/qp_eink_panel.h b/drivers/painter/eink_panel/qp_eink_panel.h new file mode 100644 index 00000000000..9eff521821d --- /dev/null +++ b/drivers/painter/eink_panel/qp_eink_panel.h @@ -0,0 +1,86 @@ +// Copyright 2023 Pablo Martinez (@elpekenin) +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "color.h" +#include "qp_internal.h" +#include "qp_surface.h" + +#ifdef QUANTUM_PAINTER_SPI_ENABLE +# include "qp_comms_spi.h" +#endif // QUANTUM_PAINTER_SPI_ENABLE + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Helper to create buffers with the apropiate size +#define EINK_BYTES_REQD(w, h) (SURFACE_REQUIRED_BUFFER_BYTE_SIZE(w, h, 2)) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Common TFT panel implementation using D/C, and RST pins. + +// Driver vtable with extras +typedef struct eink_panel_dc_reset_painter_driver_vtable_t { + painter_driver_vtable_t base; // must be first, so it can be cast to/from the painter_driver_vtable_t* type + + // Whether or not the x/y coords should be swapped on 90/270 rotation + bool swap_window_coords; + + // Opcodes for normal display operation + // some may not exist on some displays + struct { + uint8_t display_on; + uint8_t display_off; + uint8_t send_black_data; + uint8_t send_color_data; + uint8_t refresh; + } opcodes; +} eink_panel_dc_reset_painter_driver_vtable_t; + +// Device definition +typedef struct eink_panel_dc_reset_painter_device_t { + painter_driver_t base; // must be first, so it can be cast to/from the painter_device_t* type + + // have to wait between flushes to avoid damaging the screen, time in ms + uint32_t timeout; + bool can_flush; + + /** Information about the pixel format, default values (non-inverted) are + * + * Black bit: 0 for white / 1 for black + * Color bit: 0 for white or black / 1 for third color + * + * Represented as a bitmask where a 1 means "needs inversion" + * 0000 00BC + */ + HSV color; + uint8_t invert_mask; + + // Storage for each color's data + painter_device_t black_surface; + painter_device_t color_surface; + + union { +#ifdef QUANTUM_PAINTER_SPI_ENABLE + // SPI-based configurables + qp_comms_spi_dc_reset_config_t spi_dc_reset_config; +#endif // QUANTUM_PAINTER_SPI_ENABLE + + // TODO: I2C/parallel etc. + }; +} eink_panel_dc_reset_painter_device_t; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Forward declarations for injecting into concrete driver vtables + +bool qp_eink_panel_power(painter_device_t device, bool power_on); +bool qp_eink_panel_clear(painter_device_t device); + +void qp_eink_update_can_flush(painter_device_t device); +bool qp_eink_panel_flush(painter_device_t device); + +bool qp_eink_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom); +bool qp_eink_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count); + +bool qp_eink_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette); + +bool qp_eink_panel_append_pixels(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices); + +bool qp_eink_panel_append_pixdata(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte);