From f3f472165b0b4604c3020949d985fe8e5585e56d Mon Sep 17 00:00:00 2001 From: Camilowser Date: Wed, 14 Jan 2026 23:37:15 -0300 Subject: [PATCH] Add support for ssd1315 displays This PR adds supporf for ssd1315 displays. ssd1315 is mostly compatible with ssd1306 drivers, but it has some small differences, such as requiring a special initialization sequence and needing an explicit column address reset when updating. I also increased the timeout window from 10 to 100, to fix this error i was getting: `E (1225) SSD1306: I2C write failed for display update: 0x107` [(a timeout)](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/error-codes.html) The initialization sequence is based on the one used in [u8g2](https://github.com/olikraus/u8g2/blob/dbd338af5d57b219e48ea1ffdb97c4153668676c/csrc/u8x8_d_ssd1315_128x64_noname.c). Tested on an ESP32. --- display_driver.c | 2 + ssd1306_display_driver.c | 87 ++++++++++++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/display_driver.c b/display_driver.c index ca280ac..d229984 100644 --- a/display_driver.c +++ b/display_driver.c @@ -67,6 +67,8 @@ Context *display_create_port(GlobalContext *global, term opts) ctx = ili948x_display_create_port(global, opts); } else if (!strcmp(compat_string, "solomon-systech,ssd1306")) { ctx = ssd1306_display_create_port(global, opts); + } else if (!strcmp(compat_string, "solomon-systech,ssd1315")) { + ctx = ssd1306_display_create_port(global, opts); } else if (!strcmp(compat_string, "sino-wealth,sh1106")) { ctx = ssd1306_display_create_port(global, opts); } else if (!strcmp(compat_string, "sitronix,st7789") diff --git a/ssd1306_display_driver.c b/ssd1306_display_driver.c index d914a17..4132cdb 100644 --- a/ssd1306_display_driver.c +++ b/ssd1306_display_driver.c @@ -42,6 +42,7 @@ #define CHAR_WIDTH 8 #define I2C_ADDRESS 0x3C +#define I2C_CHUNK_SIZE 64 #define CTRL_BYTE_CMD_SINGLE 0x80 #define CTRL_BYTE_CMD_STREAM 0x00 @@ -53,11 +54,18 @@ #define CMD_SET_COM_SCAN_MODE 0xC8 #define CMD_SET_CHARGE_PUMP 0x8D +typedef enum +{ + DISPLAY_SSD1306, + DISPLAY_SSD1315, + DISPLAY_SH1106, +} display_type_t; + // TODO: let's change name, since also non SPI display are supported now struct SPI { term i2c_host; - bool is_sh1106; + display_type_t type; Context *ctx; }; @@ -116,8 +124,9 @@ static void do_update(Context *ctx, term display_list) i2c_master_write_byte(cmd, CTRL_BYTE_CMD_SINGLE, true); i2c_master_write_byte(cmd, 0xB0 | ypos / 8, true); - if (spi->is_sh1106) { - // set the column, otherwise the starting column will be somewhere in the middle + + if (spi->type == DISPLAY_SH1106 || spi->type == DISPLAY_SSD1315) { + // SSD1315 and SH1106 require explicit column address reset i2c_master_write_byte(cmd, CTRL_BYTE_CMD_SINGLE, true); i2c_master_write_byte(cmd, 0x00, true); i2c_master_write_byte(cmd, CTRL_BYTE_CMD_SINGLE, true); @@ -126,7 +135,7 @@ static void do_update(Context *ctx, term display_list) i2c_master_write_byte(cmd, CTRL_BYTE_DATA_STREAM, true); - if (spi->is_sh1106) { + if (spi->type == DISPLAY_SH1106) { // add 2 empty pages on sh1106 since it can have up to 132 pixels // and 128 pixel screen starts at (2, 0) i2c_master_write_byte(cmd, 0, true); @@ -138,13 +147,13 @@ static void do_update(Context *ctx, term display_list) } // no need to send the last 2 page, the position will be set on next line again - // if (spi->is_sh1106) { + // if (spi->type == DISPLAY_SH1106) { // i2c_master_write_byte(cmd, 0, true); // i2c_master_write_byte(cmd, 0, true); // } i2c_master_stop(cmd); - i2c_master_cmd_begin(i2c_num, cmd, 10 / portTICK_PERIOD_MS); + i2c_master_cmd_begin(i2c_num, cmd, 100 / portTICK_PERIOD_MS); i2c_cmd_link_delete(cmd); memset(buf, 0, memsize); @@ -176,12 +185,17 @@ static void display_init(Context *ctx, term opts) ctx->platform_data = spi; spi->ctx = ctx; + spi->type = DISPLAY_SSD1306; // Default to SSD1306 term compat_value_term = interop_kv_get_value_default(opts, ATOM_STR("\xA", "compatible"), term_nil(), ctx->global); int str_ok; char *compat_string = interop_term_to_string(compat_value_term, &str_ok); if (str_ok && compat_string) { - spi->is_sh1106 = !strcmp(compat_string, "sino-wealth,sh1106"); + if (!strcmp(compat_string, "sino-wealth,sh1106")) { + spi->type = DISPLAY_SH1106; + } else if (!strcmp(compat_string, "solomon-systech,ssd1315")) { + spi->type = DISPLAY_SSD1315; + } free(compat_string); } else { return; @@ -209,11 +223,60 @@ static void display_init(Context *ctx, term opts) i2c_master_write_byte(cmd, (I2C_ADDRESS << 1) | I2C_MASTER_WRITE, true); i2c_master_write_byte(cmd, CTRL_BYTE_CMD_STREAM, true); - i2c_master_write_byte(cmd, CMD_SET_CHARGE_PUMP, true); - i2c_master_write_byte(cmd, 0x14, true); + if (spi->type == DISPLAY_SSD1315) { + /* + * Init sequence derived from u8g2 project (BSD-2-Clause). + * Source: https://github.com/olikraus/u8g2 + * + * These values are standard hardware initialization commands + * defined by the Solomon Systech SSD1315 datasheet. + */ + + i2c_master_write_byte(cmd, 0xAE, true); // Display OFF + + i2c_master_write_byte(cmd, 0xD5, true); // Set Display Clock Divide Ratio / Oscillator Frequency + i2c_master_write_byte(cmd, 0x80, true); // 0x80 is standard/stable + + i2c_master_write_byte(cmd, 0xA8, true); // Set Multiplex Ratio + i2c_master_write_byte(cmd, 0x3F, true); // 64 MUX + + i2c_master_write_byte(cmd, 0xD3, true); // Set Display Offset + i2c_master_write_byte(cmd, 0x00, true); // No offset + + i2c_master_write_byte(cmd, 0x40, true); // Set Display Start Line to 0 + + i2c_master_write_byte(cmd, 0x8D, true); // Set Charge Pump + i2c_master_write_byte(cmd, 0x14, true); // Enable Charge Pump + + i2c_master_write_byte(cmd, 0xA1, true); // Set Segment Remap + i2c_master_write_byte(cmd, 0xC8, true); // Set COM Scan Mode + + i2c_master_write_byte(cmd, 0xDA, true); // Set COM Pins Hardware Configuration + i2c_master_write_byte(cmd, 0x12, true); // Alternative COM pin config + + i2c_master_write_byte(cmd, 0x81, true); // Set Contrast Control + i2c_master_write_byte(cmd, 0xCF, true); // Use High Contrast (0xCF) as per u8x8 + + i2c_master_write_byte(cmd, 0xD9, true); // Set Pre-charge Period + i2c_master_write_byte(cmd, 0xF1, true); // 0xF1 is required for stable 400kHz operation + + i2c_master_write_byte(cmd, 0xDB, true); // Set VCOMH Deselect Level + i2c_master_write_byte(cmd, 0x40, true); // 0x40 (approx 0.77x VCC) + + i2c_master_write_byte(cmd, 0xA4, true); // Resume to RAM content display + i2c_master_write_byte(cmd, 0xA6, true); // Normal Display (not inverted) + + i2c_master_write_byte(cmd, 0xAD, true); // Internal IREF Setting + i2c_master_write_byte(cmd, 0x10, true); // Internal Iref + + i2c_master_write_byte(cmd, 0xAF, true); // Display ON + } else { + i2c_master_write_byte(cmd, CMD_SET_CHARGE_PUMP, true); + i2c_master_write_byte(cmd, 0x14, true); - i2c_master_write_byte(cmd, CMD_SET_SEGMENT_REMAP, true); - i2c_master_write_byte(cmd, CMD_SET_COM_SCAN_MODE, true); + i2c_master_write_byte(cmd, CMD_SET_SEGMENT_REMAP, true); + i2c_master_write_byte(cmd, CMD_SET_COM_SCAN_MODE, true); + } if (invert) { i2c_master_write_byte(cmd, CMD_DISPLAY_INVERTED, true); @@ -224,7 +287,7 @@ static void display_init(Context *ctx, term opts) esp_err_t res = i2c_master_cmd_begin(i2c_num, cmd, 50 / portTICK_PERIOD_MS); if (res != ESP_OK) { - ESP_LOGE(TAG, "ssd1306 OLED configuration failed. error: 0x%.2X", res); + ESP_LOGE(TAG, "ssd1306/ssd1315 OLED configuration failed. error: 0x%.2X", res); } else { xTaskCreate(process_messages, "display", 10000, spi, 1, NULL); }