362 lines
14 KiB
C++
362 lines
14 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
#include <drivers/ssd1306.h>
|
|
#include <ssd1306_module.h>
|
|
|
|
#include <tactility/check.h>
|
|
#include <tactility/device.h>
|
|
#include <tactility/driver.h>
|
|
#include <tactility/drivers/display.h>
|
|
#include <tactility/drivers/esp32_i2c.h>
|
|
#include <tactility/drivers/esp32_i2c_master.h>
|
|
#include <tactility/drivers/i2c_controller.h>
|
|
#include <tactility/error.h>
|
|
#include <tactility/log.h>
|
|
|
|
#include <esp_err.h>
|
|
#include <esp_lcd_io_i2c.h>
|
|
#include <esp_lcd_panel_io.h>
|
|
#include <esp_lcd_panel_ops.h>
|
|
#include <esp_lcd_panel_ssd1306.h>
|
|
|
|
#include <driver/gpio.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/task.h>
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
#define TAG "SSD1306"
|
|
#define GET_CONFIG(device) (static_cast<const Ssd1306Config*>((device)->config))
|
|
|
|
struct Ssd1306Internal {
|
|
esp_lcd_panel_io_handle_t io_handle;
|
|
esp_lcd_panel_handle_t panel_handle;
|
|
// Scratch buffer for the page-format transpose in ssd1306_draw_bitmap(); sized once for the
|
|
// full panel resolution and reused every draw_bitmap() call to avoid a malloc/free per frame.
|
|
uint8_t* page_buf;
|
|
size_t page_buf_size;
|
|
};
|
|
|
|
static gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
|
|
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
|
|
}
|
|
|
|
// region Driver lifecycle
|
|
|
|
static esp_err_t create_io_handle(Device* parent, uint8_t address, esp_lcd_panel_io_handle_t* out_handle) {
|
|
// dc_bit_offset=6/dc_low_on_data=false gives a 0x00 control byte before command bytes and
|
|
// 0x40 before pixel data - the standard SSD1306 I2C control-byte convention. lcd_cmd_bits=0
|
|
// means tx_param()'s own "cmd" phase is skipped entirely (see send_command() below): every
|
|
// command byte is transmitted as if it were "data" following the correct control byte,
|
|
// rather than a separately-framed command - this chip's protocol has no such framing to begin
|
|
// with, unlike the multi-byte-per-command SPI/QSPI panels elsewhere in this repo.
|
|
esp_lcd_panel_io_i2c_config_t io_config = {
|
|
.dev_addr = address,
|
|
.on_color_trans_done = nullptr,
|
|
.user_ctx = nullptr,
|
|
.control_phase_bytes = 1,
|
|
.dc_bit_offset = 6,
|
|
.lcd_cmd_bits = 0,
|
|
.lcd_param_bits = 0,
|
|
.flags = {
|
|
.dc_low_on_data = false,
|
|
.disable_control_phase = false,
|
|
},
|
|
.scl_speed_hz = 0,
|
|
};
|
|
|
|
auto* parent_driver = device_get_driver(parent);
|
|
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c")) {
|
|
auto port = static_cast<const Esp32I2cConfig*>(parent->config)->port;
|
|
return esp_lcd_new_panel_io_i2c_v1(port, &io_config, out_handle);
|
|
}
|
|
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c-master")) {
|
|
auto bus = esp32_i2c_master_get_bus_handle(parent);
|
|
io_config.scl_speed_hz = esp32_i2c_master_get_clock_frequency(parent);
|
|
return esp_lcd_new_panel_io_i2c_v2(bus, &io_config, out_handle);
|
|
}
|
|
|
|
LOG_E(TAG, "Unsupported I2C driver");
|
|
return ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
// Sends a single SSD1306 command byte: [control byte, this byte]. lcd_cmd is passed as 0 and
|
|
// ignored on the wire (see create_io_handle()'s comment) - only param/param_size actually reach
|
|
// the bus, so this doubles as the mechanism for both standalone commands (e.g. 0xAE display-off)
|
|
// and command+parameter pairs (each byte of which is just its own call to this function, matching
|
|
// how the original hand-written driver issued them as separate I2C writes).
|
|
static bool send_command(esp_lcd_panel_io_handle_t io, uint8_t command) {
|
|
return esp_lcd_panel_io_tx_param(io, 0, &command, 1) == ESP_OK;
|
|
}
|
|
|
|
static bool send_init_sequence(esp_lcd_panel_io_handle_t io, const uint8_t* bytes, uint32_t length) {
|
|
for (uint32_t i = 0; i < length; i++) {
|
|
if (!send_command(io, bytes[i])) {
|
|
LOG_E(TAG, "Failed to send init sequence byte %lu", (unsigned long)i);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Manual reset with explicit timing, rather than going through esp_lcd_panel_reset(): this
|
|
// chip/config combination is known to need a specific assert/settle timing that the panel's
|
|
// standard reset (which we intentionally never wire up - reset_gpio_num is always -1 below) may
|
|
// not otherwise match. Runs before the panel handle is even created, so a subsequent init sees a
|
|
// freshly-reset chip.
|
|
static void perform_hardware_reset(const Ssd1306Config* config) {
|
|
gpio_num_t pin = pin_or_nc(config->pin_reset);
|
|
if (pin == GPIO_NUM_NC) {
|
|
return;
|
|
}
|
|
|
|
gpio_config_t io_conf = {
|
|
.pin_bit_mask = 1ULL << pin,
|
|
.mode = GPIO_MODE_OUTPUT,
|
|
.pull_up_en = GPIO_PULLUP_DISABLE,
|
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
|
.intr_type = GPIO_INTR_DISABLE,
|
|
};
|
|
gpio_config(&io_conf);
|
|
|
|
// SSD1306's reset line is fixed active-low in hardware.
|
|
gpio_set_level(pin, 0);
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
gpio_set_level(pin, 1);
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
}
|
|
|
|
static error_t start(Device* device) {
|
|
auto* parent = device_get_parent(device);
|
|
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
|
|
|
const auto* config = GET_CONFIG(device);
|
|
|
|
auto* internal = static_cast<Ssd1306Internal*>(malloc(sizeof(Ssd1306Internal)));
|
|
if (internal == nullptr) {
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
internal->page_buf_size = (size_t)config->horizontal_resolution * ((config->vertical_resolution + 7) / 8);
|
|
internal->page_buf = static_cast<uint8_t*>(malloc(internal->page_buf_size));
|
|
if (internal->page_buf == nullptr) {
|
|
free(internal);
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
// Lets a power rail gated by a gpio-hog node (asserted moments earlier, during the same
|
|
// kernel_init() pass) stabilize before this device tries to reset/talk to the chip. See the
|
|
// 'power-on-delay-ms' binding property.
|
|
if (config->power_on_delay_ms > 0) {
|
|
vTaskDelay(pdMS_TO_TICKS(config->power_on_delay_ms));
|
|
}
|
|
|
|
if (create_io_handle(parent, config->address, &internal->io_handle) != ESP_OK) {
|
|
LOG_E(TAG, "Failed to create panel IO");
|
|
free(internal->page_buf);
|
|
free(internal);
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
perform_hardware_reset(config);
|
|
|
|
esp_lcd_panel_ssd1306_config_t ssd1306_config = {
|
|
.height = static_cast<uint8_t>(config->vertical_resolution),
|
|
};
|
|
|
|
// color_space (not rgb_ele_order): this deprecated union member is the only one whose type
|
|
// can express ESP_LCD_COLOR_SPACE_MONOCHROME - rgb_ele_order's own enum only covers RGB/BGR.
|
|
esp_lcd_panel_dev_config_t panel_config = {
|
|
.reset_gpio_num = -1, // always -1: reset is handled manually above, not by the panel itself
|
|
.color_space = ESP_LCD_COLOR_SPACE_MONOCHROME,
|
|
.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
|
|
.bits_per_pixel = 1,
|
|
.flags = { .reset_active_high = false },
|
|
.vendor_config = &ssd1306_config,
|
|
};
|
|
|
|
if (esp_lcd_new_panel_ssd1306(internal->io_handle, &panel_config, &internal->panel_handle) != ESP_OK) {
|
|
LOG_E(TAG, "Failed to create panel");
|
|
esp_lcd_panel_io_del(internal->io_handle);
|
|
free(internal->page_buf);
|
|
free(internal);
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
// Bring-up sequence. Every failure path below must clean up fully: unlike stop_device, this
|
|
// is never retried by the kernel if start_device fails (see device_start() in
|
|
// TactilityKernel), so a partial failure here would leak.
|
|
bool ok;
|
|
if (config->init_sequence != nullptr && config->init_sequence_length > 0) {
|
|
ok = send_init_sequence(internal->io_handle, config->init_sequence, config->init_sequence_length);
|
|
} else {
|
|
ok = esp_lcd_panel_init(internal->panel_handle) == ESP_OK;
|
|
}
|
|
ok = ok && (!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK);
|
|
ok = ok && esp_lcd_panel_disp_on_off(internal->panel_handle, true) == ESP_OK;
|
|
|
|
if (!ok) {
|
|
LOG_E(TAG, "Failed to bring up panel");
|
|
esp_lcd_panel_del(internal->panel_handle);
|
|
esp_lcd_panel_io_del(internal->io_handle);
|
|
free(internal->page_buf);
|
|
free(internal);
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(100)); // Let the panel stabilize before the first real draw_bitmap()
|
|
|
|
device_set_driver_data(device, internal);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static error_t stop(Device* device) {
|
|
auto* internal = static_cast<Ssd1306Internal*>(device_get_driver_data(device));
|
|
|
|
if (internal->panel_handle != nullptr) {
|
|
if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) {
|
|
LOG_E(TAG, "Failed to delete panel");
|
|
return ERROR_RESOURCE;
|
|
}
|
|
internal->panel_handle = nullptr;
|
|
}
|
|
|
|
if (internal->io_handle != nullptr) {
|
|
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
|
|
LOG_E(TAG, "Failed to delete panel IO");
|
|
return ERROR_RESOURCE;
|
|
}
|
|
internal->io_handle = nullptr;
|
|
}
|
|
|
|
free(internal->page_buf);
|
|
free(internal);
|
|
device_set_driver_data(device, nullptr);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// endregion
|
|
|
|
// region DisplayApi
|
|
|
|
static error_t ssd1306_reset(Device* device) {
|
|
perform_hardware_reset(GET_CONFIG(device));
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static error_t ssd1306_init(Device* device) {
|
|
auto* internal = static_cast<Ssd1306Internal*>(device_get_driver_data(device));
|
|
const auto* config = GET_CONFIG(device);
|
|
bool ok;
|
|
if (config->init_sequence != nullptr && config->init_sequence_length > 0) {
|
|
ok = send_init_sequence(internal->io_handle, config->init_sequence, config->init_sequence_length);
|
|
} else {
|
|
ok = esp_lcd_panel_init(internal->panel_handle) == ESP_OK;
|
|
}
|
|
return ok ? ERROR_NONE : ERROR_RESOURCE;
|
|
}
|
|
|
|
// DISPLAY_COLOR_FORMAT_MONOCHROME's wire format (matching LVGL's LV_COLOR_FORMAT_I1, palette
|
|
// stripped) is row-major: each byte packs 8 horizontally-adjacent pixels, MSB = leftmost. The
|
|
// SSD1306's GDDRAM is page-addressed instead: each byte packs 8 *vertically* adjacent pixels of
|
|
// one column, LSB = topmost row of the page. Transpose one into the other before handing off to
|
|
// the vendor component, which expects page format directly.
|
|
static void transpose_row_major_to_pages(const uint8_t* src, int32_t width, int32_t height, uint8_t* out) {
|
|
uint32_t row_stride = (uint32_t)(width + 7) / 8;
|
|
for (int32_t y = 0; y < height; y++) {
|
|
uint8_t page = (uint8_t)(y / 8);
|
|
uint8_t bit_in_page = (uint8_t)(y % 8);
|
|
for (int32_t x = 0; x < width; x++) {
|
|
uint8_t src_byte = src[y * row_stride + x / 8];
|
|
uint8_t pixel = (src_byte >> (7 - (x % 8))) & 0x01;
|
|
out[page * width + x] |= (uint8_t)(pixel << bit_in_page);
|
|
}
|
|
}
|
|
}
|
|
|
|
static error_t ssd1306_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<Ssd1306Internal*>(device_get_driver_data(device));
|
|
int32_t width = x_end - x_start;
|
|
int32_t height = y_end - y_start;
|
|
size_t needed = (size_t)width * ((height + 7) / 8);
|
|
check(needed <= internal->page_buf_size);
|
|
|
|
memset(internal->page_buf, 0, needed);
|
|
transpose_row_major_to_pages(static_cast<const uint8_t*>(color_data), width, height, internal->page_buf);
|
|
|
|
return esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, internal->page_buf) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
|
}
|
|
|
|
// mirror/swap_xy/set_gap are not exposed: the SSD1306 vendor component doesn't implement them.
|
|
|
|
static error_t ssd1306_invert_color(Device* device, bool invert_color_data) {
|
|
auto* internal = static_cast<Ssd1306Internal*>(device_get_driver_data(device));
|
|
return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
|
}
|
|
|
|
static error_t ssd1306_disp_on_off(Device* device, bool on_off) {
|
|
auto* internal = static_cast<Ssd1306Internal*>(device_get_driver_data(device));
|
|
return esp_lcd_panel_disp_on_off(internal->panel_handle, on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
|
}
|
|
|
|
// disp_sleep is not exposed: the SSD1306 vendor component doesn't implement it either (display
|
|
// on/off via 0xAE/0xAF is this chip's equivalent, already covered by disp_on_off above).
|
|
|
|
static enum DisplayColorFormat ssd1306_get_color_format(Device*) {
|
|
return DISPLAY_COLOR_FORMAT_MONOCHROME;
|
|
}
|
|
|
|
static uint16_t ssd1306_get_resolution_x(Device* device) {
|
|
return GET_CONFIG(device)->horizontal_resolution;
|
|
}
|
|
|
|
static uint16_t ssd1306_get_resolution_y(Device* device) {
|
|
return GET_CONFIG(device)->vertical_resolution;
|
|
}
|
|
|
|
static void ssd1306_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
|
|
*out_buffer = nullptr;
|
|
}
|
|
|
|
static uint8_t ssd1306_get_frame_buffer_count(Device*) {
|
|
return 0;
|
|
}
|
|
|
|
// get_backlight is not exposed: SSD1306 is a self-emitting OLED panel with no backlight.
|
|
|
|
// endregion
|
|
|
|
static const DisplayApi ssd1306_display_api = {
|
|
.capabilities = DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF,
|
|
.reset = ssd1306_reset,
|
|
.init = ssd1306_init,
|
|
.draw_bitmap = ssd1306_draw_bitmap,
|
|
.mirror = nullptr,
|
|
.swap_xy = nullptr,
|
|
.get_swap_xy = nullptr,
|
|
.get_mirror_x = nullptr,
|
|
.get_mirror_y = nullptr,
|
|
.set_gap = nullptr,
|
|
.invert_color = ssd1306_invert_color,
|
|
.disp_on_off = ssd1306_disp_on_off,
|
|
.disp_sleep = nullptr,
|
|
.get_color_format = ssd1306_get_color_format,
|
|
.get_resolution_x = ssd1306_get_resolution_x,
|
|
.get_resolution_y = ssd1306_get_resolution_y,
|
|
.get_frame_buffer = ssd1306_get_frame_buffer,
|
|
.get_frame_buffer_count = ssd1306_get_frame_buffer_count,
|
|
.get_backlight = nullptr,
|
|
.has_capability = nullptr,
|
|
};
|
|
|
|
Driver ssd1306_driver = {
|
|
.name = "ssd1306",
|
|
.compatible = (const char*[]) { "solomon,ssd1306", nullptr },
|
|
.start_device = start,
|
|
.stop_device = stop,
|
|
.api = &ssd1306_display_api,
|
|
.device_type = &DISPLAY_TYPE,
|
|
.owner = &ssd1306_module,
|
|
.internal = nullptr
|
|
};
|