Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[QP] Add support for OLED, variable framebuffer bpp #19997

Merged
merged 12 commits into from
Oct 22, 2023
1 change: 1 addition & 0 deletions builddefs/build_keyboard.mk
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ $(eval $(call add_qmk_prefix_defs,MCU_PORT_NAME,MCU_PORT_NAME))
$(eval $(call add_qmk_prefix_defs,MCU_FAMILY,MCU_FAMILY))
$(eval $(call add_qmk_prefix_defs,MCU_SERIES,MCU_SERIES))
$(eval $(call add_qmk_prefix_defs,BOARD,BOARD))
$(eval $(call add_qmk_prefix_defs,OPT,OPT))

# Control whether intermediate file listings are generated
# e.g.:
Expand Down
165 changes: 109 additions & 56 deletions docs/quantum_painter.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,24 @@ QUANTUM_PAINTER_DRIVERS += ......

You will also likely need to select an appropriate driver in `rules.mk`, which is listed below.

!> Quantum Painter is not currently integrated with system-level operations such as disabling displays after a configurable timeout, or when the keyboard goes into suspend. Users will need to handle this manually at the current time.
!> Quantum Painter is not currently integrated with system-level operations such as when the keyboard goes into suspend. Users will need to handle this manually at the current time.

The QMK CLI can be used to convert from normal images such as PNG files or animated GIFs, as well as fonts from TTF files.

Supported devices:

| Display Panel | Panel Type | Size | Comms Transport | Driver |
|----------------|--------------------|------------------|-----------------|---------------------------------------------|
| GC9A01 | RGB LCD (circular) | 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += gc9a01_spi` |
| ILI9163 | RGB LCD | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ili9163_spi` |
| ILI9341 | RGB LCD | 240x320 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ili9341_spi` |
| ILI9488 | RGB LCD | 320x480 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ili9488_spi` |
| SSD1351 | RGB OLED | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ssd1351_spi` |
| ST7735 | RGB LCD | 132x162, 80x160 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += st7735_spi` |
| ST7789 | RGB LCD | 240x320, 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += st7789_spi` |
| RGB565 Surface | Virtual | User-defined | None | `QUANTUM_PAINTER_DRIVERS += rgb565_surface` |
| Display Panel | Panel Type | Size | Comms Transport | Driver |
|---------------|--------------------|------------------|-----------------|------------------------------------------|
| GC9A01 | RGB LCD (circular) | 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += gc9a01_spi` |
| ILI9163 | RGB LCD | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ili9163_spi` |
| ILI9341 | RGB LCD | 240x320 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ili9341_spi` |
| ILI9488 | RGB LCD | 320x480 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ili9488_spi` |
| SSD1351 | RGB OLED | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ssd1351_spi` |
| ST7735 | RGB LCD | 132x162, 80x160 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += st7735_spi` |
| ST7789 | RGB LCD | 240x320, 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += st7789_spi` |
| SH1106 (SPI) | Monochrome OLED | 128x64 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += sh1106_spi` |
| SH1106 (I2C) | Monochrome OLED | 128x64 | I2C | `QUANTUM_PAINTER_DRIVERS += sh1106_i2c` |
| Surface | Virtual | User-defined | None | `QUANTUM_PAINTER_DRIVERS += surface` |

## Quantum Painter Configuration :id=quantum-painter-config

Expand Down Expand Up @@ -188,7 +190,8 @@ Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.c...

<!-- tabs:start -->

### ** Common: Standard TFT (SPI + D/C + RST) **

### ** LCD **

Most TFT display panels use a 5-pin interface -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins.

Expand Down Expand Up @@ -302,32 +305,6 @@ The maximum number of displays can be configured by changing the following in yo

Native color format rgb888 is compatible with ILI9488

#### ** SSD1351 **

Enabling support for the SSD1351 in Quantum Painter is done by adding the following to `rules.mk`:

```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS += ssd1351_spi
```

Creating a SSD1351 device in firmware can then be done with the following API:

```c
painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
```

The device handle returned from the `qp_ssd1351_make_spi_device` function can be used to perform all other drawing operations.

The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):

```c
// 3 displays:
#define SSD1351_NUM_DEVICES 3
```

Native color format rgb565 is compatible with SSD1351

#### ** ST7735 **

Enabling support for the ST7735 in Quantum Painter is done by adding the following to `rules.mk`:
Expand Down Expand Up @@ -386,62 +363,138 @@ Native color format rgb565 is compatible with ST7789

<!-- tabs:end -->

### ** Common: Surfaces **
### ** OLED **

Quantum Painter has surface drivers which are able to target a buffer in RAM. In general, surfaces keep track of the "dirty" region -- the area that has been drawn to since the last flush -- so that when transferring to the display they can transfer the minimal amount of data to achieve the end result.
OLED displays tend to use 5-pin SPI when at larger resolutions, or when using color -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins. Smaller OLEDs may use I2C instead.

!> These generally require significant amounts of RAM, so at large sizes and/or higher bit depths, they may not be usable on all MCUs.
When using these displays, either `spi_master` or `i2c_master` must already be correctly configured for both the platform and panel you're building for.

For SPI, the pin assignments for SPI CS, D/C, and RST are specified during device construction -- for I2C the panel's address is specified instead.

<!-- tabs:start -->

#### ** RGB565 Surface **
#### ** SSD1351 **

Enabling support for the SSD1351 in Quantum Painter is done by adding the following to `rules.mk`:

```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS += ssd1351_spi
```

Creating a SSD1351 device in firmware can then be done with the following API:

```c
painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
```

The device handle returned from the `qp_ssd1351_make_spi_device` function can be used to perform all other drawing operations.

Enabling support for RGB565 surfaces in Quantum Painter is done by adding the following to `rules.mk`:
The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):

```c
// 3 displays:
#define SSD1351_NUM_DEVICES 3
```

Native color format rgb565 is compatible with SSD1351

#### ** SH1106 **
tzarc marked this conversation as resolved.
Show resolved Hide resolved

Enabling support for the SH1106 in Quantum Painter is done by adding the following to `rules.mk`:

```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS += rgb565_surface
# For SPI:
QUANTUM_PAINTER_DRIVERS += sh1106_spi
# For I2C:
QUANTUM_PAINTER_DRIVERS += sh1106_i2c
```

Creating a SH1106 device in firmware can then be done with the following APIs:

```c
// SPI-based SH1106:
painter_device_t qp_sh1106_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
// I2C-based SH1106:
painter_device_t qp_sh1106_make_i2c_device(uint16_t panel_width, uint16_t panel_height, uint8_t i2c_address);
```

Creating a RGB565 surface in firmware can then be done with the following API:
The device handle returned from the `qp_sh1106_make_???_device` function can be used to perform all other drawing operations.

The maximum number of displays of each type can be configured by changing the following in your `config.h` (default is 1):

```c
painter_device_t qp_rgb565_make_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);
// 3 SPI displays:
#define SH1106_NUM_SPI_DEVICES 3
// 3 I2C displays:
#define SH1106_NUM_I2C_DEVICES 3
```

The `buffer` is a user-supplied area of memory, and is assumed to be of the size `sizeof(uint16_t) * panel_width * panel_height`.
Native color format mono2 is compatible with SH1106

<!-- tabs:end -->

The device handle returned from the `qp_rgb565_make_surface` function can be used to perform all other drawing operations.
### ** Surface **

Quantum Painter has a surface driver which is able to target a buffer in RAM. In general, surfaces keep track of the "dirty" region -- the area that has been drawn to since the last flush -- so that when transferring to the display they can transfer the minimal amount of data to achieve the end result.

!> These generally require significant amounts of RAM, so at large sizes and/or higher bit depths, they may not be usable on all MCUs.

Enabling support for surfaces in Quantum Painter is done by adding the following to `rules.mk`:

```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS += surface
```

Creating a surface in firmware can then be done with the following APIs:

```c
// 16bpp RGB565 surface:
painter_device_t qp_make_rgb565_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);
// 1bpp monochrome surface:
painter_device_t qp_make_mono1bpp_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);
```

The `buffer` is a user-supplied area of memory, which can be statically allocated using `SURFACE_REQUIRED_BUFFER_BYTE_SIZE`:

```c
// Buffer required for a 240x80 16bpp surface:
uint8_t framebuffer[SURFACE_REQUIRED_BUFFER_BYTE_SIZE(240, 80, 16)];
```

The device handle returned from the `qp_make_?????_surface` function can be used to perform all other drawing operations.

Example:

```c
static painter_device_t my_surface;
static uint16_t my_framebuffer[320 * 240]; // Allocate a buffer for a 320x240 RGB565 display
static uint8_t my_framebuffer[SURFACE_REQUIRED_BUFFER_BYTE_SIZE(240, 80, 16)]; // Allocate a buffer for a 16bpp 240x80 RGB565 display
void keyboard_post_init_kb(void) {
my_surface = qp_rgb565_make_surface(320, 240, my_framebuffer);
my_surface = qp_rgb565_make_surface(240, 80, my_framebuffer);
qp_init(my_surface, QP_ROTATION_0);
}
tzarc marked this conversation as resolved.
Show resolved Hide resolved
```

The maximum number of RGB565 surfaces can be configured by changing the following in your `config.h` (default is 1):
The maximum number of surfaces can be configured by changing the following in your `config.h` (default is 1):

```c
// 3 surfaces:
#define RGB565_SURFACE_NUM_DEVICES 3
#define SURFACE_NUM_DEVICES 3
```

To transfer the contents of the RGB565 surface to another display, the following API can be invoked:
To transfer the contents of the surface to another display of the same pixel format, the following API can be invoked:

```c
bool qp_rgb565_surface_draw(painter_device_t surface, painter_device_t display, uint16_t x, uint16_t y);
bool qp_surface_draw(painter_device_t surface, painter_device_t display, uint16_t x, uint16_t y);
```

The `surface` is the surface to copy out from. The `display` is the target display to draw into. `x` and `y` are the target location to draw the surface pixel data. Under normal circumstances, the location should be consistent, as the dirty region is calculated with respect to the `x` and `y` coordinates -- changing those will result in partial, overlapping draws.

?> Calling `qp_flush()` on the surface resets its dirty region. Copying the surface contents to the display also automatically resets the dirty region.
!> The surface and display panel must have the same native pixel format.

<!-- tabs:end -->
?> Calling `qp_flush()` on the surface resets its dirty region. Copying the surface contents to the display also automatically resets the dirty region.

<!-- tabs:end -->

Expand Down
34 changes: 34 additions & 0 deletions drivers/painter/comms/qp_comms_dummy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later

#ifdef QUANTUM_PAINTER_DUMMY_COMMS_ENABLE

# include "qp_comms_dummy.h"

static bool dummy_comms_init(painter_device_t device) {
// No-op.
return true;
}

static bool dummy_comms_start(painter_device_t device) {
// No-op.
return true;
}

static void dummy_comms_stop(painter_device_t device) {
// No-op.
}

uint32_t dummy_comms_send(painter_device_t device, const void *data, uint32_t byte_count) {
// No-op.
return byte_count;
}

painter_comms_vtable_t dummy_comms_vtable = {
// These are all effective no-op's because they're not actually needed.
.comms_init = dummy_comms_init,
.comms_start = dummy_comms_start,
.comms_stop = dummy_comms_stop,
.comms_send = dummy_comms_send};

#endif // QUANTUM_PAINTER_DUMMY_COMMS_ENABLE
11 changes: 11 additions & 0 deletions drivers/painter/comms/qp_comms_dummy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#ifdef QUANTUM_PAINTER_DUMMY_COMMS_ENABLE

# include "qp_internal.h"

extern painter_comms_vtable_t dummy_comms_vtable;

#endif // QUANTUM_PAINTER_DUMMY_COMMS_ENABLE
94 changes: 94 additions & 0 deletions drivers/painter/comms/qp_comms_i2c.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later

#ifdef QUANTUM_PAINTER_I2C_ENABLE

# include "i2c_master.h"
# include "qp_comms_i2c.h"

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers

static uint32_t qp_comms_i2c_send_raw(painter_device_t device, const void *data, uint32_t byte_count) {
painter_driver_t * driver = (painter_driver_t *)device;
qp_comms_i2c_config_t *comms_config = (qp_comms_i2c_config_t *)driver->comms_config;
i2c_status_t res = i2c_transmit(comms_config->chip_address << 1, data, byte_count, I2C_TIMEOUT);
if (res < 0) {
return 0;
}
return byte_count;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Base I2C support

bool qp_comms_i2c_init(painter_device_t device) {
i2c_init();
return true;
}

bool qp_comms_i2c_start(painter_device_t device) {
painter_driver_t * driver = (painter_driver_t *)device;
qp_comms_i2c_config_t *comms_config = (qp_comms_i2c_config_t *)driver->comms_config;
return i2c_start(comms_config->chip_address << 1) == I2C_STATUS_SUCCESS;
}

uint32_t qp_comms_i2c_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
return qp_comms_i2c_send_raw(device, data, byte_count);
}

void qp_comms_i2c_stop(painter_device_t device) {
i2c_stop();
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Command+Data I2C support

static const uint8_t cmd_byte = 0x00;
static const uint8_t data_byte = 0x40;

void qp_comms_i2c_cmddata_send_command(painter_device_t device, uint8_t cmd) {
uint8_t buf[2] = {cmd_byte, cmd};
qp_comms_i2c_send_raw(device, &buf, 2);
}

uint32_t qp_comms_i2c_cmddata_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
uint8_t buf[1 + byte_count];
buf[0] = data_byte;
memcpy(&buf[1], data, byte_count);
if (qp_comms_i2c_send_raw(device, buf, sizeof(buf)) != sizeof(buf)) {
return 0;
}
return byte_count;
}

void qp_comms_i2c_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
uint8_t buf[32];
for (size_t i = 0; i < sequence_len;) {
uint8_t command = sequence[i];
uint8_t delay = sequence[i + 1];
uint8_t num_bytes = sequence[i + 2];
buf[0] = cmd_byte;
buf[1] = command;
memcpy(&buf[2], &sequence[i + 3], num_bytes);
qp_comms_i2c_send_raw(device, buf, num_bytes + 2);
if (delay > 0) {
wait_ms(delay);
}
i += (3 + num_bytes);
}
}

const painter_comms_with_command_vtable_t i2c_comms_cmddata_vtable = {
.base =
{
.comms_init = qp_comms_i2c_init,
.comms_start = qp_comms_i2c_start,
.comms_send = qp_comms_i2c_cmddata_send_data,
.comms_stop = qp_comms_i2c_stop,
},
.send_command = qp_comms_i2c_cmddata_send_command,
.bulk_command_sequence = qp_comms_i2c_bulk_command_sequence,
};

#endif // QUANTUM_PAINTER_I2C_ENABLE
Loading