From a54480ba93754f9c2d747dc0513dab42cacbc1bc Mon Sep 17 00:00:00 2001 From: Andy Green Date: Tue, 8 Feb 2022 17:43:03 +0000 Subject: [PATCH] lws_display: SSD1675B --- include/libwebsockets.h | 1 + include/libwebsockets/lws-ssd1675b-spi.h | 55 +++ lib/drivers/CMakeLists.txt | 1 + lib/drivers/display/ssd1675b-spi.c | 495 +++++++++++++++++++++++ 4 files changed, 552 insertions(+) create mode 100644 include/libwebsockets/lws-ssd1675b-spi.h create mode 100644 lib/drivers/display/ssd1675b-spi.c diff --git a/include/libwebsockets.h b/include/libwebsockets.h index f205175b7..ebad8492c 100644 --- a/include/libwebsockets.h +++ b/include/libwebsockets.h @@ -792,6 +792,7 @@ lws_fx_string(const lws_fx_t *a, char *buf, size_t size); #include #include #include +#include #include #if defined(LWS_WITH_NETWORK) #include diff --git a/include/libwebsockets/lws-ssd1675b-spi.h b/include/libwebsockets/lws-ssd1675b-spi.h new file mode 100644 index 000000000..20818c455 --- /dev/null +++ b/include/libwebsockets/lws-ssd1675b-spi.h @@ -0,0 +1,55 @@ +/* + * lws abstract display implementation for SSD1675B on spi + * + * Copyright (C) 2019 - 2022 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#if !defined(__LWS_DISPLAY_SSD1675B_SPI_H__) +#define __LWS_DISPLAY_SSD1675B_SPI_H__ + +typedef struct lws_display_ssd1675b_spi { + + lws_display_t disp; /* use lws_display_ssd1675b_ops to set */ + const lws_spi_ops_t *spi; /* spi ops */ + + lws_display_completion_t cb; + + const lws_gpio_ops_t *gpio; /* NULL or gpio ops */ + _lws_plat_gpio_t reset_gpio; /* if gpio ops, nReset gpio # */ + _lws_plat_gpio_t busy_gpio; /* if gpio ops, busy gpio # */ + + uint8_t spi_index; /* cs index starting from 0 */ + +} lws_display_ssd1675b_spi_t; + +int +lws_display_ssd1675b_spi_init(lws_display_state_t *lds); +int +lws_display_ssd1675b_spi_blit(lws_display_state_t *lds, const uint8_t *src, + lws_box_t *box); +int +lws_display_ssd1675b_spi_power(lws_display_state_t *lds, int state); + +#define lws_display_ssd1675b_ops \ + .init = lws_display_ssd1675b_spi_init, \ + .blit = lws_display_ssd1675b_spi_blit, \ + .power = lws_display_ssd1675b_spi_power +#endif diff --git a/lib/drivers/CMakeLists.txt b/lib/drivers/CMakeLists.txt index cf473d68d..9cc7b646c 100644 --- a/lib/drivers/CMakeLists.txt +++ b/lib/drivers/CMakeLists.txt @@ -4,6 +4,7 @@ list(APPEND SOURCES drivers/display/ili9341-spi.c drivers/display/spd1656-spi.c drivers/display/uc8176-spi.c + drivers/display/ssd1675b-spi.c drivers/i2c/lws-i2c.c drivers/i2c/bitbang/lws-bb-i2c.c drivers/spi/lws-spi.c diff --git a/lib/drivers/display/ssd1675b-spi.c b/lib/drivers/display/ssd1675b-spi.c new file mode 100644 index 000000000..99793eee3 --- /dev/null +++ b/lib/drivers/display/ssd1675b-spi.c @@ -0,0 +1,495 @@ +/* + * lws abstract display implementation for SSD1675B on SPI + * + * Copyright (C) 2019 - 2022 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Based on datasheet + * + * https://cdn-learn.adafruit.com/assets/assets/000/092/748/original/SSD1675_0.pdf + * + * This chip takes a planar approach with two distinct framebuffers for b0 and + * b1 of the red levels. But the panel is B&W so we ignore red. + * + * Notice this 2.13" B&W panel needs POSITION B on the Waveshare ESP32 + * prototype board DIP switch. + */ + +#include +#include + +enum { + SSD1675B_CMD_DRIVER_OUT_CTRL = 0x01, + SSD1675B_CMD_GATE_DRIVEV_CTRL = 0x03, + SSD1675B_CMD_SOURCE_DRIVEV_CTRL = 0x04, + SSD1675B_CMD_DEEP_SLEEP = 0x10, + SSD1675B_CMD_DATA_ENTRY_MODE = 0x11, + SSD1675B_CMD_SW_RESET = 0x12, + SSD1675B_CMD_MAIN_ACTIVATION = 0x20, + SSD1675B_CMD_DISPLAY_UPDATE_CTRL = 0x22, + SSD1675B_CMD_WRITE_BW_SRAM = 0x24, + SSD1675B_CMD_WRITE_RED_SRAM = 0x26, + SSD1675B_CMD_VCOM_VOLTAGE = 0x2C, + SSD1675B_CMD_LUT = 0x32, + SSD1675B_CMD_WRITE_DISPLAY_OPTIONS = 0x37, + SSD1675B_CMD_DUMMY_LINE = 0x3A, + SSD1675B_CMD_GATE_TIME = 0x3B, + SSD1675B_CMD_BORDER_WAVEFORM = 0x3C, + SSD1675B_CMD_SET_RAM_X = 0x44, + SSD1675B_CMD_SET_RAM_Y = 0x45, + SSD1675B_CMD_SET_COUNT_X = 0x4e, + SSD1675B_CMD_SET_COUNT_Y = 0x4f, + SSD1675B_CMD_SET_ANALOG_BLOCK_CTRL = 0x74, + SSD1675B_CMD_SET_DIGITAL_BLOCK_CTRL = 0x7e, +}; + +typedef enum { + LWSDISPST_IDLE, + LWSDISPST_INIT1, + LWSDISPST_INIT2, + LWSDISPST_INIT3, + LWSDISPST_INIT4, + LWSDISPST_WRITE1, + LWSDISPST_WRITE2, + LWSDISPST_WRITE3, + LWSDISPST_WRITE4, + LWSDISPST_WRITE5, + + LWSDISPRET_ASYNC = 1 +} lws_display_update_state_t; + +//static +const uint8_t ssd1675b_init1_full[] = { + 0, SSD1675B_CMD_SW_RESET, + /* wait idle */ +}, ssd1675b_init1_part[] = { + 1, SSD1675B_CMD_VCOM_VOLTAGE, 0x26, + /* wait idle */ +}, ssd1675b_init2_full[] = { + 1, SSD1675B_CMD_SET_ANALOG_BLOCK_CTRL, 0x54, + 1, SSD1675B_CMD_SET_DIGITAL_BLOCK_CTRL, 0x3b, + 3, SSD1675B_CMD_DRIVER_OUT_CTRL, 0xf9, 0x00, 0x00, + 1, SSD1675B_CMD_DATA_ENTRY_MODE, 0x03, + 2, SSD1675B_CMD_SET_RAM_X, 0x00, 0x0f, + 4, SSD1675B_CMD_SET_RAM_Y, 0x00, 0x00, 0xf9, 0x00, + 1, SSD1675B_CMD_BORDER_WAVEFORM, 0x03, + 1, SSD1675B_CMD_VCOM_VOLTAGE, 0x55, + 1, SSD1675B_CMD_GATE_DRIVEV_CTRL, 0x15, + 3, SSD1675B_CMD_SOURCE_DRIVEV_CTRL, 0x41, 0xa8, 0x32, + 1, SSD1675B_CMD_DUMMY_LINE, 0x30, + 1, SSD1675B_CMD_GATE_TIME, 0x0a, + 70, SSD1675B_CMD_LUT, + 0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, //LUT0: BB: VS 0 ~7 + 0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, //LUT1: BW: VS 0 ~7 + 0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, //LUT2: WB: VS 0 ~7 + 0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, //LUT3: WW: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //LUT4: VCOM:VS 0 ~7 + + 0x03, 0x03, 0x00, 0x00, 0x02, // TP0 A~D RP0 + 0x09, 0x09, 0x00, 0x00, 0x02, // TP1 A~D RP1 + 0x03, 0x03, 0x00, 0x00, 0x02, // TP2 A~D RP2 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP3 A~D RP3 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP4 A~D RP4 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP5 A~D RP5 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 + 1, SSD1675B_CMD_SET_COUNT_X, 0x00, + 2, SSD1675B_CMD_SET_COUNT_Y, 0x00, 0x00, +}, ssd1675b_init2_part[] = { + 70, SSD1675B_CMD_LUT, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //LUT0: BB: VS 0 ~7 + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //LUT1: BW: VS 0 ~7 + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //LUT2: WB: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //LUT3: WW: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //LUT4: VCOM:VS 0 ~7 + + 0x0A, 0x00, 0x00, 0x00, 0x00, // TP0 A~D RP0 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP1 A~D RP1 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP2 A~D RP2 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP3 A~D RP3 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP4 A~D RP4 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP5 A~D RP5 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 + + 7, SSD1675B_CMD_WRITE_DISPLAY_OPTIONS, 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, + 1, SSD1675B_CMD_DISPLAY_UPDATE_CTRL, 0xc0, + 0, SSD1675B_CMD_MAIN_ACTIVATION, + /* wait idle */ +}, ssd1675b_init3_part[] = { + 1, SSD1675B_CMD_BORDER_WAVEFORM, 0x01 +}, ssd1675b_off[] = { + 1, SSD1675B_CMD_DEEP_SLEEP, 0x01 +}, ssd1675b_wp1[] = { + 0, SSD1675B_CMD_WRITE_BW_SRAM, +}, ssd1675b_wp2[] = { + 0, SSD1675B_CMD_WRITE_RED_SRAM, +}, ssd1675b_complete_full[] = { + 1, SSD1675B_CMD_DISPLAY_UPDATE_CTRL, 0xc7, + 0, SSD1675B_CMD_MAIN_ACTIVATION +}; + +typedef struct lws_display_ssd1675b_spi_state { + struct lws_display_state *lds; + + uint8_t *planebuf; + + uint32_t *line[2]; + lws_surface_error_t *u[2]; + + lws_sorted_usec_list_t sul; + + size_t pb_len; + size_t pb_pos; + + int state; + int budget; +} lws_display_ssd1675b_spi_state_t; + +#define lds_to_disp(_lds) (const lws_display_ssd1675b_spi_t *)_lds->disp; +#define lds_to_priv(_lds) (lws_display_ssd1675b_spi_state_t *)_lds->priv; + +/* + * The lws greyscale line composition buffer is width x Y bytes linearly. + * + * For SSD1675B, this is processed into a private buffer layout in priv->line + * that is sent over SPI to the chip, the format is both packed and planar: the + * first half is packed width x 1bpp "B&W" bits, and the second half is packed + * width x "red" bits. We only support B&W atm. + */ + +/* MSB plane is in first half of priv linebuf */ + +#define pack_native_pixel(_line, _x, _c) \ + { *_line = (*_line & ~(1 << (((_x ^ 7) & 31)))) | \ + (_c << (((_x ^ 7) & 31))); \ + if ((_x & 31) == 31) \ + _line++; } + +static void +async_cb(lws_sorted_usec_list_t *sul); + +#define BUSY_TIMEOUT_BUDGET 160 + +static int +check_busy(lws_display_ssd1675b_spi_state_t *priv, int level) +{ + const lws_display_ssd1675b_spi_t *ea = lds_to_disp(priv->lds); + + if (ea->gpio->read(ea->busy_gpio) == level) + return 0; /* good */ + + if (!--priv->budget) { + lwsl_err("%s: timeout waiting idle %d\n", __func__, level); + return -1; /* timeout */ + } + lws_sul_schedule(priv->lds->ctx, 0, &priv->sul, async_cb, + LWS_US_PER_MS * 50); + + return 1; /* keeping on trying */ +} + +static int +spi_issue_table(struct lws_display_state *lds, const uint8_t *table, size_t len) +{ + const lws_display_ssd1675b_spi_t *ea = lds_to_disp(lds); + lws_spi_desc_t desc; + size_t pos = 0; + + memset(&desc, 0, sizeof(desc)); + desc.count_cmd = 1; + + while (pos < len) { + desc.count_write = table[pos++]; + desc.src = &table[pos++]; + desc.data = &table[pos]; + pos += desc.count_write; + + ea->spi->queue(ea->spi, &desc); + } + + return 0; +} + +static void +async_cb(lws_sorted_usec_list_t *sul) +{ + lws_display_ssd1675b_spi_state_t *priv = lws_container_of(sul, + lws_display_ssd1675b_spi_state_t, sul); + const lws_display_ssd1675b_spi_t *ea = lds_to_disp(priv->lds); + + switch (priv->state) { + + case LWSDISPST_INIT1: + /* take reset low for a short time */ + ea->gpio->set(ea->reset_gpio, 0); + priv->state++; + lws_sul_schedule(priv->lds->ctx, 0, &priv->sul, + async_cb, LWS_US_PER_MS * 10); + break; + + case LWSDISPST_INIT2: + /* park reset high again and then wait a bit */ + ea->gpio->set(ea->reset_gpio, 1); + priv->state++; + priv->budget = BUSY_TIMEOUT_BUDGET; + lws_sul_schedule(priv->lds->ctx, 0, &priv->sul, + async_cb, LWS_US_PER_MS * 20); + break; + + case LWSDISPST_INIT3: + if (check_busy(priv, 0)) + return; + + spi_issue_table(priv->lds, ssd1675b_init1_full, + LWS_ARRAY_SIZE(ssd1675b_init1_full)); + + priv->state++; + lws_sul_schedule(priv->lds->ctx, 0, &priv->sul, + async_cb, LWS_US_PER_MS * 10); + break; + + case LWSDISPST_INIT4: + if (check_busy(priv, 0)) + return; + + priv->state = LWSDISPST_IDLE; + spi_issue_table(priv->lds, ssd1675b_init2_full, + LWS_ARRAY_SIZE(ssd1675b_init2_full)); + + if (ea->cb) + ea->cb(priv->lds, 1); + break; + + case LWSDISPST_WRITE1: + + /* + * Finalize the write of the planes, LUT set then REFRESH + */ + + spi_issue_table(priv->lds, ssd1675b_complete_full, + LWS_ARRAY_SIZE(ssd1675b_complete_full)); + priv->budget = BUSY_TIMEOUT_BUDGET; + priv->state++; + + lws_sul_schedule(priv->lds->ctx, 0, &priv->sul, async_cb, + LWS_US_PER_MS * 50); + break; + + case LWSDISPST_WRITE2: + if (check_busy(priv, 0)) + return; + + if (ea->spi->free_dma) + ea->spi->free_dma(ea->spi, + (void **)&priv->line[0]); + else + lws_free_set_NULL(priv->line[0]); + lws_free_set_NULL(priv->u[0]); + + /* fully completed the blit */ + + priv->state = LWSDISPST_IDLE; + if (ea->cb) + ea->cb(priv->lds, 2); + break; + + default: + break; + } +} + +int +lws_display_ssd1675b_spi_init(struct lws_display_state *lds) +{ + const lws_display_ssd1675b_spi_t *ea = lds_to_disp(lds); + lws_display_ssd1675b_spi_state_t *priv; + + priv = lws_zalloc(sizeof(*priv), __func__); + if (!priv) + return 1; + + priv->lds = lds; + lds->priv = priv; + + ea->gpio->mode(ea->busy_gpio, LWSGGPIO_FL_READ | LWSGGPIO_FL_PULLUP); + + ea->gpio->mode(ea->reset_gpio, LWSGGPIO_FL_WRITE | LWSGGPIO_FL_PULLUP); + + ea->gpio->set(ea->reset_gpio, 1); + priv->state = LWSDISPST_INIT1; + lws_sul_schedule(lds->ctx, 0, &priv->sul, async_cb, + LWS_US_PER_MS * 200); + + return 0; +} + +/* no backlight */ + +int +lws_display_ssd1675b_spi_brightness(const struct lws_display *disp, uint8_t b) +{ + return 0; +} + +int +lws_display_ssd1675b_spi_blit(struct lws_display_state *lds, const uint8_t *src, + lws_box_t *box) +{ + const lws_display_ssd1675b_spi_t *ea = lds_to_disp(lds); + lws_display_ssd1675b_spi_state_t *priv = lds_to_priv(lds); + lws_greyscale_error_t *gedl_this, *gedl_next; + const lws_surface_info_t *ic = &ea->disp.ic; + int plane_line_bytes = (ic->wh_px[0].whole + 7) / 8; + lws_colour_error_t *edl_this, *edl_next; + const uint8_t *pc = src; + lws_display_colour_t c; + lws_spi_desc_t desc; + uint32_t *lo; + int n, m; + + if (priv->state) { + lwsl_warn("%s: ignoring as busy\n", __func__); + return 1; /* busy */ + } + + if (!priv->line[0]) { + /* + * We have to allocate the packed line and error diffusion + * buffers + */ + if (ea->spi->alloc_dma) + priv->line[0] = ea->spi->alloc_dma(ea->spi, (plane_line_bytes + 4) * 2); + else + priv->line[0] = lws_zalloc((plane_line_bytes + 4) * 2, __func__); + + if (!priv->line[0]) { + lwsl_err("%s: OOM\n", __func__); + priv->state = LWSDISPST_IDLE; + + return 1; + } + + priv->line[1] = (uint32_t *)(((uint8_t *)priv->line[0]) + plane_line_bytes + 4); + + if (lws_display_alloc_diffusion(ic, priv->u)) { + if (ea->spi->free_dma) + ea->spi->free_dma(ea->spi, + (void **)&priv->line[0]); + else + lws_free_set_NULL(priv->line[0]); + lwsl_err("%s: OOM\n", __func__); + priv->state = LWSDISPST_IDLE; + return 1; + } + } + + lo = priv->line[box->y.whole & 1]; + + switch (box->h.whole) { + case 0: /* update needs to be finalized */ + + priv->state = LWSDISPST_WRITE1; + lws_sul_schedule(priv->lds->ctx, 0, &priv->sul, async_cb, + LWS_US_PER_MS * 2); + break; + + case 1: /* single line = issue line */ + + edl_this = (lws_colour_error_t *)priv->u[(box->y.whole & 1) ^ 1]; + edl_next = (lws_colour_error_t *)priv->u[box->y.whole & 1]; + gedl_this = (lws_greyscale_error_t *)edl_this; + gedl_next = (lws_greyscale_error_t *)edl_next; + + if (!pc) { + for (n = 0; n < ic->wh_px[0].whole; n++) + pack_native_pixel(lo, n, 1 /* white */); + goto go; + } + + if (ic->greyscale) { + gedl_next[ic->wh_px[0].whole - 1].rgb[0] = 0; + + for (n = 0; n < plane_line_bytes * 8; n++) { + c = (pc[0] << 16) | (pc[0] << 8) | pc[0]; + + m = lws_display_palettize_grey(ic, ic->palette, + ic->palette_depth, c, &gedl_this[n]); + pack_native_pixel(lo, n, (uint8_t)m); + + dist_err_floyd_steinberg_grey(n, ic->wh_px[0].whole, + gedl_this, gedl_next); + if (n < ic->wh_px[0].whole) + pc++; + } + } else { + edl_next[ic->wh_px[0].whole - 1].rgb[0] = 0; + edl_next[ic->wh_px[0].whole - 1].rgb[1] = 0; + edl_next[ic->wh_px[0].whole - 1].rgb[2] = 0; + + for (n = 0; n < plane_line_bytes * 8; n++) { + c = (pc[2] << 16) | (pc[1] << 8) | pc[0]; + + m = lws_display_palettize_col(ic, ic->palette, + ic->palette_depth, c, &edl_this[n]); + pack_native_pixel(lo, n, (uint8_t)m); + + dist_err_floyd_steinberg_col(n, ic->wh_px[0].whole, + edl_this, edl_next); + + if (n < ic->wh_px[0].whole) + pc += 3; + } + } +go: + memset(&desc, 0, sizeof(desc)); + if (!box->y.whole) + spi_issue_table(priv->lds, ssd1675b_wp1, + LWS_ARRAY_SIZE(ssd1675b_wp1)); + + desc.data = (uint8_t *)priv->line[box->y.whole & 1]; + desc.flags = LWS_SPI_FLAG_DMA_BOUNCE_NOT_NEEDED; + desc.count_write = plane_line_bytes; + ea->spi->queue(ea->spi, &desc); + + return 0; + + default: /* starting update */ + break; + } + + return 0; +} + +int +lws_display_ssd1675b_spi_power(lws_display_state_t *lds, int state) +{ + const lws_display_ssd1675b_spi_t *ea = lds_to_disp(lds); + + if (!state) { + spi_issue_table(lds, ssd1675b_off, LWS_ARRAY_SIZE(ssd1675b_off)); + + if (ea->gpio) + ea->gpio->set(ea->reset_gpio, 0); + + return 0; + } + + return 0; +}