mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
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:
parent
0ee725ec32
commit
65ee223876
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -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
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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!
|
||||||
|
|||||||
156
Devices/cl32/source/cl32_detect.cpp
Normal file
156
Devices/cl32/source/cl32_detect.cpp
Normal 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);
|
||||||
|
}
|
||||||
16
Devices/cl32/source/cl32_detect.h
Normal file
16
Devices/cl32/source/cl32_detect.h
Normal 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();
|
||||||
4
Devices/cl32/source/cl32_v2.h
Normal file
4
Devices/cl32/source/cl32_v2.h
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
122
Devices/cl32/source/cl32_v2_keyboard.cpp
Normal file
122
Devices/cl32/source/cl32_v2_keyboard.cpp
Normal 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;
|
||||||
|
}
|
||||||
8
Devices/cl32/source/cl32_v2_keyboard.h
Normal file
8
Devices/cl32/source/cl32_v2_keyboard.h
Normal 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();
|
||||||
8
Devices/cl32/source/cl32_v3.h
Normal file
8
Devices/cl32/source/cl32_v3.h
Normal 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);
|
||||||
121
Devices/cl32/source/cl32_v3_keyboard.cpp
Normal file
121
Devices/cl32/source/cl32_v3_keyboard.cpp
Normal 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;
|
||||||
|
}
|
||||||
8
Devices/cl32/source/cl32_v3_keyboard.h
Normal file
8
Devices/cl32/source/cl32_v3_keyboard.h
Normal 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();
|
||||||
9
Devices/cl32/source/cl32_v4.h
Normal file
9
Devices/cl32/source/cl32_v4.h
Normal 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);
|
||||||
364
Devices/cl32/source/cl32_v4_keyboard.cpp
Normal file
364
Devices/cl32/source/cl32_v4_keyboard.cpp
Normal 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;
|
||||||
|
}
|
||||||
11
Devices/cl32/source/cl32_v4_keyboard.h
Normal file
11
Devices/cl32/source/cl32_v4_keyboard.h
Normal 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();
|
||||||
186
Devices/cl32/source/cl32_v4_power.cpp
Normal file
186
Devices/cl32/source/cl32_v4_power.cpp
Normal 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
|
||||||
|
};
|
||||||
6
Devices/cl32/source/cl32_v4_power.h
Normal file
6
Devices/cl32/source/cl32_v4_power.h
Normal 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;
|
||||||
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,6 @@
|
|||||||
|
|
||||||
gpio0 {
|
gpio0 {
|
||||||
compatible = "espressif,esp32-gpio";
|
compatible = "espressif,esp32-gpio";
|
||||||
gpio-count = <57>;
|
gpio-count = <55>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
gpio0 {
|
gpio0 {
|
||||||
compatible = "espressif,esp32-gpio";
|
compatible = "espressif,esp32-gpio";
|
||||||
gpio-count = <57>;
|
gpio-count = <55>;
|
||||||
};
|
};
|
||||||
|
|
||||||
i2c_internal {
|
i2c_internal {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
137
Libraries/esp_epaper/.gitignore
vendored
Normal 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/
|
||||||
519
Libraries/esp_epaper/ADDING_PANELS.md
Normal file
519
Libraries/esp_epaper/ADDING_PANELS.md
Normal 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)
|
||||||
22
Libraries/esp_epaper/CMakeLists.txt
Normal file
22
Libraries/esp_epaper/CMakeLists.txt
Normal 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
|
||||||
|
)
|
||||||
21
Libraries/esp_epaper/LICENSE
Normal file
21
Libraries/esp_epaper/LICENSE
Normal 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.
|
||||||
385
Libraries/esp_epaper/README.md
Normal file
385
Libraries/esp_epaper/README.md
Normal 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
|
||||||
|
|
||||||
|
[](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)
|
||||||
30
Libraries/esp_epaper/idf_component.yml
Normal file
30
Libraries/esp_epaper/idf_component.yml
Normal 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
|
||||||
129
Libraries/esp_epaper/include/epaper.h
Normal file
129
Libraries/esp_epaper/include/epaper.h
Normal 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_
|
||||||
243
Libraries/esp_epaper/include/epaper_config.h
Normal file
243
Libraries/esp_epaper/include/epaper_config.h
Normal 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_
|
||||||
111
Libraries/esp_epaper/include/epaper_lvgl.h
Normal file
111
Libraries/esp_epaper/include/epaper_lvgl.h
Normal 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_
|
||||||
82
Libraries/esp_epaper/include/epaper_panel.h
Normal file
82
Libraries/esp_epaper/include/epaper_panel.h
Normal 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_
|
||||||
167
Libraries/esp_epaper/src/controllers/acep_6color.c
Normal file
167
Libraries/esp_epaper/src/controllers/acep_6color.c
Normal 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);
|
||||||
|
}
|
||||||
230
Libraries/esp_epaper/src/controllers/bwry_4color.c
Normal file
230
Libraries/esp_epaper/src/controllers/bwry_4color.c
Normal 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);
|
||||||
|
}
|
||||||
291
Libraries/esp_epaper/src/controllers/gdey0154_lut.c
Normal file
291
Libraries/esp_epaper/src/controllers/gdey0154_lut.c
Normal 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);
|
||||||
|
}
|
||||||
291
Libraries/esp_epaper/src/controllers/ssd16xx.c
Normal file
291
Libraries/esp_epaper/src/controllers/ssd16xx.c
Normal 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);
|
||||||
|
}
|
||||||
375
Libraries/esp_epaper/src/epaper.c
Normal file
375
Libraries/esp_epaper/src/epaper.c
Normal 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;
|
||||||
|
}
|
||||||
28
Libraries/esp_epaper/src/epaper_common.c
Normal file
28
Libraries/esp_epaper/src/epaper_common.c
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Libraries/esp_epaper/src/epaper_common.h
Normal file
41
Libraries/esp_epaper/src/epaper_common.h
Normal 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_
|
||||||
622
Libraries/esp_epaper/src/epaper_lvgl.c
Normal file
622
Libraries/esp_epaper/src/epaper_lvgl.c
Normal 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;
|
||||||
|
}
|
||||||
219
Libraries/esp_epaper/src/epaper_registry.c
Normal file
219
Libraries/esp_epaper/src/epaper_registry.c
Normal 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];
|
||||||
|
}
|
||||||
173
Libraries/esp_epaper/src/epaper_spi.c
Normal file
173
Libraries/esp_epaper/src/epaper_spi.c
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Libraries/esp_epaper/src/epaper_spi.h
Normal file
33
Libraries/esp_epaper/src/epaper_spi.h
Normal 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_
|
||||||
@ -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),
|
||||||
|
|||||||
@ -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/).
|
||||||
|
|||||||
@ -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/
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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, ®, 1, data, data_size, timeout_ms);
|
esp_error = i2c_master_transmit_receive(driver_data->dev_handle, ®, 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 = ®, .buffer_size = 1},
|
{.write_buffer = ®, .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"
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 ""
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user