Device migrations, drivers and fixes (#571)

Device migrations:

- cyd-4848S040c
- guition-jc1060p470ciwy
- guition-jc2432w328c
- guition-jc3248w535c (known issue with display and touch, Shadowtrance will look into it)
- guition-jc8048w550c
- heltec-wifi-lora-32-v3
- lilygo-tdeck-max (excluding graphics)
- lilygo-tdisplay
- lilygo-tdisplay-s3
- lilygo-tdongle-s3
- lilygo-tlora-pager

Driver migrations:

- Implemented haptic driver interface in kernel
- AXS15231b
- BQ25896
- BQ27220
- CST328
- CST6xx
- DRV2605
- JD9165
- Custom LilyGO driver for T-Lora Pager
- SSD1306
- ST7701
- ST7735
- SY6970
- TCA8418

Fixes/improvements:

- Boot app: support for multiple power devices, improved UI
- lvgl_devices and related code: fixes for mapping
- Support for arrays in dts parser
This commit is contained in:
Ken Van Hoeylandt
2026-07-18 15:01:42 +02:00
committed by GitHub
parent 3b5a401594
commit d896657bf9
290 changed files with 9113 additions and 6719 deletions
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(ssd1306-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel platform-esp32 esp_lcd driver
)
@@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,52 @@
description: Solomon Systech SSD1306 monochrome OLED display panel (I2C interface)
include: ["i2c-device.yaml"]
compatible: "solomon,ssd1306"
bus: i2c
properties:
horizontal-resolution:
type: int
required: true
description: Horizontal resolution in pixels (typically 128)
vertical-resolution:
type: int
required: true
description: Vertical resolution in pixels (typically 32 or 64)
invert-color:
type: boolean
default: false
description: Invert the panel's color output
pin-reset:
type: phandles
default: GPIO_PIN_SPEC_NONE
description: Reset GPIO pin
reset-active-high:
type: boolean
default: false
description: Whether the reset pin is active high
power-on-delay-ms:
type: int
default: 0
description: >
Extra delay before reset/init, in milliseconds. Useful when the panel's power rail is
gated by a gpio-hog node: devicetree devices (including gpio-hog) are constructed and
started during kernel_init(), with no built-in settle time between one device starting
and the next, so a panel whose power was only just enabled a moment earlier may need this
to let its rail stabilize before communication starts. 0 (the default) adds no delay.
init-sequence:
type: array
element-type: uint8_t
description: >
Custom vendor bring-up sequence: a flat list of command bytes, each sent as its own
single-byte SSD1306 command (some standalone, some acting as a parameter to the byte
before them - this chip's protocol doesn't distinguish the two at the wire level, and
neither does this encoding). Omit to use the SSD1306 component's own generic
esp_lcd_panel_init() sequence instead - note that some panels are known not to
initialize correctly through it (see e.g. the Heltec WiFi LoRa 32 V3's own required
override), so most boards should supply their own sequence here.
# No 'backlight' property: SSD1306 is a self-emitting OLED panel with no backlight to control,
# unlike this repo's other display bindings.
+3
View File
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/bindings/bindings.h>
#include <drivers/ssd1306.h>
// The devicetree compiler derives the expected config typedef name from the compatible
// string's suffix (e.g. "solomon,ssd1306" -> ssd1306_config_dt), not from the node name or
// driver name, so the tag here must match that exactly.
DEFINE_DEVICETREE(ssd1306, struct Ssd1306Config)
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#include <tactility/device.h>
#include <tactility/drivers/gpio.h>
struct Ssd1306Config {
// Devicetree address hint - unlike this repo's other I2C touch chips (which sit at a fixed
// address), the SSD1306's address genuinely varies by hardware strap (0x3C or 0x3D), so this
// is actually used, not just documentation.
uint8_t address;
uint16_t horizontal_resolution;
uint16_t vertical_resolution;
bool invert_color;
// Reset pin. GPIO_PIN_SPEC_NONE skips the reset toggle entirely.
struct GpioPinSpec pin_reset;
bool reset_active_high;
// See the 'power-on-delay-ms' binding property.
uint32_t power_on_delay_ms;
// Custom vendor init sequence: a flat list of command bytes, each sent as its own
// single-byte SSD1306 command. NULL/0 falls back to the standard esp_lcd_panel_init().
const uint8_t* init_sequence;
uint32_t init_sequence_length;
};
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module ssd1306_module;
#ifdef __cplusplus
}
#endif
+32
View File
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/check.h>
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver ssd1306_driver;
static error_t start() {
/* We crash when construct fails, because if a single driver fails to construct,
* there is no guarantee that the previously constructed drivers can be destroyed */
check(driver_construct_add(&ssd1306_driver) == ERROR_NONE);
return ERROR_NONE;
}
static error_t stop() {
/* We crash when destruct fails, because if a single driver fails to destruct,
* there is no guarantee that the previously destroyed drivers can be recovered */
check(driver_remove_destruct(&ssd1306_driver) == ERROR_NONE);
return ERROR_NONE;
}
Module ssd1306_module = {
.name = "ssd1306",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
} // extern "C"
+360
View File
@@ -0,0 +1,360 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/ssd1306.h>
#include <ssd1306_module.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/esp32_i2c.h>
#include <tactility/drivers/esp32_i2c_master.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <esp_err.h>
#include <esp_lcd_io_i2c.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_lcd_panel_ssd1306.h>
#include <driver/gpio.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <cstdlib>
#include <cstring>
#define TAG "SSD1306"
#define GET_CONFIG(device) (static_cast<const Ssd1306Config*>((device)->config))
struct Ssd1306Internal {
esp_lcd_panel_io_handle_t io_handle;
esp_lcd_panel_handle_t panel_handle;
// Scratch buffer for the page-format transpose in ssd1306_draw_bitmap(); sized once for the
// full panel resolution and reused every draw_bitmap() call to avoid a malloc/free per frame.
uint8_t* page_buf;
size_t page_buf_size;
};
static gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
}
// region Driver lifecycle
static esp_err_t create_io_handle(Device* parent, uint8_t address, esp_lcd_panel_io_handle_t* out_handle) {
// dc_bit_offset=6/dc_low_on_data=false gives a 0x00 control byte before command bytes and
// 0x40 before pixel data - the standard SSD1306 I2C control-byte convention. lcd_cmd_bits=0
// means tx_param()'s own "cmd" phase is skipped entirely (see send_command() below): every
// command byte is transmitted as if it were "data" following the correct control byte,
// rather than a separately-framed command - this chip's protocol has no such framing to begin
// with, unlike the multi-byte-per-command SPI/QSPI panels elsewhere in this repo.
esp_lcd_panel_io_i2c_config_t io_config = {
.dev_addr = address,
.on_color_trans_done = nullptr,
.user_ctx = nullptr,
.control_phase_bytes = 1,
.dc_bit_offset = 6,
.lcd_cmd_bits = 0,
.lcd_param_bits = 0,
.flags = {
.dc_low_on_data = false,
.disable_control_phase = false,
},
.scl_speed_hz = 0,
};
auto* parent_driver = device_get_driver(parent);
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c")) {
auto port = static_cast<const Esp32I2cConfig*>(parent->config)->port;
return esp_lcd_new_panel_io_i2c_v1(port, &io_config, out_handle);
}
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c-master")) {
auto bus = esp32_i2c_master_get_bus_handle(parent);
io_config.scl_speed_hz = esp32_i2c_master_get_clock_frequency(parent);
return esp_lcd_new_panel_io_i2c_v2(bus, &io_config, out_handle);
}
LOG_E(TAG, "Unsupported I2C driver");
return ESP_ERR_NOT_SUPPORTED;
}
// Sends a single SSD1306 command byte: [control byte, this byte]. lcd_cmd is passed as 0 and
// ignored on the wire (see create_io_handle()'s comment) - only param/param_size actually reach
// the bus, so this doubles as the mechanism for both standalone commands (e.g. 0xAE display-off)
// and command+parameter pairs (each byte of which is just its own call to this function, matching
// how the original hand-written driver issued them as separate I2C writes).
static bool send_command(esp_lcd_panel_io_handle_t io, uint8_t command) {
return esp_lcd_panel_io_tx_param(io, 0, &command, 1) == ESP_OK;
}
static bool send_init_sequence(esp_lcd_panel_io_handle_t io, const uint8_t* bytes, uint32_t length) {
for (uint32_t i = 0; i < length; i++) {
if (!send_command(io, bytes[i])) {
LOG_E(TAG, "Failed to send init sequence byte %lu", (unsigned long)i);
return false;
}
}
return true;
}
// Manual reset with explicit timing, rather than going through esp_lcd_panel_reset(): this
// chip/config combination is known to need a specific assert/settle timing that the panel's
// standard reset (which we intentionally never wire up - reset_gpio_num is always -1 below) may
// not otherwise match. Runs before the panel handle is even created, so a subsequent init sees a
// freshly-reset chip.
static void perform_hardware_reset(const Ssd1306Config* config) {
gpio_num_t pin = pin_or_nc(config->pin_reset);
if (pin == GPIO_NUM_NC) {
return;
}
gpio_config_t io_conf = {
.pin_bit_mask = 1ULL << pin,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);
gpio_set_level(pin, config->reset_active_high ? 1 : 0);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(pin, config->reset_active_high ? 0 : 1);
vTaskDelay(pdMS_TO_TICKS(100));
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<Ssd1306Internal*>(malloc(sizeof(Ssd1306Internal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->page_buf_size = (size_t)config->horizontal_resolution * ((config->vertical_resolution + 7) / 8);
internal->page_buf = static_cast<uint8_t*>(malloc(internal->page_buf_size));
if (internal->page_buf == nullptr) {
free(internal);
return ERROR_OUT_OF_MEMORY;
}
// Lets a power rail gated by a gpio-hog node (asserted moments earlier, during the same
// kernel_init() pass) stabilize before this device tries to reset/talk to the chip. See the
// 'power-on-delay-ms' binding property.
if (config->power_on_delay_ms > 0) {
vTaskDelay(pdMS_TO_TICKS(config->power_on_delay_ms));
}
if (create_io_handle(parent, config->address, &internal->io_handle) != ESP_OK) {
LOG_E(TAG, "Failed to create panel IO");
free(internal->page_buf);
free(internal);
return ERROR_RESOURCE;
}
perform_hardware_reset(config);
esp_lcd_panel_ssd1306_config_t ssd1306_config = {
.height = static_cast<uint8_t>(config->vertical_resolution),
};
// color_space (not rgb_ele_order): this deprecated union member is the only one whose type
// can express ESP_LCD_COLOR_SPACE_MONOCHROME - rgb_ele_order's own enum only covers RGB/BGR.
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = -1, // always -1: reset is handled manually above, not by the panel itself
.color_space = ESP_LCD_COLOR_SPACE_MONOCHROME,
.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
.bits_per_pixel = 1,
.flags = { .reset_active_high = false },
.vendor_config = &ssd1306_config,
};
if (esp_lcd_new_panel_ssd1306(internal->io_handle, &panel_config, &internal->panel_handle) != ESP_OK) {
LOG_E(TAG, "Failed to create panel");
esp_lcd_panel_io_del(internal->io_handle);
free(internal->page_buf);
free(internal);
return ERROR_RESOURCE;
}
// Bring-up sequence. Every failure path below must clean up fully: unlike stop_device, this
// is never retried by the kernel if start_device fails (see device_start() in
// TactilityKernel), so a partial failure here would leak.
bool ok;
if (config->init_sequence != nullptr && config->init_sequence_length > 0) {
ok = send_init_sequence(internal->io_handle, config->init_sequence, config->init_sequence_length);
} else {
ok = esp_lcd_panel_init(internal->panel_handle) == ESP_OK;
}
ok = ok && (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK);
ok = ok && esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK;
if (!ok) {
LOG_E(TAG, "Failed to bring up panel");
esp_lcd_panel_del(internal->panel_handle);
esp_lcd_panel_io_del(internal->io_handle);
free(internal->page_buf);
free(internal);
return ERROR_RESOURCE;
}
vTaskDelay(pdMS_TO_TICKS(100)); // Let the panel stabilize before the first real draw_bitmap()
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<Ssd1306Internal*>(device_get_driver_data(device));
if (internal->panel_handle != nullptr) {
if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete panel");
return ERROR_RESOURCE;
}
internal->panel_handle = nullptr;
}
if (internal->io_handle != nullptr) {
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete panel IO");
return ERROR_RESOURCE;
}
internal->io_handle = nullptr;
}
free(internal->page_buf);
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
// region DisplayApi
static error_t ssd1306_reset(Device* device) {
perform_hardware_reset(GET_CONFIG(device));
return ERROR_NONE;
}
static error_t ssd1306_init(Device* device) {
auto* internal = static_cast<Ssd1306Internal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
bool ok;
if (config->init_sequence != nullptr && config->init_sequence_length > 0) {
ok = send_init_sequence(internal->io_handle, config->init_sequence, config->init_sequence_length);
} else {
ok = esp_lcd_panel_init(internal->panel_handle) == ESP_OK;
}
return ok ? ERROR_NONE : ERROR_RESOURCE;
}
// DISPLAY_COLOR_FORMAT_MONOCHROME's wire format (matching LVGL's LV_COLOR_FORMAT_I1, palette
// stripped) is row-major: each byte packs 8 horizontally-adjacent pixels, MSB = leftmost. The
// SSD1306's GDDRAM is page-addressed instead: each byte packs 8 *vertically* adjacent pixels of
// one column, LSB = topmost row of the page. Transpose one into the other before handing off to
// the vendor component, which expects page format directly.
static void transpose_row_major_to_pages(const uint8_t* src, int32_t width, int32_t height, uint8_t* out) {
uint32_t row_stride = (uint32_t)(width + 7) / 8;
for (int32_t y = 0; y < height; y++) {
uint8_t page = (uint8_t)(y / 8);
uint8_t bit_in_page = (uint8_t)(y % 8);
for (int32_t x = 0; x < width; x++) {
uint8_t src_byte = src[y * row_stride + x / 8];
uint8_t pixel = (src_byte >> (7 - (x % 8))) & 0x01;
out[page * width + x] |= (uint8_t)(pixel << bit_in_page);
}
}
}
static error_t ssd1306_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
auto* internal = static_cast<Ssd1306Internal*>(device_get_driver_data(device));
int32_t width = x_end - x_start;
int32_t height = y_end - y_start;
size_t needed = (size_t)width * ((height + 7) / 8);
check(needed <= internal->page_buf_size);
memset(internal->page_buf, 0, needed);
transpose_row_major_to_pages(static_cast<const uint8_t*>(color_data), width, height, internal->page_buf);
return esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, internal->page_buf) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// mirror/swap_xy/set_gap are not exposed: the SSD1306 vendor component doesn't implement them.
static error_t ssd1306_invert_color(Device* device, bool invert_color_data) {
auto* internal = static_cast<Ssd1306Internal*>(device_get_driver_data(device));
return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t ssd1306_disp_on_off(Device* device, bool on_off) {
auto* internal = static_cast<Ssd1306Internal*>(device_get_driver_data(device));
return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
}
// disp_sleep is not exposed: the SSD1306 vendor component doesn't implement it either (display
// on/off via 0xAE/0xAF is this chip's equivalent, already covered by disp_on_off above).
static enum DisplayColorFormat ssd1306_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_MONOCHROME;
}
static uint16_t ssd1306_get_resolution_x(Device* device) {
return GET_CONFIG(device)->horizontal_resolution;
}
static uint16_t ssd1306_get_resolution_y(Device* device) {
return GET_CONFIG(device)->vertical_resolution;
}
static void ssd1306_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
*out_buffer = nullptr;
}
static uint8_t ssd1306_get_frame_buffer_count(Device*) {
return 0;
}
// get_backlight is not exposed: SSD1306 is a self-emitting OLED panel with no backlight.
// endregion
static const DisplayApi ssd1306_display_api = {
.capabilities = DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF,
.reset = ssd1306_reset,
.init = ssd1306_init,
.draw_bitmap = ssd1306_draw_bitmap,
.mirror = nullptr,
.swap_xy = nullptr,
.get_swap_xy = nullptr,
.get_mirror_x = nullptr,
.get_mirror_y = nullptr,
.set_gap = nullptr,
.invert_color = ssd1306_invert_color,
.disp_on_off = ssd1306_disp_on_off,
.disp_sleep = nullptr,
.get_color_format = ssd1306_get_color_format,
.get_resolution_x = ssd1306_get_resolution_x,
.get_resolution_y = ssd1306_get_resolution_y,
.get_frame_buffer = ssd1306_get_frame_buffer,
.get_frame_buffer_count = ssd1306_get_frame_buffer_count,
.get_backlight = nullptr,
.has_capability = nullptr,
};
Driver ssd1306_driver = {
.name = "ssd1306",
.compatible = (const char*[]) { "solomon,ssd1306", nullptr },
.start_device = start,
.stop_device = stop,
.api = &ssd1306_display_api,
.device_type = &DISPLAY_TYPE,
.owner = &ssd1306_module,
.internal = nullptr
};