mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-18 11:25:07 +00:00
Replaces the EPDiy and esp_epaper based display drivers with purpose-built drivers, and removes the Chat app.
689 lines
23 KiB
C++
689 lines
23 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
#include <drivers/uc8253.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 <tactility/memory.h>
|
|
|
|
#include <driver/gpio.h>
|
|
#include <driver/spi_master.h>
|
|
#include <esp_heap_caps.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/semphr.h>
|
|
#include <freertos/task.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <initializer_list>
|
|
|
|
#define TAG "Uc8253"
|
|
#define GET_CONFIG(device) (static_cast<const Uc8253Config*>((device)->config))
|
|
|
|
// The controller keeps an old and a new image buffer. A partial update writes a window of the new
|
|
// image, refreshes that window with the partial waveform and then copies the window to the old
|
|
// buffer. Updates are done by a task, so draw_bitmap() only thresholds into a 1 bit target buffer
|
|
// and queues the changed rectangle. Rectangles that arrive close together share one update.
|
|
|
|
struct Panel {
|
|
const char* name;
|
|
uint16_t width;
|
|
uint16_t height;
|
|
};
|
|
|
|
static constexpr Panel PANELS[] = {
|
|
{ "gdeq031t10", 240, 320 },
|
|
};
|
|
|
|
static constexpr uint32_t SPI_CHUNK_BYTES = 4096;
|
|
static constexpr uint32_t BUSY_TIMEOUT_MS = 10000;
|
|
static constexpr uint32_t COALESCE_MS = 30;
|
|
static constexpr uint32_t RETRY_DELAY_MS = 200;
|
|
static constexpr int MAX_RETRIES = 3;
|
|
|
|
static constexpr uint32_t CMD_UPDATE = 1U << 0;
|
|
static constexpr uint32_t CMD_FULL_REFRESH = 1U << 1;
|
|
static constexpr uint32_t CMD_SLEEP = 1U << 2;
|
|
static constexpr uint32_t CMD_WAKE = 1U << 3;
|
|
static constexpr uint32_t CMD_EXIT = 1U << 4;
|
|
static constexpr uint32_t CMD_RETRY = 1U << 5;
|
|
|
|
static inline bool is_white(uint8_t luminance) {
|
|
return luminance >= 128;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct Region {
|
|
int x0, y0, x1, y1;
|
|
|
|
bool empty() const { return x1 <= x0 || y1 <= y0; }
|
|
|
|
void merge(const Region& other) {
|
|
if (other.empty()) return;
|
|
if (empty()) {
|
|
*this = other;
|
|
return;
|
|
}
|
|
x0 = std::min(x0, other.x0);
|
|
y0 = std::min(y0, other.y0);
|
|
x1 = std::max(x1, other.x1);
|
|
y1 = std::max(y1, other.y1);
|
|
}
|
|
};
|
|
|
|
constexpr Region EMPTY_REGION = { 0, 0, 0, 0 };
|
|
|
|
} // namespace
|
|
|
|
struct Uc8253Internal {
|
|
Device* spi_controller;
|
|
spi_device_handle_t spi;
|
|
const Panel* panel;
|
|
int pin_dc;
|
|
int pin_reset;
|
|
int pin_busy;
|
|
int pin_cs;
|
|
TaskHandle_t task;
|
|
SemaphoreHandle_t request_mutex;
|
|
SemaphoreHandle_t request_done;
|
|
portMUX_TYPE lock;
|
|
volatile bool exited;
|
|
// 1 bit per pixel, 1 is white
|
|
uint8_t* target;
|
|
// Copy of the window being sent, in internal DMA capable memory
|
|
uint8_t* snapshot;
|
|
// Changes not yet picked up by the task, guarded by lock
|
|
Region pending;
|
|
// Only used by the task
|
|
// Set when an SPI transfer failed or the panel stayed busy, cleared at the start of each update
|
|
bool update_failed;
|
|
// Failed updates in a row, reset by each new display request
|
|
int retry_count;
|
|
// Both controller buffers hold the displayed image, so partial updates are possible
|
|
bool base_valid;
|
|
bool asleep;
|
|
};
|
|
|
|
// region SPI
|
|
|
|
static void write_bytes(Uc8253Internal* internal, const uint8_t* data, size_t length) {
|
|
while (length > 0) {
|
|
const size_t chunk = std::min<size_t>(length, SPI_CHUNK_BYTES);
|
|
spi_transaction_t transaction = {};
|
|
transaction.length = chunk * 8;
|
|
transaction.tx_buffer = data;
|
|
if (spi_device_polling_transmit(internal->spi, &transaction) != ESP_OK) {
|
|
LOG_E(TAG, "SPI transmit failed");
|
|
internal->update_failed = true;
|
|
return;
|
|
}
|
|
data += chunk;
|
|
length -= chunk;
|
|
}
|
|
}
|
|
|
|
static void write_command(Uc8253Internal* internal, uint8_t command) {
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_cs), 0);
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_dc), 0);
|
|
write_bytes(internal, &command, 1);
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_cs), 1);
|
|
}
|
|
|
|
static void write_command(Uc8253Internal* internal, uint8_t command, std::initializer_list<uint8_t> data) {
|
|
write_command(internal, command);
|
|
for (const uint8_t byte : data) {
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_cs), 0);
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_dc), 1);
|
|
write_bytes(internal, &byte, 1);
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_cs), 1);
|
|
}
|
|
}
|
|
|
|
static void write_command_with_buffer(Uc8253Internal* internal, uint8_t command, const uint8_t* data, size_t length) {
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_cs), 0);
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_dc), 0);
|
|
write_bytes(internal, &command, 1);
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_dc), 1);
|
|
write_bytes(internal, data, length);
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_cs), 1);
|
|
}
|
|
|
|
// The busy pin is low while the controller is busy
|
|
static void wait_busy(Uc8253Internal* internal) {
|
|
const TickType_t start = xTaskGetTickCount();
|
|
while (!gpio_get_level(static_cast<gpio_num_t>(internal->pin_busy))) {
|
|
if (xTaskGetTickCount() - start > pdMS_TO_TICKS(BUSY_TIMEOUT_MS)) {
|
|
LOG_W(TAG, "Timeout waiting for panel");
|
|
internal->update_failed = true;
|
|
return;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(2));
|
|
}
|
|
}
|
|
|
|
// endregion
|
|
|
|
// region Panel
|
|
|
|
static void reset_panel(Uc8253Internal* internal) {
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_reset), 0);
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_reset), 1);
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
}
|
|
|
|
static void init_panel(Uc8253Internal* internal) {
|
|
// Soft reset keeps the image buffers
|
|
write_command(internal, 0x00, { 0x1E, 0x0D });
|
|
vTaskDelay(pdMS_TO_TICKS(1));
|
|
// Panel setting: black and white, waveform from OTP
|
|
write_command(internal, 0x00, { 0x1F, 0x0D });
|
|
}
|
|
|
|
static void power_on(Uc8253Internal* internal) {
|
|
write_command(internal, 0x04);
|
|
spi_controller_unlock(internal->spi_controller);
|
|
wait_busy(internal);
|
|
spi_controller_lock(internal->spi_controller);
|
|
}
|
|
|
|
static void power_off(Uc8253Internal* internal) {
|
|
write_command(internal, 0x02);
|
|
spi_controller_unlock(internal->spi_controller);
|
|
wait_busy(internal);
|
|
spi_controller_lock(internal->spi_controller);
|
|
}
|
|
|
|
static void start_refresh(Uc8253Internal* internal) {
|
|
write_command(internal, 0x12);
|
|
spi_controller_unlock(internal->spi_controller);
|
|
// The busy pin is only valid 200 microseconds after the refresh command
|
|
vTaskDelay(pdMS_TO_TICKS(1));
|
|
wait_busy(internal);
|
|
spi_controller_lock(internal->spi_controller);
|
|
}
|
|
|
|
// Sets the partial window, all values in pixels with x in multiples of 8
|
|
static void set_window(Uc8253Internal* internal, const Region& region) {
|
|
const int last_row = region.y1 - 1;
|
|
write_command(internal, 0x90, {
|
|
static_cast<uint8_t>(region.x0),
|
|
static_cast<uint8_t>(region.x1 - 1),
|
|
static_cast<uint8_t>(region.y0 >> 8),
|
|
static_cast<uint8_t>(region.y0 & 0xFF),
|
|
static_cast<uint8_t>(last_row >> 8),
|
|
static_cast<uint8_t>(last_row & 0xFF),
|
|
0x01,
|
|
});
|
|
}
|
|
|
|
// Copies a window of the target into the snapshot. Returns its size in bytes.
|
|
static size_t snapshot_window(Uc8253Internal* internal, const Region& region) {
|
|
const size_t stride = internal->panel->width / 8;
|
|
const size_t row_bytes = (region.x1 - region.x0) / 8;
|
|
uint8_t* out = internal->snapshot;
|
|
for (int y = region.y0; y < region.y1; y++) {
|
|
memcpy(out, internal->target + y * stride + region.x0 / 8, row_bytes);
|
|
out += row_bytes;
|
|
}
|
|
return row_bytes * (region.y1 - region.y0);
|
|
}
|
|
|
|
// Writes the window of the snapshot to the old or new image buffer
|
|
static void write_window(Uc8253Internal* internal, uint8_t buffer_command, const Region& region, size_t size) {
|
|
write_command(internal, 0x91);
|
|
set_window(internal, region);
|
|
write_command_with_buffer(internal, buffer_command, internal->snapshot, size);
|
|
write_command(internal, 0x92);
|
|
}
|
|
|
|
// Redraws the whole panel. The fast waveform takes about a second and ghosts. The slow
|
|
// waveform takes longer and clears ghosting.
|
|
// Returns false when a transfer failed, which leaves the base image invalid.
|
|
static bool full_update(Uc8253Internal* internal, bool fast) {
|
|
const Panel& panel = *internal->panel;
|
|
const Region full = { 0, 0, panel.width, panel.height };
|
|
const size_t size = snapshot_window(internal, full);
|
|
internal->update_failed = false;
|
|
|
|
spi_controller_lock(internal->spi_controller);
|
|
init_panel(internal);
|
|
write_command_with_buffer(internal, 0x10, internal->snapshot, size);
|
|
write_command_with_buffer(internal, 0x13, internal->snapshot, size);
|
|
if (internal->update_failed) {
|
|
spi_controller_unlock(internal->spi_controller);
|
|
internal->base_valid = false;
|
|
return false;
|
|
}
|
|
if (fast) {
|
|
// Forced temperature selects the fast waveform
|
|
write_command(internal, 0xE0, { 0x02 });
|
|
write_command(internal, 0xE5, { 0x5A });
|
|
}
|
|
write_command(internal, 0x50, { 0x97 });
|
|
power_on(internal);
|
|
if (internal->update_failed) {
|
|
spi_controller_unlock(internal->spi_controller);
|
|
internal->base_valid = false;
|
|
return false;
|
|
}
|
|
start_refresh(internal);
|
|
power_off(internal);
|
|
spi_controller_unlock(internal->spi_controller);
|
|
|
|
internal->base_valid = !internal->update_failed;
|
|
return internal->base_valid;
|
|
}
|
|
|
|
// Refreshes only the window, which takes about 0.7 seconds
|
|
// Returns false when a transfer failed, which leaves the base image invalid.
|
|
static bool partial_update(Uc8253Internal* internal, const Region& region) {
|
|
const size_t size = snapshot_window(internal, region);
|
|
internal->update_failed = false;
|
|
|
|
spi_controller_lock(internal->spi_controller);
|
|
init_panel(internal);
|
|
write_window(internal, 0x13, region, size);
|
|
if (internal->update_failed) {
|
|
spi_controller_unlock(internal->spi_controller);
|
|
internal->base_valid = false;
|
|
return false;
|
|
}
|
|
|
|
write_command(internal, 0x91);
|
|
set_window(internal, region);
|
|
// Forced temperature selects the partial waveform
|
|
write_command(internal, 0xE0, { 0x02 });
|
|
write_command(internal, 0xE5, { 0x79 });
|
|
write_command(internal, 0x50, { 0xD7 });
|
|
power_on(internal);
|
|
if (internal->update_failed) {
|
|
spi_controller_unlock(internal->spi_controller);
|
|
internal->base_valid = false;
|
|
return false;
|
|
}
|
|
start_refresh(internal);
|
|
write_command(internal, 0x92);
|
|
power_off(internal);
|
|
|
|
// The next partial update compares against what is displayed now
|
|
init_panel(internal);
|
|
write_window(internal, 0x10, region, size);
|
|
spi_controller_unlock(internal->spi_controller);
|
|
|
|
internal->base_valid = !internal->update_failed;
|
|
return internal->base_valid;
|
|
}
|
|
|
|
static void enter_sleep(Uc8253Internal* internal) {
|
|
if (internal->asleep) return;
|
|
// Only a hardware reset wakes the controller from deep sleep
|
|
if (internal->pin_reset >= 0) {
|
|
spi_controller_lock(internal->spi_controller);
|
|
write_command(internal, 0x07, { 0xA5 });
|
|
spi_controller_unlock(internal->spi_controller);
|
|
}
|
|
internal->asleep = true;
|
|
internal->base_valid = false;
|
|
}
|
|
|
|
static void leave_sleep(Uc8253Internal* internal) {
|
|
if (!internal->asleep) return;
|
|
// Deep sleep loses the buffers, so the next update redraws the whole target
|
|
if (internal->pin_reset >= 0) {
|
|
reset_panel(internal);
|
|
}
|
|
internal->asleep = false;
|
|
internal->base_valid = false;
|
|
const Region full = { 0, 0, internal->panel->width, internal->panel->height };
|
|
portENTER_CRITICAL(&internal->lock);
|
|
internal->pending.merge(full);
|
|
portEXIT_CRITICAL(&internal->lock);
|
|
}
|
|
|
|
// Queues the whole target again after a failed transfer, with a growing delay. After
|
|
// MAX_RETRIES the next display request tries again.
|
|
static void retry_update(Uc8253Internal* internal) {
|
|
const Region full = { 0, 0, internal->panel->width, internal->panel->height };
|
|
portENTER_CRITICAL(&internal->lock);
|
|
internal->pending.merge(full);
|
|
portEXIT_CRITICAL(&internal->lock);
|
|
if (internal->retry_count >= MAX_RETRIES) {
|
|
LOG_E(TAG, "Giving up after %d retries", MAX_RETRIES);
|
|
return;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS << internal->retry_count));
|
|
internal->retry_count++;
|
|
xTaskNotify(internal->task, CMD_RETRY, eSetBits);
|
|
}
|
|
|
|
static void update_task(void* arg) {
|
|
auto* internal = static_cast<Uc8253Internal*>(arg);
|
|
|
|
while (true) {
|
|
uint32_t commands = 0;
|
|
xTaskNotifyWait(0, UINT32_MAX, &commands, portMAX_DELAY);
|
|
|
|
if (commands & CMD_EXIT) {
|
|
// The panel keeps its image in deep sleep
|
|
enter_sleep(internal);
|
|
break;
|
|
}
|
|
|
|
if (commands & CMD_SLEEP) {
|
|
enter_sleep(internal);
|
|
}
|
|
|
|
if (commands & (CMD_UPDATE | CMD_FULL_REFRESH)) {
|
|
internal->retry_count = 0;
|
|
}
|
|
|
|
if (commands & CMD_WAKE) {
|
|
leave_sleep(internal);
|
|
}
|
|
|
|
if (commands & CMD_FULL_REFRESH) {
|
|
if (!internal->asleep) {
|
|
// Cleared before the snapshot, so draws during the refresh stay queued
|
|
portENTER_CRITICAL(&internal->lock);
|
|
internal->pending = EMPTY_REGION;
|
|
portEXIT_CRITICAL(&internal->lock);
|
|
if (!full_update(internal, false)) {
|
|
retry_update(internal);
|
|
}
|
|
}
|
|
xSemaphoreGive(internal->request_done);
|
|
}
|
|
|
|
if (internal->asleep) {
|
|
continue;
|
|
}
|
|
|
|
if (commands & CMD_UPDATE) {
|
|
// Rectangles of one redraw arrive within a few milliseconds
|
|
vTaskDelay(pdMS_TO_TICKS(COALESCE_MS));
|
|
}
|
|
|
|
Region pending;
|
|
portENTER_CRITICAL(&internal->lock);
|
|
pending = internal->pending;
|
|
internal->pending = EMPTY_REGION;
|
|
portEXIT_CRITICAL(&internal->lock);
|
|
|
|
if (!pending.empty()) {
|
|
const bool ok = internal->base_valid ? partial_update(internal, pending) : full_update(internal, true);
|
|
if (!ok) {
|
|
retry_update(internal);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal->exited = true;
|
|
vTaskDelete(nullptr);
|
|
}
|
|
|
|
// endregion
|
|
|
|
// region Helpers
|
|
|
|
static void notify_task(Uc8253Internal* internal, uint32_t command) {
|
|
xTaskNotify(internal->task, command, eSetBits);
|
|
}
|
|
|
|
static error_t full_refresh_blocking(Uc8253Internal* internal) {
|
|
xSemaphoreTake(internal->request_mutex, portMAX_DELAY);
|
|
notify_task(internal, CMD_FULL_REFRESH);
|
|
xSemaphoreTake(internal->request_done, portMAX_DELAY);
|
|
xSemaphoreGive(internal->request_mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// endregion
|
|
|
|
// region DisplayApi
|
|
|
|
static error_t uc8253_reset(Device* device) {
|
|
auto* internal = static_cast<Uc8253Internal*>(device_get_driver_data(device));
|
|
notify_task(internal, CMD_SLEEP | CMD_WAKE);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static error_t uc8253_init(Device*) {
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// Reports GRAYSCALE8 so LVGL renders partial tiles. The luminance is thresholded to black and white.
|
|
static error_t uc8253_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<Uc8253Internal*>(device_get_driver_data(device));
|
|
const size_t stride = internal->panel->width / 8;
|
|
const int32_t width = x_end - x_start;
|
|
const auto* source = static_cast<const uint8_t*>(color_data);
|
|
|
|
for (int32_t row = 0; row < y_end - y_start; row++) {
|
|
uint8_t* target_row = internal->target + (y_start + row) * stride;
|
|
for (int32_t col = 0; col < width; col++) {
|
|
const int32_t x = x_start + col;
|
|
const uint8_t mask = 0x80 >> (x & 7);
|
|
if (is_white(source[row * width + col])) {
|
|
target_row[x / 8] |= mask;
|
|
} else {
|
|
target_row[x / 8] &= ~mask;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Windows are in whole bytes horizontally
|
|
const Region changed = { x_start & ~7, y_start, (x_end + 7) & ~7, y_end };
|
|
portENTER_CRITICAL(&internal->lock);
|
|
internal->pending.merge(changed);
|
|
portEXIT_CRITICAL(&internal->lock);
|
|
notify_task(internal, CMD_UPDATE);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static error_t uc8253_clear(Device* device) {
|
|
auto* internal = static_cast<Uc8253Internal*>(device_get_driver_data(device));
|
|
memset(internal->target, 0xFF, internal->panel->width / 8 * internal->panel->height);
|
|
return full_refresh_blocking(internal);
|
|
}
|
|
|
|
static error_t uc8253_refresh(Device* device) {
|
|
auto* internal = static_cast<Uc8253Internal*>(device_get_driver_data(device));
|
|
return full_refresh_blocking(internal);
|
|
}
|
|
|
|
static error_t uc8253_disp_on_off(Device* device, bool on_off) {
|
|
auto* internal = static_cast<Uc8253Internal*>(device_get_driver_data(device));
|
|
notify_task(internal, on_off ? CMD_WAKE : CMD_SLEEP);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static DisplayColorFormat uc8253_get_color_format(Device*) {
|
|
return DISPLAY_COLOR_FORMAT_GRAYSCALE8;
|
|
}
|
|
|
|
static uint16_t uc8253_get_resolution_x(Device* device) {
|
|
return static_cast<Uc8253Internal*>(device_get_driver_data(device))->panel->width;
|
|
}
|
|
|
|
static uint16_t uc8253_get_resolution_y(Device* device) {
|
|
return static_cast<Uc8253Internal*>(device_get_driver_data(device))->panel->height;
|
|
}
|
|
|
|
// endregion
|
|
|
|
static const DisplayApi uc8253_display_api = {
|
|
// draw_bitmap() converts into its own target buffer and never DMAs from the given pointer
|
|
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM,
|
|
.reset = uc8253_reset,
|
|
.init = uc8253_init,
|
|
.draw_bitmap = uc8253_draw_bitmap,
|
|
.clear = uc8253_clear,
|
|
.refresh = uc8253_refresh,
|
|
.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 = uc8253_disp_on_off,
|
|
.disp_sleep = nullptr,
|
|
.get_color_format = uc8253_get_color_format,
|
|
.get_resolution_x = uc8253_get_resolution_x,
|
|
.get_resolution_y = uc8253_get_resolution_y,
|
|
.get_frame_buffer = nullptr,
|
|
.get_frame_buffer_count = nullptr,
|
|
.get_backlight = nullptr,
|
|
.has_capability = nullptr,
|
|
.get_shape = nullptr,
|
|
};
|
|
|
|
// region Driver lifecycle
|
|
|
|
static const Panel* find_panel(const char* name) {
|
|
for (const Panel& panel : PANELS) {
|
|
if (strcmp(panel.name, name) == 0) {
|
|
return &panel;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static void configure_gpio(const Uc8253Internal* internal) {
|
|
gpio_config_t outputs = {
|
|
.pin_bit_mask = (1ULL << internal->pin_dc) | (1ULL << internal->pin_cs),
|
|
.mode = GPIO_MODE_OUTPUT,
|
|
.pull_up_en = GPIO_PULLUP_DISABLE,
|
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
|
.intr_type = GPIO_INTR_DISABLE,
|
|
};
|
|
if (internal->pin_reset >= 0) {
|
|
outputs.pin_bit_mask |= 1ULL << internal->pin_reset;
|
|
}
|
|
gpio_config(&outputs);
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_cs), 1);
|
|
if (internal->pin_reset >= 0) {
|
|
gpio_set_level(static_cast<gpio_num_t>(internal->pin_reset), 1);
|
|
}
|
|
|
|
gpio_config_t input = {
|
|
.pin_bit_mask = 1ULL << internal->pin_busy,
|
|
.mode = GPIO_MODE_INPUT,
|
|
.pull_up_en = GPIO_PULLUP_DISABLE,
|
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
|
.intr_type = GPIO_INTR_DISABLE,
|
|
};
|
|
gpio_config(&input);
|
|
}
|
|
|
|
static void destroy(Uc8253Internal* internal) {
|
|
if (internal->spi != nullptr) spi_bus_remove_device(internal->spi);
|
|
if (internal->request_mutex != nullptr) vSemaphoreDelete(internal->request_mutex);
|
|
if (internal->request_done != nullptr) vSemaphoreDelete(internal->request_done);
|
|
memory_free(internal->target);
|
|
heap_caps_free(internal->snapshot);
|
|
memory_free(internal);
|
|
}
|
|
|
|
static error_t start(Device* device) {
|
|
Device* 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);
|
|
|
|
const Panel* panel = find_panel(config->panel_type);
|
|
if (panel == nullptr) {
|
|
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");
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
|
|
static const MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 };
|
|
static const MemoryPolicy external_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
|
|
auto* internal = static_cast<Uc8253Internal*>(memory_calloc_with_policy(1, sizeof(Uc8253Internal), &internal_policy));
|
|
if (internal == nullptr) {
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
const size_t frame_bytes = panel->width / 8 * panel->height;
|
|
internal->spi_controller = parent;
|
|
internal->panel = panel;
|
|
internal->pin_dc = config->pin_dc.pin;
|
|
internal->pin_reset = config->pin_reset.gpio_controller != nullptr ? static_cast<int>(config->pin_reset.pin) : -1;
|
|
internal->pin_busy = config->pin_busy.pin;
|
|
internal->pin_cs = config->pin_cs.pin;
|
|
internal->lock = portMUX_INITIALIZER_UNLOCKED;
|
|
internal->request_mutex = xSemaphoreCreateMutex();
|
|
internal->request_done = xSemaphoreCreateBinary();
|
|
internal->target = static_cast<uint8_t*>(memory_alloc_with_policy(frame_bytes, &external_policy));
|
|
internal->snapshot = static_cast<uint8_t*>(heap_caps_malloc(frame_bytes, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL));
|
|
if (!internal->request_mutex || !internal->request_done || !internal->target || !internal->snapshot) {
|
|
destroy(internal);
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
memset(internal->target, 0xFF, frame_bytes);
|
|
|
|
configure_gpio(internal);
|
|
|
|
spi_device_interface_config_t device_config = {};
|
|
device_config.clock_speed_hz = config->clock_speed_hz;
|
|
device_config.mode = 0;
|
|
device_config.spics_io_num = -1;
|
|
device_config.queue_size = 1;
|
|
if (spi_bus_add_device(spi_config->host, &device_config, &internal->spi) != ESP_OK) {
|
|
LOG_E(TAG, "Failed to add SPI device");
|
|
internal->spi = nullptr;
|
|
destroy(internal);
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
if (xTaskCreate(update_task, "uc8253", 4096, internal, 4, &internal->task) != pdPASS) {
|
|
LOG_E(TAG, "Failed to create update task");
|
|
destroy(internal);
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
device_set_driver_data(device, internal);
|
|
LOG_I(TAG, "Started %s %ux%u", panel->name, panel->width, panel->height);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static error_t stop(Device* device) {
|
|
auto* internal = static_cast<Uc8253Internal*>(device_get_driver_data(device));
|
|
|
|
notify_task(internal, CMD_EXIT);
|
|
while (!internal->exited) {
|
|
vTaskDelay(1);
|
|
}
|
|
|
|
device_set_driver_data(device, nullptr);
|
|
destroy(internal);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// endregion
|
|
|
|
Driver uc8253_driver = {
|
|
.name = "uc8253",
|
|
.compatible = (const char*[]) { "ultrachip,uc8253", nullptr },
|
|
.start_device = start,
|
|
.stop_device = stop,
|
|
.probe = nullptr,
|
|
.api = &uc8253_display_api,
|
|
.device_type = &DISPLAY_TYPE,
|
|
.internal = nullptr
|
|
};
|