feat(rlcd): add Waveshare ST7305 board support

This commit is contained in:
Adolfo Reyna
2026-07-23 23:16:58 -04:00
parent 29e80cfd65
commit a60c9840ca
13 changed files with 768 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver st7305_driver;
static Driver* const st7305_drivers[] = {
&st7305_driver,
nullptr
};
Module st7305_module = {
.name = "st7305",
.drivers = st7305_drivers
};
} // extern "C"
+509
View File
@@ -0,0 +1,509 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/st7305.h>
#include <st7305_module.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 <driver/gpio.h>
#include <esp_err.h>
#include <esp_heap_caps.h>
#include <esp_lcd_io_spi.h>
#include <esp_lcd_panel_io.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <cstdlib>
#include <cstring>
#define TAG "ST7305"
#define GET_CONFIG(device) (static_cast<const St7305Config*>((device)->config))
namespace {
constexpr uint16_t PANEL_WIDTH = 400;
constexpr uint16_t PANEL_HEIGHT = 300;
constexpr size_t PANEL_BUFFER_SIZE = PANEL_WIDTH * PANEL_HEIGHT / 8;
constexpr uint8_t PANEL_COLUMN_START = 0x12;
constexpr uint8_t PANEL_COLUMN_END = 0x2A;
constexpr uint8_t PANEL_ROW_START = 0x00;
constexpr uint8_t PANEL_ROW_END = 0xC7;
constexpr uint8_t CMD_SLEEP_IN = 0x10;
constexpr uint8_t CMD_SLEEP_OUT = 0x11;
constexpr uint8_t CMD_INVERSION_OFF = 0x20;
constexpr uint8_t CMD_INVERSION_ON = 0x21;
constexpr uint8_t CMD_DISPLAY_OFF = 0x28;
constexpr uint8_t CMD_DISPLAY_ON = 0x29;
constexpr uint8_t CMD_COLUMN_ADDRESS = 0x2A;
constexpr uint8_t CMD_ROW_ADDRESS = 0x2B;
constexpr uint8_t CMD_MEMORY_WRITE = 0x2C;
struct InitCommand {
uint8_t command;
const uint8_t* data;
size_t data_size;
uint16_t delay_ms;
};
static const uint8_t INIT_D6[] = { 0x17, 0x02 };
static const uint8_t INIT_D1[] = { 0x01 };
static const uint8_t INIT_C0[] = { 0x11, 0x04 };
static const uint8_t INIT_C1[] = { 0x41, 0x41, 0x41, 0x41 };
static const uint8_t INIT_C2[] = { 0x19, 0x19, 0x19, 0x19 };
static const uint8_t INIT_C4[] = { 0x41, 0x41, 0x41, 0x41 };
// Keep the second value exactly as the proven MicroPython RLCD sequence: decimal 19 (0x13).
static const uint8_t INIT_C5[] = { 0x19, 0x13, 0x19, 0x19 };
static const uint8_t INIT_D8[] = { 0xA6, 0xE9 };
static const uint8_t INIT_B2[] = { 0x05 };
static const uint8_t INIT_B3[] = { 0xE5, 0xF6, 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45 };
static const uint8_t INIT_B4[] = { 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45 };
static const uint8_t INIT_62[] = { 0x32, 0x03, 0x1F };
static const uint8_t INIT_B7[] = { 0x13 };
static const uint8_t INIT_B0[] = { 0x64 };
static const uint8_t INIT_C9[] = { 0x00 };
static const uint8_t INIT_36[] = { 0x48 };
static const uint8_t INIT_3A[] = { 0x11 };
static const uint8_t INIT_B9[] = { 0x20 };
static const uint8_t INIT_B8[] = { 0x29 };
static const uint8_t INIT_2A[] = { PANEL_COLUMN_START, PANEL_COLUMN_END };
static const uint8_t INIT_2B[] = { PANEL_ROW_START, PANEL_ROW_END };
static const uint8_t INIT_35[] = { 0x00 };
static const uint8_t INIT_D0[] = { 0xFF };
// Command order and voltage values are intentionally identical to lib/rlcd.py in the supplied
// MicroPython project. The inversion command sits between these two groups and display-on follows.
static const InitCommand INIT_SEQUENCE_BEFORE_INVERSION[] = {
{ 0xD6, INIT_D6, sizeof(INIT_D6), 0 },
{ 0xD1, INIT_D1, sizeof(INIT_D1), 0 },
{ 0xC0, INIT_C0, sizeof(INIT_C0), 0 },
{ 0xC1, INIT_C1, sizeof(INIT_C1), 0 },
{ 0xC2, INIT_C2, sizeof(INIT_C2), 0 },
{ 0xC4, INIT_C4, sizeof(INIT_C4), 0 },
{ 0xC5, INIT_C5, sizeof(INIT_C5), 0 },
{ 0xD8, INIT_D8, sizeof(INIT_D8), 0 },
{ 0xB2, INIT_B2, sizeof(INIT_B2), 0 },
{ 0xB3, INIT_B3, sizeof(INIT_B3), 0 },
{ 0xB4, INIT_B4, sizeof(INIT_B4), 0 },
{ 0x62, INIT_62, sizeof(INIT_62), 0 },
{ 0xB7, INIT_B7, sizeof(INIT_B7), 0 },
{ 0xB0, INIT_B0, sizeof(INIT_B0), 0 },
{ CMD_SLEEP_OUT, nullptr, 0, 200 },
{ 0xC9, INIT_C9, sizeof(INIT_C9), 0 },
{ 0x36, INIT_36, sizeof(INIT_36), 0 },
{ 0x3A, INIT_3A, sizeof(INIT_3A), 0 },
{ 0xB9, INIT_B9, sizeof(INIT_B9), 0 },
{ 0xB8, INIT_B8, sizeof(INIT_B8), 0 },
};
static const InitCommand INIT_SEQUENCE_AFTER_INVERSION[] = {
{ CMD_COLUMN_ADDRESS, INIT_2A, sizeof(INIT_2A), 0 },
{ CMD_ROW_ADDRESS, INIT_2B, sizeof(INIT_2B), 0 },
{ 0x35, INIT_35, sizeof(INIT_35), 0 },
{ 0xD0, INIT_D0, sizeof(INIT_D0), 0 },
{ 0x38, nullptr, 0, 0 },
};
struct St7305Internal {
esp_lcd_panel_io_handle_t io_handle;
SemaphoreHandle_t transfer_done;
uint8_t* panel_buffer;
};
static int pin_or_unused(const GpioPinSpec& pin) {
return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin);
}
static bool IRAM_ATTR on_color_transfer_done(
esp_lcd_panel_io_handle_t,
esp_lcd_panel_io_event_data_t*,
void* user_context
) {
auto* internal = static_cast<St7305Internal*>(user_context);
BaseType_t high_task_woken = pdFALSE;
xSemaphoreGiveFromISR(internal->transfer_done, &high_task_woken);
return high_task_woken == pdTRUE;
}
static bool send_command(
esp_lcd_panel_io_handle_t io_handle,
uint8_t command,
const void* data = nullptr,
size_t data_size = 0
) {
esp_err_t result = esp_lcd_panel_io_tx_param(io_handle, command, data, data_size);
if (result != ESP_OK) {
LOG_E(TAG, "Command 0x%02X failed: %s", command, esp_err_to_name(result));
return false;
}
return true;
}
template<size_t Size>
static bool send_init_sequence(esp_lcd_panel_io_handle_t io_handle, const InitCommand (&sequence)[Size]) {
for (const auto& item : sequence) {
if (!send_command(io_handle, item.command, item.data, item.data_size)) {
return false;
}
if (item.delay_ms > 0) {
vTaskDelay(pdMS_TO_TICKS(item.delay_ms));
}
}
return true;
}
static bool perform_hardware_reset(const St7305Config* config) {
const int reset_pin = pin_or_unused(config->pin_reset);
if (reset_pin < 0) {
LOG_E(TAG, "Reset pin is required");
return false;
}
gpio_config_t io_config = {
.pin_bit_mask = 1ULL << reset_pin,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t result = gpio_config(&io_config);
if (result != ESP_OK) {
LOG_E(TAG, "Failed to configure reset pin: %s", esp_err_to_name(result));
return false;
}
// Active-low reset, matching RLCD.reset(): released -> asserted -> released.
if (gpio_set_level(static_cast<gpio_num_t>(reset_pin), 1) != ESP_OK) {
return false;
}
vTaskDelay(pdMS_TO_TICKS(50));
if (gpio_set_level(static_cast<gpio_num_t>(reset_pin), 0) != ESP_OK) {
return false;
}
vTaskDelay(pdMS_TO_TICKS(20));
if (gpio_set_level(static_cast<gpio_num_t>(reset_pin), 1) != ESP_OK) {
return false;
}
vTaskDelay(pdMS_TO_TICKS(50));
return true;
}
static bool set_address_window(esp_lcd_panel_io_handle_t io_handle) {
return send_command(io_handle, CMD_COLUMN_ADDRESS, INIT_2A, sizeof(INIT_2A)) &&
send_command(io_handle, CMD_ROW_ADDRESS, INIT_2B, sizeof(INIT_2B));
}
static bool send_panel_buffer(St7305Internal* internal) {
if (!set_address_window(internal->io_handle)) {
return false;
}
// Parameter transactions can leave a callback signal on some ESP-IDF versions. Drain it so
// this wait can only be completed by the color transaction queued immediately below.
xSemaphoreTake(internal->transfer_done, 0);
esp_err_t result = esp_lcd_panel_io_tx_color(
internal->io_handle,
CMD_MEMORY_WRITE,
internal->panel_buffer,
PANEL_BUFFER_SIZE
);
if (result != ESP_OK) {
LOG_E(TAG, "Panel transfer failed: %s", esp_err_to_name(result));
return false;
}
xSemaphoreTake(internal->transfer_done, portMAX_DELAY);
return true;
}
// Converts Tactility's DISPLAY_COLOR_FORMAT_MONOCHROME (row-major, MSB-first, bit 1 = white)
// into the ST7305's column-major 2x4 packing. The working MicroPython canvas uses bit 1 = black
// and enables panel inversion, so each LVGL source pair is inverted before it is packed.
static void pack_monochrome_frame(const uint8_t* source, uint8_t* destination) {
constexpr size_t source_stride = PANEL_WIDTH / 8;
constexpr size_t destination_column_stride = PANEL_HEIGHT / 4;
for (uint16_t byte_x = 0; byte_x < PANEL_WIDTH / 2; byte_x++) {
const uint16_t x = byte_x * 2;
const uint8_t source_pair_shift = static_cast<uint8_t>(6 - (x & 0x07));
for (uint16_t block_y = 0; block_y < PANEL_HEIGHT / 4; block_y++) {
uint8_t packed = 0;
for (uint8_t local_y = 0; local_y < 4; local_y++) {
const uint16_t y = PANEL_HEIGHT - 1 - (block_y * 4 + local_y);
const uint8_t white_pair =
(source[y * source_stride + x / 8] >> source_pair_shift) & 0x03;
const uint8_t black_pair = white_pair ^ 0x03;
packed |= static_cast<uint8_t>(black_pair << (6 - local_y * 2));
}
destination[byte_x * destination_column_stride + block_y] = packed;
}
}
}
static bool initialize_panel(Device* device) {
auto* internal = static_cast<St7305Internal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
if (!send_init_sequence(internal->io_handle, INIT_SEQUENCE_BEFORE_INVERSION)) {
return false;
}
if (!send_command(
internal->io_handle,
config->invert_color ? CMD_INVERSION_ON : CMD_INVERSION_OFF
)) {
return false;
}
if (!send_init_sequence(internal->io_handle, INIT_SEQUENCE_AFTER_INVERSION)) {
return false;
}
if (!send_command(internal->io_handle, CMD_DISPLAY_ON)) {
return false;
}
// The Python reference starts with an all-zero hardware buffer. With inversion enabled this
// clears retained GRAM to reflective white before LVGL submits its first full frame.
memset(internal->panel_buffer, 0, PANEL_BUFFER_SIZE);
return send_panel_buffer(internal);
}
} // namespace
// region DisplayApi
static error_t st7305_reset(Device* device) {
return perform_hardware_reset(GET_CONFIG(device)) ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7305_init(Device* device) {
return initialize_panel(device) ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7305_draw_bitmap(
Device* device,
int32_t x_start,
int32_t y_start,
int32_t x_end,
int32_t y_end,
const void* color_data
) {
if (x_start != 0 || y_start != 0 || x_end != PANEL_WIDTH || y_end != PANEL_HEIGHT ||
color_data == nullptr) {
LOG_E(TAG, "ST7305 requires a complete %ux%u frame", PANEL_WIDTH, PANEL_HEIGHT);
return ERROR_INVALID_ARGUMENT;
}
auto* internal = static_cast<St7305Internal*>(device_get_driver_data(device));
pack_monochrome_frame(static_cast<const uint8_t*>(color_data), internal->panel_buffer);
return send_panel_buffer(internal) ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t st7305_invert_color(Device* device, bool invert) {
auto* internal = static_cast<St7305Internal*>(device_get_driver_data(device));
return send_command(internal->io_handle, invert ? CMD_INVERSION_ON : CMD_INVERSION_OFF)
? ERROR_NONE
: ERROR_RESOURCE;
}
static error_t st7305_disp_on_off(Device* device, bool on) {
auto* internal = static_cast<St7305Internal*>(device_get_driver_data(device));
return send_command(internal->io_handle, on ? CMD_DISPLAY_ON : CMD_DISPLAY_OFF)
? ERROR_NONE
: ERROR_RESOURCE;
}
static error_t st7305_disp_sleep(Device* device, bool sleep) {
auto* internal = static_cast<St7305Internal*>(device_get_driver_data(device));
if (!send_command(internal->io_handle, sleep ? CMD_SLEEP_IN : CMD_SLEEP_OUT)) {
return ERROR_RESOURCE;
}
vTaskDelay(pdMS_TO_TICKS(sleep ? 10 : 120));
return ERROR_NONE;
}
static DisplayColorFormat st7305_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_MONOCHROME;
}
static uint16_t st7305_get_resolution_x(Device*) {
return PANEL_WIDTH;
}
static uint16_t st7305_get_resolution_y(Device*) {
return PANEL_HEIGHT;
}
static void st7305_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
*out_buffer = nullptr;
}
static uint8_t st7305_get_frame_buffer_count(Device*) {
return 0;
}
static const DisplayApi st7305_display_api = {
.capabilities = DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF |
DISPLAY_CAPABILITY_SLEEP | DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME,
.reset = st7305_reset,
.init = st7305_init,
.draw_bitmap = st7305_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 = st7305_invert_color,
.disp_on_off = st7305_disp_on_off,
.disp_sleep = st7305_disp_sleep,
.get_color_format = st7305_get_color_format,
.get_resolution_x = st7305_get_resolution_x,
.get_resolution_y = st7305_get_resolution_y,
.get_frame_buffer = st7305_get_frame_buffer,
.get_frame_buffer_count = st7305_get_frame_buffer_count,
.get_backlight = nullptr,
.has_capability = nullptr,
};
// endregion
// region Driver lifecycle
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
const auto* config = GET_CONFIG(device);
if (config->horizontal_resolution != PANEL_WIDTH ||
config->vertical_resolution != PANEL_HEIGHT) {
LOG_E(TAG, "Unsupported resolution %ux%u", config->horizontal_resolution, config->vertical_resolution);
return ERROR_NOT_SUPPORTED;
}
GpioPinSpec cs_pin;
if (esp32_spi_get_cs_pin(device, &cs_pin) != ERROR_NONE) {
LOG_E(TAG, "Failed to resolve CS pin");
return ERROR_RESOURCE;
}
auto* internal = static_cast<St7305Internal*>(calloc(1, sizeof(St7305Internal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->transfer_done = xSemaphoreCreateBinary();
if (internal->transfer_done == nullptr) {
free(internal);
return ERROR_OUT_OF_MEMORY;
}
internal->panel_buffer = static_cast<uint8_t*>(
heap_caps_malloc(PANEL_BUFFER_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_8BIT)
);
if (internal->panel_buffer == nullptr) {
vSemaphoreDelete(internal->transfer_done);
free(internal);
return ERROR_OUT_OF_MEMORY;
}
const auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
esp_lcd_panel_io_spi_config_t io_config = {
.cs_gpio_num = pin_or_unused(cs_pin),
.dc_gpio_num = pin_or_unused(config->pin_dc),
.spi_mode = 0,
.pclk_hz = config->pixel_clock_hz,
.trans_queue_depth = config->transaction_queue_depth,
.on_color_trans_done = on_color_transfer_done,
.user_ctx = internal,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.flags = {
.dc_high_on_cmd = 0,
.dc_low_on_data = 0,
.dc_low_on_param = 0,
.octal_mode = 0,
.quad_mode = 0,
.sio_mode = 1,
.lsb_first = 0,
.cs_high_active = 0,
},
};
esp_err_t result = esp_lcd_new_panel_io_spi(
static_cast<esp_lcd_spi_bus_handle_t>(spi_config->host),
&io_config,
&internal->io_handle
);
if (result != ESP_OK) {
LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(result));
heap_caps_free(internal->panel_buffer);
vSemaphoreDelete(internal->transfer_done);
free(internal);
return ERROR_RESOURCE;
}
device_set_driver_data(device, internal);
if (!perform_hardware_reset(config) || !initialize_panel(device)) {
LOG_E(TAG, "Failed to initialize panel");
device_set_driver_data(device, nullptr);
esp_lcd_panel_io_del(internal->io_handle);
heap_caps_free(internal->panel_buffer);
vSemaphoreDelete(internal->transfer_done);
free(internal);
return ERROR_RESOURCE;
}
LOG_I(TAG, "Initialized %ux%u reflective LCD at %lu Hz",
PANEL_WIDTH,
PANEL_HEIGHT,
static_cast<unsigned long>(config->pixel_clock_hz));
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<St7305Internal*>(device_get_driver_data(device));
if (internal == nullptr) {
return ERROR_NONE;
}
send_command(internal->io_handle, CMD_DISPLAY_OFF);
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
LOG_E(TAG, "Failed to delete panel IO");
return ERROR_RESOURCE;
}
const int reset_pin = pin_or_unused(GET_CONFIG(device)->pin_reset);
if (reset_pin >= 0) {
gpio_reset_pin(static_cast<gpio_num_t>(reset_pin));
}
heap_caps_free(internal->panel_buffer);
vSemaphoreDelete(internal->transfer_done);
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
Driver st7305_driver = {
.name = "st7305",
.compatible = (const char*[]) { "sitronix,st7305", nullptr },
.start_device = start,
.stop_device = stop,
.api = &st7305_display_api,
.device_type = &DISPLAY_TYPE,
.owner = &st7305_module,
.internal = nullptr
};