mirror of
https://libwebsockets.org/repo/libwebsockets
synced 2024-11-24 17:46:45 +00:00
496 lines
14 KiB
C
496 lines
14 KiB
C
/*
|
|
* lws abstract display implementation for SSD1675B on SPI
|
|
*
|
|
* Copyright (C) 2019 - 2022 Andy Green <andy@warmcat.com>
|
|
*
|
|
* 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 <private-lib-core.h>
|
|
#include <dlo/private-lib-drivers-display-dlo.h>
|
|
|
|
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;
|
|
}
|