From ad3e941ad3ed9e3e2f9032b0151558a948e99fb9 Mon Sep 17 00:00:00 2001 From: Luc BEAUFILS Date: Thu, 13 Jun 2024 17:11:10 +0200 Subject: [PATCH] drivers: add SSD1327 display controller driver Implements the driver for the OLED SSD1327 controller. This driver is based on the ssd1306 driver due to their similarities. Only the SPI control bus is supported. Signed-off-by: Luc BEAUFILS --- drivers/display/CMakeLists.txt | 1 + drivers/display/Kconfig | 1 + drivers/display/Kconfig.ssd1327 | 23 ++ drivers/display/ssd1327.c | 352 ++++++++++++++++++++ drivers/display/ssd1327_regs.h | 48 +++ dts/bindings/display/solomon,ssd1327fb.yaml | 50 +++ 6 files changed, 475 insertions(+) create mode 100644 drivers/display/Kconfig.ssd1327 create mode 100644 drivers/display/ssd1327.c create mode 100644 drivers/display/ssd1327_regs.h create mode 100644 dts/bindings/display/solomon,ssd1327fb.yaml diff --git a/drivers/display/CMakeLists.txt b/drivers/display/CMakeLists.txt index 2122d0f236b3..7f2aa6b61d9f 100644 --- a/drivers/display/CMakeLists.txt +++ b/drivers/display/CMakeLists.txt @@ -16,6 +16,7 @@ zephyr_library_sources_ifdef(CONFIG_LS0XX ls0xx.c) zephyr_library_sources_ifdef(CONFIG_MAX7219 display_max7219.c) zephyr_library_sources_ifdef(CONFIG_OTM8009A display_otm8009a.c) zephyr_library_sources_ifdef(CONFIG_SSD1306 ssd1306.c) +zephyr_library_sources_ifdef(CONFIG_SSD1327 ssd1327.c) zephyr_library_sources_ifdef(CONFIG_SSD16XX ssd16xx.c) zephyr_library_sources_ifdef(CONFIG_ST7789V display_st7789v.c) zephyr_library_sources_ifdef(CONFIG_ST7735R display_st7735r.c) diff --git a/drivers/display/Kconfig b/drivers/display/Kconfig index 6c2cca4dcfb4..5a05a791a593 100644 --- a/drivers/display/Kconfig +++ b/drivers/display/Kconfig @@ -26,6 +26,7 @@ source "drivers/display/Kconfig.nrf_led_matrix" source "drivers/display/Kconfig.ili9xxx" source "drivers/display/Kconfig.sdl" source "drivers/display/Kconfig.ssd1306" +source "drivers/display/Kconfig.ssd1327" source "drivers/display/Kconfig.ssd16xx" source "drivers/display/Kconfig.st7735r" source "drivers/display/Kconfig.st7789v" diff --git a/drivers/display/Kconfig.ssd1327 b/drivers/display/Kconfig.ssd1327 new file mode 100644 index 000000000000..1afd3d3566c5 --- /dev/null +++ b/drivers/display/Kconfig.ssd1327 @@ -0,0 +1,23 @@ +# SSD1327 display controller configuration options + +# Copyright (c) 2024 Savoir-faire Linux +# SPDX-License-Identifier: Apache-2.0 + +menuconfig SSD1327 + bool "SSD1327 display driver" + default y + depends on DT_HAS_SOLOMON_SSD1327FB_ENABLED + select MIPI_DBI + help + Enable driver for SSD1327 display. + +if SSD1327 + +config SSD1327_DEFAULT_CONTRAST + int "SSD1327 default contrast" + default 128 + range 0 255 + help + SSD1327 default contrast. + +endif # SSD1327 diff --git a/drivers/display/ssd1327.c b/drivers/display/ssd1327.c new file mode 100644 index 000000000000..65c9054c66e9 --- /dev/null +++ b/drivers/display/ssd1327.c @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2024 Savoir-faire Linux + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(ssd1327, CONFIG_DISPLAY_LOG_LEVEL); + +#include +#include +#include +#include +#include +#include +#include + +#include "ssd1327_regs.h" + +#define SSD1327_ENABLE_VDD 0x01 +#define SSD1327_ENABLE_SECOND_PRECHARGE 0x62 +#define SSD1327_VCOMH_VOLTAGE 0x0f +#define SSD1327_PHASES_VALUE 0xf1 +#define SSD1327_DEFAULT_PRECHARGE_V 0x08 +#define SSD1327_UNLOCK_COMMAND 0x12 + +struct ssd1327_config { + const struct device *mipi_dev; + const struct mipi_dbi_config dbi_config; + uint16_t height; + uint16_t width; + uint8_t oscillator_freq; + uint8_t start_line; + uint8_t display_offset; + uint8_t multiplex_ratio; + uint8_t prechargep; + uint8_t remap_value; + bool color_inversion; +}; + +struct ssd1327_data { + uint8_t contrast; + uint8_t scan_mode; +}; + +static inline int ssd1327_write_bus_cmd(const struct device *dev, const uint8_t cmd, + const uint8_t *data, size_t len) +{ + const struct ssd1327_config *config = dev->config; + int err; + + /* Values given after the memory register must be sent with pin D/C set to 0. */ + /* Data is sent as a command following the mipi_cbi api */ + err = mipi_dbi_command_write(config->mipi_dev, &config->dbi_config, cmd, NULL, 0); + if (err) { + return err; + } + for (size_t i = 0; i < len; i++) { + err = mipi_dbi_command_write(config->mipi_dev, &config->dbi_config, + data[i], NULL, 0); + if (err) { + return err; + } + } + mipi_dbi_release(config->mipi_dev, &config->dbi_config); + + return 0; +} + +static inline int ssd1327_set_timing_setting(const struct device *dev) +{ + const struct ssd1327_config *config = dev->config; + uint8_t buf; + + buf = SSD1327_PHASES_VALUE; + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_PHASE_LENGTH, &buf, 1)) { + return -EIO; + } + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_OSC_FREQ, &config->oscillator_freq, 1)) { + return -EIO; + } + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_PRECHARGE_PERIOD, &config->prechargep, 1)) { + return -EIO; + } + if (ssd1327_write_bus_cmd(dev, SSD1327_LINEAR_LUT, NULL, 0)) { + return -EIO; + } + buf = SSD1327_DEFAULT_PRECHARGE_V; + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_PRECHARGE_VOLTAGE, &buf, 1)) { + return -EIO; + } + buf = SSD1327_VCOMH_VOLTAGE; + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_VCOMH, &buf, 1)) { + return -EIO; + } + buf = SSD1327_ENABLE_SECOND_PRECHARGE; + if (ssd1327_write_bus_cmd(dev, SSD1327_FUNCTION_SELECTION_B, &buf, 1)) { + return -EIO; + } + buf = SSD1327_UNLOCK_COMMAND; + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_COMMAND_LOCK, &buf, 1)) { + return -EIO; + } + + return 0; +} + +static inline int ssd1327_set_hardware_config(const struct device *dev) +{ + const struct ssd1327_config *config = dev->config; + uint8_t buf; + + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_DISPLAY_START_LINE, &config->start_line, 1)) { + return -EIO; + } + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_DISPLAY_OFFSET, &config->display_offset, 1)) { + return -EIO; + } + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_NORMAL_DISPLAY, NULL, 0)) { + return -EIO; + } + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_SEGMENT_MAP_REMAPED, &config->remap_value, 1)) { + return -EIO; + } + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_MULTIPLEX_RATIO, &config->multiplex_ratio, 1)) { + return -EIO; + } + buf = SSD1327_ENABLE_VDD; + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_FUNCTION_A, &buf, 1)) { + return -EIO; + } + + return 0; +} + +static int ssd1327_resume(const struct device *dev) +{ + return ssd1327_write_bus_cmd(dev, SSD1327_DISPLAY_ON, NULL, 0); +} + +static int ssd1327_suspend(const struct device *dev) +{ + return ssd1327_write_bus_cmd(dev, SSD1327_DISPLAY_OFF, NULL, 0); +} + +static int ssd1327_set_display(const struct device *dev) +{ + const struct ssd1327_config *config = dev->config; + uint8_t x_position[] = { + 0, + config->width - 1 + }; + uint8_t y_position[] = { + 0, + config->height - 1 + }; + + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_COLUMN_ADDR, x_position, sizeof(x_position))) { + return -EIO; + } + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_ROW_ADDR, y_position, sizeof(y_position))) { + return -EIO; + } + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_SEGMENT_MAP_REMAPED, &config->remap_value, 1)) { + return -EIO; + } + + return 0; +} + +static int ssd1327_write(const struct device *dev, const uint16_t x, const uint16_t y, + const struct display_buffer_descriptor *desc, const void *buf) +{ + const struct ssd1327_config *config = dev->config; + struct display_buffer_descriptor mipi_desc; + int err; + size_t buf_len; + uint8_t x_position[] = { x, x + desc->width - 1 }; + uint8_t y_position[] = { y, y + desc->height - 1 }; + + if (desc->pitch < desc->width) { + LOG_ERR("Pitch is smaller than width"); + return -1; + } + mipi_desc.pitch = desc->pitch; + + /* Following the datasheet, in the GDDRAM, two segment are split in one register */ + buf_len = MIN(desc->buf_size, desc->height * desc->width / 2); + if (buf == NULL || buf_len == 0U) { + LOG_ERR("Display buffer is not available"); + return -1; + } + mipi_desc.buf_size = buf_len; + + if (desc->pitch > desc->width) { + LOG_ERR("Unsupported mode"); + return -1; + } + + if ((y & 0x7) != 0U) { + LOG_ERR("Unsupported origin"); + return -1; + } + mipi_desc.height = desc->height; + mipi_desc.width = desc->width; + + LOG_DBG("x %u, y %u, pitch %u, width %u, height %u, buf_len %u", x, y, desc->pitch, + desc->width, desc->height, buf_len); + + err = ssd1327_write_bus_cmd(dev, SSD1327_SET_COLUMN_ADDR, x_position, sizeof(x_position)); + if (err) { + return err; + } + + err = ssd1327_write_bus_cmd(dev, SSD1327_SET_ROW_ADDR, y_position, sizeof(y_position)); + if (err) { + return err; + } + + err = mipi_dbi_write_display(config->mipi_dev, &config->dbi_config, buf, &mipi_desc, + PIXEL_FORMAT_MONO10); + if (err) { + return err; + } + return mipi_dbi_release(config->mipi_dev, &config->dbi_config); +} + +static int ssd1327_set_contrast(const struct device *dev, const uint8_t contrast) +{ + return ssd1327_write_bus_cmd(dev, SSD1327_SET_CONTRAST_CTRL, &contrast, 1); +} + +static void ssd1327_get_capabilities(const struct device *dev, + struct display_capabilities *caps) +{ + const struct ssd1327_config *config = dev->config; + + memset(caps, 0, sizeof(struct display_capabilities)); + caps->x_resolution = config->width; + caps->y_resolution = config->height; + caps->supported_pixel_formats = PIXEL_FORMAT_MONO10; + caps->current_pixel_format = PIXEL_FORMAT_MONO10; + caps->screen_info = SCREEN_INFO_MONO_VTILED; +} + +static int ssd1327_set_pixel_format(const struct device *dev, + const enum display_pixel_format pf) +{ + if (pf == PIXEL_FORMAT_MONO10) { + return 0; + } + LOG_ERR("Unsupported pixel format"); + return -ENOTSUP; +} + +static int ssd1327_init_device(const struct device *dev) +{ + const struct ssd1327_config *config = dev->config; + uint8_t buf; + + /* Turn display off */ + if (ssd1327_suspend(dev)) { + return -EIO; + } + + if (ssd1327_set_display(dev)) { + return -EIO; + } + + if (ssd1327_set_contrast(dev, CONFIG_SSD1327_DEFAULT_CONTRAST)) { + return -EIO; + } + + if (ssd1327_set_hardware_config(dev)) { + return -EIO; + } + + buf = (config->color_inversion ? + SSD1327_SET_REVERSE_DISPLAY : SSD1327_SET_NORMAL_DISPLAY); + if (ssd1327_write_bus_cmd(dev, SSD1327_SET_ENTIRE_DISPLAY_OFF, &buf, 1)) { + return -EIO; + } + + if (ssd1327_set_timing_setting(dev)) { + return -EIO; + } + + if (ssd1327_resume(dev)) { + return -EIO; + } + + return 0; +} + +static int ssd1327_init(const struct device *dev) +{ + const struct ssd1327_config *config = dev->config; + + LOG_DBG("Initializing device"); + + if (!device_is_ready(config->mipi_dev)) { + LOG_ERR("MIPI Device not ready!"); + return -EINVAL; + } + + if (mipi_dbi_reset(config->mipi_dev, SSD1327_RESET_DELAY)) { + LOG_ERR("Failed to reset device!"); + return -EIO; + } + k_msleep(SSD1327_RESET_DELAY); + + if (ssd1327_init_device(dev)) { + LOG_ERR("Failed to initialize device!"); + return -EIO; + } + + return 0; +} + +static struct display_driver_api ssd1327_driver_api = { + .blanking_on = ssd1327_suspend, + .blanking_off = ssd1327_resume, + .write = ssd1327_write, + .set_contrast = ssd1327_set_contrast, + .get_capabilities = ssd1327_get_capabilities, + .set_pixel_format = ssd1327_set_pixel_format, +}; + +#define SSD1327_DEFINE(node_id) \ + static struct ssd1327_data data##node_id; \ + static const struct ssd1327_config config##node_id = { \ + .mipi_dev = DEVICE_DT_GET(DT_PARENT(node_id)), \ + .dbi_config = { .mode = MIPI_DBI_MODE_SPI_4WIRE, \ + .config = MIPI_DBI_SPI_CONFIG_DT(node_id, \ + SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | \ + SPI_HOLD_ON_CS | SPI_LOCK_ON, 0), \ + }, \ + .height = DT_PROP(node_id, height), \ + .width = DT_PROP(node_id, width), \ + .oscillator_freq = DT_PROP(node_id, oscillator_freq), \ + .display_offset = DT_PROP(node_id, display_offset), \ + .start_line = DT_PROP(node_id, start_line), \ + .multiplex_ratio = DT_PROP(node_id, multiplex_ratio), \ + .prechargep = DT_PROP(node_id, prechargep), \ + .remap_value = DT_PROP(node_id, remap_value), \ + .color_inversion = DT_PROP(node_id, inversion_on), \ + }; \ + \ + DEVICE_DT_DEFINE(node_id, ssd1327_init, NULL, &data##node_id, &config##node_id, \ + POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &ssd1327_driver_api); + +DT_FOREACH_STATUS_OKAY(solomon_ssd1327fb, SSD1327_DEFINE) diff --git a/drivers/display/ssd1327_regs.h b/drivers/display/ssd1327_regs.h new file mode 100644 index 000000000000..443a1375a55d --- /dev/null +++ b/drivers/display/ssd1327_regs.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Savoir-faire Linux + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __SSD1327_REGS_H__ +#define __SSD1327_REGS_H__ + +/* + * Fundamental Command Table + */ +#define SSD1327_SET_COLUMN_ADDR 0x15 +#define SSD1327_SET_ROW_ADDR 0x75 + +#define SSD1327_SET_CONTRAST_CTRL 0x81 + +#define SSD1327_SET_SEGMENT_MAP_REMAPED 0xa0 +#define SSD1327_SET_DISPLAY_START_LINE 0xa1 +#define SSD1327_SET_DISPLAY_OFFSET 0xa2 + +#define SSD1327_SET_NORMAL_DISPLAY 0xa4 +#define SSD1327_SET_ENTIRE_DISPLAY_ON 0xa5 +#define SSD1327_SET_ENTIRE_DISPLAY_OFF 0xa6 +#define SSD1327_SET_REVERSE_DISPLAY 0xa7 +#define SSD1327_SET_MULTIPLEX_RATIO 0xa8 + +#define SSD1327_DISPLAY_OFF 0xae +#define SSD1327_DISPLAY_ON 0xaf + +#define SSD1327_SET_FUNCTION_A 0xab +#define SSD1327_SET_PHASE_LENGTH 0xb1 +#define SSD1327_SET_OSC_FREQ 0xb3 +#define SSD1327_SET_PRECHARGE_PERIOD 0xb6 +#define SSD1327_FUNCTION_SELECTION_B 0xd5 + +#define SSD1327_LINEAR_LUT 0xb9 + +#define SSD1327_SET_PRECHARGE_VOLTAGE 0xbc +#define SSD1327_SET_VCOMH 0xbe + + +#define SSD1327_SET_COMMAND_LOCK 0xfd + +/* Time constant in ms */ +#define SSD1327_RESET_DELAY 10 + +#endif diff --git a/dts/bindings/display/solomon,ssd1327fb.yaml b/dts/bindings/display/solomon,ssd1327fb.yaml new file mode 100644 index 000000000000..c543dafd88f2 --- /dev/null +++ b/dts/bindings/display/solomon,ssd1327fb.yaml @@ -0,0 +1,50 @@ +# Copyright (c) 2024, Savoir-faire Linux +# SPDX-License-Identifier: Apache-2.0 + +description: SSD1327 128x128 dot-matrix display controller on MIPI_DBI bus + +include: [mipi-dbi-spi-device.yaml, display-controller.yaml] + +compatible: "solomon,ssd1327fb" + +properties: + oscillator-freq: + type: int + required: true + description: Front clock divider / oscillator frequency + + display-offset: + type: int + required: true + description: Vertical offset by com from 0 ~ 127 + + start-line: + type: int + required: true + description: Start line of display RAM to be displayed by selecting a value from 0 to 127 + + multiplex-ratio: + type: int + required: true + description: Multiplex ratio from 16MUX to 128MUX + + prechargep: + type: int + required: true + description: Pre-charge period ranging from 0 to 15 DCLK's + + remap-value: + type: int + required: true + description: Remap register + + Has multiple configurations (see each bit setting in the datasheet) + - Column Address Remapping (A[0]) + - Nibble Remapping (A[1]) + - Address increment mode (A[2]) + - COM Remapping (A[4]) + - Splitting of Odd / Even COM Signals (A[6]) + + inversion-on: + type: boolean + description: Turn on display color inverting