// 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 #include #define TAG "SSD1306" #define GET_CONFIG(device) (static_cast((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(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(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(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(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(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(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(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(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(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(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(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 };