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"]
|
||||
path = Libraries/cJSON/cJSON
|
||||
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_AUTO_START_APP_ID="")
|
||||
add_compile_definitions(CONFIG_TT_USER_DATA_LOCATION_INTERNAL)
|
||||
add_compile_definitions(CONFIG_TT_LVGL_STATUSBAR_COLORS_INVERTED=false)
|
||||
|
||||
endif ()
|
||||
|
||||
project(Tactility)
|
||||
|
||||
@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES source/*.c*)
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS "source"
|
||||
REQUIRES TactilityKernel
|
||||
REQUIRES TactilityKernel tca8418-module
|
||||
)
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/esp32_wifi.h>
|
||||
|
||||
#include <bindings/tca8418.h>
|
||||
#include <bindings/bm8563.h>
|
||||
#include <bindings/esp_epaper.h>
|
||||
|
||||
@ -26,8 +25,10 @@
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
// Top Stemma/Qwiic port 1 shares this bus with the keyboard and RTC (SDA: pin 1 <-> SCL: pin 2).
|
||||
// External I2C devices on that port are reachable via i2c0.
|
||||
// Top Stemma/Qwiic port 1 shares this bus with the RTC (SDA: pin 1 <-> SCL: pin 2). External
|
||||
// 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 {
|
||||
compatible = "espressif,esp32-i2c-master";
|
||||
port = <I2C_NUM_0>;
|
||||
@ -35,47 +36,6 @@
|
||||
pin-sda = <&gpio0 1 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 {
|
||||
compatible = "belling,bm8563";
|
||||
reg = <0x51>;
|
||||
|
||||
@ -20,3 +20,7 @@ display.dpi=139
|
||||
|
||||
lvgl.colorDepth=8
|
||||
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 "cl32_detect.h"
|
||||
#include "cl32_v4_keyboard.h"
|
||||
#include "cl32_v4_power.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
struct Module cl32_module = {
|
||||
.name = "cl32"
|
||||
static error_t start() {
|
||||
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 {
|
||||
compatible = "espressif,esp32-gpio";
|
||||
gpio-count = <57>;
|
||||
gpio-count = <55>;
|
||||
};
|
||||
};
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
|
||||
gpio0 {
|
||||
compatible = "espressif,esp32-gpio";
|
||||
gpio-count = <57>;
|
||||
gpio-count = <55>;
|
||||
};
|
||||
|
||||
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.colorDepth=16
|
||||
lvgl.uiScale=70
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
@ -26,6 +26,7 @@ lvgl.colorDepth=8
|
||||
# Monochrome theme: the default colour theme renders accent-coloured UI that
|
||||
# thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_THEME_MONO).
|
||||
lvgl.theme=Mono
|
||||
lvgl.statusbarColorsInverted=true
|
||||
|
||||
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
|
||||
# thresholds to invisible on a 1bpp panel (enables CONFIG_LV_USE_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.
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ display.dpi=235
|
||||
lvgl.colorDepth=8
|
||||
lvgl.fontSize=24
|
||||
lvgl.theme=Mono
|
||||
lvgl.statusbarColorsInverted=true
|
||||
|
||||
sdkconfig.CONFIG_EPD_DISPLAY_TYPE_ED047TC2=y
|
||||
sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0
|
||||
@ -54,6 +54,7 @@ static void create_gt911_touch(Device* i2c0) {
|
||||
.name = "touch0",
|
||||
.config = nullptr,
|
||||
.parent = nullptr,
|
||||
.flags = {},
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
@ -86,6 +87,7 @@ void tab5_create_devices_v1(Device* i2c0) {
|
||||
.name = "display0",
|
||||
.config = nullptr,
|
||||
.parent = nullptr,
|
||||
.flags = {},
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ void tab5_create_devices_v2(Device* i2c0) {
|
||||
.name = "display0",
|
||||
.config = nullptr,
|
||||
.parent = nullptr,
|
||||
.flags = {},
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ void create_st7123_touch(Device* i2c0) {
|
||||
.name = "touch0",
|
||||
.config = nullptr,
|
||||
.parent = nullptr,
|
||||
.flags = {},
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ void tab5_create_devices_v3(Device* i2c0) {
|
||||
.name = "display0",
|
||||
.config = nullptr,
|
||||
.parent = nullptr,
|
||||
.flags = {},
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ static void tab5_create_power_control(Device* io_expander1) {
|
||||
.name = "power_control0",
|
||||
.config = nullptr,
|
||||
.parent = nullptr,
|
||||
.flags = {},
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
|
||||
@ -658,6 +658,7 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
|
||||
|
||||
static const KeyboardApi tab5_keyboard_api = {
|
||||
.read_key = tab5_keyboard_read_key,
|
||||
.get_backlight = nullptr,
|
||||
.is_present = tab5_keyboard_is_attached,
|
||||
};
|
||||
|
||||
@ -693,6 +694,7 @@ void tab5_create_keyboard(Device* i2c2) {
|
||||
.name = "keyboard0",
|
||||
.config = nullptr,
|
||||
.parent = nullptr,
|
||||
.flags = {},
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
|
||||
gpio0 {
|
||||
compatible = "espressif,esp32-gpio";
|
||||
gpio-count = <57>;
|
||||
gpio-count = <55>;
|
||||
};
|
||||
|
||||
display_backlight_pwm {
|
||||
|
||||
@ -63,3 +63,13 @@ properties:
|
||||
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
|
||||
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" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <epaper_config.h>
|
||||
@ -37,6 +38,11 @@ struct EspEpaperConfig {
|
||||
* counter-clockwise (LV_DISPLAY_ROTATION_*). Not changeable at runtime.
|
||||
*/
|
||||
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
|
||||
|
||||
@ -33,6 +33,9 @@ struct EspEpaperInternal {
|
||||
epd_panel_info_t info;
|
||||
/** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */
|
||||
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. */
|
||||
SemaphoreHandle_t panel_mutex;
|
||||
/** 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;
|
||||
}
|
||||
|
||||
memcpy(internal->last_frame_buffer, source, internal->info.buffer_size);
|
||||
|
||||
spi_controller_lock(internal->spi_controller);
|
||||
const esp_err_t ret = epd_update(internal->epd, source, config->update_mode);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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,
|
||||
.init = esp_epaper_init,
|
||||
.draw_bitmap = esp_epaper_draw_bitmap,
|
||||
.clear = nullptr,
|
||||
.refresh = nullptr,
|
||||
.clear = esp_epaper_clear,
|
||||
.refresh = esp_epaper_refresh,
|
||||
.mirror = nullptr,
|
||||
.swap_xy = nullptr,
|
||||
.get_swap_xy = nullptr,
|
||||
@ -238,6 +295,9 @@ static void free_internal(EspEpaperInternal* internal) {
|
||||
if (internal->rotate_buffer != nullptr) {
|
||||
free(internal->rotate_buffer);
|
||||
}
|
||||
if (internal->last_frame_buffer != nullptr) {
|
||||
free(internal->last_frame_buffer);
|
||||
}
|
||||
if (internal->panel_mutex != nullptr) {
|
||||
vSemaphoreDelete(internal->panel_mutex);
|
||||
}
|
||||
@ -284,6 +344,8 @@ static error_t start(Device* device) {
|
||||
epd_config.panel.type = panel_type;
|
||||
epd_config.panel.width = config->width;
|
||||
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;
|
||||
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;
|
||||
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
|
||||
// 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) {
|
||||
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) {
|
||||
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.
|
||||
keymap-lc:
|
||||
type: array
|
||||
element-type: uint8_t
|
||||
element-type: uint32_t
|
||||
required: true
|
||||
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.
|
||||
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
|
||||
codepoint - byte range covers Latin-1 - for character and non-character keys).
|
||||
sym-row/sym-col instead). Non-zero values are sent as-is via KeyboardKeyData::key (any
|
||||
Unicode codepoint, including the CodePoint enum's non-character values) for character and
|
||||
non-character keys.
|
||||
keymap-uc:
|
||||
type: array
|
||||
element-type: uint8_t
|
||||
element-type: uint32_t
|
||||
description: >
|
||||
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).
|
||||
keymap-sy:
|
||||
type: array
|
||||
element-type: uint8_t
|
||||
element-type: uint32_t
|
||||
description: >
|
||||
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).
|
||||
|
||||
@ -16,11 +16,11 @@ struct Tca8418Config {
|
||||
uint8_t rows;
|
||||
uint8_t columns;
|
||||
bool reverse_columns;
|
||||
const uint8_t* keymap_lc;
|
||||
const uint32_t* keymap_lc;
|
||||
uint32_t keymap_lc_length;
|
||||
const uint8_t* keymap_uc;
|
||||
const uint32_t* keymap_uc;
|
||||
uint32_t keymap_uc_length;
|
||||
const uint8_t* keymap_sy;
|
||||
const uint32_t* keymap_sy;
|
||||
uint32_t keymap_sy_length;
|
||||
uint8_t shift_row;
|
||||
uint8_t shift_col;
|
||||
|
||||
@ -177,7 +177,7 @@ static error_t stop(Device* device) {
|
||||
|
||||
// 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;
|
||||
if (keymap == nullptr || index >= keymap_length) {
|
||||
return 0;
|
||||
@ -209,7 +209,7 @@ static void handle_key_event(Tca8418Internal* internal, const Tca8418Config* con
|
||||
} else if (is_sym) {
|
||||
internal->sym_pressed = true;
|
||||
} else {
|
||||
const uint8_t* keymap = config->keymap_lc;
|
||||
const uint32_t* keymap = config->keymap_lc;
|
||||
uint32_t keymap_length = config->keymap_lc_length;
|
||||
if (internal->sym_pressed) {
|
||||
keymap = config->keymap_sy;
|
||||
@ -218,8 +218,12 @@ static void handle_key_event(Tca8418Internal* internal, const Tca8418Config* con
|
||||
keymap = config->keymap_uc;
|
||||
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) {
|
||||
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
|
||||
// queued as an immediate press+release pair.
|
||||
push_pending(internal, chr, true);
|
||||
@ -275,6 +279,8 @@ static error_t tca8418_read_key(Device* device, KeyboardKeyData* data) {
|
||||
|
||||
static constexpr KeyboardApi tca8418_api = {
|
||||
.read_key = tca8418_read_key,
|
||||
.get_backlight = nullptr,
|
||||
.is_present = nullptr,
|
||||
};
|
||||
|
||||
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_unsubscribe),
|
||||
DEFINE_MODULE_SYMBOL(app_event_poll),
|
||||
DEFINE_MODULE_SYMBOL(app_event_emit_close),
|
||||
// app/execute
|
||||
DEFINE_MODULE_SYMBOL(app_execute),
|
||||
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
|
||||
|
||||
- `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`
|
||||
- `__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/).
|
||||
|
||||
@ -22,6 +22,12 @@ extern "C" {
|
||||
// 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 _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
|
||||
extern void __cxa_pure_virtual();
|
||||
// cxx_guards.cpp
|
||||
@ -99,6 +105,10 @@ static const ModuleSymbol SYMBOLS[] = {
|
||||
#ifdef ESP_PLATFORM
|
||||
DEFINE_MODULE_SYMBOL(_Znwj), // operator new(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
|
||||
{ "_ZSt7nothrow", (void*)&std::nothrow },
|
||||
DEFINE_MODULE_SYMBOL(__cxa_pure_virtual), // class-related, see https://arobenko.github.io/bare_metal_cpp/
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
namespace {
|
||||
|
||||
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 {
|
||||
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);
|
||||
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
|
||||
if (esp_error != ESP_OK) {
|
||||
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
|
||||
} else {
|
||||
if (esp_error == ESP_OK) {
|
||||
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);
|
||||
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);
|
||||
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
|
||||
if (esp_error != ESP_OK) {
|
||||
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
|
||||
} else {
|
||||
if (esp_error == ESP_OK) {
|
||||
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);
|
||||
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);
|
||||
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
|
||||
if (esp_error != ESP_OK) {
|
||||
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
|
||||
} else {
|
||||
if (esp_error == ESP_OK) {
|
||||
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);
|
||||
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);
|
||||
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
|
||||
if (esp_error != ESP_OK) {
|
||||
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
|
||||
} else {
|
||||
if (esp_error == ESP_OK) {
|
||||
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);
|
||||
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);
|
||||
esp_err_t esp_error = ensure_address(driver_data, address, timeout_ms);
|
||||
if (esp_error != ESP_OK) {
|
||||
LOG_E(TAG, "change_address(0x%02X) failed: %s", address, esp_err_to_name(esp_error));
|
||||
} else {
|
||||
if (esp_error == ESP_OK) {
|
||||
i2c_master_transmit_multi_buffer_info_t buffers[2] = {
|
||||
{.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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
return esp_err_to_error(esp_error);
|
||||
}
|
||||
@ -286,11 +255,11 @@ static error_t stop(Device* device) {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -300,7 +269,7 @@ static constexpr I2cControllerApi ESP32_I2C_MASTER_API = {
|
||||
.write_read = write_read,
|
||||
.read_register = read_register,
|
||||
.write_register = write_register,
|
||||
.probe = probe
|
||||
.probe = probe,
|
||||
};
|
||||
|
||||
extern Module platform_esp32_module;
|
||||
@ -313,7 +282,7 @@ Driver esp32_i2c_master_driver = {
|
||||
.api = &ESP32_I2C_MASTER_API,
|
||||
.device_type = &I2C_CONTROLLER_TYPE,
|
||||
.owner = &platform_esp32_module,
|
||||
.internal = nullptr
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@ -503,6 +503,7 @@ static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) {
|
||||
.name = "usb_keyboard0",
|
||||
.config = nullptr,
|
||||
.parent = nullptr,
|
||||
.flags = 0,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
|
||||
@ -84,8 +84,6 @@ extern "C" {
|
||||
// GCC integer arithmetic helpers (needed on 32-bit targets for 64-bit ops)
|
||||
long long __divdi3(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
|
||||
extern double __adddf3(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)
|
||||
long long __divdi3(long long a, 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
|
||||
}
|
||||
|
||||
@ -284,8 +285,6 @@ static const ModuleSymbol platform_esp32_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(__gtdf2),
|
||||
DEFINE_MODULE_SYMBOL(__divdi3),
|
||||
DEFINE_MODULE_SYMBOL(__moddi3),
|
||||
DEFINE_MODULE_SYMBOL(__udivdi3),
|
||||
DEFINE_MODULE_SYMBOL(__umoddi3),
|
||||
#else
|
||||
DEFINE_MODULE_SYMBOL(__adddf3),
|
||||
DEFINE_MODULE_SYMBOL(__subdf3),
|
||||
@ -317,6 +316,9 @@ static const ModuleSymbol platform_esp32_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(__clzsi2),
|
||||
DEFINE_MODULE_SYMBOL(__divdi3),
|
||||
DEFINE_MODULE_SYMBOL(__udivdi3),
|
||||
DEFINE_MODULE_SYMBOL(__atomic_load_8),
|
||||
DEFINE_MODULE_SYMBOL(__atomic_store_8),
|
||||
DEFINE_MODULE_SYMBOL(__atomic_exchange_8),
|
||||
#endif
|
||||
MODULE_SYMBOL_TERMINATOR,
|
||||
};
|
||||
|
||||
@ -64,6 +64,11 @@ menu "Tactility App"
|
||||
range 0 200
|
||||
help
|
||||
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
|
||||
string "Auto Start App ID"
|
||||
default ""
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
#include <Tactility/PubSub.h>
|
||||
#include <Tactility/RecursiveMutex.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/lvgl/Statusbar.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
@ -18,8 +17,18 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
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";
|
||||
|
||||
static void onUpdateTime();
|
||||
@ -159,6 +168,23 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
|
||||
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_style_pad_ver(obj, 0, 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);
|
||||
|
||||
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);
|
||||
update_time(statusbar);
|
||||
|
||||
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);
|
||||
obj_set_style_bg_invisible(left_spacer);
|
||||
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);
|
||||
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
|
||||
auto* image = lv_image_create(obj);
|
||||
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_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);
|
||||
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);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* @param[in,out] device the GPIO controller device
|
||||
|
||||
@ -91,7 +91,7 @@ Driver battery_sense_power_supply_driver = {
|
||||
.api = &BATTERY_SENSE_POWER_SUPPLY_API,
|
||||
.device_type = &POWER_SUPPLY_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
struct BatterySenseInternal {
|
||||
@ -99,7 +99,15 @@ struct BatterySenseInternal {
|
||||
};
|
||||
|
||||
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) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
@ -117,6 +117,21 @@ error_t gpio_controller_get_pin_count(Device* device, uint32_t* count) {
|
||||
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) {
|
||||
auto* data = new(std::nothrow) GpioControllerData(pin_count, controller_context);
|
||||
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_deinit_descriptors),
|
||||
DEFINE_MODULE_SYMBOL(gpio_controller_get_controller_context),
|
||||
DEFINE_MODULE_SYMBOL(gpio_controller_get_level),
|
||||
DEFINE_MODULE_SYMBOL(GPIO_CONTROLLER_TYPE),
|
||||
// drivers/grove
|
||||
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}=y\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")
|
||||
if theme == "DefaultDark":
|
||||
output_file.write("CONFIG_LV_THEME_DEFAULT_DARK=y\n")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user