Implement CL-32 Support (#610)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
tactility_add_module(esp-epaper-module
|
||||
SRCS source/esp_epaper.cpp source/module.cpp
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel platform-esp32 driver esp_epaper
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
description: >
|
||||
E-paper display panels driven by the tuanpmt/esp_epaper ESP-IDF component
|
||||
(SSD1680/1681 BW, GDEY0154D67 and ACeP/BWRY color controllers). The node must
|
||||
be a child of an SPI controller: the display shares the parent's SPI host,
|
||||
clock and MOSI pins, and manages its DC/RESET/BUSY/CS pins itself via the
|
||||
component. Only 1bpp black/white panels are supported by the kernel display
|
||||
bridge (DISPLAY_COLOR_FORMAT_MONOCHROME); color panels fail to start.
|
||||
|
||||
compatible: "tuanpmt,esp-epaper"
|
||||
|
||||
bus: spi
|
||||
|
||||
properties:
|
||||
pin-dc:
|
||||
type: phandles
|
||||
required: true
|
||||
description: Data/Command GPIO pin
|
||||
pin-reset:
|
||||
type: phandles
|
||||
required: true
|
||||
description: Reset GPIO pin (the esp_epaper component drives it unconditionally)
|
||||
pin-busy:
|
||||
type: phandles
|
||||
required: true
|
||||
description: Busy GPIO pin (active high)
|
||||
pin-cs:
|
||||
type: phandles
|
||||
required: true
|
||||
description: Chip-select GPIO pin (driven manually by the component)
|
||||
clock-speed-hz:
|
||||
type: int
|
||||
default: 4000000
|
||||
description: SPI clock frequency in Hz (must be > 0)
|
||||
panel-type:
|
||||
type: text
|
||||
required: true
|
||||
description: >
|
||||
esp_epaper panel registry name. One of "gdey0154d67", "gdep073e01",
|
||||
"gdey037f51", "gdey029t71h", "ssd16xx-154", "ssd16xx-213", "ssd16xx-266",
|
||||
"ssd16xx-270", "ssd16xx-290", "ssd16xx-370", "ssd16xx-420".
|
||||
width:
|
||||
type: int
|
||||
default: 0
|
||||
description: Horizontal resolution override in pixels (0 = the panel's default, maximum 2048)
|
||||
height:
|
||||
type: int
|
||||
default: 0
|
||||
description: Vertical resolution override in pixels (0 = the panel's default, maximum 2048)
|
||||
update-mode:
|
||||
type: int
|
||||
default: 0
|
||||
description: >
|
||||
epd_update_mode_t used for frame updates: 0 = full, 1 = fast, 2 = partial.
|
||||
Full is the safest default; fast and partial refresh quickly but ghost.
|
||||
Values above 2 are rejected at driver start.
|
||||
rotation:
|
||||
type: int
|
||||
default: 0
|
||||
description: >
|
||||
Fixed display rotation applied at start. 0 = 0 degrees, 1 = 90 degrees,
|
||||
2 = 180 degrees, 3 = 270 degrees counter-clockwise (matching
|
||||
LV_DISPLAY_ROTATION_*). The kernel bridge reports the rotated resolution,
|
||||
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.
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/esp_epaper.h>
|
||||
|
||||
DEFINE_DEVICETREE(esp_epaper, struct EspEpaperConfig)
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <epaper_config.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
/**
|
||||
* @brief Panel configuration for an esp_epaper-driven e-paper display.
|
||||
*
|
||||
* The struct field order matches the binding's property order and must stay in
|
||||
* sync with it: the devicetree compiler emits struct initializers in binding
|
||||
* order, not declaration order.
|
||||
*/
|
||||
struct EspEpaperConfig {
|
||||
struct GpioPinSpec pin_dc;
|
||||
struct GpioPinSpec pin_reset;
|
||||
struct GpioPinSpec pin_busy;
|
||||
struct GpioPinSpec pin_cs;
|
||||
/** SPI clock frequency in Hz */
|
||||
int clock_speed_hz;
|
||||
/** Panel registry name, e.g. "ssd16xx-290" (see the binding's panel-type) */
|
||||
const char* panel_type;
|
||||
/** Horizontal resolution override; 0 = the panel's default */
|
||||
uint16_t width;
|
||||
/** Vertical resolution override; 0 = the panel's default */
|
||||
uint16_t height;
|
||||
/** epd_update_mode_t used for frame updates */
|
||||
epd_update_mode_t update_mode;
|
||||
/**
|
||||
* Fixed display rotation, 0 = 0 degrees, 1 = 90, 2 = 180, 3 = 270 degrees
|
||||
* counter-clockwise (LV_DISPLAY_ROTATION_*). Not changeable at runtime.
|
||||
*/
|
||||
uint8_t rotation;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module esp_epaper_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,353 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/esp_epaper.h>
|
||||
#include <esp_epaper_module.h>
|
||||
#include "esp_epaper_rotate.h"
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/drivers/esp32_spi.h>
|
||||
#include <tactility/drivers/spi_controller.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <epaper.h>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
constexpr auto* TAG = "esp_epaper";
|
||||
#define GET_CONFIG(device) (static_cast<const EspEpaperConfig*>((device)->config))
|
||||
|
||||
/** Width/height overrides above this are rejected as nonsense. */
|
||||
constexpr uint16_t MAX_PANEL_DIMENSION = 2048;
|
||||
|
||||
struct EspEpaperInternal {
|
||||
/** Opaque esp_epaper device, owns the panel's pins and SPI device. */
|
||||
epd_handle_t epd;
|
||||
epd_panel_info_t info;
|
||||
/** Scratch buffer in native panel layout, for rotated frames (rotation != 0). */
|
||||
uint8_t* rotate_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. */
|
||||
bool display_on;
|
||||
};
|
||||
|
||||
static uint16_t esp_epaper_get_display_width(const EspEpaperInternal* internal, uint8_t rotation) {
|
||||
return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.height : internal->info.width;
|
||||
}
|
||||
|
||||
static uint16_t esp_epaper_get_display_height(const EspEpaperInternal* internal, uint8_t rotation) {
|
||||
return esp_epaper_rotation_swaps_axes(rotation) ? internal->info.width : internal->info.height;
|
||||
}
|
||||
|
||||
static bool resolve_panel_type(const char* name, epd_panel_type_t* out_type) {
|
||||
struct PanelMapping {
|
||||
const char* name;
|
||||
epd_panel_type_t type;
|
||||
};
|
||||
static constexpr PanelMapping kPanelMappings[] = {
|
||||
{ "gdey0154d67", EPD_PANEL_GDEY0154D67 },
|
||||
{ "gdep073e01", EPD_PANEL_GDEP073E01 },
|
||||
{ "gdey037f51", EPD_PANEL_GDEY037F51 },
|
||||
{ "gdey029t71h", EPD_PANEL_GDEY029T71H },
|
||||
{ "ssd16xx-154", EPD_PANEL_SSD16XX_154 },
|
||||
{ "ssd16xx-213", EPD_PANEL_SSD16XX_213 },
|
||||
{ "ssd16xx-266", EPD_PANEL_SSD16XX_266 },
|
||||
{ "ssd16xx-270", EPD_PANEL_SSD16XX_270 },
|
||||
{ "ssd16xx-290", EPD_PANEL_SSD16XX_290 },
|
||||
{ "ssd16xx-370", EPD_PANEL_SSD16XX_370 },
|
||||
{ "ssd16xx-420", EPD_PANEL_SSD16XX_420 },
|
||||
};
|
||||
for (const auto& mapping : kPanelMappings) {
|
||||
if (std::strcmp(name, mapping.name) == 0) {
|
||||
*out_type = mapping.type;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static error_t esp_epaper_reset(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
// epd_wake() toggles the reset pin and re-runs the full init sequence.
|
||||
const esp_err_t ret = epd_wake(internal->epd);
|
||||
// epd_wake() re-inits the panel, so it is awake (and drawable) again.
|
||||
if (ret == ESP_OK) {
|
||||
internal->display_on = true;
|
||||
}
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t esp_epaper_init(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
const esp_err_t ret = epd_wake(internal->epd);
|
||||
if (ret == ESP_OK) {
|
||||
internal->display_on = true;
|
||||
}
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ret == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces
|
||||
// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode
|
||||
// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect.
|
||||
// hres/vres are the rotated (display) resolution reported by get_resolution_*; color_data is
|
||||
// row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1 with the palette header already stripped by
|
||||
// the caller); bit 1 = white, bit 0 = black. epd_update() expects exactly that layout and polarity.
|
||||
static error_t esp_epaper_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
const uint16_t display_width = esp_epaper_get_display_width(internal, config->rotation);
|
||||
const uint16_t display_height = esp_epaper_get_display_height(internal, config->rotation);
|
||||
if (x_start != 0 || y_start != 0 || x_end != display_width || y_end != display_height) {
|
||||
LOG_W(TAG, "draw_bitmap: only full-frame draws are supported (got %ld,%ld..%ld,%ld)", (long)x_start, (long)y_start, (long)x_end, (long)y_end);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
|
||||
if (!internal->display_on) {
|
||||
// Display is off (deep sleep). Drop the frame; the mismatch is fine because the next
|
||||
// power-on triggers a full refresh of the current render anyway.
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
const uint8_t* source;
|
||||
if (config->rotation == 0) {
|
||||
source = static_cast<const uint8_t*>(color_data);
|
||||
} else {
|
||||
memset(internal->rotate_buffer, 0, internal->info.buffer_size);
|
||||
esp_epaper_rotate_frame(static_cast<const uint8_t*>(color_data), internal->rotate_buffer,
|
||||
internal->info.width, internal->info.height, config->rotation);
|
||||
source = internal->rotate_buffer;
|
||||
}
|
||||
|
||||
const esp_err_t ret = epd_update(internal->epd, source, config->update_mode);
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "epd_update 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));
|
||||
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
if (on_off == internal->display_on) {
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
if (on_off) {
|
||||
if (epd_wake(internal->epd) != ESP_OK) {
|
||||
LOG_E(TAG, "epd_wake failed");
|
||||
ok = false;
|
||||
}
|
||||
} else {
|
||||
if (epd_sleep(internal->epd) != ESP_OK) {
|
||||
LOG_E(TAG, "epd_sleep failed");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
internal->display_on = on_off;
|
||||
}
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
return ok ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static DisplayColorFormat esp_epaper_get_color_format(Device*) {
|
||||
return DISPLAY_COLOR_FORMAT_MONOCHROME;
|
||||
}
|
||||
|
||||
static uint16_t esp_epaper_get_resolution_x(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
return esp_epaper_get_display_width(internal, GET_CONFIG(device)->rotation);
|
||||
}
|
||||
|
||||
static uint16_t esp_epaper_get_resolution_y(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
return esp_epaper_get_display_height(internal, GET_CONFIG(device)->rotation);
|
||||
}
|
||||
|
||||
static void esp_epaper_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
|
||||
*out_buffer = nullptr;
|
||||
}
|
||||
|
||||
static uint8_t esp_epaper_get_frame_buffer_count(Device*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const DisplayApi esp_epaper_display_api = {
|
||||
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH,
|
||||
.reset = esp_epaper_reset,
|
||||
.init = esp_epaper_init,
|
||||
.draw_bitmap = esp_epaper_draw_bitmap,
|
||||
.mirror = nullptr,
|
||||
.swap_xy = nullptr,
|
||||
.get_swap_xy = nullptr,
|
||||
.get_mirror_x = nullptr,
|
||||
.get_mirror_y = nullptr,
|
||||
.set_gap = nullptr,
|
||||
.get_gap_x = nullptr,
|
||||
.get_gap_y = nullptr,
|
||||
.invert_color = nullptr,
|
||||
.disp_on_off = esp_epaper_disp_on_off,
|
||||
.disp_sleep = nullptr,
|
||||
.get_color_format = esp_epaper_get_color_format,
|
||||
.get_resolution_x = esp_epaper_get_resolution_x,
|
||||
.get_resolution_y = esp_epaper_get_resolution_y,
|
||||
.get_frame_buffer = esp_epaper_get_frame_buffer,
|
||||
.get_frame_buffer_count = esp_epaper_get_frame_buffer_count,
|
||||
.get_backlight = nullptr,
|
||||
.has_capability = nullptr,
|
||||
};
|
||||
|
||||
static void free_internal(EspEpaperInternal* internal) {
|
||||
if (internal->epd != nullptr) {
|
||||
epd_deinit(internal->epd);
|
||||
}
|
||||
if (internal->rotate_buffer != nullptr) {
|
||||
free(internal->rotate_buffer);
|
||||
}
|
||||
if (internal->panel_mutex != nullptr) {
|
||||
vSemaphoreDelete(internal->panel_mutex);
|
||||
}
|
||||
free(internal);
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
|
||||
|
||||
const auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
epd_panel_type_t panel_type;
|
||||
if (!resolve_panel_type(config->panel_type, &panel_type)) {
|
||||
LOG_E(TAG, "Unknown panel type: %s", config->panel_type);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->clock_speed_hz == 0) {
|
||||
LOG_E(TAG, "Invalid clock_speed_hz (0)");
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->update_mode > EPD_UPDATE_PARTIAL) {
|
||||
LOG_E(TAG, "Invalid update_mode %d (expected %d-%d)", (int)config->update_mode, (int)EPD_UPDATE_FULL, (int)EPD_UPDATE_PARTIAL);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->width > MAX_PANEL_DIMENSION || config->height > MAX_PANEL_DIMENSION) {
|
||||
LOG_E(TAG, "Invalid panel size %ux%u (maximum %ux%u)", (unsigned)config->width, (unsigned)config->height, (unsigned)MAX_PANEL_DIMENSION, (unsigned)MAX_PANEL_DIMENSION);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
epd_config_t epd_config = EPD_CONFIG_DEFAULT();
|
||||
epd_config.pins.busy = config->pin_busy.pin;
|
||||
epd_config.pins.rst = config->pin_reset.pin;
|
||||
epd_config.pins.dc = config->pin_dc.pin;
|
||||
epd_config.pins.cs = config->pin_cs.pin;
|
||||
epd_config.pins.sck = spi_config->pin_sclk.pin;
|
||||
epd_config.pins.mosi = spi_config->pin_mosi.pin;
|
||||
epd_config.spi.host = spi_config->host;
|
||||
epd_config.spi.speed_hz = config->clock_speed_hz;
|
||||
epd_config.panel.type = panel_type;
|
||||
epd_config.panel.width = config->width;
|
||||
epd_config.panel.height = config->height;
|
||||
|
||||
epd_handle_t epd = nullptr;
|
||||
if (epd_init(&epd_config, &epd) != ESP_OK) {
|
||||
LOG_E(TAG, "epd_init failed");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* internal = static_cast<EspEpaperInternal*>(calloc(1, sizeof(EspEpaperInternal)));
|
||||
if (internal == nullptr) {
|
||||
epd_deinit(epd);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
internal->epd = epd;
|
||||
internal->panel_mutex = xSemaphoreCreateMutex();
|
||||
if (internal->panel_mutex == nullptr) {
|
||||
free_internal(internal);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
if (epd_get_info(epd, &internal->info) != ESP_OK) {
|
||||
LOG_E(TAG, "epd_get_info failed");
|
||||
free_internal(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (internal->info.color_mode != EPD_COLOR_BW) {
|
||||
LOG_E(TAG, "Panel color mode %d is not supported by the kernel display bridge (monochrome only)", internal->info.color_mode);
|
||||
free_internal(internal);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->rotation > 3) {
|
||||
LOG_E(TAG, "Invalid rotation %u (expected 0-3)", config->rotation);
|
||||
free_internal(internal);
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (config->rotation != 0) {
|
||||
internal->rotate_buffer = static_cast<uint8_t*>(malloc(internal->info.buffer_size));
|
||||
if (internal->rotate_buffer == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate %lu-byte rotation buffer", internal->info.buffer_size);
|
||||
free_internal(internal);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
|
||||
internal->display_on = true;
|
||||
device_set_driver_data(device, internal);
|
||||
|
||||
LOG_I(TAG, "Started %ux%u panel (buffer %lu bytes, rotation %u)", internal->info.width, internal->info.height, internal->info.buffer_size, config->rotation);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<EspEpaperInternal*>(device_get_driver_data(device));
|
||||
|
||||
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
|
||||
if (internal->display_on) {
|
||||
// Leave the panel in deep sleep.
|
||||
epd_sleep(internal->epd);
|
||||
internal->display_on = false;
|
||||
}
|
||||
xSemaphoreGive(internal->panel_mutex);
|
||||
|
||||
free_internal(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Driver esp_epaper_driver = {
|
||||
.name = "esp_epaper",
|
||||
.compatible = (const char*[]) { "tuanpmt,esp-epaper", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &esp_epaper_display_api,
|
||||
.device_type = &DISPLAY_TYPE,
|
||||
.owner = &esp_epaper_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Rotates a full 1bpp frame (row-major, MSB-first) from the display's rotated
|
||||
* space into the panel's native space. The display is the panel's native
|
||||
* width x height turned counter-clockwise by `rotation` (0 = 0, 1 = 90, 2 = 180,
|
||||
* 3 = 270 degrees), so its own dimensions are the swapped native ones for
|
||||
* 90/270. Each mapping is the inverse of LVGL's lv_display_rotate_area()
|
||||
* (Libraries/lvgl/src/display/lv_display.c), i.e. the exact transform LVGL
|
||||
* applies when displaying on the panel.
|
||||
*
|
||||
* src points to the display frame (stride = ceil(display_width / 8) bytes,
|
||||
* MSB-first bits), dst to the native frame (stride = ceil(width / 8) bytes).
|
||||
* dst is overwritten; it may not alias src.
|
||||
*/
|
||||
static inline bool esp_epaper_rotation_swaps_axes(uint8_t rotation) {
|
||||
return rotation == 1 || rotation == 3;
|
||||
}
|
||||
|
||||
static inline void esp_epaper_rotate_frame(const uint8_t* src, uint8_t* dst, uint16_t width, uint16_t height, uint8_t rotation) {
|
||||
const uint16_t display_width = esp_epaper_rotation_swaps_axes(rotation) ? height : width;
|
||||
const uint32_t src_stride = (display_width + 7) / 8;
|
||||
const uint32_t dst_stride = (width + 7) / 8;
|
||||
|
||||
memset(dst, 0, dst_stride * height);
|
||||
|
||||
for (uint16_t y = 0; y < height; y++) {
|
||||
for (uint16_t x = 0; x < width; x++) {
|
||||
// Display pixel (u, v) that lands on native pixel (x, y).
|
||||
uint16_t u;
|
||||
uint16_t v;
|
||||
switch (rotation) {
|
||||
case 0:
|
||||
u = x;
|
||||
v = y;
|
||||
break;
|
||||
case 1:
|
||||
u = height - 1 - y;
|
||||
v = x;
|
||||
break;
|
||||
case 2:
|
||||
u = width - 1 - x;
|
||||
v = height - 1 - y;
|
||||
break;
|
||||
case 3:
|
||||
default:
|
||||
u = y;
|
||||
v = width - 1 - x;
|
||||
break;
|
||||
}
|
||||
if (src[(uint32_t)v * src_stride + u / 8] & (0x80 >> (u % 8))) {
|
||||
dst[(uint32_t)y * dst_stride + x / 8] |= (0x80 >> (x % 8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern Driver esp_epaper_driver;
|
||||
|
||||
extern "C" {
|
||||
|
||||
static Driver* const esp_epaper_drivers[] = {
|
||||
&esp_epaper_driver,
|
||||
nullptr
|
||||
};
|
||||
|
||||
Module esp_epaper_module = {
|
||||
.name = "esp_epaper",
|
||||
.drivers = esp_epaper_drivers
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
Reference in New Issue
Block a user