CL-32 drivers & more (#652)

- CL-32 features added:
  - Hardware revision detection
  - v0.3 and v0.4 keyboard 
  - v0.4 power supply
  - Inlined epd library (was a module) and made changes to the device definition (don't do full update after 5 small updates)
- Statusbar: add support for inverted colors
- e-paper driver improvements and warning fixes
- Implemented gpio_controller_get_level()
- Keyboard support improvements (unicode for tca drivers)
This commit is contained in:
Ken Van Hoeylandt 2026-09-14 00:06:55 +02:00 committed by GitHub
parent 0ee725ec32
commit 65ee223876
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 5456 additions and 122 deletions

3
.gitmodules vendored
View File

@ -16,6 +16,3 @@
[submodule "Libraries/cJSON/cJSON"] [submodule "Libraries/cJSON/cJSON"]
path = Libraries/cJSON/cJSON path = Libraries/cJSON/cJSON
url = https://github.com/DaveGamble/cJSON.git url = https://github.com/DaveGamble/cJSON.git
[submodule "Libraries/esp_epaper"]
path = Libraries/esp_epaper
url = https://github.com/NellowTCS/esp_epaper.git

View File

@ -85,6 +85,8 @@ else ()
add_compile_definitions(CONFIG_TT_LAUNCHER_APP_ID="tactility.launcher") add_compile_definitions(CONFIG_TT_LAUNCHER_APP_ID="tactility.launcher")
add_compile_definitions(CONFIG_TT_AUTO_START_APP_ID="") add_compile_definitions(CONFIG_TT_AUTO_START_APP_ID="")
add_compile_definitions(CONFIG_TT_USER_DATA_LOCATION_INTERNAL) add_compile_definitions(CONFIG_TT_USER_DATA_LOCATION_INTERNAL)
add_compile_definitions(CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED=false)
endif () endif ()
project(Tactility) project(Tactility)

View File

@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES source/*.c*)
idf_component_register( idf_component_register(
SRCS ${SOURCE_FILES} SRCS ${SOURCE_FILES}
INCLUDE_DIRS "source" INCLUDE_DIRS "source"
REQUIRES TactilityKernel REQUIRES TactilityKernel tca8418-module
) )

View File

@ -8,7 +8,6 @@
#include <tactility/bindings/esp32_sdspi.h> #include <tactility/bindings/esp32_sdspi.h>
#include <tactility/bindings/esp32_wifi.h> #include <tactility/bindings/esp32_wifi.h>
#include <bindings/tca8418.h>
#include <bindings/bm8563.h> #include <bindings/bm8563.h>
#include <bindings/esp_epaper.h> #include <bindings/esp_epaper.h>
@ -26,8 +25,10 @@
status = "disabled"; status = "disabled";
}; };
// Top Stemma/Qwiic port 1 shares this bus with the keyboard and RTC (SDA: pin 1 <-> SCL: pin 2). // Top Stemma/Qwiic port 1 shares this bus with the RTC (SDA: pin 1 <-> SCL: pin 2). External
// External I2C devices on that port are reachable via i2c0. // I2C devices on that port are reachable via i2c0. The tca8418 keyboard (revision 2 hardware)
// and power-supply chip (revision 3 hardware) are mutually exclusive on this bus and are
// created dynamically based on hardware detection - see cl32_detect.cpp.
i2c0 { i2c0 {
compatible = "espressif,esp32-i2c-master"; compatible = "espressif,esp32-i2c-master";
port = <I2C_NUM_0>; port = <I2C_NUM_0>;
@ -35,47 +36,6 @@
pin-sda = <&gpio0 1 GPIO_FLAG_PULL_UP>; pin-sda = <&gpio0 1 GPIO_FLAG_PULL_UP>;
pin-scl = <&gpio0 2 GPIO_FLAG_PULL_UP>; pin-scl = <&gpio0 2 GPIO_FLAG_PULL_UP>;
keyboard {
compatible = "ti,tca8418";
reg = <0x34>;
rows = <8>;
columns = <10>;
keymap-lc = [
37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8
57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT
9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i
111 112 13 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP
0 97 115 100 102 103 104 106 107 0 // a s d f g h j k
108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU
122 120 99 118 98 32 32 110 109 0 // z x c v b n m
20 18 19 60 62 47 92 61 13 0 // LEFT DOWN RIGHT < > / \ = RUN
];
keymap-uc = [
37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8
57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT
9 81 87 69 82 84 89 85 73 0 // TAB Q W E R T Y U I
79 80 13 40 41 45 59 58 3 0 // O P ENTER ( ) - ; : STOP
0 65 83 68 70 71 72 74 75 0 // A S D F G H J K
76 17 35 123 125 42 44 46 2 0 // L UP # { } * , . MENU
90 88 67 86 66 32 32 78 77 0 // Z X C V B N M
20 18 19 60 62 47 92 61 13 0 // LEFT DOWN RIGHT < > / \ = RUN
];
keymap-sy = [
37 49 50 51 52 53 54 55 56 0 // % 1 2 3 4 5 6 7 8
57 48 8 91 93 43 34 39 27 0 // 9 0 BKSP [ ] + " ' EXIT
9 113 119 101 114 116 121 117 105 0 // TAB q w e r t y u i
111 112 13 40 41 45 59 58 3 0 // o p ENTER ( ) - ; : STOP
0 97 115 100 102 103 104 106 107 0 // a s d f g h j k
108 17 35 123 125 42 44 46 2 0 // l UP # { } * , . MENU
122 120 99 118 98 32 32 110 109 0 // z x c v b n m
20 18 19 60 62 47 92 61 13 0 // LEFT DOWN RIGHT < > / \ = RUN
];
shift-row = <4>;
shift-col = <0>;
sym-row = <5>;
sym-col = <8>;
};
rtc: bm8563 { rtc: bm8563 {
compatible = "belling,bm8563"; compatible = "belling,bm8563";
reg = <0x51>; reg = <0x51>;

View File

@ -20,3 +20,7 @@ display.dpi=139
lvgl.colorDepth=8 lvgl.colorDepth=8
lvgl.theme=Mono lvgl.theme=Mono
lvgl.uiDensity=compact
lvgl.statusbarColorsInverted=true
cdn.warningMessage=Only hardware revision 0.2 and 0.3 are supported for now!

View File

@ -0,0 +1,156 @@
// SPDX-License-Identifier: Apache-2.0
#include "cl32_detect.h"
#include "cl32_v2_keyboard.h"
#include "cl32_v3_keyboard.h"
#include "cl32_v2.h"
#include "cl32_v3.h"
#include "cl32_v4.h"
#include "cl32_v4_keyboard.h"
#include "cl32_v4_power.h"
#include <tactility/device.h>
#include <tactility/device_listener.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/log.h>
#include <cstring>
constexpr auto* TAG = "cl32-detect";
// Revision 2 and 3 boards are both plain tca8418 keyboards (different physical key layouts) with
// no core chip. Revision 4 boards have the core chip (power + keyboard) at CL32_V4_CORE_I2C_ADDRESS.
static Cl32HardwareRevision cl32_revision = Cl32HardwareRevision::Unknown;
static Device cl32_v4_power_device {};
static bool cl32_v4_power_created = false;
// The probe-once latch for on_i2c0_started(). File-scope (not function-local) so
// cl32_teardown_devices() can reset it for a later start/probe cycle.
static bool did_probe = false;
static void create_v2_devices(Device* i2c0) {
cl32_create_keyboard(i2c0);
}
static void create_v3_devices(Device* i2c0) {
cl32_v3_create_keyboard(i2c0);
}
static bool create_v4_power_device(Device* i2c0) {
cl32_v4_power_device = Device { .address = 0, .name = "cl32-v4-power", .config = nullptr, .parent = nullptr, .flags = 0, .internal = nullptr };
error_t error = device_construct(&cl32_v4_power_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to construct cl32-v4-power: %s", error_to_string(error));
return false;
}
device_set_parent(&cl32_v4_power_device, i2c0);
device_set_driver(&cl32_v4_power_device, &cl32_v4_power_driver);
error = device_add(&cl32_v4_power_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to add cl32-v4-power: %s", error_to_string(error));
device_destruct(&cl32_v4_power_device);
return false;
}
error = device_start(&cl32_v4_power_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to start cl32-v4-power: %s", error_to_string(error));
device_remove(&cl32_v4_power_device);
device_destruct(&cl32_v4_power_device);
return false;
}
return true;
}
static void create_v4_devices(Device* i2c0) {
cl32_v4_create_keyboard(i2c0);
cl32_v4_power_created = create_v4_power_device(i2c0);
}
// Stops, removes and destructs cl32_v4_power_device if it was successfully created.
// cl32_v4_power_driver's own stop() tears down its power-supply child device.
static void destroy_v4_power_device() {
if (!cl32_v4_power_created) {
return;
}
device_stop(&cl32_v4_power_device);
device_remove(&cl32_v4_power_device);
device_destruct(&cl32_v4_power_device);
cl32_v4_power_created = false;
}
static Cl32HardwareRevision cl32_detect(Device* i2c0) {
uint8_t probe_value = 0;
if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, CL32_V4_KEYBOARD_PROBE_REGISTER, &probe_value, CL32_V4_CORE_TIMEOUT) == ERROR_NONE) {
LOG_I(TAG, "Detected V4 hardware by V4 keyboard presence");
return Cl32HardwareRevision::Revision4;
}
if (i2c_controller_has_device_at_address(i2c0, CL32_V3_FUEL_GAUGE_I2C_ADDRESS, CL32_V3_TIMEOUT) == ERROR_NONE) {
LOG_I(TAG, "Detected V3 hardware by MAX17048G fuel gauge presence");
return Cl32HardwareRevision::Revision3;
}
LOG_I(TAG, "No core chip or fuel gauge detected, assuming revision 2 hardware");
return Cl32HardwareRevision::Revision2;
}
// Fires for every device's start/stop in the system.
static void on_i2c0_started(Device* device, DeviceEvent event, void* context) {
(void)context;
if (did_probe || event != DEVICE_EVENT_STARTED || strcmp(device->name, "i2c0") != 0) {
return;
}
did_probe = true;
cl32_revision = cl32_detect(device);
switch (cl32_revision) {
case Cl32HardwareRevision::Revision2:
create_v2_devices(device);
break;
case Cl32HardwareRevision::Revision3:
create_v3_devices(device);
break;
case Cl32HardwareRevision::Revision4:
create_v4_devices(device);
break;
default:
LOG_W(TAG, "Unknown/unsupported hardware revision");
break;
}
}
Cl32HardwareRevision cl32_hardware_revision() {
return cl32_revision;
}
void cl32_teardown_devices() {
// Revision 2/3 keyboards bind the shared ti,tca8418 driver (owned by tca8418-module), so they
// don't block cl32_module's own driver destruction - torn down anyway for symmetry and so a
// later start can recreate them cleanly.
cl32_destroy_keyboard();
cl32_v3_destroy_keyboard();
// Revision 4 devices bind cl32-owned drivers (cl32_v4_keyboard_driver, cl32_v4_power_driver).
// Must be torn down before cl32_module's driver-removal loop runs, or driver_destruct() finds
// a started device still using the driver and fails.
cl32_v4_destroy_keyboard();
destroy_v4_power_device();
did_probe = false;
}
void cl32_power_detect_start() {
device_listener_add(on_i2c0_started, nullptr);
}
void cl32_power_detect_stop() {
device_listener_remove(on_i2c0_started);
}

View File

@ -0,0 +1,16 @@
#pragma once
enum class Cl32HardwareRevision {
Unknown,
Revision2,
Revision3,
Revision4,
};
Cl32HardwareRevision cl32_hardware_revision();
void cl32_power_detect_start();
void cl32_power_detect_stop();
/** Stops, removes and destructs the devices for the relevant hardware revision. */
void cl32_teardown_devices();

View File

@ -0,0 +1,4 @@
#pragma once
#include <cstdint>

View File

@ -0,0 +1,122 @@
// SPDX-License-Identifier: Apache-2.0
#include "cl32_v2_keyboard.h"
#include <drivers/tca8418.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/log.h>
constexpr auto* TAG = "cl32-keyboard";
// Keymaps and matrix layout, copied from the original cl32.dts "keyboard" node (removed once
// keyboard creation became conditional on revision-2 hardware detection).
static constexpr uint32_t KEYMAP_LC[80] = {
37, 49, 50, 51, 52, 53, 54, 55, 56, 0, // % 1 2 3 4 5 6 7 8
57, 48, 8, 91, 93, 43, 34, 39, 27, 0, // 9 0 BKSP [ ] + " ' EXIT
9, 113, 119, 101, 114, 116, 121, 117, 105, 0, // TAB q w e r t y u i
111, 112, 13, 40, 41, 45, 59, 58, 3, 0, // o p ENTER ( ) - ; : STOP
0, 97, 115, 100, 102, 103, 104, 106, 107, 0, // a s d f g h j k
108, 17, 35, 123, 125, 42, 44, 46, 2, 0, // l UP # { } * , . MENU
122, 120, 99, 118, 98, 32, 32, 110, 109, 0, // z x c v b n m
CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 13, 0, // LEFT DOWN RIGHT < > / \ = RUN
};
static constexpr uint32_t KEYMAP_UC[80] = {
37, 49, 50, 51, 52, 53, 54, 55, 56, 0, // % 1 2 3 4 5 6 7 8
57, 48, 8, 91, 93, 43, 34, 39, 27, 0, // 9 0 BKSP [ ] + " ' EXIT
9, 81, 87, 69, 82, 84, 89, 85, 73, 0, // TAB Q W E R T Y U I
79, 80, 13, 40, 41, 45, 59, 58, 3, 0, // O P ENTER ( ) - ; : STOP
0, 65, 83, 68, 70, 71, 72, 74, 75, 0, // A S D F G H J K
76, 17, 35, 123, 125, 42, 44, 46, 2, 0, // L UP # { } * , . MENU
90, 88, 67, 86, 66, 32, 32, 78, 77, 0, // Z X C V B N M
CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 13, 0, // LEFT DOWN RIGHT < > / \ = RUN
};
static constexpr uint32_t KEYMAP_SY[80] = {
37, 49, 50, 51, 52, 53, 54, 55, 56, 0, // % 1 2 3 4 5 6 7 8
57, 48, 8, 91, 93, 43, 34, 39, 27, 0, // 9 0 BKSP [ ] + " ' EXIT
9, 113, 119, 101, 114, 116, 121, 117, 105, 0, // TAB q w e r t y u i
111, 112, 13, 40, 41, 45, 59, 58, 3, 0, // o p ENTER ( ) - ; : STOP
0, 97, 115, 100, 102, 103, 104, 106, 107, 0, // a s d f g h j k
108, 17, 35, 123, 125, 42, 44, 46, 2, 0, // l UP # { } * , . MENU
122, 120, 99, 118, 98, 32, 32, 110, 109, 0, // z x c v b n m
CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ARROW_RIGHT, 60, 62, 47, 92, 61, 13, 0, // LEFT DOWN RIGHT < > / \ = RUN
};
static Tca8418Config cl32_keyboard_config {};
static Device cl32_keyboard_device {};
static bool cl32_keyboard_created = false;
bool cl32_create_keyboard(Device* i2c0) {
cl32_keyboard_config = Tca8418Config {
.address = 0x34,
.rows = 8,
.columns = 10,
.reverse_columns = false,
.keymap_lc = KEYMAP_LC,
.keymap_lc_length = sizeof(KEYMAP_LC) / sizeof(KEYMAP_LC[0]),
.keymap_uc = KEYMAP_UC,
.keymap_uc_length = sizeof(KEYMAP_UC) / sizeof(KEYMAP_UC[0]),
.keymap_sy = KEYMAP_SY,
.keymap_sy_length = sizeof(KEYMAP_SY) / sizeof(KEYMAP_SY[0]),
.shift_row = 4,
.shift_col = 0,
.sym_row = 5,
.sym_col = 8,
.pin_reset = GPIO_PIN_SPEC_NONE,
};
cl32_keyboard_device = Device {
.address = 0,
.name = "keyboard",
.config = &cl32_keyboard_config,
.parent = nullptr,
.flags = 0,
.internal = nullptr,
};
error_t error = device_construct(&cl32_keyboard_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to construct keyboard: %s", error_to_string(error));
return false;
}
device_set_parent(&cl32_keyboard_device, i2c0);
Driver* driver = driver_find_compatible("ti,tca8418");
if (driver == nullptr) {
LOG_E(TAG, "No driver registered for ti,tca8418");
device_destruct(&cl32_keyboard_device);
return false;
}
device_set_driver(&cl32_keyboard_device, driver);
error = device_add(&cl32_keyboard_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to add keyboard: %s", error_to_string(error));
device_destruct(&cl32_keyboard_device);
return false;
}
error = device_start(&cl32_keyboard_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to start keyboard: %s", error_to_string(error));
device_remove(&cl32_keyboard_device);
device_destruct(&cl32_keyboard_device);
return false;
}
cl32_keyboard_created = true;
return true;
}
void cl32_destroy_keyboard() {
if (!cl32_keyboard_created) {
return;
}
device_stop(&cl32_keyboard_device);
device_remove(&cl32_keyboard_device);
device_destruct(&cl32_keyboard_device);
cl32_keyboard_created = false;
}

View File

@ -0,0 +1,8 @@
#pragma once
#include <tactility/device.h>
/** @return true if the device was successfully constructed, added and started. */
bool cl32_create_keyboard(struct Device* i2c0);
void cl32_destroy_keyboard();

View File

@ -0,0 +1,8 @@
#pragma once
#include <tactility/freertos/freertos.h>
#include <cstdint>
constexpr uint8_t CL32_V3_FUEL_GAUGE_I2C_ADDRESS = 0x36;
constexpr TickType_t CL32_V3_TIMEOUT = pdMS_TO_TICKS(50);

View File

@ -0,0 +1,121 @@
// SPDX-License-Identifier: Apache-2.0
#include "cl32_v3_keyboard.h"
#include <drivers/tca8418.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gpio.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/log.h>
constexpr auto* TAG = "cl32-v3-keyboard";
// Reference: https://github.com/CL-32/CL-32/blob/8618d991eb6d9cf291eccb8400e94f303420cece/Software/CL-32/lib/CL32/src/CL32_keyboard.cpp
// Keymaps and matrix layout, transcribed from CL-32/CL-32's CL32_keyboard.cpp CL32_keyboard::_matrix
// (same tca8418 chip/wiring as cl32_v2_keyboard.cpp, different physical key layout).
static constexpr uint32_t KEYMAP_LC[70] = {
CODEPOINT_TAB, 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 0, // TAB q w e r t y u i
0, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 0, // SHIFT a s d f g h j k
0, 'z', 'x', 'c', ' ', ' ', 'v', 'b', 'n', 0, // FN z x c (space)(space) v b n
'=', '.', '0', '+', CODEPOINT_ARROW_UP, CODEPOINT_ARROW_RIGHT, CODEPOINT_ENTER, CODEPOINT_ENTER, 'm', 0, // = . 0 + UP RIGHT ENTER ENTER m
'3', '2', '1', '-', CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ESCAPE, CODEPOINT_ENTER, 'l', 0, // 3 2 1 - LEFT DOWN ESC ENTER l
'6', '5', '4', '*', 67, 68, 8, 'p', 'o', 0, // 6 5 4 * FILE MENU BKSP p o
'9', '8', '7', '/', 0, 0, 0, 0, 0, 0, // 9 8 7 /
};
static constexpr uint32_t KEYMAP_UC[70] = {
CODEPOINT_TAB, 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 0,
0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 0,
0, 'Z', 'X', 'C', ' ', ' ', 'V', 'B', 'N', 0,
'=', '.', '0', '+', CODEPOINT_ARROW_UP, CODEPOINT_ARROW_RIGHT, CODEPOINT_ENTER, CODEPOINT_ENTER, 'M', 0,
'3', '2', '1', '-', CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ESCAPE, CODEPOINT_ENTER, 'L', 0,
'6', '5', '4', '*', 67, 68, 8, 'P', 'O', 0,
'9', '8', '7', '/', 0, 0, 0, 0, 0, 0,
};
static constexpr uint32_t KEYMAP_SY[70] = {
CODEPOINT_TAB, '!', '"', '@', 0xA3 /* GBP sign */, '$', '%', '^', '&', 0,
0, '|', '#', ',', '?', '\'', ':', ';', '{', 0,
0, ' ', '\\', '~', ' ', ' ', '<', '>', '[', 0,
'=', '.', '0', '+', CODEPOINT_ARROW_UP, CODEPOINT_ARROW_RIGHT, CODEPOINT_ENTER, CODEPOINT_ENTER, ']', 0,
'3', '2', '1', '-', CODEPOINT_ARROW_LEFT, CODEPOINT_ARROW_DOWN, CODEPOINT_ESCAPE, CODEPOINT_ENTER, '}', 0,
'6', '5', '4', '*', 67, 68, 8, ')', '(', 0,
'9', '8', '7', '/', 0, 0, 0, 0, 0, 0,
};
static Tca8418Config cl32_v3_keyboard_config {};
static Device cl32_v3_keyboard_device {};
static bool cl32_v3_keyboard_created = false;
bool cl32_v3_create_keyboard(Device* i2c0) {
cl32_v3_keyboard_config = Tca8418Config {
.address = 0x34,
.rows = 7,
.columns = 10,
.reverse_columns = false,
.keymap_lc = KEYMAP_LC,
.keymap_lc_length = sizeof(KEYMAP_LC) / sizeof(KEYMAP_LC[0]),
.keymap_uc = KEYMAP_UC,
.keymap_uc_length = sizeof(KEYMAP_UC) / sizeof(KEYMAP_UC[0]),
.keymap_sy = KEYMAP_SY,
.keymap_sy_length = sizeof(KEYMAP_SY) / sizeof(KEYMAP_SY[0]),
.shift_row = 1,
.shift_col = 0,
.sym_row = 2,
.sym_col = 0,
.pin_reset = GPIO_PIN_SPEC_NONE,
};
cl32_v3_keyboard_device = Device {
.address = 0,
.name = "cl32-v3-keyboard",
.config = &cl32_v3_keyboard_config,
.parent = nullptr,
.flags = 0,
.internal = nullptr,
};
error_t error = device_construct(&cl32_v3_keyboard_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to construct keyboard: %s", error_to_string(error));
return false;
}
device_set_parent(&cl32_v3_keyboard_device, i2c0);
Driver* driver = driver_find_compatible("ti,tca8418");
if (driver == nullptr) {
LOG_E(TAG, "No driver registered for ti,tca8418");
device_destruct(&cl32_v3_keyboard_device);
return false;
}
device_set_driver(&cl32_v3_keyboard_device, driver);
error = device_add(&cl32_v3_keyboard_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to add keyboard: %s", error_to_string(error));
device_destruct(&cl32_v3_keyboard_device);
return false;
}
error = device_start(&cl32_v3_keyboard_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to start keyboard: %s", error_to_string(error));
device_remove(&cl32_v3_keyboard_device);
device_destruct(&cl32_v3_keyboard_device);
return false;
}
cl32_v3_keyboard_created = true;
return true;
}
void cl32_v3_destroy_keyboard() {
if (!cl32_v3_keyboard_created) {
return;
}
device_stop(&cl32_v3_keyboard_device);
device_remove(&cl32_v3_keyboard_device);
device_destruct(&cl32_v3_keyboard_device);
cl32_v3_keyboard_created = false;
}

View File

@ -0,0 +1,8 @@
#pragma once
#include <tactility/device.h>
/** @return true if the device was successfully constructed, added and started. */
bool cl32_v3_create_keyboard(struct Device* i2c0);
void cl32_v3_destroy_keyboard();

View File

@ -0,0 +1,9 @@
#pragma once
#include <tactility/freertos/freertos.h>
#include <cstdint>
constexpr uint8_t CL32_V4_KEYBOARD_PROBE_REGISTER = 0x02;
constexpr uint8_t CL32_V4_CORE_I2C_ADDRESS = 0x08;
constexpr TickType_t CL32_V4_CORE_TIMEOUT = pdMS_TO_TICKS(50);

View File

@ -0,0 +1,364 @@
// SPDX-License-Identifier: Apache-2.0
#include "cl32_v4.h"
#include "cl32_v4_keyboard.h"
#include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <new>
extern Module cl32_module;
constexpr auto* TAG = "cl32-v4-keyboard";
// v4's core chip reports keyboard events on the same I2C address as the power-supply function
// (see cl32_v4_power.h) - CL-32/CL-32's CL32_core class talks to both over one address.
// Registers below come from that project's regMap.h (CL32_INTERRUPT/EVENT_COUNT/EVENT1).
constexpr uint8_t REG_INTERRUPT = 0x02; // bit0: keyboard event pending
constexpr uint8_t REG_EVENT_COUNT = 0x03;
constexpr uint8_t REG_EVENT1 = 0x04; // reading this register pops one event off the chip's FIFO
constexpr uint8_t INTERRUPT_BIT_KEYBOARD = 0x01;
// Upper bound on events drained per read_key() call, same purpose as tca8418.cpp's equivalent:
// caps the damage if the chip ever reports a count that never reaches 0.
constexpr uint8_t MAX_EVENTS_PER_DRAIN = 16;
// HID Keyboard/Keypad usage codes (USB HID Usage Tables page 0x07) this chip's matrix produces,
// named after CL-32/CL-32's CL32_core::_matrix keymap (CL32_core.cpp).
constexpr uint8_t KEY_FN = 0x3A; // repurposed F1 position
constexpr uint8_t KEY_FILE = 0x43; // repurposed F10 position
constexpr uint8_t KEY_MENU = 0x44; // repurposed F11 position
constexpr uint8_t KEY_ENTER = 0x28;
constexpr uint8_t KEY_ESCAPE = 0x29;
constexpr uint8_t KEY_BACKSPACE = 0x2A;
constexpr uint8_t KEY_SPACE = 0x2C;
constexpr uint8_t KEY_EQUALS = 0x2E;
constexpr uint8_t KEY_PERIOD = 0x37;
constexpr uint8_t KEY_ARROW_RIGHT = 0x4F;
constexpr uint8_t KEY_ARROW_LEFT = 0x50;
constexpr uint8_t KEY_ARROW_DOWN = 0x51;
constexpr uint8_t KEY_ARROW_UP = 0x52;
constexpr uint8_t KEYPAD_SLASH = 0x54;
constexpr uint8_t KEYPAD_ASTERISK = 0x55;
constexpr uint8_t KEYPAD_MINUS = 0x56;
constexpr uint8_t KEYPAD_PLUS = 0x57;
constexpr uint8_t KEYPAD_ENTER = 0x58;
constexpr uint8_t KEYPAD_1 = 0x59;
constexpr uint8_t KEYPAD_9 = 0x61;
constexpr uint8_t KEYPAD_0 = 0x62;
constexpr uint8_t KEYPAD_PERIOD = 0x63;
constexpr uint8_t KEY_SHIFT = 0xE1;
enum class ToggleState { Unpressed, OnePress, Locked };
static ToggleState next_toggle_state(ToggleState state) {
switch (state) {
case ToggleState::Unpressed: return ToggleState::OnePress;
case ToggleState::OnePress: return ToggleState::Locked;
case ToggleState::Locked: return ToggleState::Unpressed;
}
return ToggleState::Unpressed;
}
static const char* toggle_state_name(ToggleState state) {
switch (state) {
case ToggleState::Unpressed: return "unpressed";
case ToggleState::OnePress: return "one-press";
case ToggleState::Locked: return "locked";
}
return "?";
}
constexpr uint8_t PENDING_CAPACITY = 32;
struct Cl32V4KeyEvent {
uint32_t key;
bool pressed;
uint8_t hid_keycode;
uint8_t hid_modifier;
};
struct Cl32V4KeyboardInternal {
ToggleState shift_state = ToggleState::Unpressed;
ToggleState fn_state = ToggleState::Unpressed;
Cl32V4KeyEvent pending[PENDING_CAPACITY];
uint8_t pending_head = 0;
uint8_t pending_count = 0;
};
static void push_pending(Cl32V4KeyboardInternal* internal, uint32_t key, bool pressed, uint8_t hid_keycode, uint8_t hid_modifier) {
if (internal->pending_count >= PENDING_CAPACITY) {
LOG_W(TAG, "Pending event queue full, dropping event");
return;
}
uint8_t tail = (internal->pending_head + internal->pending_count) % PENDING_CAPACITY;
internal->pending[tail] = { key, pressed, hid_keycode, hid_modifier };
internal->pending_count++;
}
static bool pop_pending(Cl32V4KeyboardInternal* internal, Cl32V4KeyEvent* out_event) {
if (internal->pending_count == 0) {
return false;
}
*out_event = internal->pending[internal->pending_head];
internal->pending_head = (internal->pending_head + 1) % PENDING_CAPACITY;
internal->pending_count--;
return true;
}
// Maps a HID code to a Unicode codepoint, mirroring CL32_core::_matrix. Fn selects the symbol
// layer (that table's third column); Shift selects uppercase. Returns false for a code with no
// Unicode/LVGL representation (Page Up/Down) - callers report KeyboardKeyData::hid_keycode
// instead, same as tab5_keyboard.cpp's F1-F12 handling.
static bool translate_key(uint8_t code, bool shift, bool fn, uint32_t* out_key) {
if (code >= 0x04 && code <= 0x1D) {
if (fn) {
static constexpr uint32_t FN_SYMBOLS[26] = {
'|', '>', '~', ',', '@', '?', '\'', ':', '&', ';', '{', '}', ']', '[',
'(', ')', '!', 0x00A3 /* GBP sign */, '#', '$', '^', '<', '"', '\\', '%', 0,
};
uint32_t symbol = FN_SYMBOLS[code - 0x04];
if (symbol != 0) {
*out_key = symbol;
return true;
}
// 'z' has no symbol layer in the reference matrix - fall through to the plain letter.
}
uint32_t c = 'a' + (code - 0x04);
*out_key = shift ? (c - 0x20) : c;
return true;
}
if (fn) {
switch (code) {
case KEY_ARROW_RIGHT: *out_key = CODEPOINT_END; return true;
case KEY_ARROW_LEFT: *out_key = CODEPOINT_HOME; return true;
case KEY_ARROW_DOWN: return false; // Page Down - hid_keycode only
case KEY_ARROW_UP: return false; // Page Up - hid_keycode only
default: break;
}
}
switch (code) {
case KEY_ENTER: *out_key = CODEPOINT_ENTER; return true;
case KEY_ESCAPE: *out_key = CODEPOINT_ESCAPE; return true;
case KEY_BACKSPACE: *out_key = CODEPOINT_BACKSPACE; return true;
case KEY_SPACE: *out_key = ' '; return true;
case KEY_EQUALS: *out_key = '='; return true;
case KEY_PERIOD: *out_key = '.'; return true;
case KEY_ARROW_RIGHT: *out_key = CODEPOINT_ARROW_RIGHT; return true;
case KEY_ARROW_LEFT: *out_key = CODEPOINT_ARROW_LEFT; return true;
case KEY_ARROW_DOWN: *out_key = CODEPOINT_ARROW_DOWN; return true;
case KEY_ARROW_UP: *out_key = CODEPOINT_ARROW_UP; return true;
case KEYPAD_SLASH: *out_key = '/'; return true;
case KEYPAD_ASTERISK: *out_key = '*'; return true;
case KEYPAD_MINUS: *out_key = '-'; return true;
case KEYPAD_PLUS: *out_key = '+'; return true;
case KEYPAD_ENTER: *out_key = CODEPOINT_ENTER; return true;
case KEYPAD_0: *out_key = '0'; return true;
case KEYPAD_PERIOD: *out_key = '.'; return true;
default:
if (code >= KEYPAD_1 && code <= KEYPAD_9) {
*out_key = static_cast<uint32_t>('1' + (code - KEYPAD_1));
return true;
}
return false;
}
}
static void handle_key_down(Cl32V4KeyboardInternal* internal, uint8_t code) {
if (code == KEY_SHIFT) {
internal->shift_state = next_toggle_state(internal->shift_state);
LOG_I(TAG, "code=0x%02X shift -> %s", code, toggle_state_name(internal->shift_state));
return;
}
if (code == KEY_FN) {
internal->fn_state = next_toggle_state(internal->fn_state);
LOG_I(TAG, "code=0x%02X fn -> %s", code, toggle_state_name(internal->fn_state));
return;
}
const bool shift = internal->shift_state != ToggleState::Unpressed;
const uint8_t hid_modifier = shift ? 0x02 : 0x00; // bit1 = LeftShift
// No app-level menu system at the driver layer - surface the raw HID code only, same as
// tab5_keyboard.cpp's F1-F12 handling.
if (code == KEY_MENU || code == KEY_FILE) {
LOG_I(TAG, "code=0x%02X (menu/file) shift=%s fn=%s hid_modifier=0x%02X",
code, toggle_state_name(internal->shift_state), toggle_state_name(internal->fn_state), hid_modifier);
push_pending(internal, 0, true, code, hid_modifier);
push_pending(internal, 0, false, code, hid_modifier);
return;
}
const bool fn = internal->fn_state != ToggleState::Unpressed;
uint32_t key = 0;
const bool has_key = translate_key(code, shift, fn, &key);
const char printable = (has_key && key >= 0x20 && key < 0x7F) ? static_cast<char>(key) : '.';
LOG_I(TAG, "code=0x%02X key='%c' key_hex=0x%04X shift=%s fn=%s hid_modifier=0x%02X",
code, printable, static_cast<unsigned int>(has_key ? key : 0),
toggle_state_name(internal->shift_state), toggle_state_name(internal->fn_state), hid_modifier);
// LVGL only registers a key on a RELEASED->PRESSED edge, and this chip doesn't report
// reliable release events for character keys - queue an immediate press+release pair, same as
// tca8418.cpp.
push_pending(internal, has_key ? key : 0, true, code, hid_modifier);
push_pending(internal, has_key ? key : 0, false, code, hid_modifier);
if (internal->shift_state == ToggleState::OnePress) {
internal->shift_state = ToggleState::Unpressed;
}
if (internal->fn_state == ToggleState::OnePress) {
internal->fn_state = ToggleState::Unpressed;
}
}
static void drain_events(Device* i2c0, Cl32V4KeyboardInternal* internal) {
uint8_t interrupt_status = 0;
if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_INTERRUPT, &interrupt_status, CL32_V4_CORE_TIMEOUT) != ERROR_NONE) {
return;
}
if ((interrupt_status & INTERRUPT_BIT_KEYBOARD) == 0) {
return;
}
uint8_t count = 0;
if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_EVENT_COUNT, &count, CL32_V4_CORE_TIMEOUT) != ERROR_NONE) {
return;
}
for (uint8_t drained = 0; drained < MAX_EVENTS_PER_DRAIN && count > 0; drained++) {
uint8_t raw = 0;
if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_EVENT1, &raw, CL32_V4_CORE_TIMEOUT) != ERROR_NONE) {
break;
}
LOG_I(TAG, "raw event=0x%02X (%s)", raw, (raw & 0x80) != 0 ? "down" : "up");
if ((raw & 0x80) != 0) {
handle_key_down(internal, static_cast<uint8_t>(raw & 0x7F));
}
if (i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_EVENT_COUNT, &count, CL32_V4_CORE_TIMEOUT) != ERROR_NONE) {
break;
}
}
// Only the keyboard bit is ours to clear - the power-supply function on this same chip owns
// the other status bits in this register.
i2c_controller_register8_reset_bits(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_INTERRUPT, INTERRUPT_BIT_KEYBOARD, CL32_V4_CORE_TIMEOUT);
}
static error_t v3_read_key(Device* device, KeyboardKeyData* data) {
auto* internal = static_cast<Cl32V4KeyboardInternal*>(device_get_driver_data(device));
Cl32V4KeyEvent event;
if (internal->pending_count == 0) {
drain_events(device_get_parent(device), internal);
}
if (pop_pending(internal, &event)) {
data->key = event.key;
data->pressed = event.pressed;
data->continue_reading = internal->pending_count > 0;
data->ctrl = false;
data->alt = false;
data->hid_keycode = event.hid_keycode;
data->hid_modifier = event.hid_modifier;
} else {
data->key = 0;
data->pressed = false;
data->continue_reading = false;
data->ctrl = false;
data->alt = false;
data->hid_keycode = 0;
data->hid_modifier = 0;
}
return ERROR_NONE;
}
static constexpr KeyboardApi cl32_v4_keyboard_api = {
.read_key = v3_read_key,
.get_backlight = nullptr,
.is_present = nullptr,
};
static error_t start(Device* device) {
if (device_get_type(device_get_parent(device)) != &I2C_CONTROLLER_TYPE) {
LOG_E(TAG, "Parent is not an I2C controller");
return ERROR_RESOURCE;
}
auto* internal = new(std::nothrow) Cl32V4KeyboardInternal();
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<Cl32V4KeyboardInternal*>(device_get_driver_data(device));
delete internal;
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
Driver cl32_v4_keyboard_driver = {
.name = "cl32-v4-keyboard",
.compatible = (const char*[]) { "cl32-v4-keyboard", nullptr },
.start_device = start,
.stop_device = stop,
.api = &cl32_v4_keyboard_api,
.device_type = &KEYBOARD_TYPE,
.owner = &cl32_module,
.internal = nullptr
};
static Device cl32_v4_keyboard_device {};
static bool cl32_v4_keyboard_created = false;
bool cl32_v4_create_keyboard(Device* i2c0) {
cl32_v4_keyboard_device = Device { .address = 0, .name = "cl32-v4-keyboard", .config = nullptr, .parent = nullptr, .flags = 0, .internal = nullptr };
error_t error = device_construct(&cl32_v4_keyboard_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to construct keyboard: %s", error_to_string(error));
return false;
}
device_set_parent(&cl32_v4_keyboard_device, i2c0);
device_set_driver(&cl32_v4_keyboard_device, &cl32_v4_keyboard_driver);
error = device_add(&cl32_v4_keyboard_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to add keyboard: %s", error_to_string(error));
device_destruct(&cl32_v4_keyboard_device);
return false;
}
error = device_start(&cl32_v4_keyboard_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to start keyboard: %s", error_to_string(error));
device_remove(&cl32_v4_keyboard_device);
device_destruct(&cl32_v4_keyboard_device);
return false;
}
cl32_v4_keyboard_created = true;
return true;
}
void cl32_v4_destroy_keyboard() {
if (!cl32_v4_keyboard_created) {
return;
}
device_stop(&cl32_v4_keyboard_device);
device_remove(&cl32_v4_keyboard_device);
device_destruct(&cl32_v4_keyboard_device);
cl32_v4_keyboard_created = false;
}

View File

@ -0,0 +1,11 @@
#pragma once
#include <tactility/device.h>
#include <tactility/driver.h>
extern struct Driver cl32_v4_keyboard_driver;
/** @return true if the device was successfully constructed, added and started. */
bool cl32_v4_create_keyboard(struct Device* i2c0);
void cl32_v4_destroy_keyboard();

View File

@ -0,0 +1,186 @@
// SPDX-License-Identifier: Apache-2.0
#include "cl32_v4_power.h"
#include "cl32_v4.h"
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/i2c_controller.h>
#include <tactility/drivers/power_supply.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <new>
extern Module cl32_module;
constexpr auto* TAG = "cl32-v4-power";
constexpr uint8_t REG_VOLTAGE = 0x14;
constexpr int MV_PER_LSB = 25;
constexpr int MIN_MV = 3200;
constexpr int MAX_MV = 4200;
// ---------------------------------------------------------------------------
// Power supply
// ---------------------------------------------------------------------------
static bool ps_supports_property(Device*, PowerSupplyProperty property) {
return property == POWER_SUPPLY_PROP_VOLTAGE || property == POWER_SUPPLY_PROP_CAPACITY;
}
static error_t read_battery_mv(Device* chip_device, int* out_mv) {
auto* i2c0 = device_get_parent(chip_device);
uint8_t raw = 0;
error_t error = i2c_controller_register8_get(i2c0, CL32_V4_CORE_I2C_ADDRESS, REG_VOLTAGE, &raw, CL32_V4_CORE_TIMEOUT);
if (error != ERROR_NONE) {
return error;
}
*out_mv = raw * MV_PER_LSB;
return ERROR_NONE;
}
static int estimate_capacity_from_mv(int battery_mv) {
if (battery_mv <= MIN_MV) return 0;
if (battery_mv >= MAX_MV) return 100;
return (battery_mv - MIN_MV) * 100 / (MAX_MV - MIN_MV);
}
static error_t ps_get_property(Device* device, PowerSupplyProperty property, PowerSupplyPropertyValue* out_value) {
if (property != POWER_SUPPLY_PROP_VOLTAGE && property != POWER_SUPPLY_PROP_CAPACITY) {
return ERROR_NOT_SUPPORTED;
}
int battery_mv;
error_t error = read_battery_mv(device_get_parent(device), &battery_mv);
if (error != ERROR_NONE) {
return error;
}
out_value->int_value = (property == POWER_SUPPLY_PROP_VOLTAGE) ? battery_mv : estimate_capacity_from_mv(battery_mv);
return ERROR_NONE;
}
static bool ps_supports_charge_control(Device*) { return false; }
static bool ps_is_allowed_to_charge(Device*) { return false; }
static error_t ps_set_allowed_to_charge(Device*, bool) { return ERROR_NOT_SUPPORTED; }
static bool ps_supports_quick_charge(Device*) { return false; }
static bool ps_is_quick_charge_enabled(Device*) { return false; }
static error_t ps_set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; }
static bool ps_supports_power_off(Device*) { return false; }
static error_t ps_power_off(Device*) { return ERROR_NOT_SUPPORTED; }
static constexpr PowerSupplyApi CL32_V4_POWER_SUPPLY_API = {
.supports_property = ps_supports_property,
.get_property = ps_get_property,
.supports_charge_control = ps_supports_charge_control,
.is_allowed_to_charge = ps_is_allowed_to_charge,
.set_allowed_to_charge = ps_set_allowed_to_charge,
.supports_quick_charge = ps_supports_quick_charge,
.is_quick_charge_enabled = ps_is_quick_charge_enabled,
.set_quick_charge_enabled = ps_set_quick_charge_enabled,
.supports_power_off = ps_supports_power_off,
.power_off = ps_power_off,
};
// Registered in cl32_module's driver list so driver_bind() has a valid ->internal, but never
// matched against a devicetree node: wired up directly by pointer from cl32_v4_power_driver's start().
Driver cl32_v4_power_supply_driver = {
.name = "cl32-v4-power-supply",
.compatible = (const char*[]) { "cl32-v4-power-supply", nullptr },
.start_device = nullptr,
.stop_device = nullptr,
.api = &CL32_V4_POWER_SUPPLY_API,
.device_type = &POWER_SUPPLY_TYPE,
.owner = &cl32_module,
.internal = nullptr
};
static error_t create_power_supply_child(Device* parent, Device*& out_child) {
auto* child = new(std::nothrow) Device { .address = 0, .name = "cl32-v4-power-supply", .config = nullptr, .parent = nullptr, .flags = 0, .internal = nullptr };
if (child == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
error_t error = device_construct(child);
if (error != ERROR_NONE) {
delete child;
return error;
}
device_set_parent(child, parent);
device_set_driver(child, &cl32_v4_power_supply_driver);
error = device_add(child);
if (error != ERROR_NONE) {
device_destruct(child);
delete child;
return error;
}
error = device_start(child);
if (error != ERROR_NONE) {
device_remove(child);
device_destruct(child);
delete child;
return error;
}
out_child = child;
return ERROR_NONE;
}
static void destroy_power_supply_child(Device* child) {
check(device_stop(child) == ERROR_NONE);
check(device_remove(child) == ERROR_NONE);
check(device_destruct(child) == ERROR_NONE);
delete child;
}
// ---------------------------------------------------------------------------
// Chip driver
// ---------------------------------------------------------------------------
struct Cl32V4PowerInternal {
Device* power_supply_device = nullptr;
};
static error_t start(Device* device) {
if (device_get_type(device_get_parent(device)) != &I2C_CONTROLLER_TYPE) {
LOG_E(TAG, "Parent is not an I2C controller");
return ERROR_RESOURCE;
}
auto* internal = new(std::nothrow) Cl32V4PowerInternal();
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
error_t error = create_power_supply_child(device, internal->power_supply_device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to create power-supply device");
delete internal;
return error;
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<Cl32V4PowerInternal*>(device_get_driver_data(device));
destroy_power_supply_child(internal->power_supply_device);
device_set_driver_data(device, nullptr);
delete internal;
return ERROR_NONE;
}
Driver cl32_v4_power_driver = {
.name = "cl32-v4-power",
.compatible = (const char*[]) { "cl32-v4-power", nullptr },
.start_device = start,
.stop_device = stop,
.api = nullptr,
.device_type = nullptr,
.owner = &cl32_module,
.internal = nullptr
};

View File

@ -0,0 +1,6 @@
#pragma once
#include <tactility/driver.h>
extern struct Driver cl32_v4_power_driver;
extern struct Driver cl32_v4_power_supply_driver;

View File

@ -1,9 +1,36 @@
#include <tactility/module.h> #include <tactility/module.h>
#include "cl32_detect.h"
#include "cl32_v4_keyboard.h"
#include "cl32_v4_power.h"
extern "C" { extern "C" {
struct Module cl32_module = { static error_t start() {
.name = "cl32" cl32_power_detect_start();
return ERROR_NONE;
}
static error_t stop() {
cl32_power_detect_stop();
cl32_teardown_devices();
return ERROR_NONE;
}
static Driver* const cl32_drivers[] = {
&cl32_v4_power_driver,
&cl32_v4_power_supply_driver,
&cl32_v4_keyboard_driver,
nullptr
};
Module cl32_module = {
.name = "cl32",
.start = start,
.stop = stop,
.drivers = cl32_drivers,
.symbols = nullptr,
.internal = nullptr
}; };
} }

View File

@ -9,6 +9,6 @@
gpio0 { gpio0 {
compatible = "espressif,esp32-gpio"; compatible = "espressif,esp32-gpio";
gpio-count = <57>; gpio-count = <55>;
}; };
}; };

View File

@ -38,7 +38,7 @@
gpio0 { gpio0 {
compatible = "espressif,esp32-gpio"; compatible = "espressif,esp32-gpio";
gpio-count = <57>; gpio-count = <55>;
}; };
i2c_internal { i2c_internal {

View File

@ -22,5 +22,4 @@ cdn.infoMessage=Due to the small size of the screen, the icons don't render prop
lvgl.theme=Mono lvgl.theme=Mono
lvgl.colorDepth=16 lvgl.colorDepth=16
lvgl.uiScale=70
lvgl.uiDensity=compact lvgl.uiDensity=compact

View File

@ -26,6 +26,7 @@ lvgl.colorDepth=8
# Monochrome theme: the default colour theme renders accent-coloured UI that # Monochrome theme: the default colour theme renders accent-coloured UI that
# thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_THEME_MONO). # thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_THEME_MONO).
lvgl.theme=Mono lvgl.theme=Mono
lvgl.statusbarColorsInverted=true
cdn.warningMessage=Display not reliably working. Use at your own risk. cdn.warningMessage=Display not reliably working. Use at your own risk.

View File

@ -25,6 +25,7 @@ lvgl.colorDepth=8
# Monochrome theme: the default colour theme renders accent-coloured UI that # Monochrome theme: the default colour theme renders accent-coloured UI that
# thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_THEME_MONO). # thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_THEME_MONO).
lvgl.theme=Mono lvgl.theme=Mono
lvgl.statusbarColorsInverted=true
cdn.warningMessage=Only for hardware revision 1.0! Display not reliably working. Use at your own risk. cdn.warningMessage=Only for hardware revision 1.0! Display not reliably working. Use at your own risk.

View File

@ -22,6 +22,7 @@ display.dpi=235
lvgl.colorDepth=8 lvgl.colorDepth=8
lvgl.fontSize=24 lvgl.fontSize=24
lvgl.theme=Mono lvgl.theme=Mono
lvgl.statusbarColorsInverted=true
sdkconfig.CONFIG_EPD_DISPLAY_TYPE_ED047TC2=y sdkconfig.CONFIG_EPD_DISPLAY_TYPE_ED047TC2=y
sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0 sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0

View File

@ -54,6 +54,7 @@ static void create_gt911_touch(Device* i2c0) {
.name = "touch0", .name = "touch0",
.config = nullptr, .config = nullptr,
.parent = nullptr, .parent = nullptr,
.flags = {},
.internal = nullptr, .internal = nullptr,
}; };
@ -86,6 +87,7 @@ void tab5_create_devices_v1(Device* i2c0) {
.name = "display0", .name = "display0",
.config = nullptr, .config = nullptr,
.parent = nullptr, .parent = nullptr,
.flags = {},
.internal = nullptr, .internal = nullptr,
}; };

View File

@ -25,6 +25,7 @@ void tab5_create_devices_v2(Device* i2c0) {
.name = "display0", .name = "display0",
.config = nullptr, .config = nullptr,
.parent = nullptr, .parent = nullptr,
.flags = {},
.internal = nullptr, .internal = nullptr,
}; };

View File

@ -19,6 +19,7 @@ void create_st7123_touch(Device* i2c0) {
.name = "touch0", .name = "touch0",
.config = nullptr, .config = nullptr,
.parent = nullptr, .parent = nullptr,
.flags = {},
.internal = nullptr, .internal = nullptr,
}; };

View File

@ -27,6 +27,7 @@ void tab5_create_devices_v3(Device* i2c0) {
.name = "display0", .name = "display0",
.config = nullptr, .config = nullptr,
.parent = nullptr, .parent = nullptr,
.flags = {},
.internal = nullptr, .internal = nullptr,
}; };

View File

@ -25,6 +25,7 @@ static void tab5_create_power_control(Device* io_expander1) {
.name = "power_control0", .name = "power_control0",
.config = nullptr, .config = nullptr,
.parent = nullptr, .parent = nullptr,
.flags = {},
.internal = nullptr, .internal = nullptr,
}; };

View File

@ -658,6 +658,7 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
static const KeyboardApi tab5_keyboard_api = { static const KeyboardApi tab5_keyboard_api = {
.read_key = tab5_keyboard_read_key, .read_key = tab5_keyboard_read_key,
.get_backlight = nullptr,
.is_present = tab5_keyboard_is_attached, .is_present = tab5_keyboard_is_attached,
}; };
@ -693,6 +694,7 @@ void tab5_create_keyboard(Device* i2c2) {
.name = "keyboard0", .name = "keyboard0",
.config = nullptr, .config = nullptr,
.parent = nullptr, .parent = nullptr,
.flags = {},
.internal = nullptr, .internal = nullptr,
}; };

View File

@ -37,7 +37,7 @@
gpio0 { gpio0 {
compatible = "espressif,esp32-gpio"; compatible = "espressif,esp32-gpio";
gpio-count = <57>; gpio-count = <55>;
}; };
display_backlight_pwm { display_backlight_pwm {

View File

@ -63,3 +63,13 @@ properties:
so LVGL renders in rotated space and the driver rotates the 1bpp frame so LVGL renders in rotated space and the driver rotates the 1bpp frame
back to the panel's native layout before updating. Not changeable at back to the panel's native layout before updating. Not changeable at
runtime. runtime.
mirror-x:
type: boolean
default: false
description: >
Mirror the panel horizontally. Applied once at start via the underlying
esp_epaper component's epd_init(); not changeable at runtime.
mirror-y:
type: boolean
default: false
description: Mirror the panel vertically. Same fixed-at-start behavior as mirror-x.

View File

@ -5,6 +5,7 @@
extern "C" { extern "C" {
#endif #endif
#include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <epaper_config.h> #include <epaper_config.h>
@ -37,6 +38,11 @@ struct EspEpaperConfig {
* counter-clockwise (LV_DISPLAY_ROTATION_*). Not changeable at runtime. * counter-clockwise (LV_DISPLAY_ROTATION_*). Not changeable at runtime.
*/ */
uint8_t rotation; uint8_t rotation;
/** Mirror the panel horizontally. Fixed at start; the underlying esp_epaper component applies
* it at epd_init() time with no runtime setter. */
bool mirror_x;
/** Mirror the panel vertically. Fixed at start, same as mirror_x. */
bool mirror_y;
}; };
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -33,6 +33,9 @@ struct EspEpaperInternal {
epd_panel_info_t info; epd_panel_info_t info;
/** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */ /** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */
uint8_t* rotate_buffer; uint8_t* rotate_buffer;
/** Copy of the last frame sent to epd_update(), in native panel layout - refresh() resends
* this at EPD_UPDATE_FULL to clear ghosting without new content from LVGL. */
uint8_t* last_frame_buffer;
/** Serializes panel/SPI access between draw_bitmap and power state changes. */ /** Serializes panel/SPI access between draw_bitmap and power state changes. */
SemaphoreHandle_t panel_mutex; SemaphoreHandle_t panel_mutex;
/** disp_on_off state; the panel is in deep sleep while false. */ /** disp_on_off state; the panel is in deep sleep while false. */
@ -138,6 +141,8 @@ static error_t esp_epaper_draw_bitmap(Device* device, int32_t x_start, int32_t y
source = internal->rotate_buffer; source = internal->rotate_buffer;
} }
memcpy(internal->last_frame_buffer, source, internal->info.buffer_size);
spi_controller_lock(internal->spi_controller); spi_controller_lock(internal->spi_controller);
const esp_err_t ret = epd_update(internal->epd, source, config->update_mode); const esp_err_t ret = epd_update(internal->epd, source, config->update_mode);
spi_controller_unlock(internal->spi_controller); spi_controller_unlock(internal->spi_controller);
@ -149,6 +154,58 @@ static error_t esp_epaper_draw_bitmap(Device* device, int32_t x_start, int32_t y
return ERROR_NONE; return ERROR_NONE;
} }
// Resends the last drawn frame at EPD_UPDATE_FULL (ignoring config->update_mode) to clear
// ghosting left by earlier fast/partial draw_bitmap() calls, without changing visible content.
// Called periodically by lvgl-module's idle-repaint mechanism for displays with
// DISPLAY_CAPABILITY_SLOW_REFRESH (see Modules/lvgl-module/source/devices/devices.cpp).
static error_t esp_epaper_refresh(Device* device) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
if (!internal->display_on) {
xSemaphoreGive(internal->panel_mutex);
return ERROR_NONE;
}
spi_controller_lock(internal->spi_controller);
const esp_err_t ret = epd_update(internal->epd, internal->last_frame_buffer, EPD_UPDATE_FULL);
spi_controller_unlock(internal->spi_controller);
xSemaphoreGive(internal->panel_mutex);
if (ret != ESP_OK) {
LOG_E(TAG, "epd_update (refresh) failed: %s", esp_err_to_name(ret));
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
// White-fills the panel with a full-quality pass, via the library's epd_clear()/epd_fill()
// (fills its own internal framebuffer with 0xFF, then epd_update()s it at EPD_UPDATE_FULL).
// Keeps last_frame_buffer in sync so a later refresh() resends white rather than stale content.
static error_t esp_epaper_clear(Device* device) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
if (!internal->display_on) {
xSemaphoreGive(internal->panel_mutex);
return ERROR_NONE;
}
spi_controller_lock(internal->spi_controller);
const esp_err_t ret = epd_clear(internal->epd);
spi_controller_unlock(internal->spi_controller);
if (ret == ESP_OK) {
memset(internal->last_frame_buffer, 0xFF, internal->info.buffer_size);
}
xSemaphoreGive(internal->panel_mutex);
if (ret != ESP_OK) {
LOG_E(TAG, "epd_clear failed: %s", esp_err_to_name(ret));
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
static error_t esp_epaper_disp_on_off(Device* device, bool on_off) { static error_t esp_epaper_disp_on_off(Device* device, bool on_off) {
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device)); auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
@ -207,8 +264,8 @@ static const DisplayApi esp_epaper_display_api = {
.reset = esp_epaper_reset, .reset = esp_epaper_reset,
.init = esp_epaper_init, .init = esp_epaper_init,
.draw_bitmap = esp_epaper_draw_bitmap, .draw_bitmap = esp_epaper_draw_bitmap,
.clear = nullptr, .clear = esp_epaper_clear,
.refresh = nullptr, .refresh = esp_epaper_refresh,
.mirror = nullptr, .mirror = nullptr,
.swap_xy = nullptr, .swap_xy = nullptr,
.get_swap_xy = nullptr, .get_swap_xy = nullptr,
@ -238,6 +295,9 @@ static void free_internal(EspEpaperInternal* internal) {
if (internal->rotate_buffer != nullptr) { if (internal->rotate_buffer != nullptr) {
free(internal->rotate_buffer); free(internal->rotate_buffer);
} }
if (internal->last_frame_buffer != nullptr) {
free(internal->last_frame_buffer);
}
if (internal->panel_mutex != nullptr) { if (internal->panel_mutex != nullptr) {
vSemaphoreDelete(internal->panel_mutex); vSemaphoreDelete(internal->panel_mutex);
} }
@ -284,6 +344,8 @@ static error_t start(Device* device) {
epd_config.panel.type = panel_type; epd_config.panel.type = panel_type;
epd_config.panel.width = config->width; epd_config.panel.width = config->width;
epd_config.panel.height = config->height; epd_config.panel.height = config->height;
epd_config.panel.mirror_x = config->mirror_x;
epd_config.panel.mirror_y = config->mirror_y;
epd_handle_t epd = nullptr; epd_handle_t epd = nullptr;
if (epd_init(&epd_config, &epd) != ESP_OK) { if (epd_init(&epd_config, &epd) != ESP_OK) {
@ -332,6 +394,16 @@ static error_t start(Device* device) {
} }
} }
// White-initialized (0xFF - see draw_bitmap()'s bit-polarity comment) so a refresh() before
// the first draw_bitmap() is a harmless all-white pass rather than all-black.
internal->last_frame_buffer = static_cast<uint8_t*>(malloc(internal->info.buffer_size));
if (internal->last_frame_buffer == nullptr) {
LOG_E(TAG, "Failed to allocate %lu-byte last-frame buffer", internal->info.buffer_size);
free_internal(internal);
return ERROR_OUT_OF_MEMORY;
}
memset(internal->last_frame_buffer, 0xFF, internal->info.buffer_size);
internal->display_on = true; internal->display_on = true;
device_set_driver_data(device, internal); device_set_driver_data(device, internal);

View File

@ -74,7 +74,23 @@ static error_t reset_controller_pin(const GpioPinSpec& pin, uint8_t pulses) {
// GT911's I2C address depends on the controller's INT pin level at power-up (board-strapped, not // GT911's I2C address depends on the controller's INT pin level at power-up (board-strapped, not
// devicetree-fixed), so both known addresses are probed regardless of the devicetree "reg" hint. // devicetree-fixed), so both known addresses are probed regardless of the devicetree "reg" hint.
static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out_handle) { static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out_handle) {
esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG(); // Amended copy of ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG() to avoid compiler warnings
esp_lcd_panel_io_i2c_config_t io_config = {
.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS,
.scl_speed_hz = 100000,
.control_phase_bytes = 1,
.dc_bit_offset = 0,
.lcd_cmd_bits = 16,
.lcd_param_bits = 0,
.on_color_trans_done = nullptr,
.user_ctx = nullptr,
.flags = {
.dc_low_on_data = 0,
.disable_control_phase = 1,
},
.transaction_timeout_ms = 0,
};
if (i2c_controller_has_device_at_address(parent, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, pdMS_TO_TICKS(10)) == ERROR_NONE) { if (i2c_controller_has_device_at_address(parent, ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS, pdMS_TO_TICKS(10)) == ERROR_NONE) {
io_config.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS; io_config.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS;

View File

@ -28,23 +28,24 @@ properties:
before comparing against shift-row/shift-col/sym-row/sym-col. before comparing against shift-row/shift-col/sym-row/sym-col.
keymap-lc: keymap-lc:
type: array type: array
element-type: uint8_t element-type: uint32_t
required: true required: true
description: > description: >
Base (lowercase) layer keymap, rows*columns bytes in row-major order (already in Base (lowercase) layer keymap, rows*columns values in row-major order (already in
silkscreen/keymap column order - see reverse-columns). 0 = no key at this position (e.g. silkscreen/keymap column order - see reverse-columns). 0 = no key at this position (e.g.
a blank matrix position, or a position handled as a modifier via shift-row/shift-col/ a blank matrix position, or a position handled as a modifier via shift-row/shift-col/
sym-row/sym-col instead). Non-zero bytes are sent as-is via KeyboardKeyData::key (a Unicode sym-row/sym-col instead). Non-zero values are sent as-is via KeyboardKeyData::key (any
codepoint - byte range covers Latin-1 - for character and non-character keys). Unicode codepoint, including the CodePoint enum's non-character values) for character and
non-character keys.
keymap-uc: keymap-uc:
type: array type: array
element-type: uint8_t element-type: uint32_t
description: > description: >
Uppercase/shift layer keymap, same shape as keymap-lc. Omit if this keyboard has no Uppercase/shift layer keymap, same shape as keymap-lc. Omit if this keyboard has no
separate uppercase layer (shift-row/shift-col should then also be left unset). separate uppercase layer (shift-row/shift-col should then also be left unset).
keymap-sy: keymap-sy:
type: array type: array
element-type: uint8_t element-type: uint32_t
description: > description: >
Symbol layer keymap, same shape as keymap-lc. Omit if this keyboard has no symbol layer Symbol layer keymap, same shape as keymap-lc. Omit if this keyboard has no symbol layer
(sym-row/sym-col should then also be left unset). (sym-row/sym-col should then also be left unset).

View File

@ -16,11 +16,11 @@ struct Tca8418Config {
uint8_t rows; uint8_t rows;
uint8_t columns; uint8_t columns;
bool reverse_columns; bool reverse_columns;
const uint8_t* keymap_lc; const uint32_t* keymap_lc;
uint32_t keymap_lc_length; uint32_t keymap_lc_length;
const uint8_t* keymap_uc; const uint32_t* keymap_uc;
uint32_t keymap_uc_length; uint32_t keymap_uc_length;
const uint8_t* keymap_sy; const uint32_t* keymap_sy;
uint32_t keymap_sy_length; uint32_t keymap_sy_length;
uint8_t shift_row; uint8_t shift_row;
uint8_t shift_col; uint8_t shift_col;

View File

@ -177,7 +177,7 @@ static error_t stop(Device* device) {
// region KeyboardApi // region KeyboardApi
static uint8_t keymap_lookup(const uint8_t* keymap, uint32_t keymap_length, uint8_t row, uint8_t vcol, uint8_t columns) { static uint32_t keymap_lookup(const uint32_t* keymap, uint32_t keymap_length, uint8_t row, uint8_t vcol, uint8_t columns) {
uint32_t index = static_cast<uint32_t>(row) * columns + vcol; uint32_t index = static_cast<uint32_t>(row) * columns + vcol;
if (keymap == nullptr || index >= keymap_length) { if (keymap == nullptr || index >= keymap_length) {
return 0; return 0;
@ -209,7 +209,7 @@ static void handle_key_event(Tca8418Internal* internal, const Tca8418Config* con
} else if (is_sym) { } else if (is_sym) {
internal->sym_pressed = true; internal->sym_pressed = true;
} else { } else {
const uint8_t* keymap = config->keymap_lc; const uint32_t* keymap = config->keymap_lc;
uint32_t keymap_length = config->keymap_lc_length; uint32_t keymap_length = config->keymap_lc_length;
if (internal->sym_pressed) { if (internal->sym_pressed) {
keymap = config->keymap_sy; keymap = config->keymap_sy;
@ -218,8 +218,12 @@ static void handle_key_event(Tca8418Internal* internal, const Tca8418Config* con
keymap = config->keymap_uc; keymap = config->keymap_uc;
keymap_length = config->keymap_uc_length; keymap_length = config->keymap_uc_length;
} }
uint8_t chr = keymap_lookup(keymap, keymap_length, row, vcol, config->columns); uint32_t chr = keymap_lookup(keymap, keymap_length, row, vcol, config->columns);
if (chr != 0) { if (chr != 0) {
uint32_t index = static_cast<uint32_t>(row) * config->columns + vcol;
LOG_D(TAG, "key index=%u value=0x%08X ('%c')", index, static_cast<unsigned int>(chr),
(chr >= 0x20 && chr < 0x7F) ? static_cast<char>(chr) : '?');
// LVGL only registers a key on a RELEASED->PRESSED edge, so every keystroke is // LVGL only registers a key on a RELEASED->PRESSED edge, so every keystroke is
// queued as an immediate press+release pair. // queued as an immediate press+release pair.
push_pending(internal, chr, true); push_pending(internal, chr, true);
@ -275,6 +279,8 @@ static error_t tca8418_read_key(Device* device, KeyboardKeyData* data) {
static constexpr KeyboardApi tca8418_api = { static constexpr KeyboardApi tca8418_api = {
.read_key = tca8418_read_key, .read_key = tca8418_read_key,
.get_backlight = nullptr,
.is_present = nullptr,
}; };
Driver tca8418_driver = { Driver tca8418_driver = {

@ -1 +0,0 @@
Subproject commit 0bf4f144b543048f10bdadd49f0578858bdc2a39

137
Libraries/esp_epaper/.gitignore vendored Normal file
View File

@ -0,0 +1,137 @@
# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
### C ###
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
### C++ ###
# Prerequisites
# Compiled Object files
*.slo
# Precompiled Headers
# Compiled Dynamic libraries
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
# Executables
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,c,c++
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
build/
sdkconfig
sdkconfig.old
managed_components/
ref/
dist/

View File

@ -0,0 +1,519 @@
# Adding a New Panel
This guide explains how to add support for a new e-paper panel using the data-driven architecture.
## Architecture Overview
```
epaper_config.h epaper_registry.c controllers/
+----------------+ +-------------------+ +----------------+
| EPD_PANEL_xxx | ----> | panel_registry[] | ---> | ssd16xx.c |
| (enum) | | - name | | gdey0154_lut.c |
+----------------+ | - width/height | | acep_6color.c |
| - color_mode | | bwry_4color.c |
| - bits_per_pixel | +----------------+
| - caps |
| - ctrl (enum) | ---> controller_ops[]
+-------------------+ (vtable lookup)
```
## Quick Start: Same Controller, Different Size
**For BW panels using existing SSD16xx controller** (covers 90% of GoodDisplay panels), adding support requires only **1 line of code**:
### Step 1: Add Panel Type Enum
In `include/epaper_config.h`:
```c
typedef enum {
// ... existing panels ...
EPD_PANEL_SSD16XX_290, // 2.9" BW 128x296
EPD_PANEL_SSD16XX_YOUR_NEW, // <-- Add here
EPD_PANEL_SSD16XX_420, // 4.2" BW 400x300
EPD_PANEL_COUNT, // Must be BEFORE aliases!
} epd_panel_type_t;
```
### Step 2: Add to Panel Registry
In `src/epaper_registry.c`, add one line to `panel_registry[]`:
```c
static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = {
// ... existing panels ...
[EPD_PANEL_SSD16XX_YOUR_NEW] = {
.name = "SSD16xx_YOUR",
.width = YOUR_WIDTH,
.height = YOUR_HEIGHT,
.color_mode = EPD_COLOR_BW,
.bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_SSD16XX, // Reuse existing controller
},
};
```
**That's it!** The existing `EPD_CTRL_SSD16XX` controller handles everything.
### Currently Supported Generic Panels
| Panel Type | Size | Resolution | Notes |
|------------|------|------------|-------|
| `EPD_PANEL_SSD16XX_154` | 1.54" | 200x200 | GDEM0154I61, generic 1.54" |
| `EPD_PANEL_SSD16XX_213` | 2.13" | 122x250 | GDEY0213B74, GDEM0213I61 |
| `EPD_PANEL_SSD16XX_266` | 2.66" | 152x296 | GDEY0266T90, GDEY0266T90H |
| `EPD_PANEL_SSD16XX_270` | 2.7" | 176x264 | GDEY027T91, GDEM027Q72 |
| `EPD_PANEL_SSD16XX_290` | 2.9" | 128x296 | GDEY029T94, GDEY029T71H |
| `EPD_PANEL_SSD16XX_370` | 3.7" | 280x480 | GDEY037T03 |
| `EPD_PANEL_SSD16XX_420` | 4.2" | 400x300 | GDEY042T81, GDEQ0426T82 |
---
## Adding a New Controller
For panels with **custom LUT tables**, **different controllers**, or **special features**, you need to add a new controller.
### Step 1: Create Controller File
Create `src/controllers/your_controller.c`:
```c
/**
* @file your_controller.c
* @brief Driver for YOUR_CONTROLLER e-paper displays
*/
#include "../epaper_common.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <string.h>
static const char *TAG = "your_ctrl";
/*=============================================================================
* Initialization
*============================================================================*/
esp_err_t your_controller_init(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
uint16_t w = epd_get_width(dev);
uint16_t h = epd_get_height(dev);
RESET(spi);
vTaskDelay(pdMS_TO_TICKS(50));
WAIT(dev); // Uses polarity from panel caps
CMD(spi, 0x12); // SWRESET
WAIT(dev);
CMD(spi, 0x01); // Driver output control
DATA(spi, (h - 1) & 0xFF);
DATA(spi, ((h - 1) >> 8) & 0xFF);
DATA(spi, 0x00);
CMD(spi, 0x11); // Data entry mode
DATA(spi, 0x03);
// Set RAM address
CMD(spi, 0x44);
DATA(spi, 0x00);
DATA(spi, (w / 8) - 1);
CMD(spi, 0x45);
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, (h - 1) & 0xFF);
DATA(spi, ((h - 1) >> 8) & 0xFF);
WAIT(dev);
ESP_LOGI(TAG, "Panel initialized: %dx%d", w, h);
return ESP_OK;
}
/*=============================================================================
* Update Functions
*============================================================================*/
esp_err_t your_controller_update(epd_device_t *dev, epd_update_mode_t mode)
{
epd_spi_t *spi = epd_get_spi(dev);
switch (mode) {
case EPD_UPDATE_PARTIAL:
CMD(spi, 0x22);
DATA(spi, 0xFF); // Partial update sequence
break;
case EPD_UPDATE_FAST:
CMD(spi, 0x22);
DATA(spi, 0xC7); // Fast update
break;
case EPD_UPDATE_FULL:
default:
CMD(spi, 0x22);
DATA(spi, 0xF7); // Full update
break;
}
CMD(spi, 0x20); // Master activation
WAIT(dev);
return ESP_OK;
}
/*=============================================================================
* RAM Write Functions
*============================================================================*/
esp_err_t your_controller_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len)
{
epd_spi_t *spi = epd_get_spi(dev);
bool partial_ready = epd_is_partial_ready(dev);
// Reset RAM address
CMD(spi, 0x4E);
DATA(spi, 0x00);
CMD(spi, 0x4F);
DATA(spi, 0x00);
DATA(spi, 0x00);
// Write to current RAM (0x24)
CMD(spi, 0x24);
epd_spi_write_data_bulk(spi, data, len);
// Only write to base RAM (0x26) when setting up partial refresh
if (!partial_ready) {
CMD(spi, 0x4E);
DATA(spi, 0x00);
CMD(spi, 0x4F);
DATA(spi, 0x00);
DATA(spi, 0x00);
CMD(spi, 0x26);
epd_spi_write_data_bulk(spi, data, len);
}
return ESP_OK;
}
/*=============================================================================
* Power Management
*============================================================================*/
esp_err_t your_controller_sleep(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
CMD(spi, 0x10); // Deep sleep
DATA(spi, 0x01);
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "Panel in deep sleep");
return ESP_OK;
}
esp_err_t your_controller_wake(epd_device_t *dev)
{
return your_controller_init(dev);
}
```
### Step 2: Add Controller Type Enum
In `include/epaper_panel.h`:
```c
typedef enum {
EPD_CTRL_SSD16XX,
EPD_CTRL_GDEY0154_LUT,
EPD_CTRL_ACEP_6COLOR,
EPD_CTRL_BWRY_4COLOR,
EPD_CTRL_YOUR_CONTROLLER, // <-- Add here
EPD_CTRL_COUNT
} epd_controller_type_t;
```
### Step 3: Register Controller Operations
In `src/epaper_registry.c`:
```c
// Declare functions
extern esp_err_t your_controller_init(epd_device_t *dev);
extern esp_err_t your_controller_update(epd_device_t *dev, epd_update_mode_t mode);
extern esp_err_t your_controller_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len);
extern esp_err_t your_controller_sleep(epd_device_t *dev);
extern esp_err_t your_controller_wake(epd_device_t *dev);
// Add to controller_ops array
static const epd_controller_ops_t controller_ops[EPD_CTRL_COUNT] = {
// ... existing controllers ...
[EPD_CTRL_YOUR_CONTROLLER] = {
.init = your_controller_init,
.update = your_controller_update,
.write_ram = your_controller_write_ram,
.write_ram_partial = NULL, // Optional
.sleep = your_controller_sleep,
.wake = your_controller_wake,
},
};
```
### Step 4: Add Panel to Registry
In `src/epaper_registry.c`:
```c
static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = {
// ... existing panels ...
[EPD_PANEL_YOUR_PANEL] = {
.name = "YOUR_PANEL",
.width = 200,
.height = 200,
.color_mode = EPD_COLOR_BW,
.bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_YOUR_CONTROLLER,
},
};
```
### Step 5: Update CMakeLists.txt
```cmake
idf_component_register(
SRCS
"src/epaper.c"
"src/epaper_spi.c"
"src/epaper_lvgl.c"
"src/epaper_common.c"
"src/epaper_registry.c"
"src/controllers/ssd16xx.c"
"src/controllers/gdey0154_lut.c"
"src/controllers/acep_6color.c"
"src/controllers/bwry_4color.c"
"src/controllers/your_controller.c" # <-- Add here
INCLUDE_DIRS "include"
REQUIRES driver esp_driver_gpio esp_driver_spi esp_timer
PRIV_REQUIRES lvgl esp_psram
)
```
---
## Panel Descriptor Reference
```c
typedef struct {
const char *name; // Panel name for logging
uint16_t width; // Width in pixels
uint16_t height; // Height in pixels
epd_color_mode_t color_mode; // EPD_COLOR_BW, EPD_COLOR_6COLOR, etc.
uint8_t bits_per_pixel; // 1, 2, or 4
uint32_t caps; // EPD_CAP_* flags
epd_controller_type_t ctrl; // Controller type
const void *init_data; // Optional panel-specific data
} epd_panel_desc_t;
```
### Capability Flags
| Flag | Description |
|------|-------------|
| `EPD_CAP_PARTIAL` | Supports partial refresh |
| `EPD_CAP_FAST` | Supports fast refresh mode |
| `EPD_CAP_GRAYSCALE` | Supports grayscale mode |
| `EPD_CAP_BUSY_INV` | Inverted busy signal (HIGH = ready) |
### Color Modes
| Mode | BPP | Buffer Size | Description |
|------|-----|-------------|-------------|
| `EPD_COLOR_BW` | 1 | W*H/8 | Black/White |
| `EPD_COLOR_BWR` | 2 | W*H/4 | Black/White/Red |
| `EPD_COLOR_BWY` | 2 | W*H/4 | Black/White/Yellow |
| `EPD_COLOR_4GRAY` | 2 | W*H/4 | 4 Gray levels |
| `EPD_COLOR_4COLOR` | 2 | W*H/4 | 4-color BWRY |
| `EPD_COLOR_6COLOR` | 4 | W*H/2 | 6 Colors |
| `EPD_COLOR_7COLOR` | 4 | W*H/2 | 7 Colors |
---
## Controller Operations Interface
```c
typedef struct {
esp_err_t (*init)(epd_device_t *dev);
esp_err_t (*update)(epd_device_t *dev, epd_update_mode_t mode);
esp_err_t (*write_ram)(epd_device_t *dev, const uint8_t *data, uint32_t len);
esp_err_t (*write_ram_partial)(epd_device_t *dev, uint16_t x, uint16_t y,
uint16_t w, uint16_t h, const uint8_t *data);
esp_err_t (*sleep)(epd_device_t *dev);
esp_err_t (*wake)(epd_device_t *dev);
} epd_controller_ops_t;
```
| Function | Required | Description |
|----------|----------|-------------|
| `init` | Yes | Initialize panel hardware |
| `update` | Yes | Trigger display refresh (handles all modes) |
| `write_ram` | Yes | Write framebuffer to display RAM |
| `write_ram_partial` | No | Windowed RAM write for partial regions |
| `sleep` | No | Enter deep sleep mode |
| `wake` | No | Wake from sleep (usually calls init) |
---
## Helper Functions & Macros
Available in `epaper_common.h`:
```c
// Device access
epd_spi_t* epd_get_spi(epd_device_t *dev);
uint16_t epd_get_width(epd_device_t *dev);
uint16_t epd_get_height(epd_device_t *dev);
const epd_panel_desc_t* epd_get_panel(epd_device_t *dev);
bool epd_is_partial_ready(epd_device_t *dev);
// SPI macros
#define CMD(spi, c) epd_spi_write_cmd(spi, c)
#define DATA(spi, d) epd_spi_write_data(spi, d)
#define RESET(spi) epd_spi_reset(spi)
// Busy wait with polarity support
#define WAIT(dev) epd_wait_busy_polarity(epd_get_spi(dev), epd_get_panel(dev)->caps, 0)
```
---
## Common Issues and Solutions
### Busy Signal Polarity
Some panels use inverted busy signal. Add `EPD_CAP_BUSY_INV` to capabilities:
```c
// Standard: LOW = ready, HIGH = busy
.caps = EPD_CAP_PARTIAL,
// Inverted: HIGH = ready, LOW = busy (e.g., GDEP073E01)
.caps = EPD_CAP_BUSY_INV,
```
The `WAIT(dev)` macro automatically handles polarity based on panel caps.
### Partial Refresh RAM Management
For partial refresh to work correctly:
1. **First call** (`partial_ready = false`): Write to both RAM 0x24 and 0x26
2. **Subsequent calls** (`partial_ready = true`): Write only to RAM 0x24
Use `epd_is_partial_ready(dev)` to check the state:
```c
esp_err_t your_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len)
{
bool partial_ready = epd_is_partial_ready(dev);
// Always write to current RAM
CMD(spi, 0x24);
epd_spi_write_data_bulk(spi, data, len);
// Only write to base RAM when setting up partial mode
if (!partial_ready) {
CMD(spi, 0x26);
epd_spi_write_data_bulk(spi, data, len);
}
return ESP_OK;
}
```
### Multi-Color Panel RAM Format
6/7-color panels use 4-bit per pixel (2 pixels per byte):
```c
// High nibble = even pixel, Low nibble = odd pixel
static void set_pixel_4bpp(uint8_t *fb, int x, int y, int width, uint8_t color)
{
uint32_t addr = (y * width + x) / 2;
if (x % 2 == 0) {
fb[addr] = (fb[addr] & 0x0F) | (color << 4);
} else {
fb[addr] = (fb[addr] & 0xF0) | (color & 0x0F);
}
}
```
4-color BWRY panels use 2-bit per pixel (4 pixels per byte):
```c
// Pixel order: [P0:7-6][P1:5-4][P2:3-2][P3:1-0]
static void set_pixel_2bpp(uint8_t *fb, int x, int y, int width, uint8_t color)
{
uint32_t pixel_idx = y * width + x;
uint32_t byte_idx = pixel_idx / 4;
uint8_t bit_offset = (3 - (pixel_idx % 4)) * 2; // 6, 4, 2, 0
uint8_t mask = 0x03 << bit_offset;
fb[byte_idx] = (fb[byte_idx] & ~mask) | ((color & 0x03) << bit_offset);
}
```
---
## Adding Board Preset (Optional)
Create a configuration macro for easy setup in `include/epaper_config.h`:
```c
#define EPD_CONFIG_YOUR_BOARD() { \
.pins = { \
.busy = GPIO_NUM_8, \
.rst = GPIO_NUM_9, \
.dc = GPIO_NUM_10, \
.cs = GPIO_NUM_11, \
.sck = GPIO_NUM_12, \
.mosi = GPIO_NUM_13, \
}, \
.spi = { \
.host = SPI2_HOST, \
.speed_hz = 10000000, \
}, \
.panel = { \
.type = EPD_PANEL_YOUR_PANEL, \
.width = 0, \
.height = 0, \
.mirror_x = false, \
.mirror_y = false, \
.rotation = 0, \
}, \
}
```
---
## Testing
1. **Build test**: `idf.py build`
2. **Basic test**: Fill screen with white, then black
3. **Pattern test**: Draw checkerboard or gradient
4. **Partial test**: Update small region repeatedly
5. **Sleep/wake test**: Verify low power mode
---
## Resources
- [Good Display Wiki](https://www.good-display.com/companyfile/101.html) - Datasheets and app notes
- [Waveshare E-Paper Wiki](https://www.waveshare.com/wiki/E-Paper) - Reference implementations
- [ESP-IDF SPI Master](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/spi_master.html)

View File

@ -0,0 +1,22 @@
idf_component_register(
SRCS
"src/epaper.c"
"src/epaper_spi.c"
"src/epaper_lvgl.c"
"src/epaper_common.c"
"src/epaper_registry.c"
"src/controllers/ssd16xx.c"
"src/controllers/gdey0154_lut.c"
"src/controllers/acep_6color.c"
"src/controllers/bwry_4color.c"
INCLUDE_DIRS
"include"
REQUIRES
driver
esp_driver_gpio
esp_driver_spi
esp_timer
PRIV_REQUIRES
lvgl
esp_psram
)

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,385 @@
# ESP E-Paper Component
Forked from https://github.com/NellowTCS/esp_epaper which is a fork of https://github.com/tuanpmt/esp_epaper
[![Component Registry](https://components.espressif.com/components/tuanpmt/esp_epaper/badge.svg)](https://components.espressif.com/components/tuanpmt/esp_epaper)
A flexible e-paper display driver component for ESP-IDF with LVGL 9 integration. Designed for IoT devices, electronic shelf labels, photo frames, and low-power display applications.
## Key Features
### Multi-Color Panel Support
- **Black/White (1-bit)**: Classic e-paper with fastest refresh
- **4-Color BWRY (2-bit)**: Black, White, Red, Yellow
- **6-Color (4-bit)**: Black, White, Yellow, Red, Blue, Green - ideal for photo frames
### Floyd-Steinberg Dithering
Advanced error-diffusion dithering algorithm for photo-quality images:
- Smooth gradients using only available colors
- Automatic RGB565 to panel palette conversion
- PSRAM support for large displays (800x480)
### LVGL 9 Partial Refresh
Smart partial refresh for BW panels:
- Base image tracking for differential updates
- Automatic mode switching (partial <-> full)
- Configurable ghosting prevention
### Data-Driven Architecture
- **Panel Registry**: Add new panels with a single line of code
- **Controller Abstraction**: Reusable controller implementations
- **Capability Flags**: Runtime feature detection (partial, fast, grayscale)
### Memory Efficient
- Automatic PSRAM allocation for large buffers
- Chunked SPI transfers for DMA compatibility
- Optional dithering buffer
## Supported Panels
### Specific Panels (Custom LUT/Features)
| Panel | Size | Resolution | Colors | Partial Refresh |
|-------|------|------------|--------|-----------------|
| GDEY0154D67 | 1.54" | 200x200 | BW | Yes (custom LUT) |
| GDEP073E01 | 7.3" | 800x480 | 6-Color | No |
| GDEY037F51 | 3.7" | 240x416 | 4-Color BWRY | No |
### Generic SSD16xx Panels
Adding new BW panels requires only **1 line of code** - see [ADDING_PANELS.md](ADDING_PANELS.md).
| Panel Type | Size | Resolution | Compatible Models |
|------------|------|------------|-------------------|
| SSD16XX_154 | 1.54" | 200x200 | GDEM0154I61, etc. |
| SSD16XX_213 | 2.13" | 122x250 | GDEY0213B74, GDEM0213I61 |
| SSD16XX_266 | 2.66" | 152x296 | GDEY0266T90, GDEY0266T90H |
| SSD16XX_270 | 2.7" | 176x264 | GDEY027T91, GDEM027Q72 |
| SSD16XX_290 | 2.9" | 128x296 | GDEY029T94, GDEY029T71H |
| SSD16XX_370 | 3.7" | 280x480 | GDEY037T03 |
| SSD16XX_420 | 4.2" | 400x300 | GDEY042T81, GDEQ0426T82 |
## Installation
### Using ESP-IDF Component Registry (Recommended)
Add to your project's `idf_component.yml`:
```yaml
dependencies:
tuanpmt/esp_epaper: "^1.0.4"
lvgl/lvgl: "^9.4.0"
```
Then run:
```bash
idf.py reconfigure
```
### Manual Installation
Clone or copy this repository to your project's `components` folder:
```bash
cd your_project/components
git clone https://github.com/tuanpmt/esp_epaper.git
```
## Quick Start
### Basic Usage
```c
#include "epaper.h"
#include "epaper_lvgl.h"
#include "lvgl.h"
void app_main(void)
{
lv_init();
// Use preset configuration for ESP32-S3-ePaper-1.54
epd_config_t cfg = EPD_CONFIG_ESP32S3_154();
// Initialize e-paper
epd_handle_t epd;
ESP_ERROR_CHECK(epd_init(&cfg, &epd));
// Initialize LVGL display
epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT();
lvgl_cfg.epd = epd;
lv_display_t *disp = epd_lvgl_init(&lvgl_cfg);
// Create UI with LVGL...
lv_obj_t *label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "Hello E-Paper!");
lv_obj_center(label);
// Refresh display
epd_lvgl_refresh(disp);
}
```
### 6-Color Panel with Dithering (PhotoPainter)
```c
epd_config_t cfg = EPD_CONFIG_73_6COLOR();
epd_handle_t epd;
epd_init(&cfg, &epd);
epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT();
lvgl_cfg.epd = epd;
lvgl_cfg.update_mode = EPD_UPDATE_FULL;
lvgl_cfg.dither_mode = EPD_DITHER_FLOYD_STEINBERG;
lv_display_t *disp = epd_lvgl_init(&lvgl_cfg);
```
### 4-Color BWRY Panel
```c
epd_config_t cfg = EPD_CONFIG_ESP32_WROOM_4COLOR();
epd_handle_t epd;
epd_init(&cfg, &epd);
epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT();
lvgl_cfg.epd = epd;
lvgl_cfg.dither_mode = EPD_DITHER_ORDERED; // Good for BWRY
lv_display_t *disp = epd_lvgl_init(&lvgl_cfg);
```
## Configuration Presets
```c
// Waveshare ESP32-S3-ePaper-1.54 (1.54" BW)
epd_config_t cfg = EPD_CONFIG_ESP32S3_154();
// Waveshare ESP32-S3-PhotoPainter (7.3" 6-Color)
epd_config_t cfg = EPD_CONFIG_73_6COLOR();
// Good Display ESP32-WROOM-32D (2.66" BW)
epd_config_t cfg = EPD_CONFIG_ESP32_WROOM();
// Good Display ESP32-WROOM-32D + 3.7" 4-Color BWRY
epd_config_t cfg = EPD_CONFIG_ESP32_WROOM_4COLOR();
```
### Custom Configuration
```c
epd_config_t cfg = {
.pins = {
.busy = GPIO_NUM_8,
.rst = GPIO_NUM_9,
.dc = GPIO_NUM_10,
.cs = GPIO_NUM_11,
.sck = GPIO_NUM_12,
.mosi = GPIO_NUM_13,
},
.spi = {
.host = SPI2_HOST,
.speed_hz = 10000000, // 10MHz
},
.panel = {
.type = EPD_PANEL_GDEY0154D67,
.width = 0, // 0 = use panel default
.height = 0,
},
};
```
## Update Modes
| Mode | Description | Use Case |
|------|-------------|----------|
| `EPD_UPDATE_FULL` | Full refresh with flashing | Initial display, clearing ghosting |
| `EPD_UPDATE_PARTIAL` | Fast partial update | UI updates, counters |
| `EPD_UPDATE_FAST` | Fast mode (panel dependent) | Animations |
## Architecture Overview
```
+------------------+ +-------------------+ +------------------+
| Application |---->| epaper_lvgl.c |---->| epaper.c |
| (LVGL UI) | | (RGB565->EPD fmt) | | (Device Manager) |
+------------------+ +-------------------+ +------------------+
|
+--------------------------------+
|
v
+------------------+ +-------------------+
| epaper_registry.c|---->| controllers/ |
| (Panel Database) | | - ssd16xx.c |
| | | - gdey0154_lut.c |
| - Panel specs | | - acep_6color.c |
| - Capabilities | | - bwry_4color.c |
| - Controller map | +-------------------+
+------------------+
```
### Panel Registry (Data-Driven)
All panels are defined in a single registry table:
```c
static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = {
[EPD_PANEL_GDEY0154D67] = {
.name = "GDEY0154D67",
.width = 200, .height = 200,
.color_mode = EPD_COLOR_BW,
.bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_GDEY0154_LUT,
},
// ... more panels
};
```
### Capability Flags
```c
#define EPD_CAP_PARTIAL (1 << 0) // Supports partial refresh
#define EPD_CAP_FAST (1 << 1) // Supports fast refresh
#define EPD_CAP_GRAYSCALE (1 << 2) // Supports grayscale mode
#define EPD_CAP_BUSY_INV (1 << 3) // Inverted busy signal
```
## Floyd-Steinberg Dithering
The component implements Floyd-Steinberg error-diffusion dithering for photo-quality images on limited color e-paper displays.
### How It Works
1. **RGB565 -> RGB888 Conversion**: LVGL buffer is converted with proper bit expansion
2. **Palette Matching**: Each pixel is matched to the nearest e-paper color
3. **Error Diffusion**: Quantization error is distributed to neighbors (7/16 right, 3/16 bottom-left, 5/16 bottom, 1/16 bottom-right)
### Dithering Modes
| Mode | Description | Memory | Quality |
|------|-------------|--------|---------|
| `EPD_DITHER_NONE` | Direct color mapping | None | Basic |
| `EPD_DITHER_ORDERED` | Bayer 4x4 pattern | None | Good |
| `EPD_DITHER_FLOYD_STEINBERG` | Error diffusion | W*H*3 | Best |
### Color Palette
| Color | RGB Value | E-Paper Code |
|-------|-----------|--------------|
| Black | (0, 0, 0) | 0x00 |
| White | (255, 255, 255) | 0x01 |
| Yellow | (255, 255, 0) | 0x02 |
| Red | (255, 0, 0) | 0x03 |
| Blue | (0, 0, 255) | 0x05 |
| Green | (0, 255, 0) | 0x06 |
## Partial Refresh
BW panels support partial refresh for fast updates without full-screen flashing.
### How It Works
1. **Base Image**: First update writes to both current RAM (0x24) and base RAM (0x26)
2. **Partial Updates**: Subsequent updates only write to current RAM (0x24)
3. **Differential Update**: Display compares RAMs and only refreshes changed pixels
### Usage
```c
epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT();
lvgl_cfg.epd = epd;
lvgl_cfg.update_mode = EPD_UPDATE_PARTIAL;
lvgl_cfg.use_partial_refresh = true;
lv_display_t *disp = epd_lvgl_init(&lvgl_cfg);
// First refresh sets base image (full refresh)
epd_lvgl_refresh(disp);
// Subsequent refreshes use partial mode
lv_label_set_text(label, "Count: 1");
epd_lvgl_refresh(disp); // Fast partial update
// Force full refresh periodically to clear ghosting
epd_lvgl_force_full_refresh(disp);
epd_lvgl_refresh(disp);
```
**Note**: Multi-color panels (6-color, 4-color BWRY) do not support partial refresh.
## Examples
See the `examples/` folder for complete examples:
| Example | Board | Description |
|---------|-------|-------------|
| [esp32s3_epaper_154](examples/esp32s3_epaper_154) | ESP32-S3-ePaper-1.54 | 1.54" BW with grayscale dithering |
| [esp32s3_photopainter](examples/esp32s3_photopainter) | ESP32-S3-PhotoPainter | 7.3" 6-Color with dithering |
| [esp32_wroom_generic](examples/esp32_wroom_generic) | ESP32-WROOM-32D | Generic SSD16xx, responsive UI |
| [esp32_wroom_4color](examples/esp32_wroom_4color) | ESP32-WROOM-32D | 3.7" 4-Color BWRY |
### Building Examples
```bash
cd examples/esp32s3_epaper_154
idf.py set-target esp32s3
idf.py build
idf.py -p PORT flash monitor
```
## API Reference
### Core Functions
```c
esp_err_t epd_init(const epd_config_t *config, epd_handle_t *handle);
esp_err_t epd_deinit(epd_handle_t handle);
esp_err_t epd_get_info(epd_handle_t handle, epd_panel_info_t *info);
uint8_t* epd_get_framebuffer(epd_handle_t handle);
esp_err_t epd_update(epd_handle_t handle, const uint8_t *data, epd_update_mode_t mode);
esp_err_t epd_fill(epd_handle_t handle, uint8_t color);
esp_err_t epd_sleep(epd_handle_t handle);
esp_err_t epd_wake(epd_handle_t handle);
```
### LVGL Functions
```c
lv_display_t* epd_lvgl_init(const epd_lvgl_config_t *config);
void epd_lvgl_deinit(lv_display_t *disp);
void epd_lvgl_refresh(lv_display_t *disp);
void epd_lvgl_force_full_refresh(lv_display_t *disp);
void epd_lvgl_set_update_mode(lv_display_t *disp, epd_update_mode_t mode);
void epd_lvgl_set_dither_mode(lv_display_t *disp, epd_dither_mode_t mode);
```
## Adding New Panels
See [ADDING_PANELS.md](ADDING_PANELS.md) for detailed instructions on adding support for new e-paper panels.
**Quick summary**:
- **Same controller, different size**: Add 1 line to panel registry
- **New controller**: Add controller ops + panel registry entry
## Memory Requirements
| Panel | Resolution | BPP | Framebuffer | RGB Buffer (dithering) |
|-------|------------|-----|-------------|------------------------|
| 1.54" BW | 200x200 | 1 | 5 KB | 120 KB |
| 2.13" BW | 122x250 | 1 | 3.8 KB | 92 KB |
| 2.66" BW | 152x296 | 1 | 5.6 KB | 135 KB |
| 3.7" BW | 280x480 | 1 | 16.8 KB | 403 KB |
| 4.2" BW | 400x300 | 1 | 15 KB | 360 KB |
| 3.7" BWRY | 240x416 | 2 | 25 KB | 300 KB |
| 7.3" 6-Color | 800x480 | 4 | 192 KB | 1.15 MB |
**Note**: Large buffers (>32KB) are automatically allocated from PSRAM when available.
## License
MIT License - See [LICENSE](LICENSE)
## Author
- **tuanpmt** - [GitHub](https://github.com/tuanpmt)

View File

@ -0,0 +1,30 @@
version: "2.0.0"
description: "E-Paper display driver for ESP-IDF with LVGL 9 integration. Supports BW, 3-color, and 6-color panels with Floyd-Steinberg dithering."
url: "https://github.com/tuanpmt/esp_epaper"
repository: "https://github.com/tuanpmt/esp_epaper.git"
documentation: "https://github.com/tuanpmt/esp_epaper/blob/master/README.md"
issues: "https://github.com/tuanpmt/esp_epaper/issues"
maintainers:
- "tuanpmt"
dependencies:
idf:
version: ">=5.0.0"
lvgl/lvgl:
version: "^9.0.0"
public: true
targets:
- esp32
- esp32s2
- esp32s3
- esp32c3
- esp32c6
- esp32p4
license: "MIT"
examples:
- path: examples/esp32s3_epaper_154
- path: examples/esp32s3_photopainter
- path: examples/esp32_wroom_generic

View File

@ -0,0 +1,129 @@
#ifndef _EPAPER_H_
#define _EPAPER_H_
#include <stdint.h>
#include <stdbool.h>
#include "epaper_config.h"
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
// Opaque handle
typedef struct epd_device* epd_handle_t;
// Panel info (read-only after init)
typedef struct {
uint16_t width;
uint16_t height;
uint32_t buffer_size; // In bytes
epd_color_mode_t color_mode;
} epd_panel_info_t;
/**
* @brief Initialize e-paper display
* @param config Device configuration
* @param handle Output handle
* @return ESP_OK on success
*/
esp_err_t epd_init(const epd_config_t *config, epd_handle_t *handle);
/**
* @brief Deinitialize e-paper display
*/
esp_err_t epd_deinit(epd_handle_t handle);
/**
* @brief Get panel information
*/
esp_err_t epd_get_info(epd_handle_t handle, epd_panel_info_t *info);
/**
* @brief Clear display (full white)
*/
esp_err_t epd_clear(epd_handle_t handle);
/**
* @brief Fill display with color (0x00=black, 0xFF=white)
*/
esp_err_t epd_fill(epd_handle_t handle, uint8_t color);
/**
* @brief Update full screen with buffer
* @param buffer Pixel data (1 bit per pixel, MSB first)
* @param mode Update mode (full/fast/partial)
*/
esp_err_t epd_update(epd_handle_t handle, const uint8_t *buffer, epd_update_mode_t mode);
/**
* @brief Partial update
* @param x Start X (must be multiple of 8)
* @param y Start Y
* @param w Width (must be multiple of 8)
* @param h Height
* @param buffer Pixel data
*/
esp_err_t epd_update_partial(epd_handle_t handle, uint16_t x, uint16_t y,
uint16_t w, uint16_t h, const uint8_t *buffer);
/**
* @brief Set base image for partial updates
*/
esp_err_t epd_set_base_image(epd_handle_t handle, const uint8_t *buffer);
/**
* @brief Enter deep sleep mode
*/
esp_err_t epd_sleep(epd_handle_t handle);
/**
* @brief Wake from deep sleep
*/
esp_err_t epd_wake(epd_handle_t handle);
/**
* @brief Check if display is busy
*/
bool epd_is_busy(epd_handle_t handle);
/**
* @brief Wait until display is ready
* @param timeout_ms Timeout in milliseconds (0 = infinite)
*/
esp_err_t epd_wait_busy(epd_handle_t handle, uint32_t timeout_ms);
// ============ LVGL Integration Interface ============
/**
* @brief Flush callback for LVGL
* Call this from lv_display_set_flush_cb
*
* Usage in LVGL 9:
* lv_display_t *disp = lv_display_create(width, height);
* lv_display_set_flush_cb(disp, my_flush_cb);
* lv_display_set_user_data(disp, epd_handle);
*
* In my_flush_cb:
* epd_handle_t h = lv_display_get_user_data(disp);
* epd_lvgl_flush(h, area, px_map);
* lv_display_flush_ready(disp);
*/
esp_err_t epd_lvgl_flush(epd_handle_t handle, const void *area, const uint8_t *px_map);
/**
* @brief Get framebuffer for direct LVGL rendering
* @return Pointer to internal framebuffer
*/
uint8_t* epd_get_framebuffer(epd_handle_t handle);
/**
* @brief Flush framebuffer to display
*/
esp_err_t epd_flush_framebuffer(epd_handle_t handle, epd_update_mode_t mode);
#ifdef __cplusplus
}
#endif
#endif // _EPAPER_H_

View File

@ -0,0 +1,243 @@
#ifndef _EPAPER_CONFIG_H_
#define _EPAPER_CONFIG_H_
#include <stdint.h>
#include <stdbool.h>
#include "driver/spi_master.h"
// Default pinout (ESP32-WROOM-32D)
#define EPD_DEFAULT_PIN_BUSY 14
#define EPD_DEFAULT_PIN_RST 15
#define EPD_DEFAULT_PIN_DC 16
#define EPD_DEFAULT_PIN_CS 17
#define EPD_DEFAULT_PIN_SCK 18
#define EPD_DEFAULT_PIN_MOSI 23
#define EPD_DEFAULT_SPI_HOST SPI2_HOST
#define EPD_DEFAULT_SPI_SPEED 4000000 // 4MHz
// Panel types
typedef enum {
// Specific panels (with custom LUT or special features)
EPD_PANEL_GDEY0154D67 = 0, // 1.54" BW 200x200 (custom LUT for better partial)
EPD_PANEL_GDEP073E01, // 7.3" 6-Color 800x480
EPD_PANEL_GDEY037F51, // 3.7" 4-Color BWRY 240x416
EPD_PANEL_GDEY029T71H, // 2.9" BW 168x384 (SSD1685, sources S8..S175)
// Generic SSD16xx BW panels (same driver, different sizes)
EPD_PANEL_SSD16XX_154, // 1.54" BW 200x200 (generic, no custom LUT)
EPD_PANEL_SSD16XX_213, // 2.13" BW 122x250 (GDEY0213B74, etc.)
EPD_PANEL_SSD16XX_266, // 2.66" BW 152x296 (GDEY0266T90, etc.)
EPD_PANEL_SSD16XX_270, // 2.7" BW 176x264 (GDEY027T91, etc.)
EPD_PANEL_SSD16XX_290, // 2.9" BW 128x296 (GDEY029T94, etc.)
EPD_PANEL_SSD16XX_370, // 3.7" BW 280x480 (GDEY037T03, etc.)
EPD_PANEL_SSD16XX_420, // 4.2" BW 400x300 (GDEY042T81, etc.)
EPD_PANEL_COUNT, // Must be BEFORE aliases!
// Legacy aliases (for backward compatibility, don't affect count)
EPD_PANEL_GDEY0266T90 = EPD_PANEL_SSD16XX_266,
} epd_panel_type_t;
// Board presets
typedef enum {
EPD_BOARD_CUSTOM = 0,
EPD_BOARD_ESP32_WROOM, // Default ESP32-WROOM-32D
EPD_BOARD_ESP32S3_EPAPER_154, // ESP32-S3-ePaper-1.54 board
EPD_BOARD_ESP32S3_EPAPER_73, // ESP32-S3 + 7.3" 6-Color board
} epd_board_preset_t;
// Color mode
typedef enum {
EPD_COLOR_BW = 0, // Black/White (1-bit)
EPD_COLOR_BWR, // Black/White/Red (3-color)
EPD_COLOR_BWY, // Black/White/Yellow (3-color)
EPD_COLOR_4GRAY, // 4 Gray levels
EPD_COLOR_4COLOR, // 4-color BWRY (Black/White/Yellow/Red, 2-bit)
EPD_COLOR_7COLOR, // 7-color (B/W/R/Y/Orange/Green/Blue)
EPD_COLOR_6COLOR, // 6-color (B/W/R/Y/Green/Blue)
} epd_color_mode_t;
// E-paper color indices (for multi-color panels)
#define EPD_PIXEL_BLACK 0x0
#define EPD_PIXEL_WHITE 0x1
#define EPD_PIXEL_YELLOW 0x2
#define EPD_PIXEL_RED 0x3
#define EPD_PIXEL_ORANGE 0x4
#define EPD_PIXEL_BLUE 0x5
#define EPD_PIXEL_GREEN 0x6
// Dithering mode
typedef enum {
EPD_DITHER_NONE = 0, // No dithering (direct color mapping)
EPD_DITHER_FLOYD_STEINBERG, // Floyd-Steinberg error diffusion
EPD_DITHER_ORDERED, // Ordered/Bayer dithering
} epd_dither_mode_t;
// Update mode
typedef enum {
EPD_UPDATE_FULL = 0, // Full refresh
EPD_UPDATE_FAST, // Fast refresh
EPD_UPDATE_PARTIAL, // Partial refresh
} epd_update_mode_t;
// Pin configuration
typedef struct {
int busy;
int rst;
int dc;
int cs;
int sck;
int mosi;
} epd_pin_config_t;
// SPI configuration
typedef struct {
int host; // SPI host (SPI2_HOST, SPI3_HOST)
int speed_hz; // Clock speed
} epd_spi_config_t;
// Panel configuration
typedef struct {
epd_panel_type_t type;
uint16_t width; // Override width (0 = use default)
uint16_t height; // Override height (0 = use default)
bool mirror_x;
bool mirror_y;
uint8_t rotation; // 0, 90, 180, 270
} epd_panel_config_t;
// Full device configuration
typedef struct {
epd_pin_config_t pins;
epd_spi_config_t spi;
epd_panel_config_t panel;
} epd_config_t;
// Default configuration macro (ESP32-WROOM-32D)
#define EPD_CONFIG_DEFAULT() { \
.pins = { \
.busy = EPD_DEFAULT_PIN_BUSY, \
.rst = EPD_DEFAULT_PIN_RST, \
.dc = EPD_DEFAULT_PIN_DC, \
.cs = EPD_DEFAULT_PIN_CS, \
.sck = EPD_DEFAULT_PIN_SCK, \
.mosi = EPD_DEFAULT_PIN_MOSI, \
}, \
.spi = { \
.host = EPD_DEFAULT_SPI_HOST, \
.speed_hz = EPD_DEFAULT_SPI_SPEED, \
}, \
.panel = { \
.type = EPD_PANEL_GDEY0266T90, \
.width = 0, \
.height = 0, \
.mirror_x = false, \
.mirror_y = false, \
.rotation = 0, \
}, \
}
// ESP32-S3-ePaper-1.54 board configuration
// Pinout: DC=10, CS=11, SCK=12, MOSI=13, RST=9, BUSY=8
#define EPD_CONFIG_ESP32S3_154() { \
.pins = { \
.busy = 8, \
.rst = 9, \
.dc = 10, \
.cs = 11, \
.sck = 12, \
.mosi = 13, \
}, \
.spi = { \
.host = SPI2_HOST, \
.speed_hz = 40000000, \
}, \
.panel = { \
.type = EPD_PANEL_GDEY0154D67, \
.width = 0, \
.height = 0, \
.mirror_x = false, \
.mirror_y = false, \
.rotation = 0, \
}, \
}
// 7.3" 6-Color E-Paper configuration (800x480)
// Adjust pins according to your board
#define EPD_CONFIG_73_6COLOR() { \
.pins = { \
.busy = 13, \
.rst = 12, \
.dc = 8, \
.cs = 9, \
.sck = 10, \
.mosi = 11, \
}, \
.spi = { \
.host = SPI2_HOST, \
.speed_hz = 10000000, \
}, \
.panel = { \
.type = EPD_PANEL_GDEP073E01, \
.width = 0, \
.height = 0, \
.mirror_x = false, \
.mirror_y = false, \
.rotation = 0, \
}, \
}
// Good Display ESP32-WROOM-32D development board
// Default panel: 2.66" BW (152x296)
// Pinout: BUSY=13, RST=12, DC=14, CS=27, SCK=18, MOSI=23
#define EPD_CONFIG_ESP32_WROOM() { \
.pins = { \
.busy = 13, \
.rst = 12, \
.dc = 14, \
.cs = 27, \
.sck = 18, \
.mosi = 23, \
}, \
.spi = { \
.host = SPI2_HOST, \
.speed_hz = 10000000, \
}, \
.panel = { \
.type = EPD_PANEL_SSD16XX_266, \
.width = 0, \
.height = 0, \
.mirror_x = false, \
.mirror_y = false, \
.rotation = 0, \
}, \
}
// Good Display ESP32-WROOM-32D + 3.7" 4-Color BWRY (GDEY037F51)
// 240x416 resolution, no PSRAM required (streaming mode)
// Pinout: BUSY=13, RST=12, DC=14, CS=27, SCK=18, MOSI=23
#define EPD_CONFIG_ESP32_WROOM_4COLOR() { \
.pins = { \
.busy = 13, \
.rst = 12, \
.dc = 14, \
.cs = 27, \
.sck = 18, \
.mosi = 23, \
}, \
.spi = { \
.host = SPI2_HOST, \
.speed_hz = 10000000, \
}, \
.panel = { \
.type = EPD_PANEL_GDEY037F51, \
.width = 0, \
.height = 0, \
.mirror_x = false, \
.mirror_y = false, \
.rotation = 0, \
}, \
}
#endif // _EPAPER_CONFIG_H_

View File

@ -0,0 +1,111 @@
#ifndef _EPAPER_LVGL_H_
#define _EPAPER_LVGL_H_
#include "epaper.h"
#include "lvgl.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief LVGL e-paper port configuration
*/
typedef struct {
epd_handle_t epd; // E-paper handle (required)
epd_update_mode_t update_mode; // Default update mode
epd_dither_mode_t dither_mode; // Dithering algorithm
bool use_partial_refresh; // Enable partial refresh optimization
uint32_t partial_threshold; // Min dirty area (pixels) for partial refresh
} epd_lvgl_config_t;
#define EPD_LVGL_CONFIG_DEFAULT() { \
.epd = NULL, \
.update_mode = EPD_UPDATE_FULL, \
.dither_mode = EPD_DITHER_NONE, \
.use_partial_refresh = false, \
.partial_threshold = 1000, \
}
/**
* @brief Initialize LVGL display driver for e-paper
*
* @param config LVGL port configuration
* @return lv_display_t* LVGL display object, NULL on failure
*
* Example:
* @code
* epd_config_t epd_cfg = EPD_CONFIG_DEFAULT();
* epd_handle_t epd;
* epd_init(&epd_cfg, &epd);
*
* epd_lvgl_config_t lvgl_cfg = EPD_LVGL_CONFIG_DEFAULT();
* lvgl_cfg.epd = epd;
* lvgl_cfg.use_partial_refresh = true;
*
* lv_display_t *disp = epd_lvgl_init(&lvgl_cfg);
* @endcode
*/
lv_display_t* epd_lvgl_init(const epd_lvgl_config_t *config);
/**
* @brief Deinitialize LVGL e-paper driver
*/
void epd_lvgl_deinit(lv_display_t *disp);
/**
* @brief Set update mode for next refresh
* @param disp LVGL display
* @param mode EPD_UPDATE_FULL, EPD_UPDATE_FAST, EPD_UPDATE_PARTIAL
*/
void epd_lvgl_set_update_mode(lv_display_t *disp, epd_update_mode_t mode);
/**
* @brief Force full refresh on next update
* Call this periodically (e.g., every 10 partial updates) to prevent ghosting
*/
void epd_lvgl_force_full_refresh(lv_display_t *disp);
/**
* @brief Manually trigger display refresh
* Use when lv_refr_now() doesn't work as expected
*/
void epd_lvgl_refresh(lv_display_t *disp);
/**
* @brief Get e-paper handle from display
*/
epd_handle_t epd_lvgl_get_epd(lv_display_t *disp);
/**
* @brief Set dithering mode
* @param disp LVGL display
* @param mode EPD_DITHER_NONE, EPD_DITHER_FLOYD_STEINBERG
*
* Floyd-Steinberg dithering provides better image quality for photos/gradients
* but requires additional memory (width * height * 3 bytes for RGB buffer)
*/
void epd_lvgl_set_dither_mode(lv_display_t *disp, epd_dither_mode_t mode);
/**
* @brief Convert RGB888 to nearest e-paper color
* @param r Red component (0-255)
* @param g Green component (0-255)
* @param b Blue component (0-255)
* @param mode Color mode of the panel
* @return Nearest EPD_PIXEL_xxx value
*/
uint8_t epd_rgb_to_epaper_color(uint8_t r, uint8_t g, uint8_t b, epd_color_mode_t mode);
/**
* @brief Create LVGL color from e-paper palette
* @param epaper_color EPD_PIXEL_xxx value
* @return lv_color_t LVGL color value
*/
lv_color_t epd_epaper_to_lv_color(uint8_t epaper_color);
#ifdef __cplusplus
}
#endif
#endif // _EPAPER_LVGL_H_

View File

@ -0,0 +1,82 @@
#ifndef _EPAPER_PANEL_H_
#define _EPAPER_PANEL_H_
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#include "epaper_config.h"
// Forward declaration
typedef struct epd_device epd_device_t;
/*=============================================================================
* Capability Flags
*============================================================================*/
#define EPD_CAP_PARTIAL (1 << 0) // Supports partial refresh
#define EPD_CAP_FAST (1 << 1) // Supports fast refresh
#define EPD_CAP_GRAYSCALE (1 << 2) // Supports grayscale mode
#define EPD_CAP_BUSY_INV (1 << 3) // Inverted busy signal (HIGH=ready)
/*=============================================================================
* Controller Types
*============================================================================*/
typedef enum {
EPD_CTRL_SSD16XX, // SSD1680/1681 BW panels (generic)
EPD_CTRL_GDEY0154_LUT, // GDEY0154D67 with custom LUT
EPD_CTRL_ACEP_6COLOR, // ACeP 6-color controller (GDEP073E01)
EPD_CTRL_BWRY_4COLOR, // BWRY 4-color controller (GDEY037F51)
EPD_CTRL_COUNT
} epd_controller_type_t;
/*=============================================================================
* Panel Descriptor (Data-Driven)
*============================================================================*/
typedef struct {
const char *name; // Panel name for logging
uint16_t width; // Default width in pixels
uint16_t height; // Default height in pixels
epd_color_mode_t color_mode; // Color mode
uint8_t bits_per_pixel; // 1, 2, or 4
uint32_t caps; // Capability flags (EPD_CAP_*)
epd_controller_type_t ctrl; // Controller type
uint8_t source_shift_bytes; // GDDRAM X window offset in bytes (GDEY029T71H: 1)
bool fast_full_update; // SSD1685 fast full update: bypass RED + temp override
bool ssd1685_partial; // SSD1685 partial: 0x21=(0x00,0x00) + 0x22=0xDC
uint8_t full_refresh_interval; // Periodic full refresh after N partials (0 = disabled)
const void *init_data; // Panel-specific init data (LUT, etc.)
} epd_panel_desc_t;
/*=============================================================================
* Controller Operations Interface
*============================================================================*/
typedef struct {
esp_err_t (*init)(epd_device_t *dev);
esp_err_t (*update)(epd_device_t *dev, epd_update_mode_t mode);
esp_err_t (*write_ram)(epd_device_t *dev, const uint8_t *data, uint32_t len);
esp_err_t (*write_ram_partial)(epd_device_t *dev, uint16_t x, uint16_t y,
uint16_t w, uint16_t h, const uint8_t *data);
esp_err_t (*sleep)(epd_device_t *dev);
esp_err_t (*wake)(epd_device_t *dev);
} epd_controller_ops_t;
/*=============================================================================
* Registry Functions
*============================================================================*/
// Get panel descriptor by type
const epd_panel_desc_t* epd_get_panel_desc(epd_panel_type_t type);
// Get controller operations by type
const epd_controller_ops_t* epd_get_controller_ops(epd_controller_type_t ctrl);
// Check if panel supports a capability
static inline bool epd_panel_has_cap(const epd_panel_desc_t *panel, uint32_t cap) {
return panel && (panel->caps & cap);
}
// Calculate buffer size for panel
static inline uint32_t epd_calc_buffer_size(uint16_t w, uint16_t h, uint8_t bpp) {
return ((uint32_t)w * h * bpp + 7) / 8;
}
#endif // _EPAPER_PANEL_H_

View File

@ -0,0 +1,167 @@
/**
* @file acep_6color.c
* @brief Driver for ACeP 6-Color E-Paper Displays (GDEP073E01, etc.)
*
* Colors: Black, White, Yellow, Red, Blue, Green
* Format: 4-bit per pixel (2 pixels per byte)
*
* Note: This panel does NOT support partial or fast refresh.
* Uses inverted busy signal (HIGH = ready, LOW = busy).
*/
#include "../epaper_common.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "acep6c";
/*=============================================================================
* Initialization
*============================================================================*/
esp_err_t acep6c_init(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
uint16_t w = epd_get_width(dev);
uint16_t h = epd_get_height(dev);
RESET(spi);
vTaskDelay(pdMS_TO_TICKS(50));
WAIT(dev);
vTaskDelay(pdMS_TO_TICKS(50));
// Command sequence for 6-color e-paper
CMD(spi, 0xAA); // CMDH
DATA(spi, 0x49);
DATA(spi, 0x55);
DATA(spi, 0x20);
DATA(spi, 0x08);
DATA(spi, 0x09);
DATA(spi, 0x18);
CMD(spi, 0x01);
DATA(spi, 0x3F);
CMD(spi, 0x00);
DATA(spi, 0x5F);
DATA(spi, 0x69);
CMD(spi, 0x03);
DATA(spi, 0x00);
DATA(spi, 0x54);
DATA(spi, 0x00);
DATA(spi, 0x44);
CMD(spi, 0x05);
DATA(spi, 0x40);
DATA(spi, 0x1F);
DATA(spi, 0x1F);
DATA(spi, 0x2C);
CMD(spi, 0x06);
DATA(spi, 0x6F);
DATA(spi, 0x1F);
DATA(spi, 0x17);
DATA(spi, 0x49);
CMD(spi, 0x08);
DATA(spi, 0x6F);
DATA(spi, 0x1F);
DATA(spi, 0x1F);
DATA(spi, 0x22);
CMD(spi, 0x30);
DATA(spi, 0x03);
CMD(spi, 0x50);
DATA(spi, 0x3F);
CMD(spi, 0x60);
DATA(spi, 0x02);
DATA(spi, 0x00);
// Resolution
CMD(spi, 0x61);
DATA(spi, (w >> 8) & 0xFF);
DATA(spi, w & 0xFF);
DATA(spi, (h >> 8) & 0xFF);
DATA(spi, h & 0xFF);
CMD(spi, 0x84);
DATA(spi, 0x01);
CMD(spi, 0xE3);
DATA(spi, 0x2F);
CMD(spi, 0x04); // Power on
WAIT(dev);
ESP_LOGI(TAG, "Panel initialized (%dx%d 6-color)", w, h);
return ESP_OK;
}
/*=============================================================================
* Update (only full refresh supported)
*============================================================================*/
esp_err_t acep6c_update(epd_device_t *dev, epd_update_mode_t mode)
{
(void)mode; // Only full refresh supported
epd_spi_t *spi = epd_get_spi(dev);
CMD(spi, 0x04); // POWER_ON
WAIT(dev);
CMD(spi, 0x06);
DATA(spi, 0x6F);
DATA(spi, 0x1F);
DATA(spi, 0x17);
DATA(spi, 0x49);
CMD(spi, 0x12); // DISPLAY_REFRESH
DATA(spi, 0x00);
WAIT(dev);
CMD(spi, 0x02); // POWER_OFF
DATA(spi, 0x00);
WAIT(dev);
return ESP_OK;
}
/*=============================================================================
* RAM Write
*============================================================================*/
esp_err_t acep6c_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len)
{
epd_spi_t *spi = epd_get_spi(dev);
CMD(spi, 0x10); // Write RAM
epd_spi_write_data_bulk(spi, data, len);
return ESP_OK;
}
/*=============================================================================
* Power Management
*============================================================================*/
esp_err_t acep6c_sleep(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
CMD(spi, 0x07); // DEEP_SLEEP
DATA(spi, 0xA5);
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "Panel entering deep sleep");
return ESP_OK;
}
esp_err_t acep6c_wake(epd_device_t *dev)
{
return acep6c_init(dev);
}

View File

@ -0,0 +1,230 @@
/**
* @file bwry_4color.c
* @brief Driver for BWRY 4-Color E-Paper Displays (GDEY037F51, etc.)
*
* Colors: Black, White, Yellow, Red
* Format: 2-bit per pixel (4 pixels per byte)
*
* Note: This panel does NOT support partial or fast refresh.
* Uses inverted busy signal (HIGH = ready, LOW = busy).
*/
#include "../epaper_common.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "bwry4c";
/*=============================================================================
* Initialization
*============================================================================*/
esp_err_t bwry4c_init(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
uint16_t w = epd_get_width(dev);
uint16_t h = epd_get_height(dev);
vTaskDelay(pdMS_TO_TICKS(100));
RESET(spi);
vTaskDelay(pdMS_TO_TICKS(50));
WAIT(dev);
// PSR - Panel Setting
CMD(spi, 0x00);
DATA(spi, 0x0F);
DATA(spi, 0x29);
// PWR - Power Setting
CMD(spi, 0x01);
DATA(spi, 0x07);
DATA(spi, 0x00);
DATA(spi, 0x22);
DATA(spi, 0x78);
DATA(spi, 0x0A);
DATA(spi, 0x22);
// PFS - Power off sequence setting
CMD(spi, 0x03);
DATA(spi, 0x10);
DATA(spi, 0x54);
DATA(spi, 0x44);
// BTST - Booster soft start
CMD(spi, 0x06);
DATA(spi, 0xC0);
DATA(spi, 0xC0);
DATA(spi, 0xC0);
// PLL control
CMD(spi, 0x30);
DATA(spi, 0x08);
// TSE - Temperature sensor enable
CMD(spi, 0x41);
DATA(spi, 0x00);
// CDI - VCOM and data interval setting
CMD(spi, 0x50);
DATA(spi, 0x37);
// TCON setting
CMD(spi, 0x60);
DATA(spi, 0x02);
DATA(spi, 0x02);
// Resolution setting
CMD(spi, 0x61);
DATA(spi, w >> 8);
DATA(spi, w & 0xFF);
DATA(spi, h >> 8);
DATA(spi, h & 0xFF);
// FLG - Get flag status
CMD(spi, 0x65);
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, 0x00);
// PWM settings
CMD(spi, 0xE7);
DATA(spi, 0x1C);
CMD(spi, 0xE3);
DATA(spi, 0x22);
// Enter special register mode
CMD(spi, 0xFF);
DATA(spi, 0xA5);
CMD(spi, 0xEF);
DATA(spi, 0x01);
DATA(spi, 0x1E);
DATA(spi, 0x0A);
DATA(spi, 0x1B);
DATA(spi, 0x0B);
DATA(spi, 0x17);
CMD(spi, 0xC3);
DATA(spi, 0xFD);
CMD(spi, 0xDC);
DATA(spi, 0x01);
CMD(spi, 0xDD);
DATA(spi, 0x08);
CMD(spi, 0xDE);
DATA(spi, 0x41);
CMD(spi, 0xFD);
DATA(spi, 0x01);
CMD(spi, 0xE8);
DATA(spi, 0x03);
CMD(spi, 0xDA);
DATA(spi, 0x07);
CMD(spi, 0xC9);
DATA(spi, 0x00);
CMD(spi, 0xA8);
DATA(spi, 0x0F);
// Exit special register mode
CMD(spi, 0xFF);
DATA(spi, 0xE3);
CMD(spi, 0xE9);
DATA(spi, 0x01);
// Power on
CMD(spi, 0x04);
WAIT(dev);
// Additional PWM settings after power on
CMD(spi, 0xFF);
DATA(spi, 0xA5);
CMD(spi, 0xEF);
DATA(spi, 0x03);
DATA(spi, 0x1E);
DATA(spi, 0x0A);
DATA(spi, 0x1B);
DATA(spi, 0x0E);
DATA(spi, 0x15);
CMD(spi, 0xDC);
DATA(spi, 0x01);
CMD(spi, 0xDD);
DATA(spi, 0x08);
CMD(spi, 0xDE);
DATA(spi, 0x41);
CMD(spi, 0xFF);
DATA(spi, 0xE3);
ESP_LOGI(TAG, "Panel initialized (%dx%d 4-color BWRY)", w, h);
return ESP_OK;
}
/*=============================================================================
* Update (only full refresh supported)
*============================================================================*/
esp_err_t bwry4c_update(epd_device_t *dev, epd_update_mode_t mode)
{
(void)mode; // Only full refresh supported
epd_spi_t *spi = epd_get_spi(dev);
CMD(spi, 0x12); // Display Refresh
DATA(spi, 0x00);
WAIT(dev);
return ESP_OK;
}
/*=============================================================================
* RAM Write
*============================================================================*/
esp_err_t bwry4c_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len)
{
epd_spi_t *spi = epd_get_spi(dev);
CMD(spi, 0x10); // Write RAM
epd_spi_write_data_bulk(spi, data, len);
return ESP_OK;
}
/*=============================================================================
* Power Management
*============================================================================*/
esp_err_t bwry4c_sleep(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
CMD(spi, 0x02); // Power off
DATA(spi, 0x00);
WAIT(dev);
CMD(spi, 0x07); // Deep sleep
DATA(spi, 0xA5);
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "Panel entering deep sleep");
return ESP_OK;
}
esp_err_t bwry4c_wake(epd_device_t *dev)
{
return bwry4c_init(dev);
}

View File

@ -0,0 +1,291 @@
/**
* @file gdey0154_lut.c
* @brief Driver for GDEY0154D67 1.54" B/W e-paper display with custom LUT
*
* This panel uses custom waveform lookup tables for optimized
* full and partial refresh performance.
*/
#include "../epaper_common.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <string.h>
static const char *TAG = "gdey0154";
/*=============================================================================
* Waveform LUTs
*============================================================================*/
static const uint8_t LUT_FULL[159] = {
0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x8, 0x1, 0x0, 0x8, 0x1, 0x0, 0x2,
0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0,
0x22, 0x17, 0x41, 0x0, 0x32, 0x20
};
static const uint8_t LUT_PARTIAL[159] = {
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0,
0x02, 0x17, 0x41, 0xB0, 0x32, 0x28,
};
/*=============================================================================
* LUT Loading
*============================================================================*/
static void load_lut(epd_spi_t *spi, const uint8_t *lut)
{
CMD(spi, 0x32);
epd_spi_write_data_bulk(spi, lut, 153);
epd_spi_wait_busy(spi, 0);
CMD(spi, 0x3F);
DATA(spi, lut[153]);
CMD(spi, 0x03);
DATA(spi, lut[154]);
CMD(spi, 0x04);
DATA(spi, lut[155]);
DATA(spi, lut[156]);
DATA(spi, lut[157]);
CMD(spi, 0x2C);
DATA(spi, lut[158]);
}
/*=============================================================================
* Initialization
*============================================================================*/
esp_err_t gdey0154_init(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
uint16_t w = epd_get_width(dev);
uint16_t h = epd_get_height(dev);
RESET(spi);
vTaskDelay(pdMS_TO_TICKS(50));
epd_spi_wait_busy(spi, 0);
CMD(spi, 0x12); // SWRESET
epd_spi_wait_busy(spi, 0);
CMD(spi, 0x01); // Driver output control
DATA(spi, 0xC7);
DATA(spi, 0x00);
DATA(spi, 0x01);
CMD(spi, 0x11); // Data entry mode
DATA(spi, 0x01);
// Set RAM window
CMD(spi, 0x44);
DATA(spi, 0);
DATA(spi, (w - 1) / 8);
CMD(spi, 0x45);
DATA(spi, (h - 1) & 0xFF);
DATA(spi, ((h - 1) >> 8) & 0xFF);
DATA(spi, 0);
DATA(spi, 0);
CMD(spi, 0x3C); // Border waveform
DATA(spi, 0x01);
CMD(spi, 0x18); // Temperature sensor
DATA(spi, 0x80);
CMD(spi, 0x22); // Load temperature
DATA(spi, 0xB1);
CMD(spi, 0x20);
// Set cursor
CMD(spi, 0x4E);
DATA(spi, 0);
CMD(spi, 0x4F);
DATA(spi, (h - 1) & 0xFF);
DATA(spi, ((h - 1) >> 8) & 0xFF);
epd_spi_wait_busy(spi, 0);
load_lut(spi, LUT_FULL);
ESP_LOGD(TAG, "Init complete (%dx%d)", w, h);
return ESP_OK;
}
/*=============================================================================
* Update Functions
*============================================================================*/
static esp_err_t gdey0154_update_full(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
load_lut(spi, LUT_FULL);
CMD(spi, 0x22);
DATA(spi, 0xC7);
CMD(spi, 0x20);
epd_spi_wait_busy(spi, 0);
return ESP_OK;
}
static esp_err_t gdey0154_update_partial(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
RESET(spi);
vTaskDelay(pdMS_TO_TICKS(50));
epd_spi_wait_busy(spi, 0);
load_lut(spi, LUT_PARTIAL);
CMD(spi, 0x37);
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, 0x40);
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, 0x00);
CMD(spi, 0x3C); // Border waveform
DATA(spi, 0x80);
CMD(spi, 0x22);
DATA(spi, 0xCF);
CMD(spi, 0x20);
epd_spi_wait_busy(spi, 0);
return ESP_OK;
}
esp_err_t gdey0154_update(epd_device_t *dev, epd_update_mode_t mode)
{
switch (mode) {
case EPD_UPDATE_PARTIAL:
case EPD_UPDATE_FAST:
return gdey0154_update_partial(dev);
case EPD_UPDATE_FULL:
default:
return gdey0154_update_full(dev);
}
}
/*=============================================================================
* RAM Write Functions
*============================================================================*/
esp_err_t gdey0154_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len)
{
epd_spi_t *spi = epd_get_spi(dev);
bool partial_ready = epd_is_partial_ready(dev);
// Write to current RAM (0x24)
CMD(spi, 0x24);
epd_spi_write_data_bulk(spi, data, len);
// Only write to base image RAM (0x26) when setting up for partial refresh
// When partial_ready is true, we only update 0x24 so the display can
// compare old (0x26) vs new (0x24) for partial update
if (!partial_ready) {
CMD(spi, 0x26);
epd_spi_write_data_bulk(spi, data, len);
}
return ESP_OK;
}
esp_err_t gdey0154_write_ram_partial(epd_device_t *dev, uint16_t x, uint16_t y,
uint16_t w, uint16_t h, const uint8_t *data)
{
epd_spi_t *spi = epd_get_spi(dev);
uint16_t panel_h = epd_get_height(dev);
uint16_t x_start = x / 8;
uint16_t x_end = x_start + w / 8 - 1;
uint16_t y_start = panel_h - 1 - y;
uint16_t y_end = y_start - h + 1;
CMD(spi, 0x44);
DATA(spi, x_start);
DATA(spi, x_end);
CMD(spi, 0x45);
DATA(spi, y_start & 0xFF);
DATA(spi, (y_start >> 8) & 0xFF);
DATA(spi, y_end & 0xFF);
DATA(spi, (y_end >> 8) & 0xFF);
CMD(spi, 0x4E);
DATA(spi, x_start);
CMD(spi, 0x4F);
DATA(spi, y_start & 0xFF);
DATA(spi, (y_start >> 8) & 0xFF);
CMD(spi, 0x24);
epd_spi_write_data_bulk(spi, data, (w / 8) * h);
return ESP_OK;
}
/*=============================================================================
* Power Management
*============================================================================*/
esp_err_t gdey0154_sleep(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
CMD(spi, 0x10);
DATA(spi, 0x01);
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "Panel in deep sleep");
return ESP_OK;
}
esp_err_t gdey0154_wake(epd_device_t *dev)
{
return gdey0154_init(dev);
}

View File

@ -0,0 +1,291 @@
/**
* @file ssd16xx.c
* @brief Generic driver for SSD1680/SSD1681 based B/W e-paper displays
*
* Supports panels: 1.54", 2.13", 2.66", 2.7", 2.9", 3.7", 4.2"
* All use same controller with different resolutions.
*/
#include "../epaper_common.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include <string.h>
static const char *TAG = "ssd16xx";
/*=============================================================================
* Initialization
*============================================================================*/
esp_err_t ssd16xx_init(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
const epd_panel_desc_t *panel = epd_get_panel(dev);
uint8_t source_shift = panel->source_shift_bytes;
uint16_t w = epd_get_width(dev);
uint16_t h = epd_get_height(dev);
ESP_LOGI(TAG, "Init SSD16xx panel: %dx%d", w, h);
RESET(spi);
vTaskDelay(pdMS_TO_TICKS(10));
WAIT(dev);
CMD(spi, 0x12); // SWRESET
WAIT(dev);
CMD(spi, 0x01); // Driver output control
DATA(spi, (h - 1) & 0xFF);
DATA(spi, (h - 1) >> 8);
DATA(spi, 0x00);
CMD(spi, 0x11); // Data entry mode
DATA(spi, 0x03); // Y increment, X increment
CMD(spi, 0x44); // Set RAM X start/end (offset by source_shift for shifted panels)
DATA(spi, source_shift);
DATA(spi, source_shift + (w / 8) - 1);
CMD(spi, 0x45); // Set RAM Y start/end
DATA(spi, 0x00);
DATA(spi, 0x00);
DATA(spi, (h - 1) & 0xFF);
DATA(spi, (h - 1) >> 8);
CMD(spi, 0x3C); // Border waveform
DATA(spi, 0x05);
// Display update control. The second byte selects the OTP configuration: shifted
// panels (source_shift_bytes != 0, e.g. GDEY029T71H) need 0x00 here, others 0x80.
// GxEPD2: "OTP set for 168x384, but TFT shifted: S8..S175 connected to TFT sources".
CMD(spi, 0x21); // Display update control
DATA(spi, 0x00);
DATA(spi, panel->source_shift_bytes ? 0x00 : 0x80);
CMD(spi, 0x18); // Temperature sensor
DATA(spi, 0x80); // Internal sensor
CMD(spi, 0x4E); // Set RAM X counter (offset by source_shift)
DATA(spi, source_shift);
CMD(spi, 0x4F); // Set RAM Y counter
DATA(spi, 0x00);
DATA(spi, 0x00);
WAIT(dev);
ESP_LOGI(TAG, "Panel initialized");
return ESP_OK;
}
/*=============================================================================
* Update Functions
*============================================================================*/
static esp_err_t ssd16xx_update_full(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
const epd_panel_desc_t *panel = epd_get_panel(dev);
// Display update control: bypass RED as 0. GxEPD2 writes this before every full
// update. The fast variant additionally overrides the temperature register.
CMD(spi, 0x21);
DATA(spi, 0x40);
DATA(spi, 0x00);
if (panel->fast_full_update) {
CMD(spi, 0x1A); // Write temperature register
DATA(spi, 0x6E);
DATA(spi, 0x00);
CMD(spi, 0x22); // Load temperature value
DATA(spi, 0x91);
CMD(spi, 0x20);
vTaskDelay(pdMS_TO_TICKS(2));
CMD(spi, 0x22); // Display update (reuse LUT)
DATA(spi, 0xC7);
} else {
CMD(spi, 0x22); // Display update (loads LUT, drives, powers down)
DATA(spi, 0xF7);
}
CMD(spi, 0x20); // Master activation
WAIT(dev);
return ESP_OK;
}
static esp_err_t ssd16xx_update_fast(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
// Fast init sequence
RESET(spi);
vTaskDelay(pdMS_TO_TICKS(10));
CMD(spi, 0x12); // SWRESET
WAIT(dev);
CMD(spi, 0x18); // Temperature sensor
DATA(spi, 0x80);
CMD(spi, 0x22); // Load temperature
DATA(spi, 0xB1);
CMD(spi, 0x20);
WAIT(dev);
CMD(spi, 0x1A); // Write temperature register
DATA(spi, 0x64); // 100°C for faster refresh
DATA(spi, 0x00);
CMD(spi, 0x22); // Load temperature
DATA(spi, 0x91);
CMD(spi, 0x20);
WAIT(dev);
// Fast update sequence
CMD(spi, 0x22);
DATA(spi, 0xC7);
CMD(spi, 0x20);
WAIT(dev);
return ESP_OK;
}
static esp_err_t ssd16xx_update_partial(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
const epd_panel_desc_t *panel = epd_get_panel(dev);
if (panel->ssd1685_partial) {
// SSD1685 partial (GxEPD2 GDEY029T71H::_Update_Part): restore RED normal and
// run the powered fast partial LUT (0x22=0xDC), which keeps the panel powered
// so the next update starts without a full power-up cycle.
CMD(spi, 0x21); // Display update control
DATA(spi, 0x00);
DATA(spi, 0x00);
CMD(spi, 0x22); // Display update
DATA(spi, 0xDC);
} else {
CMD(spi, 0x3C); // Border waveform for partial
DATA(spi, 0x80);
CMD(spi, 0x22); // Display update
DATA(spi, 0xFF); // Partial update sequence
}
CMD(spi, 0x20); // Master activation
WAIT(dev);
return ESP_OK;
}
esp_err_t ssd16xx_update(epd_device_t *dev, epd_update_mode_t mode)
{
switch (mode) {
case EPD_UPDATE_FAST:
return ssd16xx_update_fast(dev);
case EPD_UPDATE_PARTIAL:
return ssd16xx_update_partial(dev);
case EPD_UPDATE_FULL:
default:
return ssd16xx_update_full(dev);
}
}
/*=============================================================================
* RAM Write Functions
*============================================================================*/
esp_err_t ssd16xx_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len)
{
epd_spi_t *spi = epd_get_spi(dev);
const epd_panel_desc_t *panel = epd_get_panel(dev);
bool partial_ready = epd_is_partial_ready(dev);
// Reset RAM address to (source_shift, 0)
CMD(spi, 0x4E);
DATA(spi, panel->source_shift_bytes);
CMD(spi, 0x4F);
DATA(spi, 0x00);
DATA(spi, 0x00);
// Write to current RAM (0x24)
CMD(spi, 0x24);
epd_spi_write_data_bulk(spi, data, len);
// Only write to base image RAM (0x26) when setting up for partial refresh
// When partial_ready is true, we only update 0x24 so the display can
// compare old (0x26) vs new (0x24) for partial update
if (!partial_ready) {
CMD(spi, 0x4E);
DATA(spi, panel->source_shift_bytes);
CMD(spi, 0x4F);
DATA(spi, 0x00);
DATA(spi, 0x00);
CMD(spi, 0x26);
epd_spi_write_data_bulk(spi, data, len);
}
return ESP_OK;
}
esp_err_t ssd16xx_write_ram_partial(epd_device_t *dev, uint16_t x, uint16_t y,
uint16_t w, uint16_t h, const uint8_t *data)
{
epd_spi_t *spi = epd_get_spi(dev);
const epd_panel_desc_t *panel = epd_get_panel(dev);
uint16_t panel_h = epd_get_height(dev);
uint16_t x_start = x / 8 + panel->source_shift_bytes;
uint16_t x_end = x_start + w / 8 - 1;
uint16_t y_start = panel_h - 1 - y;
uint16_t y_end = y_start - h + 1;
// Set window
CMD(spi, 0x44);
DATA(spi, x_start);
DATA(spi, x_end);
CMD(spi, 0x45);
DATA(spi, y_start & 0xFF);
DATA(spi, (y_start >> 8) & 0xFF);
DATA(spi, y_end & 0xFF);
DATA(spi, (y_end >> 8) & 0xFF);
// Set cursor
CMD(spi, 0x4E);
DATA(spi, x_start);
CMD(spi, 0x4F);
DATA(spi, y_start & 0xFF);
DATA(spi, (y_start >> 8) & 0xFF);
// Write RAM
CMD(spi, 0x24);
epd_spi_write_data_bulk(spi, data, (w / 8) * h);
return ESP_OK;
}
/*=============================================================================
* Power Management
*============================================================================*/
esp_err_t ssd16xx_sleep(epd_device_t *dev)
{
epd_spi_t *spi = epd_get_spi(dev);
CMD(spi, 0x10); // Deep sleep
DATA(spi, 0x01);
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "Panel in deep sleep");
return ESP_OK;
}
esp_err_t ssd16xx_wake(epd_device_t *dev)
{
return ssd16xx_init(dev);
}

View File

@ -0,0 +1,375 @@
#include "epaper.h"
#include "epaper_panel.h"
#include "epaper_spi.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include <stdlib.h>
#include <string.h>
static const char *TAG = "epaper";
/*=============================================================================
* Device Structure
*============================================================================*/
struct epd_device {
epd_config_t config;
epd_spi_t spi;
const epd_panel_desc_t *panel;
const epd_controller_ops_t *ops;
uint16_t width;
uint16_t height;
uint32_t buffer_size;
uint8_t *framebuffer;
bool initialized;
bool partial_ready;
uint32_t partial_count;
};
/*=============================================================================
* Device Access Functions (used by controllers)
*============================================================================*/
epd_spi_t* epd_get_spi(epd_device_t *dev)
{
return dev ? &dev->spi : NULL;
}
uint16_t epd_get_width(epd_device_t *dev)
{
return dev ? dev->width : 0;
}
uint16_t epd_get_height(epd_device_t *dev)
{
return dev ? dev->height : 0;
}
const epd_panel_desc_t* epd_get_panel(epd_device_t *dev)
{
return dev ? dev->panel : NULL;
}
bool epd_is_partial_ready(epd_device_t *dev)
{
return dev ? dev->partial_ready : false;
}
/*=============================================================================
* Initialization
*============================================================================*/
esp_err_t epd_init(const epd_config_t *config, epd_handle_t *handle)
{
if (!config || !handle) {
return ESP_ERR_INVALID_ARG;
}
// Get panel descriptor from registry
const epd_panel_desc_t *panel = epd_get_panel_desc(config->panel.type);
if (!panel) {
ESP_LOGE(TAG, "Unknown panel type: %d", config->panel.type);
return ESP_ERR_NOT_SUPPORTED;
}
// Get controller operations
const epd_controller_ops_t *ops = epd_get_controller_ops(panel->ctrl);
if (!ops) {
ESP_LOGE(TAG, "Unknown controller type: %d", panel->ctrl);
return ESP_ERR_NOT_SUPPORTED;
}
// Allocate device
epd_device_t *dev = calloc(1, sizeof(epd_device_t));
if (!dev) {
return ESP_ERR_NO_MEM;
}
// Copy config and set references
memcpy(&dev->config, config, sizeof(epd_config_t));
dev->panel = panel;
dev->ops = ops;
// Set dimensions (allow override from config)
dev->width = config->panel.width > 0 ? config->panel.width : panel->width;
dev->height = config->panel.height > 0 ? config->panel.height : panel->height;
// Calculate buffer size using panel's bits_per_pixel
dev->buffer_size = epd_calc_buffer_size(dev->width, dev->height, panel->bits_per_pixel);
// Allocate framebuffer (prefer PSRAM for large buffers)
if (dev->buffer_size > 32768) {
dev->framebuffer = heap_caps_malloc(dev->buffer_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
} else {
dev->framebuffer = heap_caps_malloc(dev->buffer_size, MALLOC_CAP_DMA);
}
if (!dev->framebuffer) {
ESP_LOGE(TAG, "Failed to allocate framebuffer (%lu bytes)", dev->buffer_size);
free(dev);
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "Framebuffer allocated: %lu bytes", dev->buffer_size);
memset(dev->framebuffer, 0xFF, dev->buffer_size); // White
// Initialize SPI
esp_err_t ret = epd_spi_init(&dev->spi, &config->pins, &config->spi);
if (ret != ESP_OK) {
free(dev->framebuffer);
free(dev);
return ret;
}
// Initialize panel via controller
ret = dev->ops->init(dev);
if (ret != ESP_OK) {
epd_spi_deinit(&dev->spi);
free(dev->framebuffer);
free(dev);
return ret;
}
dev->initialized = true;
*handle = dev;
ESP_LOGI(TAG, "Initialized %s (%dx%d)", panel->name, dev->width, dev->height);
return ESP_OK;
}
esp_err_t epd_deinit(epd_handle_t handle)
{
if (!handle) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
if (dev->ops && dev->ops->sleep) {
dev->ops->sleep(dev);
}
epd_spi_deinit(&dev->spi);
if (dev->framebuffer) {
free(dev->framebuffer);
}
free(dev);
return ESP_OK;
}
/*=============================================================================
* Info & Status
*============================================================================*/
esp_err_t epd_get_info(epd_handle_t handle, epd_panel_info_t *info)
{
if (!handle || !info) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
info->width = dev->width;
info->height = dev->height;
info->buffer_size = dev->buffer_size;
info->color_mode = dev->panel->color_mode;
return ESP_OK;
}
bool epd_is_busy(epd_handle_t handle)
{
if (!handle) return false;
epd_device_t *dev = handle;
return epd_spi_is_busy(&dev->spi) != 0;
}
esp_err_t epd_wait_busy(epd_handle_t handle, uint32_t timeout_ms)
{
if (!handle) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
epd_spi_wait_busy(&dev->spi, timeout_ms);
return ESP_OK;
}
/*=============================================================================
* Display Operations
*============================================================================*/
esp_err_t epd_clear(epd_handle_t handle)
{
return epd_fill(handle, 0xFF);
}
esp_err_t epd_fill(epd_handle_t handle, uint8_t color)
{
if (!handle) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
memset(dev->framebuffer, color, dev->buffer_size);
return epd_update(handle, dev->framebuffer, EPD_UPDATE_FULL);
}
esp_err_t epd_update(epd_handle_t handle, const uint8_t *buffer, epd_update_mode_t mode)
{
if (!handle || !buffer) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
esp_err_t ret;
// Check capabilities and fallback if needed
if (mode == EPD_UPDATE_PARTIAL && !epd_panel_has_cap(dev->panel, EPD_CAP_PARTIAL)) {
ESP_LOGD(TAG, "Partial not supported, using full refresh");
mode = EPD_UPDATE_FULL;
}
if (mode == EPD_UPDATE_FAST && !epd_panel_has_cap(dev->panel, EPD_CAP_FAST)) {
ESP_LOGD(TAG, "Fast not supported, using full refresh");
mode = EPD_UPDATE_FULL;
}
// Periodic full refresh: repeated partial writes on the same GDDRAM base
// image accumulate ghosting, so every Nth partial refresh the whole panel.
// The conversion redraws the identical current buffer, so the base image
// stays valid and subsequent partials can continue immediately.
bool converted_to_full = false;
if (mode == EPD_UPDATE_PARTIAL && dev->partial_ready && dev->panel->full_refresh_interval > 0) {
if (++dev->partial_count >= dev->panel->full_refresh_interval) {
dev->partial_count = 0;
mode = EPD_UPDATE_FULL;
converted_to_full = true;
ESP_LOGI(TAG, "Periodic full refresh after %d partial updates", dev->panel->full_refresh_interval);
}
}
// Copy to framebuffer (callers may pass the internal framebuffer itself)
if (buffer != dev->framebuffer) {
memcpy(dev->framebuffer, buffer, dev->buffer_size);
}
// For partial mode on first call, do a full refresh to set base image
if (mode == EPD_UPDATE_PARTIAL && !dev->partial_ready) {
if (!dev->initialized) {
ret = dev->ops->init(dev);
if (ret != ESP_OK) return ret;
dev->initialized = true;
}
ret = dev->ops->write_ram(dev, buffer, dev->buffer_size);
if (ret != ESP_OK) return ret;
ret = dev->ops->update(dev, EPD_UPDATE_FULL);
if (ret != ESP_OK) return ret;
dev->partial_ready = true;
ESP_LOGI(TAG, "Partial mode ready");
return ESP_OK;
}
// Full refresh resets partial mode (unless it was the periodic conversion,
// which refreshed the same base image and keeps partial mode valid)
if (mode == EPD_UPDATE_FULL && !converted_to_full) {
dev->partial_ready = false;
if (!dev->initialized) {
ret = dev->ops->init(dev);
if (ret != ESP_OK) return ret;
dev->initialized = true;
}
}
// Write RAM and update
ret = dev->ops->write_ram(dev, buffer, dev->buffer_size);
if (ret != ESP_OK) return ret;
return dev->ops->update(dev, mode);
}
esp_err_t epd_update_partial(epd_handle_t handle, uint16_t x, uint16_t y,
uint16_t w, uint16_t h, const uint8_t *buffer)
{
if (!handle || !buffer) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
if (!epd_panel_has_cap(dev->panel, EPD_CAP_PARTIAL)) {
return ESP_ERR_NOT_SUPPORTED;
}
if (!dev->ops->write_ram_partial) {
return ESP_ERR_NOT_SUPPORTED;
}
esp_err_t ret = dev->ops->write_ram_partial(dev, x, y, w, h, buffer);
if (ret != ESP_OK) return ret;
return dev->ops->update(dev, EPD_UPDATE_PARTIAL);
}
esp_err_t epd_set_base_image(epd_handle_t handle, const uint8_t *buffer)
{
if (!handle || !buffer) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
memcpy(dev->framebuffer, buffer, dev->buffer_size);
return ESP_OK;
}
/*=============================================================================
* Power Management
*============================================================================*/
esp_err_t epd_sleep(epd_handle_t handle)
{
if (!handle) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
if (dev->ops->sleep) {
// Deep sleep clears GDDRAM, so the base image for partial updates is
// lost; the next partial update must re-establish it with a full refresh.
dev->partial_ready = false;
dev->partial_count = 0;
dev->initialized = false;
return dev->ops->sleep(dev);
}
return ESP_OK;
}
esp_err_t epd_wake(epd_handle_t handle)
{
if (!handle) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
if (dev->ops->wake) {
esp_err_t ret = dev->ops->wake(dev);
if (ret == ESP_OK) {
dev->initialized = true;
// Wake re-initializes the panel and clears GDDRAM, so the next
// partial update must re-establish the base with a full refresh.
dev->partial_ready = false;
dev->partial_count = 0;
}
return ret;
}
return dev->ops->init(dev);
}
/*=============================================================================
* Framebuffer Access
*============================================================================*/
uint8_t* epd_get_framebuffer(epd_handle_t handle)
{
if (!handle) return NULL;
return handle->framebuffer;
}
esp_err_t epd_flush_framebuffer(epd_handle_t handle, epd_update_mode_t mode)
{
if (!handle) return ESP_ERR_INVALID_ARG;
return epd_update(handle, handle->framebuffer, mode);
}
esp_err_t epd_lvgl_flush(epd_handle_t handle, const void *area, const uint8_t *px_map)
{
if (!handle || !px_map) return ESP_ERR_INVALID_ARG;
epd_device_t *dev = handle;
memcpy(dev->framebuffer, px_map, dev->buffer_size);
return ESP_OK;
}

View File

@ -0,0 +1,28 @@
#include "epaper_common.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "epd_common";
void epd_wait_busy_polarity(epd_spi_t *spi, uint32_t caps, uint32_t timeout_ms)
{
bool inverted = (caps & EPD_CAP_BUSY_INV);
uint32_t start = xTaskGetTickCount();
// For inverted: wait while LOW (busy), ready when HIGH
// For normal: wait while HIGH (busy), ready when LOW
int busy_level = inverted ? 0 : 1;
while (gpio_get_level(spi->pin_busy) == busy_level) {
if (timeout_ms > 0) {
uint32_t elapsed = (xTaskGetTickCount() - start) * portTICK_PERIOD_MS;
if (elapsed >= timeout_ms) {
ESP_LOGW(TAG, "Wait busy timeout (%lu ms)", timeout_ms);
return;
}
}
vTaskDelay(pdMS_TO_TICKS(5));
}
}

View File

@ -0,0 +1,41 @@
#ifndef _EPAPER_COMMON_H_
#define _EPAPER_COMMON_H_
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#include "epaper_spi.h"
#include "epaper_panel.h"
// Forward declaration
typedef struct epd_device epd_device_t;
/*=============================================================================
* Device Access Functions (implemented in epaper.c)
*============================================================================*/
epd_spi_t* epd_get_spi(epd_device_t *dev);
uint16_t epd_get_width(epd_device_t *dev);
uint16_t epd_get_height(epd_device_t *dev);
const epd_panel_desc_t* epd_get_panel(epd_device_t *dev);
bool epd_is_partial_ready(epd_device_t *dev);
/*=============================================================================
* Common SPI Macros
*============================================================================*/
#define CMD(spi, c) epd_spi_write_cmd(spi, c)
#define DATA(spi, d) epd_spi_write_data(spi, d)
#define RESET(spi) epd_spi_reset(spi)
/*=============================================================================
* Busy Wait with Polarity Support
*============================================================================*/
// Wait for panel ready, respecting busy signal polarity
// caps: panel capability flags (use EPD_CAP_BUSY_INV for inverted)
// timeout_ms: timeout in ms, 0 = infinite
void epd_wait_busy_polarity(epd_spi_t *spi, uint32_t caps, uint32_t timeout_ms);
// Convenience macro for controllers
#define WAIT(dev) epd_wait_busy_polarity(epd_get_spi(dev), epd_get_panel(dev)->caps, 0)
#endif // _EPAPER_COMMON_H_

View File

@ -0,0 +1,622 @@
#include "epaper_lvgl.h"
#include "epaper_panel.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
#include <stdlib.h>
static const char *TAG = "epd_lvgl";
// External function to get panel descriptor
extern const epd_panel_desc_t* epd_get_panel(void *dev);
/*=============================================================================
* Color Palette for Multi-Color E-Paper
*============================================================================*/
// Pre-defined RGB values for e-paper colors
static const struct {
uint8_t r, g, b;
uint8_t epaper_color;
} color_palette[] = {
{0, 0, 0, EPD_PIXEL_BLACK}, // Black
{255, 255, 255, EPD_PIXEL_WHITE}, // White
{255, 255, 0, EPD_PIXEL_YELLOW}, // Yellow
{255, 0, 0, EPD_PIXEL_RED}, // Red
{0, 0, 255, EPD_PIXEL_BLUE}, // Blue
{0, 255, 0, EPD_PIXEL_GREEN}, // Green
};
#define PALETTE_SIZE (sizeof(color_palette) / sizeof(color_palette[0]))
// BW-only palette
static const struct {
uint8_t r, g, b;
uint8_t epaper_color;
} bw_palette[] = {
{0, 0, 0, EPD_PIXEL_BLACK},
{255, 255, 255, EPD_PIXEL_WHITE},
};
#define BW_PALETTE_SIZE 2
/*=============================================================================
* Port Context
*============================================================================*/
typedef struct {
epd_handle_t epd;
epd_update_mode_t update_mode;
epd_dither_mode_t dither_mode;
epd_color_mode_t color_mode;
uint8_t bits_per_pixel; // From panel descriptor
bool use_partial_refresh;
uint32_t partial_threshold;
uint8_t *lvgl_buf; // RGB565 buffer for LVGL
uint8_t *rgb_buf; // RGB888 buffer for dithering
uint32_t lvgl_buf_size;
uint16_t width;
uint16_t height;
bool force_full;
uint32_t partial_count;
bool partial_render_mode; // True if using small buffer with partial rendering
bool fb_dirty; // Framebuffer has been modified
} epd_lvgl_ctx_t;
/*=============================================================================
* Color Conversion Functions
*============================================================================*/
static inline int clamp_byte(int v) {
return v < 0 ? 0 : (v > 255 ? 255 : v);
}
// 4-color BWRY palette
static const struct {
uint8_t r, g, b;
uint8_t epaper_color;
} bwry_palette[] = {
{0, 0, 0, EPD_PIXEL_BLACK}, // Black
{255, 255, 255, EPD_PIXEL_WHITE}, // White
{255, 255, 0, EPD_PIXEL_YELLOW}, // Yellow
{255, 0, 0, EPD_PIXEL_RED}, // Red
};
#define BWRY_PALETTE_SIZE 4
// Find nearest palette color (returns palette index)
static uint8_t find_nearest_color_idx(int r, int g, int b, epd_color_mode_t mode)
{
uint32_t min_dist = UINT32_MAX;
uint8_t best = 0;
if (mode == EPD_COLOR_BW) {
// BW mode: simple threshold
int gray = (r * 299 + g * 587 + b * 114) / 1000;
return (gray < 128) ? 0 : 1; // 0=black, 1=white
}
if (mode == EPD_COLOR_4COLOR) {
// 4-color BWRY mode
for (int i = 0; i < BWRY_PALETTE_SIZE; i++) {
int dr = r - bwry_palette[i].r;
int dg = g - bwry_palette[i].g;
int db = b - bwry_palette[i].b;
uint32_t dist = dr * dr + dg * dg + db * db;
if (dist < min_dist) {
min_dist = dist;
best = i;
}
}
return best;
}
// Multi-color mode (6-color, 7-color)
for (int i = 0; i < PALETTE_SIZE; i++) {
int dr = r - color_palette[i].r;
int dg = g - color_palette[i].g;
int db = b - color_palette[i].b;
uint32_t dist = dr * dr + dg * dg + db * db;
if (dist < min_dist) {
min_dist = dist;
best = i;
}
}
return best;
}
// Convert RGB to e-paper color
uint8_t epd_rgb_to_epaper_color(uint8_t r, uint8_t g, uint8_t b, epd_color_mode_t mode)
{
if (mode == EPD_COLOR_BW) {
int gray = (r * 299 + g * 587 + b * 114) / 1000;
return (gray < 128) ? EPD_PIXEL_BLACK : EPD_PIXEL_WHITE;
}
if (mode == EPD_COLOR_4COLOR) {
uint8_t idx = find_nearest_color_idx(r, g, b, mode);
return bwry_palette[idx].epaper_color;
}
uint8_t idx = find_nearest_color_idx(r, g, b, mode);
return color_palette[idx].epaper_color;
}
// Convert e-paper color to LVGL color
lv_color_t epd_epaper_to_lv_color(uint8_t epaper_color)
{
switch (epaper_color) {
case EPD_PIXEL_BLACK: return lv_color_make(0, 0, 0);
case EPD_PIXEL_WHITE: return lv_color_make(255, 255, 255);
case EPD_PIXEL_YELLOW: return lv_color_make(255, 255, 0);
case EPD_PIXEL_RED: return lv_color_make(255, 0, 0);
case EPD_PIXEL_BLUE: return lv_color_make(0, 0, 255);
case EPD_PIXEL_GREEN: return lv_color_make(0, 255, 0);
default: return lv_color_make(255, 255, 255);
}
}
/*=============================================================================
* Dithering Algorithms
*============================================================================*/
// Bayer 4x4 ordered dithering matrix (values 0-15, scaled to 0-255)
static const uint8_t BAYER_4X4[4][4] = {
{ 0, 128, 32, 160 },
{ 192, 64, 224, 96 },
{ 48, 176, 16, 144 },
{ 240, 112, 208, 80 }
};
// Set pixel in framebuffer based on color mode
static inline void set_fb_pixel(uint8_t *fb, int x, int y, int width, uint8_t color, epd_color_mode_t mode)
{
if (mode == EPD_COLOR_6COLOR || mode == EPD_COLOR_7COLOR) {
// 4-bit per pixel (2 pixels per byte)
uint32_t addr = (y * width + x) / 2;
if (x % 2 == 0) {
// High nibble (left pixel)
fb[addr] = (fb[addr] & 0x0F) | (color << 4);
} else {
// Low nibble (right pixel)
fb[addr] = (fb[addr] & 0xF0) | (color & 0x0F);
}
} else if (mode == EPD_COLOR_4COLOR || mode == EPD_COLOR_4GRAY) {
// 2-bit per pixel (4 pixels per byte)
// Pixel order: [P0:7-6][P1:5-4][P2:3-2][P3:1-0]
uint32_t pixel_idx = y * width + x;
uint32_t byte_idx = pixel_idx / 4;
uint8_t bit_offset = (3 - (pixel_idx % 4)) * 2; // 6, 4, 2, 0
uint8_t mask = 0x03 << bit_offset;
fb[byte_idx] = (fb[byte_idx] & ~mask) | ((color & 0x03) << bit_offset);
} else {
// 1-bit per pixel (BW)
uint16_t byte_idx = y * (width / 8) + (x / 8);
uint8_t bit = 7 - (x % 8);
if (color == EPD_PIXEL_WHITE) {
fb[byte_idx] |= (1 << bit);
} else {
fb[byte_idx] &= ~(1 << bit);
}
}
}
static void apply_floyd_steinberg_dithering(epd_lvgl_ctx_t *ctx, uint8_t *fb)
{
if (!ctx->rgb_buf || !fb) return;
ESP_LOGI(TAG, "Applying Floyd-Steinberg dithering...");
for (int y = 0; y < ctx->height; y++) {
// Yield every 20 rows to prevent watchdog
if (y % 20 == 0) {
vTaskDelay(1);
}
for (int x = 0; x < ctx->width; x++) {
int idx = (y * ctx->width + x) * 3;
// Get current pixel RGB
int r = ctx->rgb_buf[idx + 0];
int g = ctx->rgb_buf[idx + 1];
int b = ctx->rgb_buf[idx + 2];
// Find nearest palette color
uint8_t pal_idx = find_nearest_color_idx(r, g, b, ctx->color_mode);
uint8_t epaper_color;
int pal_r, pal_g, pal_b;
if (ctx->color_mode == EPD_COLOR_BW) {
epaper_color = (pal_idx == 0) ? EPD_PIXEL_BLACK : EPD_PIXEL_WHITE;
pal_r = bw_palette[pal_idx].r;
pal_g = bw_palette[pal_idx].g;
pal_b = bw_palette[pal_idx].b;
} else if (ctx->color_mode == EPD_COLOR_4COLOR) {
epaper_color = bwry_palette[pal_idx].epaper_color;
pal_r = bwry_palette[pal_idx].r;
pal_g = bwry_palette[pal_idx].g;
pal_b = bwry_palette[pal_idx].b;
} else {
epaper_color = color_palette[pal_idx].epaper_color;
pal_r = color_palette[pal_idx].r;
pal_g = color_palette[pal_idx].g;
pal_b = color_palette[pal_idx].b;
}
// Set pixel in framebuffer
set_fb_pixel(fb, x, y, ctx->width, epaper_color, ctx->color_mode);
// Calculate quantization error
int err_r = r - pal_r;
int err_g = g - pal_g;
int err_b = b - pal_b;
// Distribute error to neighbors (Floyd-Steinberg)
// Right pixel: 7/16
if (x + 1 < ctx->width) {
int ni = idx + 3;
ctx->rgb_buf[ni + 0] = clamp_byte(ctx->rgb_buf[ni + 0] + err_r * 7 / 16);
ctx->rgb_buf[ni + 1] = clamp_byte(ctx->rgb_buf[ni + 1] + err_g * 7 / 16);
ctx->rgb_buf[ni + 2] = clamp_byte(ctx->rgb_buf[ni + 2] + err_b * 7 / 16);
}
// Bottom-left pixel: 3/16
if (y + 1 < ctx->height && x > 0) {
int ni = ((y + 1) * ctx->width + x - 1) * 3;
ctx->rgb_buf[ni + 0] = clamp_byte(ctx->rgb_buf[ni + 0] + err_r * 3 / 16);
ctx->rgb_buf[ni + 1] = clamp_byte(ctx->rgb_buf[ni + 1] + err_g * 3 / 16);
ctx->rgb_buf[ni + 2] = clamp_byte(ctx->rgb_buf[ni + 2] + err_b * 3 / 16);
}
// Bottom pixel: 5/16
if (y + 1 < ctx->height) {
int ni = ((y + 1) * ctx->width + x) * 3;
ctx->rgb_buf[ni + 0] = clamp_byte(ctx->rgb_buf[ni + 0] + err_r * 5 / 16);
ctx->rgb_buf[ni + 1] = clamp_byte(ctx->rgb_buf[ni + 1] + err_g * 5 / 16);
ctx->rgb_buf[ni + 2] = clamp_byte(ctx->rgb_buf[ni + 2] + err_b * 5 / 16);
}
// Bottom-right pixel: 1/16
if (y + 1 < ctx->height && x + 1 < ctx->width) {
int ni = ((y + 1) * ctx->width + x + 1) * 3;
ctx->rgb_buf[ni + 0] = clamp_byte(ctx->rgb_buf[ni + 0] + err_r / 16);
ctx->rgb_buf[ni + 1] = clamp_byte(ctx->rgb_buf[ni + 1] + err_g / 16);
ctx->rgb_buf[ni + 2] = clamp_byte(ctx->rgb_buf[ni + 2] + err_b / 16);
}
}
}
ESP_LOGI(TAG, "Dithering complete");
}
/*=============================================================================
* Flush Callback - Convert RGB565 to E-Paper Format
*============================================================================*/
static void epd_lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *color_p)
{
epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp);
if (!ctx || !ctx->epd) {
lv_display_flush_ready(disp);
return;
}
uint16_t *buffer = (uint16_t *)color_p;
uint8_t *fb = epd_get_framebuffer(ctx->epd);
if (!fb) {
lv_display_flush_ready(disp);
return;
}
bool use_floyd_steinberg = (ctx->dither_mode == EPD_DITHER_FLOYD_STEINBERG) && ctx->rgb_buf;
// Use ordered dither when: explicitly set OR when Floyd-Steinberg requested but no RGB buffer
bool use_ordered_dither = (ctx->dither_mode == EPD_DITHER_ORDERED) ||
((ctx->dither_mode == EPD_DITHER_FLOYD_STEINBERG) && !ctx->rgb_buf);
if (use_floyd_steinberg) {
// Store RGB888 in buffer for dithering
for (int y = area->y1; y <= area->y2; y++) {
for (int x = area->x1; x <= area->x2; x++) {
// RGB565 to RGB888 with proper bit expansion
// This ensures white (31,63,31) -> (255,255,255)
uint16_t c = *buffer++;
uint8_t r5 = (c >> 11) & 0x1F;
uint8_t g6 = (c >> 5) & 0x3F;
uint8_t b5 = c & 0x1F;
uint8_t r = (r5 << 3) | (r5 >> 2); // 5-bit to 8-bit
uint8_t g = (g6 << 2) | (g6 >> 4); // 6-bit to 8-bit
uint8_t b = (b5 << 3) | (b5 >> 2); // 5-bit to 8-bit
int idx = (y * ctx->width + x) * 3;
ctx->rgb_buf[idx + 0] = r;
ctx->rgb_buf[idx + 1] = g;
ctx->rgb_buf[idx + 2] = b;
}
// Yield periodically
if ((y % 50) == 0) {
vTaskDelay(1);
}
}
// Apply dithering after all pixels collected
bool is_last = (area->x2 == ctx->width - 1) && (area->y2 == ctx->height - 1);
if (is_last) {
// Clear framebuffer first using bits_per_pixel from panel
uint32_t fb_size = epd_calc_buffer_size(ctx->width, ctx->height, ctx->bits_per_pixel);
uint8_t fill_byte;
if (ctx->bits_per_pixel == 4) {
fill_byte = (EPD_PIXEL_WHITE << 4) | EPD_PIXEL_WHITE;
} else if (ctx->bits_per_pixel == 2) {
fill_byte = 0x55; // White (01) for all 4 pixels
} else {
fill_byte = 0xFF; // 1-bit white
}
memset(fb, fill_byte, fb_size);
apply_floyd_steinberg_dithering(ctx, fb);
}
} else {
// Direct conversion with optional ordered (Bayer) dithering
// Clear framebuffer only on first chunk (top-left corner)
bool is_first = (area->x1 == 0 && area->y1 == 0);
if (is_first) {
uint32_t fb_size = epd_calc_buffer_size(ctx->width, ctx->height, ctx->bits_per_pixel);
uint8_t fill_byte;
if (ctx->bits_per_pixel == 4) {
fill_byte = (EPD_PIXEL_WHITE << 4) | EPD_PIXEL_WHITE;
} else if (ctx->bits_per_pixel == 2) {
fill_byte = 0x55; // White (01) for all 4 pixels
} else {
fill_byte = 0xFF; // 1-bit white
}
memset(fb, fill_byte, fb_size);
}
for (int y = area->y1; y <= area->y2; y++) {
for (int x = area->x1; x <= area->x2; x++) {
// RGB565 to RGB888 with proper bit expansion
uint16_t c = *buffer++;
uint8_t r5 = (c >> 11) & 0x1F;
uint8_t g6 = (c >> 5) & 0x3F;
uint8_t b5 = c & 0x1F;
uint8_t r = (r5 << 3) | (r5 >> 2);
uint8_t g = (g6 << 2) | (g6 >> 4);
uint8_t b = (b5 << 3) | (b5 >> 2);
uint8_t epaper_color;
if (use_ordered_dither) {
// Bayer ordered dithering (no extra buffer needed)
uint8_t threshold = BAYER_4X4[y % 4][x % 4];
if (ctx->color_mode == EPD_COLOR_BW) {
int gray = (r * 299 + g * 587 + b * 114) / 1000;
epaper_color = (gray > threshold) ? EPD_PIXEL_WHITE : EPD_PIXEL_BLACK;
} else if (ctx->color_mode == EPD_COLOR_4COLOR) {
// Ordered dithering for 4-color BWRY
// Adjust RGB values with dither threshold for better color selection
int dr = r + (threshold - 128) / 2;
int dg = g + (threshold - 128) / 2;
int db = b + (threshold - 128) / 2;
dr = dr < 0 ? 0 : (dr > 255 ? 255 : dr);
dg = dg < 0 ? 0 : (dg > 255 ? 255 : dg);
db = db < 0 ? 0 : (db > 255 ? 255 : db);
epaper_color = epd_rgb_to_epaper_color(dr, dg, db, ctx->color_mode);
} else {
// Other multi-color modes
epaper_color = epd_rgb_to_epaper_color(r, g, b, ctx->color_mode);
}
} else {
// Simple threshold conversion (no dithering)
epaper_color = epd_rgb_to_epaper_color(r, g, b, ctx->color_mode);
}
// Set pixel (format depends on color mode)
set_fb_pixel(fb, x, y, ctx->width, epaper_color, ctx->color_mode);
}
// Yield periodically
if ((y % 50) == 0) {
vTaskDelay(1);
}
}
}
// Mark framebuffer as dirty
ctx->fb_dirty = true;
// Check if this is the last chunk (bottom-right corner)
bool is_last = (area->x2 >= ctx->width - 1) && (area->y2 >= ctx->height - 1);
// Only update display when:
// 1. Full render mode (not partial) - update every flush
// 2. Partial render mode - only update on last chunk
if (!ctx->partial_render_mode || is_last) {
// Determine update mode
epd_update_mode_t mode = ctx->update_mode;
if (ctx->force_full) {
mode = EPD_UPDATE_FULL;
ctx->force_full = false;
ctx->partial_count = 0;
}
// Update display
epd_flush_framebuffer(ctx->epd, mode);
ctx->fb_dirty = false;
}
lv_display_flush_ready(disp);
}
/*=============================================================================
* Public API
*============================================================================*/
lv_display_t* epd_lvgl_init(const epd_lvgl_config_t *config)
{
if (!config || !config->epd) {
ESP_LOGE(TAG, "Invalid config");
return NULL;
}
// Get panel info
epd_panel_info_t info;
if (epd_get_info(config->epd, &info) != ESP_OK) {
ESP_LOGE(TAG, "Failed to get panel info");
return NULL;
}
// Allocate context
epd_lvgl_ctx_t *ctx = calloc(1, sizeof(epd_lvgl_ctx_t));
if (!ctx) {
ESP_LOGE(TAG, "Failed to allocate context");
return NULL;
}
ctx->epd = config->epd;
ctx->update_mode = config->update_mode;
ctx->dither_mode = config->dither_mode;
ctx->color_mode = info.color_mode;
ctx->use_partial_refresh = config->use_partial_refresh;
ctx->partial_threshold = config->partial_threshold;
ctx->width = info.width;
ctx->height = info.height;
ctx->force_full = false;
ctx->partial_count = 0;
// Get bits_per_pixel from panel descriptor
const epd_panel_desc_t *panel = epd_get_panel(config->epd);
ctx->bits_per_pixel = panel ? panel->bits_per_pixel : 1;
// Calculate buffer size based on available memory
// Full screen buffer for PSRAM, partial buffer for internal RAM only
uint32_t full_buf_size = info.width * info.height * sizeof(uint16_t);
bool use_partial_render = false;
// Try SPIRAM first for full buffer
ctx->lvgl_buf = heap_caps_malloc(full_buf_size, MALLOC_CAP_SPIRAM);
if (ctx->lvgl_buf) {
ctx->lvgl_buf_size = full_buf_size;
ESP_LOGI(TAG, "Using SPIRAM for full buffer: %lu bytes", ctx->lvgl_buf_size);
} else {
// No SPIRAM - use partial rendering with smaller buffer
// Buffer for 10 lines at a time (saves memory on no-PSRAM boards)
uint32_t partial_lines = 10;
uint32_t partial_buf_size = info.width * partial_lines * sizeof(uint16_t);
ctx->lvgl_buf = heap_caps_malloc(partial_buf_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if (!ctx->lvgl_buf) {
ctx->lvgl_buf = malloc(partial_buf_size);
}
if (ctx->lvgl_buf) {
ctx->lvgl_buf_size = partial_buf_size;
ctx->partial_render_mode = true;
use_partial_render = true;
ESP_LOGI(TAG, "No SPIRAM, using partial render buffer: %lu bytes (%lu lines)",
ctx->lvgl_buf_size, partial_lines);
}
}
if (!ctx->lvgl_buf) {
ESP_LOGE(TAG, "Failed to allocate LVGL buffer");
free(ctx);
return NULL;
}
// Allocate RGB buffer for dithering if enabled
if (config->dither_mode == EPD_DITHER_FLOYD_STEINBERG) {
size_t rgb_buf_size = info.width * info.height * 3; // RGB888
ctx->rgb_buf = heap_caps_malloc(rgb_buf_size, MALLOC_CAP_SPIRAM);
if (!ctx->rgb_buf) {
ctx->rgb_buf = heap_caps_malloc(rgb_buf_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
}
if (ctx->rgb_buf) {
memset(ctx->rgb_buf, 255, rgb_buf_size); // White
ESP_LOGI(TAG, "Floyd-Steinberg dithering enabled, RGB buffer: %zu bytes", rgb_buf_size);
} else {
ESP_LOGW(TAG, "Not enough RAM for Floyd-Steinberg, using Bayer ordered dithering");
}
}
// Create LVGL display
lv_display_t *disp = lv_display_create(info.width, info.height);
if (!disp) {
ESP_LOGE(TAG, "Failed to create display");
if (ctx->rgb_buf) free(ctx->rgb_buf);
free(ctx->lvgl_buf);
free(ctx);
return NULL;
}
lv_display_set_user_data(disp, ctx);
lv_display_set_flush_cb(disp, epd_lvgl_flush_cb);
// Use partial render mode if buffer is smaller than full screen
lv_display_render_mode_t render_mode = use_partial_render ?
LV_DISPLAY_RENDER_MODE_PARTIAL : LV_DISPLAY_RENDER_MODE_FULL;
lv_display_set_buffers(disp, ctx->lvgl_buf, NULL, ctx->lvgl_buf_size, render_mode);
ESP_LOGI(TAG, "LVGL display initialized: %dx%d, dither=%d, render=%s",
info.width, info.height, config->dither_mode,
use_partial_render ? "partial" : "full");
return disp;
}
void epd_lvgl_deinit(lv_display_t *disp)
{
if (!disp) return;
epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp);
if (ctx) {
if (ctx->lvgl_buf) free(ctx->lvgl_buf);
if (ctx->rgb_buf) free(ctx->rgb_buf);
free(ctx);
}
lv_display_delete(disp);
}
void epd_lvgl_set_update_mode(lv_display_t *disp, epd_update_mode_t mode)
{
if (!disp) return;
epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp);
if (ctx) {
ctx->update_mode = mode;
}
}
void epd_lvgl_set_dither_mode(lv_display_t *disp, epd_dither_mode_t mode)
{
if (!disp) return;
epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp);
if (ctx) {
ctx->dither_mode = mode;
ESP_LOGI(TAG, "Dither mode set to %d", mode);
}
}
void epd_lvgl_force_full_refresh(lv_display_t *disp)
{
if (!disp) return;
epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp);
if (ctx) {
ctx->force_full = true;
}
}
void epd_lvgl_refresh(lv_display_t *disp)
{
if (!disp) return;
lv_obj_invalidate(lv_screen_active());
lv_refr_now(disp);
}
epd_handle_t epd_lvgl_get_epd(lv_display_t *disp)
{
if (!disp) return NULL;
epd_lvgl_ctx_t *ctx = lv_display_get_user_data(disp);
return ctx ? ctx->epd : NULL;
}

View File

@ -0,0 +1,219 @@
/**
* @file epaper_registry.c
* @brief Data-driven panel registry and controller lookup
*
* To add a new panel:
* 1. Add enum in epaper_config.h
* 2. Add entry in panel_registry[] below
*
* To add a new controller:
* 1. Add enum in epaper_panel.h (epd_controller_type_t)
* 2. Implement controller in src/controllers/
* 3. Add entry in controller_ops[] below
*/
#include "epaper_panel.h"
#include "epaper_config.h"
/*=============================================================================
* Controller Operations (forward declarations)
*============================================================================*/
// SSD16xx generic BW controller
extern esp_err_t ssd16xx_init(epd_device_t *dev);
extern esp_err_t ssd16xx_update(epd_device_t *dev, epd_update_mode_t mode);
extern esp_err_t ssd16xx_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len);
extern esp_err_t ssd16xx_write_ram_partial(epd_device_t *dev, uint16_t x, uint16_t y,
uint16_t w, uint16_t h, const uint8_t *data);
extern esp_err_t ssd16xx_sleep(epd_device_t *dev);
extern esp_err_t ssd16xx_wake(epd_device_t *dev);
// GDEY0154D67 with custom LUT
extern esp_err_t gdey0154_init(epd_device_t *dev);
extern esp_err_t gdey0154_update(epd_device_t *dev, epd_update_mode_t mode);
extern esp_err_t gdey0154_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len);
extern esp_err_t gdey0154_write_ram_partial(epd_device_t *dev, uint16_t x, uint16_t y,
uint16_t w, uint16_t h, const uint8_t *data);
extern esp_err_t gdey0154_sleep(epd_device_t *dev);
extern esp_err_t gdey0154_wake(epd_device_t *dev);
// ACeP 6-color controller
extern esp_err_t acep6c_init(epd_device_t *dev);
extern esp_err_t acep6c_update(epd_device_t *dev, epd_update_mode_t mode);
extern esp_err_t acep6c_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len);
extern esp_err_t acep6c_sleep(epd_device_t *dev);
extern esp_err_t acep6c_wake(epd_device_t *dev);
// BWRY 4-color controller
extern esp_err_t bwry4c_init(epd_device_t *dev);
extern esp_err_t bwry4c_update(epd_device_t *dev, epd_update_mode_t mode);
extern esp_err_t bwry4c_write_ram(epd_device_t *dev, const uint8_t *data, uint32_t len);
extern esp_err_t bwry4c_sleep(epd_device_t *dev);
extern esp_err_t bwry4c_wake(epd_device_t *dev);
/*=============================================================================
* Controller Operations Table
*============================================================================*/
static const epd_controller_ops_t controller_ops[EPD_CTRL_COUNT] = {
[EPD_CTRL_SSD16XX] = {
.init = ssd16xx_init,
.update = ssd16xx_update,
.write_ram = ssd16xx_write_ram,
.write_ram_partial = ssd16xx_write_ram_partial,
.sleep = ssd16xx_sleep,
.wake = ssd16xx_wake,
},
[EPD_CTRL_GDEY0154_LUT] = {
.init = gdey0154_init,
.update = gdey0154_update,
.write_ram = gdey0154_write_ram,
.write_ram_partial = gdey0154_write_ram_partial,
.sleep = gdey0154_sleep,
.wake = gdey0154_wake,
},
[EPD_CTRL_ACEP_6COLOR] = {
.init = acep6c_init,
.update = acep6c_update,
.write_ram = acep6c_write_ram,
.write_ram_partial = NULL, // Not supported
.sleep = acep6c_sleep,
.wake = acep6c_wake,
},
[EPD_CTRL_BWRY_4COLOR] = {
.init = bwry4c_init,
.update = bwry4c_update,
.write_ram = bwry4c_write_ram,
.write_ram_partial = NULL, // Not supported
.sleep = bwry4c_sleep,
.wake = bwry4c_wake,
},
};
/*=============================================================================
* Panel Registry Table
*
* Format: { name, width, height, color_mode, bpp, caps, controller, init_data }
*============================================================================*/
static const epd_panel_desc_t panel_registry[EPD_PANEL_COUNT] = {
// Specific panels with custom features
[EPD_PANEL_GDEY0154D67] = {
.name = "GDEY0154D67",
.width = 200, .height = 200,
.color_mode = EPD_COLOR_BW, .bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_GDEY0154_LUT,
.init_data = NULL,
},
[EPD_PANEL_GDEP073E01] = {
.name = "GDEP073E01",
.width = 800, .height = 480,
.color_mode = EPD_COLOR_6COLOR, .bits_per_pixel = 4,
.caps = EPD_CAP_BUSY_INV, // Inverted busy, no partial/fast
.ctrl = EPD_CTRL_ACEP_6COLOR,
.init_data = NULL,
},
[EPD_PANEL_GDEY037F51] = {
.name = "GDEY037F51",
.width = 240, .height = 416,
.color_mode = EPD_COLOR_4COLOR, .bits_per_pixel = 2,
.caps = EPD_CAP_BUSY_INV, // No partial/fast support
.ctrl = EPD_CTRL_BWRY_4COLOR,
.init_data = NULL,
},
[EPD_PANEL_GDEY029T71H] = {
.name = "GDEY029T71H",
.width = 168, .height = 384,
.color_mode = EPD_COLOR_BW, .bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_SSD16XX,
.source_shift_bytes = 1,
.fast_full_update = false,
.ssd1685_partial = true,
.full_refresh_interval = 0,
.init_data = NULL,
},
// Generic SSD16xx BW panels (same controller, different sizes)
[EPD_PANEL_SSD16XX_154] = {
.name = "SSD16xx_154",
.width = 200, .height = 200,
.color_mode = EPD_COLOR_BW, .bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_SSD16XX,
.init_data = NULL,
},
[EPD_PANEL_SSD16XX_213] = {
.name = "SSD16xx_213",
.width = 122, .height = 250,
.color_mode = EPD_COLOR_BW, .bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_SSD16XX,
.init_data = NULL,
},
[EPD_PANEL_SSD16XX_266] = {
.name = "SSD16xx_266",
.width = 152, .height = 296,
.color_mode = EPD_COLOR_BW, .bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_SSD16XX,
.init_data = NULL,
},
[EPD_PANEL_SSD16XX_270] = {
.name = "SSD16xx_270",
.width = 176, .height = 264,
.color_mode = EPD_COLOR_BW, .bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_SSD16XX,
.init_data = NULL,
},
[EPD_PANEL_SSD16XX_290] = {
.name = "SSD16xx_290",
.width = 128, .height = 296,
.color_mode = EPD_COLOR_BW, .bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_SSD16XX,
.init_data = NULL,
},
[EPD_PANEL_SSD16XX_370] = {
.name = "SSD16xx_370",
.width = 280, .height = 480,
.color_mode = EPD_COLOR_BW, .bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_SSD16XX,
.init_data = NULL,
},
[EPD_PANEL_SSD16XX_420] = {
.name = "SSD16xx_420",
.width = 400, .height = 300,
.color_mode = EPD_COLOR_BW, .bits_per_pixel = 1,
.caps = EPD_CAP_PARTIAL | EPD_CAP_FAST,
.ctrl = EPD_CTRL_SSD16XX,
.init_data = NULL,
},
};
/*=============================================================================
* Registry Lookup Functions
*============================================================================*/
const epd_panel_desc_t* epd_get_panel_desc(epd_panel_type_t type)
{
if (type >= EPD_PANEL_COUNT) {
return NULL;
}
// Check if panel entry is valid (has a name)
if (panel_registry[type].name == NULL) {
return NULL;
}
return &panel_registry[type];
}
const epd_controller_ops_t* epd_get_controller_ops(epd_controller_type_t ctrl)
{
if (ctrl >= EPD_CTRL_COUNT) {
return NULL;
}
return &controller_ops[ctrl];
}

View File

@ -0,0 +1,173 @@
#include "epaper_spi.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "epd_spi";
#define SPI_MAX_CHUNK_SIZE 4096 // 4KB per transfer (safe for all ESP32 variants)
esp_err_t epd_spi_init(epd_spi_t *spi, const epd_pin_config_t *pins, const epd_spi_config_t *spi_cfg)
{
// Configure GPIO pins
gpio_config_t io_conf = {
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
// DC, RST, CS as output
io_conf.pin_bit_mask = (1ULL << pins->dc) | (1ULL << pins->rst) | (1ULL << pins->cs);
gpio_config(&io_conf);
// BUSY as input
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL << pins->busy);
gpio_config(&io_conf);
// Store pins
spi->pin_dc = pins->dc;
spi->pin_rst = pins->rst;
spi->pin_busy = pins->busy;
spi->pin_cs = pins->cs;
// Set initial state
gpio_set_level(pins->cs, 1);
gpio_set_level(pins->dc, 1);
gpio_set_level(pins->rst, 1);
// Configure SPI bus (large buffer for big displays like 800x480)
spi_bus_config_t buscfg = {
.mosi_io_num = pins->mosi,
.miso_io_num = -1, // Not used
.sclk_io_num = pins->sck,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = SPI_MAX_CHUNK_SIZE, // Match chunk size
};
spi->host = spi_cfg->host;
esp_err_t ret = spi_bus_initialize(spi_cfg->host, &buscfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "SPI bus init failed: %s", esp_err_to_name(ret));
return ret;
}
// ESP_ERR_INVALID_STATE means the host was already initialized by someone else - this
// component doesn't own it and must not free it on deinit.
spi->owns_bus = (ret == ESP_OK);
// Add SPI device
spi_device_interface_config_t devcfg = {
.clock_speed_hz = spi_cfg->speed_hz,
.mode = 0,
.spics_io_num = -1, // Manual CS control
.queue_size = 1,
};
ret = spi_bus_add_device(spi_cfg->host, &devcfg, &spi->spi);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SPI device add failed: %s", esp_err_to_name(ret));
if (spi->owns_bus) {
spi_bus_free(spi->host);
spi->owns_bus = false;
}
return ret;
}
ESP_LOGI(TAG, "SPI initialized");
return ESP_OK;
}
esp_err_t epd_spi_deinit(epd_spi_t *spi)
{
if (spi->spi) {
spi_bus_remove_device(spi->spi);
spi->spi = NULL;
}
// spi_bus_free() requires all devices removed first, and only the owner of a host may free
// it - see spi_master.h's spi_bus_free() docs.
if (spi->owns_bus) {
spi_bus_free(spi->host);
spi->owns_bus = false;
}
return ESP_OK;
}
void epd_spi_write_cmd(epd_spi_t *spi, uint8_t cmd)
{
gpio_set_level(spi->pin_cs, 0);
gpio_set_level(spi->pin_dc, 0); // Command mode
spi_transaction_t t = {
.length = 8,
.tx_buffer = &cmd,
};
spi_device_polling_transmit(spi->spi, &t);
gpio_set_level(spi->pin_cs, 1);
}
void epd_spi_write_data(epd_spi_t *spi, uint8_t data)
{
gpio_set_level(spi->pin_cs, 0);
gpio_set_level(spi->pin_dc, 1); // Data mode
spi_transaction_t t = {
.length = 8,
.tx_buffer = &data,
};
spi_device_polling_transmit(spi->spi, &t);
gpio_set_level(spi->pin_cs, 1);
}
void epd_spi_write_data_bulk(epd_spi_t *spi, const uint8_t *data, uint32_t len)
{
gpio_set_level(spi->pin_cs, 0);
gpio_set_level(spi->pin_dc, 1); // Data mode
// Send in chunks to avoid SPI transfer limit
uint32_t offset = 0;
while (offset < len) {
uint32_t chunk_size = (len - offset > SPI_MAX_CHUNK_SIZE) ? SPI_MAX_CHUNK_SIZE : (len - offset);
spi_transaction_t t = {
.length = chunk_size * 8,
.tx_buffer = data + offset,
};
spi_device_polling_transmit(spi->spi, &t);
offset += chunk_size;
}
gpio_set_level(spi->pin_cs, 1);
}
void epd_spi_reset(epd_spi_t *spi)
{
gpio_set_level(spi->pin_rst, 0);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(spi->pin_rst, 1);
vTaskDelay(pdMS_TO_TICKS(10));
}
int epd_spi_is_busy(epd_spi_t *spi)
{
return gpio_get_level(spi->pin_busy);
}
void epd_spi_wait_busy(epd_spi_t *spi, uint32_t timeout_ms)
{
uint32_t start = xTaskGetTickCount();
while (gpio_get_level(spi->pin_busy)) {
if (timeout_ms > 0) {
uint32_t elapsed = (xTaskGetTickCount() - start) * portTICK_PERIOD_MS;
if (elapsed >= timeout_ms) {
ESP_LOGW(TAG, "Wait busy timeout");
return;
}
}
vTaskDelay(pdMS_TO_TICKS(5)); // 5ms like reference
}
}

View File

@ -0,0 +1,33 @@
#ifndef _EPAPER_SPI_H_
#define _EPAPER_SPI_H_
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#include "driver/spi_master.h"
#include "epaper_config.h"
typedef struct {
spi_device_handle_t spi;
spi_host_device_t host;
/** True if epd_spi_init() called spi_bus_initialize() successfully (vs. the bus already
* being initialized by someone else) - only an owned bus gets freed on deinit. */
bool owns_bus;
int pin_dc;
int pin_rst;
int pin_busy;
int pin_cs;
} epd_spi_t;
esp_err_t epd_spi_init(epd_spi_t *spi, const epd_pin_config_t *pins, const epd_spi_config_t *spi_cfg);
esp_err_t epd_spi_deinit(epd_spi_t *spi);
void epd_spi_write_cmd(epd_spi_t *spi, uint8_t cmd);
void epd_spi_write_data(epd_spi_t *spi, uint8_t data);
void epd_spi_write_data_bulk(epd_spi_t *spi, const uint8_t *data, uint32_t len);
void epd_spi_reset(epd_spi_t *spi);
int epd_spi_is_busy(epd_spi_t *spi);
void epd_spi_wait_busy(epd_spi_t *spi, uint32_t timeout_ms);
#endif // _EPAPER_SPI_H_

View File

@ -26,6 +26,7 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_event_subscribe_with_app_id), DEFINE_MODULE_SYMBOL(app_event_subscribe_with_app_id),
DEFINE_MODULE_SYMBOL(app_event_unsubscribe), DEFINE_MODULE_SYMBOL(app_event_unsubscribe),
DEFINE_MODULE_SYMBOL(app_event_poll), DEFINE_MODULE_SYMBOL(app_event_poll),
DEFINE_MODULE_SYMBOL(app_event_emit_close),
// app/execute // app/execute
DEFINE_MODULE_SYMBOL(app_execute), DEFINE_MODULE_SYMBOL(app_execute),
DEFINE_MODULE_SYMBOL(app_execute_for_result), DEFINE_MODULE_SYMBOL(app_execute_for_result),

View File

@ -16,6 +16,9 @@ instead of `_Znwj`), so this module doesn't build for the POSIX simulator.
### Compiler/runtime ABI support ### Compiler/runtime ABI support
- `operator new(unsigned int)` / `operator delete(void*, unsigned int)` (`_Znwj` / `_ZdlPvj`) - `operator new(unsigned int)` / `operator delete(void*, unsigned int)` (`_Znwj` / `_ZdlPvj`)
- `operator new[](unsigned int)` / `operator delete[](void*, unsigned int)` (`_Znaj` / `_ZdaPvj`)
- `operator delete(void*)` / `operator delete[](void*)` (`_ZdlPv` / `_ZdaPv`) - the unsized forms,
used instead of the above when the compiler determines no size is needed
- `std::nothrow` - `std::nothrow`
- `__cxa_pure_virtual` - called through a pure-virtual slot before a derived class's vtable is - `__cxa_pure_virtual` - called through a pure-virtual slot before a derived class's vtable is
fully constructed; see [Bare metal C++](https://arobenko.github.io/bare_metal_cpp/). fully constructed; see [Bare metal C++](https://arobenko.github.io/bare_metal_cpp/).

View File

@ -22,6 +22,12 @@ extern "C" {
// libstdc++ exports these under different (m-suffixed) mangled names, so they don't apply there. // libstdc++ exports these under different (m-suffixed) mangled names, so they don't apply there.
extern void* _Znwj(uint32_t size); // operator new(unsigned int) extern void* _Znwj(uint32_t size); // operator new(unsigned int)
extern void _ZdlPvj(void* p, uint64_t size); // operator delete(void*, unsigned int) extern void _ZdlPvj(void* p, uint64_t size); // operator delete(void*, unsigned int)
extern void* _Znaj(uint32_t size); // operator new[](unsigned int)
extern void _ZdaPvj(void* p, uint64_t size); // operator delete[](void*, unsigned int)
// Unsized forms: the compiler picks these over the sized ones above depending on context
// (e.g. trivially-destructible types needing no array cookie), so both must be exported.
extern void _ZdlPv(void* p); // operator delete(void*)
extern void _ZdaPv(void* p); // operator delete[](void*)
#endif #endif
extern void __cxa_pure_virtual(); extern void __cxa_pure_virtual();
// cxx_guards.cpp // cxx_guards.cpp
@ -99,6 +105,10 @@ static const ModuleSymbol SYMBOLS[] = {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
DEFINE_MODULE_SYMBOL(_Znwj), // operator new(unsigned int) DEFINE_MODULE_SYMBOL(_Znwj), // operator new(unsigned int)
DEFINE_MODULE_SYMBOL(_ZdlPvj), // operator delete(void*, unsigned int) DEFINE_MODULE_SYMBOL(_ZdlPvj), // operator delete(void*, unsigned int)
DEFINE_MODULE_SYMBOL(_Znaj), // operator new[](unsigned int)
DEFINE_MODULE_SYMBOL(_ZdaPvj), // operator delete[](void*, unsigned int)
DEFINE_MODULE_SYMBOL(_ZdlPv), // operator delete(void*)
DEFINE_MODULE_SYMBOL(_ZdaPv), // operator delete[](void*)
#endif #endif
{ "_ZSt7nothrow", (void*)&std::nothrow }, { "_ZSt7nothrow", (void*)&std::nothrow },
DEFINE_MODULE_SYMBOL(__cxa_pure_virtual), // class-related, see https://arobenko.github.io/bare_metal_cpp/ DEFINE_MODULE_SYMBOL(__cxa_pure_virtual), // class-related, see https://arobenko.github.io/bare_metal_cpp/

View File

@ -12,7 +12,7 @@
namespace { namespace {
constexpr auto* TAG = "http-download"; constexpr auto* TAG = "http-download";
constexpr size_t DOWNLOAD_TASK_STACK_DEPTH = 4608 / sizeof(StackType_t); constexpr size_t DOWNLOAD_TASK_STACK_DEPTH = 5120;
struct DownloadContext { struct DownloadContext {
std::string url; std::string url;

View File

@ -66,13 +66,8 @@ static error_t read(Device* device, uint8_t address, uint8_t* data, size_t data_
lock(driver_data); lock(driver_data);
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
if (esp_error != ESP_OK) { if (esp_error == ESP_OK) {
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
} else {
esp_error = i2c_master_receive(driver_data->dev_handle, data, data_size, timeout_ms); esp_error = i2c_master_receive(driver_data->dev_handle, data, data_size, timeout_ms);
if (esp_error != ESP_OK) {
LOG_E(TAG, "receive(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
}
} }
unlock(driver_data); unlock(driver_data);
return esp_err_to_error(esp_error); return esp_err_to_error(esp_error);
@ -86,13 +81,8 @@ static error_t write(Device* device, uint8_t address, const uint8_t* data, uint1
lock(driver_data); lock(driver_data);
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
if (esp_error != ESP_OK) { if (esp_error == ESP_OK) {
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
} else {
esp_error = i2c_master_transmit(driver_data->dev_handle, data, data_size, timeout_ms); esp_error = i2c_master_transmit(driver_data->dev_handle, data, data_size, timeout_ms);
if (esp_error != ESP_OK) {
LOG_E(TAG, "transmit(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
}
} }
unlock(driver_data); unlock(driver_data);
return esp_err_to_error(esp_error); return esp_err_to_error(esp_error);
@ -106,13 +96,8 @@ static error_t write_read(Device* device, uint8_t address, const uint8_t* write_
lock(driver_data); lock(driver_data);
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
if (esp_error != ESP_OK) { if (esp_error == ESP_OK) {
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
} else {
esp_error = i2c_master_transmit_receive(driver_data->dev_handle, write_data, write_data_size, read_data, read_data_size, timeout_ms); esp_error = i2c_master_transmit_receive(driver_data->dev_handle, write_data, write_data_size, read_data, read_data_size, timeout_ms);
if (esp_error != ESP_OK) {
LOG_E(TAG, "transmit_receive(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
}
} }
unlock(driver_data); unlock(driver_data);
return esp_err_to_error(esp_error); return esp_err_to_error(esp_error);
@ -126,13 +111,8 @@ static error_t read_register(Device* device, uint8_t address, uint8_t reg, uint8
lock(driver_data); lock(driver_data);
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
if (esp_error != ESP_OK) { if (esp_error == ESP_OK) {
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
} else {
esp_error = i2c_master_transmit_receive(driver_data->dev_handle, &reg, 1, data, data_size, timeout_ms); esp_error = i2c_master_transmit_receive(driver_data->dev_handle, &reg, 1, data, data_size, timeout_ms);
if (esp_error != ESP_OK) {
LOG_E(TAG, "read_register(0x%02X, reg=0x%02X) failed: %s", address, reg, esp_err_to_name(esp_error));
}
} }
unlock(driver_data); unlock(driver_data);
return esp_err_to_error(esp_error); return esp_err_to_error(esp_error);
@ -146,17 +126,12 @@ static error_t write_register(Device* device, uint8_t address, uint8_t reg, cons
lock(driver_data); lock(driver_data);
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms); esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
if (esp_error != ESP_OK) { if (esp_error == ESP_OK) {
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
} else {
i2c_master_transmit_multi_buffer_info_t buffers[2] = { i2c_master_transmit_multi_buffer_info_t buffers[2] = {
{.write_buffer = &reg, .buffer_size = 1}, {.write_buffer = &reg, .buffer_size = 1},
{.write_buffer = data, .buffer_size = data_size} {.write_buffer = data, .buffer_size = data_size},
}; };
esp_error = i2c_master_multi_buffer_transmit(driver_data->dev_handle, buffers, 2, timeout_ms); esp_error = i2c_master_multi_buffer_transmit(driver_data->dev_handle, buffers, 2, timeout_ms);
if (esp_error != ESP_OK) {
LOG_E(TAG, "write_register(0x%02X, reg=0x%02X) failed: %s", address, reg, esp_err_to_name(esp_error));
}
} }
unlock(driver_data); unlock(driver_data);
return esp_err_to_error(esp_error); return esp_err_to_error(esp_error);
@ -169,12 +144,6 @@ static error_t probe(Device* device, uint8_t address, TickType_t timeout) {
lock(driver_data); lock(driver_data);
esp_err_t esp_error = i2c_master_probe(driver_data->bus_handle, address, timeout_ms); esp_err_t esp_error = i2c_master_probe(driver_data->bus_handle, address, timeout_ms);
if (esp_error == ESP_ERR_NOT_FOUND) {
// Expected outcome when no device acks - e.g. hot-plug attach polling
LOG_D(TAG, "probe(0x%02X): not found", address);
} else if (esp_error != ESP_OK) {
LOG_E(TAG, "probe(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
}
unlock(driver_data); unlock(driver_data);
return esp_err_to_error(esp_error); return esp_err_to_error(esp_error);
} }
@ -286,11 +255,11 @@ static error_t stop(Device* device) {
return ERROR_NONE; return ERROR_NONE;
} }
i2c_master_bus_handle_t esp32_i2c_master_get_bus_handle(struct Device* device) { i2c_master_bus_handle_t esp32_i2c_master_get_bus_handle(Device* device) {
return GET_DATA(device)->bus_handle; return GET_DATA(device)->bus_handle;
} }
uint32_t esp32_i2c_master_get_clock_frequency(struct Device* device) { uint32_t esp32_i2c_master_get_clock_frequency(Device* device) {
return GET_CONFIG(device)->clockFrequency; return GET_CONFIG(device)->clockFrequency;
} }
@ -300,7 +269,7 @@ static constexpr I2cControllerApi ESP32_I2C_MASTER_API = {
.write_read = write_read, .write_read = write_read,
.read_register = read_register, .read_register = read_register,
.write_register = write_register, .write_register = write_register,
.probe = probe .probe = probe,
}; };
extern Module platform_esp32_module; extern Module platform_esp32_module;
@ -313,7 +282,7 @@ Driver esp32_i2c_master_driver = {
.api = &ESP32_I2C_MASTER_API, .api = &ESP32_I2C_MASTER_API,
.device_type = &I2C_CONTROLLER_TYPE, .device_type = &I2C_CONTROLLER_TYPE,
.owner = &platform_esp32_module, .owner = &platform_esp32_module,
.internal = nullptr .internal = nullptr,
}; };
} // extern "C" } // extern "C"

View File

@ -503,6 +503,7 @@ static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) {
.name = "usb_keyboard0", .name = "usb_keyboard0",
.config = nullptr, .config = nullptr,
.parent = nullptr, .parent = nullptr,
.flags = 0,
.internal = nullptr, .internal = nullptr,
}; };

View File

@ -84,8 +84,6 @@ extern "C" {
// GCC integer arithmetic helpers (needed on 32-bit targets for 64-bit ops) // GCC integer arithmetic helpers (needed on 32-bit targets for 64-bit ops)
long long __divdi3(long long a, long long b); long long __divdi3(long long a, long long b);
long long __moddi3(long long a, long long b); long long __moddi3(long long a, long long b);
unsigned long long __udivdi3(unsigned long long a, unsigned long long b);
unsigned long long __umoddi3(unsigned long long a, unsigned long long b);
#else #else
extern double __adddf3(double a, double b); extern double __adddf3(double a, double b);
extern double __subdf3(double a, double b); extern double __subdf3(double a, double b);
@ -120,6 +118,9 @@ extern "C" {
// GCC 64-bit integer arithmetic helpers (needed for 64-bit div on 32-bit RISC-V) // GCC 64-bit integer arithmetic helpers (needed for 64-bit div on 32-bit RISC-V)
long long __divdi3(long long a, long long b); long long __divdi3(long long a, long long b);
unsigned long long __udivdi3(unsigned long long a, unsigned long long b); unsigned long long __udivdi3(unsigned long long a, unsigned long long b);
unsigned long long (__atomic_load_8)(const volatile void*, int);
void (__atomic_store_8)(volatile void*, unsigned long long, int);
unsigned long long (__atomic_exchange_8)(volatile void*, unsigned long long, int);
#endif #endif
} }
@ -284,8 +285,6 @@ static const ModuleSymbol platform_esp32_symbols[] = {
DEFINE_MODULE_SYMBOL(__gtdf2), DEFINE_MODULE_SYMBOL(__gtdf2),
DEFINE_MODULE_SYMBOL(__divdi3), DEFINE_MODULE_SYMBOL(__divdi3),
DEFINE_MODULE_SYMBOL(__moddi3), DEFINE_MODULE_SYMBOL(__moddi3),
DEFINE_MODULE_SYMBOL(__udivdi3),
DEFINE_MODULE_SYMBOL(__umoddi3),
#else #else
DEFINE_MODULE_SYMBOL(__adddf3), DEFINE_MODULE_SYMBOL(__adddf3),
DEFINE_MODULE_SYMBOL(__subdf3), DEFINE_MODULE_SYMBOL(__subdf3),
@ -317,6 +316,9 @@ static const ModuleSymbol platform_esp32_symbols[] = {
DEFINE_MODULE_SYMBOL(__clzsi2), DEFINE_MODULE_SYMBOL(__clzsi2),
DEFINE_MODULE_SYMBOL(__divdi3), DEFINE_MODULE_SYMBOL(__divdi3),
DEFINE_MODULE_SYMBOL(__udivdi3), DEFINE_MODULE_SYMBOL(__udivdi3),
DEFINE_MODULE_SYMBOL(__atomic_load_8),
DEFINE_MODULE_SYMBOL(__atomic_store_8),
DEFINE_MODULE_SYMBOL(__atomic_exchange_8),
#endif #endif
MODULE_SYMBOL_TERMINATOR, MODULE_SYMBOL_TERMINATOR,
}; };

View File

@ -64,6 +64,11 @@ menu "Tactility App"
range 0 200 range 0 200
help help
Shared icon size in pixels Shared icon size in pixels
config TT_LVGL_STATUSBAR_COLORS_INVERTED
bool "Statusbar colors inverted"
default n
help
Invert the statusbar's foreground/background colors
config TT_AUTO_START_APP_ID config TT_AUTO_START_APP_ID
string "Auto Start App ID" string "Auto Start App ID"
default "" default ""

View File

@ -2,7 +2,6 @@
#include <Tactility/PubSub.h> #include <Tactility/PubSub.h>
#include <Tactility/RecursiveMutex.h> #include <Tactility/RecursiveMutex.h>
#include <Tactility/Tactility.h>
#include <Tactility/Timer.h> #include <Tactility/Timer.h>
#include <Tactility/lvgl/Statusbar.h> #include <Tactility/lvgl/Statusbar.h>
#include <Tactility/lvgl/Style.h> #include <Tactility/lvgl/Style.h>
@ -18,8 +17,18 @@
#include <memory> #include <memory>
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
namespace tt::lvgl { namespace tt::lvgl {
#if defined(CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED) && CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED
constexpr bool STATUSBAR_COLORS_INVERTED = true;
#else
constexpr bool STATUSBAR_COLORS_INVERTED = false;
#endif
constexpr auto* TAG = "statusbar"; constexpr auto* TAG = "statusbar";
static void onUpdateTime(); static void onUpdateTime();
@ -159,6 +168,23 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
auto* statusbar = reinterpret_cast<Statusbar*>(obj); auto* statusbar = reinterpret_cast<Statusbar*>(obj);
const auto fg_color = STATUSBAR_COLORS_INVERTED ? lv_color_black() : lv_color_white();
const auto bg_color = STATUSBAR_COLORS_INVERTED ? lv_color_white() : lv_color_black();
lv_obj_set_style_bg_color(obj, bg_color, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_STATE_DEFAULT);
// Inverted statusbar creates a separator between statusbar and the rest of the UI below it
if (STATUSBAR_COLORS_INVERTED) {
// Show border as a horizontal line on the bottom
lv_obj_set_style_border_color(obj, fg_color, LV_STATE_DEFAULT);
lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 1, LV_STATE_DEFAULT);
} else {
// No border
lv_obj_set_style_border_color(obj, bg_color, LV_STATE_DEFAULT);
}
lv_obj_set_width(obj, LV_PCT(100)); lv_obj_set_width(obj, LV_PCT(100));
lv_obj_set_style_pad_ver(obj, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_ver(obj, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_hor(obj, 2, LV_STATE_DEFAULT); lv_obj_set_style_pad_hor(obj, 2, LV_STATE_DEFAULT);
@ -171,21 +197,23 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
lv_obj_set_style_pad_column(obj, icon_padding, LV_STATE_DEFAULT); lv_obj_set_style_pad_column(obj, icon_padding, LV_STATE_DEFAULT);
statusbar->time = lv_label_create(obj); statusbar->time = lv_label_create(obj);
lv_obj_set_style_text_color(statusbar->time, lv_color_white(), LV_STATE_DEFAULT); lv_obj_set_style_text_color(statusbar->time, fg_color, LV_STATE_DEFAULT);
lv_obj_set_style_margin_left(statusbar->time, 4, LV_STATE_DEFAULT); lv_obj_set_style_margin_left(statusbar->time, 4, LV_STATE_DEFAULT);
update_time(statusbar); update_time(statusbar);
auto* left_spacer = lv_obj_create(obj); auto* left_spacer = lv_obj_create(obj);
lv_obj_remove_flag(left_spacer, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(left_spacer, 1, 1); lv_obj_set_size(left_spacer, 1, 1);
obj_set_style_bg_invisible(left_spacer);
lv_obj_set_flex_grow(left_spacer, 1); lv_obj_set_flex_grow(left_spacer, 1);
lv_obj_set_style_bg_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT);
lv_obj_set_style_border_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT);
statusbar_data.mutex.lock(MAX_TICKS); statusbar_data.mutex.lock(MAX_TICKS);
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
auto* image = lv_image_create(obj); auto* image = lv_image_create(obj);
lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work
lv_obj_set_style_text_font(image, lvgl_get_statusbar_icon_font(), LV_STATE_DEFAULT); lv_obj_set_style_text_font(image, lvgl_get_statusbar_icon_font(), LV_STATE_DEFAULT);
lv_obj_set_style_text_color(image, lv_color_white(), LV_STATE_DEFAULT); lv_obj_set_style_text_color(image, fg_color, LV_STATE_DEFAULT);
lv_obj_set_style_pad_all(image, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_all(image, 0, LV_STATE_DEFAULT);
statusbar->icons[i] = image; statusbar->icons[i] = image;

View File

@ -201,6 +201,16 @@ error_t gpio_descriptor_disable_interrupt(struct GpioDescriptor* descriptor);
*/ */
error_t gpio_controller_get_pin_count(struct Device* device, uint32_t* count); error_t gpio_controller_get_pin_count(struct Device* device, uint32_t* count);
/**
* @brief Gets the logical level of a GPIO pin directly by pin number, without acquiring it first.
* @param[in] device the GPIO controller device
* @param[in] pin the pin number
* @param[out] high pointer to store the pin level
* @retval ERROR_OUT_OF_RANGE if pin is not a valid pin number for this controller
* @return ERROR_NONE if successful
*/
error_t gpio_controller_get_level(struct Device* device, gpio_pin_t pin, bool* high);
/** /**
* @brief Initializes GPIO descriptors for a controller. * @brief Initializes GPIO descriptors for a controller.
* @param[in,out] device the GPIO controller device * @param[in,out] device the GPIO controller device

View File

@ -91,7 +91,7 @@ Driver battery_sense_power_supply_driver = {
.api = &BATTERY_SENSE_POWER_SUPPLY_API, .api = &BATTERY_SENSE_POWER_SUPPLY_API,
.device_type = &POWER_SUPPLY_TYPE, .device_type = &POWER_SUPPLY_TYPE,
.owner = nullptr, .owner = nullptr,
.internal = nullptr .internal = nullptr,
}; };
struct BatterySenseInternal { struct BatterySenseInternal {
@ -99,7 +99,15 @@ struct BatterySenseInternal {
}; };
static error_t create_power_supply_child(Device* parent, Device*& out_child) { static error_t create_power_supply_child(Device* parent, Device*& out_child) {
auto* child = new(std::nothrow) Device { .address = 0, .name = "battery-sense-power-supply", .config = nullptr, .parent = nullptr, .internal = nullptr }; auto* child = new(std::nothrow) Device {
.address = 0,
.name = "battery-sense-power-supply",
.config = nullptr,
.parent = nullptr,
.flags = 0,
.internal = nullptr,
};
if (child == nullptr) { if (child == nullptr) {
return ERROR_OUT_OF_MEMORY; return ERROR_OUT_OF_MEMORY;
} }

View File

@ -117,6 +117,21 @@ error_t gpio_controller_get_pin_count(Device* device, uint32_t* count) {
return ERROR_NONE; return ERROR_NONE;
} }
error_t gpio_controller_get_level(Device* device, gpio_pin_t pin, bool* high) {
auto* data = static_cast<struct GpioControllerData*>(device_get_driver_data(device));
mutex_lock(&data->mutex);
if (pin >= data->pin_count) {
mutex_unlock(&data->mutex);
return ERROR_OUT_OF_RANGE;
}
GpioDescriptor* desc = &data->descriptors[pin];
mutex_unlock(&data->mutex);
const auto* driver = device_get_driver(device);
return GPIO_INTERNAL_API(driver)->get_level(desc, high);
}
error_t gpio_controller_init_descriptors(Device* device, uint32_t pin_count, void* controller_context) { error_t gpio_controller_init_descriptors(Device* device, uint32_t pin_count, void* controller_context) {
auto* data = new(std::nothrow) GpioControllerData(pin_count, controller_context); auto* data = new(std::nothrow) GpioControllerData(pin_count, controller_context);
if (!data) return ERROR_OUT_OF_MEMORY; if (!data) return ERROR_OUT_OF_MEMORY;

View File

@ -204,6 +204,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(gpio_controller_init_descriptors), DEFINE_MODULE_SYMBOL(gpio_controller_init_descriptors),
DEFINE_MODULE_SYMBOL(gpio_controller_deinit_descriptors), DEFINE_MODULE_SYMBOL(gpio_controller_deinit_descriptors),
DEFINE_MODULE_SYMBOL(gpio_controller_get_controller_context), DEFINE_MODULE_SYMBOL(gpio_controller_get_controller_context),
DEFINE_MODULE_SYMBOL(gpio_controller_get_level),
DEFINE_MODULE_SYMBOL(GPIO_CONTROLLER_TYPE), DEFINE_MODULE_SYMBOL(GPIO_CONTROLLER_TYPE),
// drivers/grove // drivers/grove
DEFINE_MODULE_SYMBOL(grove_set_mode), DEFINE_MODULE_SYMBOL(grove_set_mode),

View File

@ -286,6 +286,9 @@ def write_lvgl_variables(output_file, device_properties: dict):
output_file.write(f"CONFIG_LV_COLOR_DEPTH={color_depth}\n") output_file.write(f"CONFIG_LV_COLOR_DEPTH={color_depth}\n")
output_file.write(f"CONFIG_LV_COLOR_DEPTH_{color_depth}=y\n") output_file.write(f"CONFIG_LV_COLOR_DEPTH_{color_depth}=y\n")
output_file.write("CONFIG_LV_DISP_DEF_REFR_PERIOD=10\n") output_file.write("CONFIG_LV_DISP_DEF_REFR_PERIOD=10\n")
has_statusbar_colors_inverted = get_boolean_property_or_false(device_properties, "lvgl.statusbarColorsInverted")
if has_statusbar_colors_inverted:
output_file.write("CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED=y\n")
theme = get_property_or_default(device_properties, "lvgl.theme", "DefaultDark") theme = get_property_or_default(device_properties, "lvgl.theme", "DefaultDark")
if theme == "DefaultDark": if theme == "DefaultDark":
output_file.write("CONFIG_LV_THEME_DEFAULT_DARK=y\n") output_file.write("CONFIG_LV_THEME_DEFAULT_DARK=y\n")