// SPDX-License-Identifier: Apache-2.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define TAG "ST7305" #define GET_CONFIG(device) (static_cast((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(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(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 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(reset_pin), 1) != ESP_OK) { return false; } vTaskDelay(pdMS_TO_TICKS(50)); if (gpio_set_level(static_cast(reset_pin), 0) != ESP_OK) { return false; } vTaskDelay(pdMS_TO_TICKS(20)); if (gpio_set_level(static_cast(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(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(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(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(device_get_driver_data(device)); pack_monochrome_frame(static_cast(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(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(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(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(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( 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(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, // The panel is write-only. Match the working MicroPython SPI configuration instead // of enabling ESP-IDF's bidirectional three-wire mode on MOSI; direction switching // in that mode can corrupt long framebuffer writes and show up as vertical stripes. .sio_mode = 0, .lsb_first = 0, .cs_high_active = 0, }, }; esp_err_t result = esp_lcd_new_panel_io_spi( static_cast(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(config->pixel_clock_hz)); return ERROR_NONE; } static error_t stop(Device* device) { auto* internal = static_cast(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(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 };