diff --git a/drivers/display/CMakeLists.txt b/drivers/display/CMakeLists.txt index 185e3a1b2b4..7071303dfb6 100644 --- a/drivers/display/CMakeLists.txt +++ b/drivers/display/CMakeLists.txt @@ -20,6 +20,7 @@ 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_SSD1322 ssd1322.c) +zephyr_library_sources_ifdef(CONFIG_ST7567 display_st7567.c) zephyr_library_sources_ifdef(CONFIG_ST7789V display_st7789v.c) zephyr_library_sources_ifdef(CONFIG_ST7735R display_st7735r.c) zephyr_library_sources_ifdef(CONFIG_ST7796S display_st7796s.c) diff --git a/drivers/display/Kconfig b/drivers/display/Kconfig index 593c34dc163..13206b8a63c 100644 --- a/drivers/display/Kconfig +++ b/drivers/display/Kconfig @@ -30,6 +30,7 @@ source "drivers/display/Kconfig.ssd1306" source "drivers/display/Kconfig.ssd1327" source "drivers/display/Kconfig.ssd16xx" source "drivers/display/Kconfig.ssd1322" +source "drivers/display/Kconfig.st7567" source "drivers/display/Kconfig.st7735r" source "drivers/display/Kconfig.st7789v" source "drivers/display/Kconfig.st7796s" diff --git a/drivers/display/Kconfig.st7567 b/drivers/display/Kconfig.st7567 new file mode 100644 index 00000000000..1a8b5d7e559 --- /dev/null +++ b/drivers/display/Kconfig.st7567 @@ -0,0 +1,22 @@ +# Copyright (c) 2025 MASSDRIVER EI (massdriver.space) +# SPDX-License-Identifier: Apache-2.0 + +menuconfig ST7567 + bool "ST7567 display controller" + default y + depends on DT_HAS_SITRONIX_ST7567_ENABLED + select I2C if $(dt_compat_on_bus,$(DT_COMPAT_SITRONIX_ST7567),i2c) + select SPI if $(dt_compat_on_bus,$(DT_COMPAT_SITRONIX_ST7567),spi) + help + Enable driver for ST7567 display controller. + +if ST7567 + +config ST7567_DEFAULT_CONTRAST + int "ST7567 default contrast" + default 8 + range 0 15 + help + ST7567 default contrast. + +endif # ST7567 diff --git a/drivers/display/display_st7567.c b/drivers/display/display_st7567.c new file mode 100644 index 00000000000..15491f05618 --- /dev/null +++ b/drivers/display/display_st7567.c @@ -0,0 +1,491 @@ +/* + * Copyright (c) 2025 MASSDRIVER EI (massdriver.space) + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(display_st7567, CONFIG_DISPLAY_LOG_LEVEL); + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "display_st7567_regs.h" + +union st7567_bus { + struct i2c_dt_spec i2c; + struct spi_dt_spec spi; +}; + +typedef bool (*st7567_bus_ready_fn)(const struct device *dev); +typedef int (*st7567_write_bus_fn)(const struct device *dev, uint8_t *buf, size_t len, + bool command); +typedef const char *(*st7567_bus_name_fn)(const struct device *dev); + +struct st7567_config { + union st7567_bus bus; + struct gpio_dt_spec data_cmd; + struct gpio_dt_spec reset; + st7567_bus_ready_fn bus_ready; + st7567_write_bus_fn write_bus; + st7567_bus_name_fn bus_name; + uint16_t height; + uint16_t width; + uint8_t column_offset; + uint8_t line_offset; + uint8_t regulation_ratio; + bool com_invdir; + bool segment_invdir; + bool inversion_on; + bool bias; +}; + +struct st7567_data { + enum display_pixel_format pf; +}; + +#if DT_HAS_COMPAT_ON_BUS_STATUS_OKAY(sitronix_st7567, i2c) + +static bool st7567_bus_ready_i2c(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + + return i2c_is_ready_dt(&config->bus.i2c); +} + +static int st7567_write_bus_i2c(const struct device *dev, uint8_t *buf, size_t len, bool command) +{ + const struct st7567_config *config = dev->config; + + return i2c_burst_write_dt( + &config->bus.i2c, + command ? ST7567_CONTROL_ALL_BYTES_CMD : ST7567_CONTROL_ALL_BYTES_DATA, buf, len); +} + +static const char *st7567_bus_name_i2c(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + + return config->bus.i2c.bus->name; +} + +#endif + +#if DT_HAS_COMPAT_ON_BUS_STATUS_OKAY(sitronix_st7567, spi) + +static bool st7567_bus_ready_spi(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + + if (gpio_pin_configure_dt(&config->data_cmd, GPIO_OUTPUT_INACTIVE) < 0) { + return false; + } + + return spi_is_ready_dt(&config->bus.spi); +} + +static int st7567_write_bus_spi(const struct device *dev, uint8_t *buf, size_t len, bool command) +{ + const struct st7567_config *config = dev->config; + int ret; + + gpio_pin_set_dt(&config->data_cmd, command ? 0 : 1); + struct spi_buf tx_buf = {.buf = buf, .len = len}; + + struct spi_buf_set tx_bufs = {.buffers = &tx_buf, .count = 1}; + + ret = spi_write_dt(&config->bus.spi, &tx_bufs); + + return ret; +} + +static const char *st7567_bus_name_spi(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + + return config->bus.spi.bus->name; +} + +#endif + +static inline bool st7567_bus_ready(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + + return config->bus_ready(dev); +} + +static inline int st7567_write_bus(const struct device *dev, uint8_t *buf, size_t len, bool command) +{ + const struct st7567_config *config = dev->config; + + return config->write_bus(dev, buf, len, command); +} + +static inline int st7567_set_panel_orientation(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + uint8_t cmd_buf[] = {(config->segment_invdir ? ST7567_SET_SEGMENT_MAP_FLIPPED + : ST7567_SET_SEGMENT_MAP_NORMAL), + (config->com_invdir ? ST7567_SET_COM_OUTPUT_SCAN_FLIPPED + : ST7567_SET_COM_OUTPUT_SCAN_NORMAL)}; + + return st7567_write_bus(dev, cmd_buf, sizeof(cmd_buf), true); +} + +static inline int st7567_set_hardware_config(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + int ret; + uint8_t cmd_buf[1]; + + cmd_buf[0] = ST7567_SET_BIAS | (config->bias ? 1 : 0); + ret = st7567_write_bus(dev, cmd_buf, 1, true); + if (ret < 0) { + return ret; + } + + cmd_buf[0] = ST7567_POWER_CONTROL | ST7567_POWER_CONTROL_VB; + ret = st7567_write_bus(dev, cmd_buf, 1, true); + if (ret < 0) { + return ret; + } + + cmd_buf[0] = ST7567_POWER_CONTROL | ST7567_POWER_CONTROL_VB | ST7567_POWER_CONTROL_VR; + ret = st7567_write_bus(dev, cmd_buf, 1, true); + if (ret < 0) { + return ret; + } + + cmd_buf[0] = ST7567_POWER_CONTROL | ST7567_POWER_CONTROL_VB | ST7567_POWER_CONTROL_VR | + ST7567_POWER_CONTROL_VF; + ret = st7567_write_bus(dev, cmd_buf, 1, true); + if (ret < 0) { + return ret; + } + + cmd_buf[0] = ST7567_SET_REGULATION_RATIO | (config->regulation_ratio & 0x7); + ret = st7567_write_bus(dev, cmd_buf, 1, true); + if (ret < 0) { + return ret; + } + + cmd_buf[0] = ST7567_LINE_SCROLL | (config->line_offset & 0x3F); + ret = st7567_write_bus(dev, cmd_buf, 1, true); + if (ret < 0) { + return ret; + } + + return ret; +} + +static int st7567_resume(const struct device *dev) +{ + uint8_t cmd_buf[] = { + ST7567_DISPLAY_ALL_PIXEL_NORMAL, + ST7567_DISPLAY_ON, + }; + + return st7567_write_bus(dev, cmd_buf, sizeof(cmd_buf), true); +} + +static int st7567_suspend(const struct device *dev) +{ + uint8_t cmd_buf[] = { + ST7567_DISPLAY_OFF, + ST7567_DISPLAY_ALL_PIXEL_ON, + }; + + return st7567_write_bus(dev, cmd_buf, sizeof(cmd_buf), true); +} + +static int st7567_write_default(const struct device *dev, const uint16_t x, const uint16_t y, + const struct display_buffer_descriptor *desc, const void *buf, + const size_t buf_len) +{ + int ret; + uint8_t cmd_buf[3]; + + for (int i = 0; i < desc->height / 8; i++) { + cmd_buf[0] = ST7567_COLUMN_LSB | (x & 0xF); + cmd_buf[1] = ST7567_COLUMN_MSB | ((x >> 4) & 0xF); + cmd_buf[2] = ST7567_PAGE | ((y >> 3) + i); + ret = st7567_write_bus(dev, cmd_buf, sizeof(cmd_buf), true); + if (ret < 0) { + return ret; + } + ret = st7567_write_bus(dev, ((uint8_t *)buf + i * desc->pitch), desc->pitch, false); + if (ret < 0) { + return ret; + } + } + + return ret; +} + +static int st7567_write(const struct device *dev, const uint16_t x, const uint16_t y, + const struct display_buffer_descriptor *desc, const void *buf) +{ + size_t buf_len; + + if (desc->pitch < desc->width) { + LOG_ERR("Pitch is smaller than width"); + return -EINVAL; + } + + buf_len = MIN(desc->buf_size, desc->height * desc->width / 8); + if (buf == NULL || buf_len == 0U) { + LOG_ERR("Display buffer is not available"); + return -EINVAL; + } + + if (desc->pitch > desc->width) { + LOG_ERR("Unsupported mode"); + return -EINVAL; + } + + if ((y & 0x7) != 0U) { + LOG_ERR("Y coordinate must be aligned on page boundary"); + return -EINVAL; + } + + 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); + + return st7567_write_default(dev, x, y, desc, buf, buf_len); +} + +static int st7567_set_contrast(const struct device *dev, const uint8_t contrast) +{ + uint8_t cmd_buf[] = { + ST7567_SET_CONTRAST_CTRL, + contrast, + }; + + return st7567_write_bus(dev, cmd_buf, sizeof(cmd_buf), true); +} + +static void st7567_get_capabilities(const struct device *dev, struct display_capabilities *caps) +{ + const struct st7567_config *config = dev->config; + struct st7567_data *data = dev->data; + + caps->x_resolution = config->width; + caps->y_resolution = config->height; + caps->supported_pixel_formats = PIXEL_FORMAT_MONO10 | PIXEL_FORMAT_MONO01; + caps->current_pixel_format = data->pf; + caps->screen_info = SCREEN_INFO_MONO_VTILED; + caps->current_orientation = DISPLAY_ORIENTATION_NORMAL; +} + +static int st7567_set_pixel_format(const struct device *dev, const enum display_pixel_format pf) +{ + struct st7567_data *data = dev->data; + const struct st7567_config *config = dev->config; + uint8_t cmd; + int ret; + + if (pf == data->pf) { + return 0; + } + + if (pf == PIXEL_FORMAT_MONO10) { + cmd = config->inversion_on ? ST7567_SET_REVERSE_DISPLAY : ST7567_SET_NORMAL_DISPLAY; + } else if (pf == PIXEL_FORMAT_MONO01) { + cmd = config->inversion_on ? ST7567_SET_NORMAL_DISPLAY : ST7567_SET_REVERSE_DISPLAY; + } else { + LOG_WRN("Unsupported pixel format"); + return -ENOTSUP; + } + + ret = st7567_write_bus(dev, &cmd, 1, true); + if (ret) { + LOG_WRN("Couldn't set inversion"); + return ret; + } + + data->pf = pf; + + return 0; +} + +static int st7567_reset(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + uint8_t cmd_buf[] = { + ST7567_DISPLAY_OFF, + (config->inversion_on ? ST7567_SET_REVERSE_DISPLAY : ST7567_SET_NORMAL_DISPLAY), + }; + + /* Reset if pin connected */ + if (config->reset.port) { + k_sleep(K_MSEC(ST7567_RESET_DELAY)); + gpio_pin_set_dt(&config->reset, 1); + k_sleep(K_MSEC(ST7567_RESET_DELAY)); + gpio_pin_set_dt(&config->reset, 0); + k_sleep(K_MSEC(ST7567_RESET_DELAY)); + } + + return st7567_write_bus(dev, cmd_buf, sizeof(cmd_buf), true); +} + +static int st7567_clear(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + int ret; + uint8_t buf = 0; + + uint8_t cmd_buf[] = { + ST7567_COLUMN_LSB, + ST7567_COLUMN_MSB, + ST7567_PAGE, + }; + + for (int y = 0; y < config->height; y += 8) { + for (int x = 0; x < config->width; x++) { + cmd_buf[0] = ST7567_COLUMN_LSB | (x & 0xF); + cmd_buf[1] = ST7567_COLUMN_MSB | ((x >> 4) & 0xF); + cmd_buf[2] = ST7567_PAGE | (y >> 3); + ret = st7567_write_bus(dev, cmd_buf, sizeof(cmd_buf), true); + if (ret < 0) { + LOG_ERR("Error clearing display"); + return ret; + } + ret = st7567_write_bus(dev, (uint8_t *)&buf, 1, false); + if (ret < 0) { + LOG_ERR("Error clearing display"); + return ret; + } + } + } + return ret; +} + +static int st7567_init_device(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + struct st7567_data *data = dev->data; + int ret; + uint8_t cmd_buf[] = { + ST7567_DISPLAY_OFF, + (config->inversion_on ? ST7567_SET_REVERSE_DISPLAY : ST7567_SET_NORMAL_DISPLAY), + }; + + ret = st7567_reset(dev); + if (ret < 0) { + return ret; + } + + ret = st7567_suspend(dev); + if (ret < 0) { + return ret; + } + + ret = st7567_set_hardware_config(dev); + if (ret < 0) { + return ret; + } + + ret = st7567_set_panel_orientation(dev); + if (ret < 0) { + return ret; + } + + /* Set inversion */ + ret = st7567_write_bus(dev, cmd_buf, sizeof(cmd_buf), true); + if (ret < 0) { + return ret; + } + + data->pf = config->inversion_on ? PIXEL_FORMAT_MONO10 : PIXEL_FORMAT_MONO01; + + ret = st7567_set_contrast(dev, CONFIG_ST7567_DEFAULT_CONTRAST); + if (ret < 0) { + return ret; + } + + /* Clear display, RAM is undefined at power up */ + ret = st7567_clear(dev); + if (ret < 0) { + return ret; + } + ret = st7567_resume(dev); + + return ret; +} + +static int st7567_init(const struct device *dev) +{ + const struct st7567_config *config = dev->config; + int ret; + + if (!st7567_bus_ready(dev)) { + LOG_ERR("Bus device %s not ready!", config->bus_name(dev)); + return -EINVAL; + } + + if (config->reset.port) { + ret = gpio_pin_configure_dt(&config->reset, GPIO_OUTPUT_INACTIVE); + if (ret < 0) { + LOG_ERR("Couldn't configure reset pin"); + return ret; + } + if (!gpio_is_ready_dt(&config->reset)) { + LOG_ERR("Reset GPIO device not ready"); + return -ENODEV; + } + } + + if (st7567_init_device(dev)) { + LOG_ERR("Failed to initialize device!"); + return -EIO; + } + + return 0; +} + +static DEVICE_API(display, st7567_driver_api) = { + .blanking_on = st7567_suspend, + .blanking_off = st7567_resume, + .write = st7567_write, + .set_contrast = st7567_set_contrast, + .get_capabilities = st7567_get_capabilities, + .set_pixel_format = st7567_set_pixel_format, +}; + +#define ST7567_CONFIG_SPI(node_id) \ + .bus = {.spi = SPI_DT_SPEC_GET( \ + node_id, SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB | SPI_WORD_SET(8), 0)}, \ + .bus_ready = st7567_bus_ready_spi, .write_bus = st7567_write_bus_spi, \ + .bus_name = st7567_bus_name_spi, .data_cmd = GPIO_DT_SPEC_GET(node_id, data_cmd_gpios), + +#define ST7567_CONFIG_I2C(node_id) \ + .bus = {.i2c = I2C_DT_SPEC_GET(node_id)}, .bus_ready = st7567_bus_ready_i2c, \ + .write_bus = st7567_write_bus_i2c, .bus_name = st7567_bus_name_i2c, .data_cmd = {0}, + +#define ST7567_DEFINE(node_id) \ + static struct st7567_data data##node_id; \ + static const struct st7567_config config##node_id = { \ + .reset = GPIO_DT_SPEC_GET_OR(node_id, reset_gpios, {0}), \ + .height = DT_PROP(node_id, height), \ + .width = DT_PROP(node_id, width), \ + .column_offset = DT_PROP(node_id, column_offset), \ + .line_offset = DT_PROP(node_id, line_offset), \ + .segment_invdir = DT_PROP(node_id, segment_invdir), \ + .com_invdir = DT_PROP(node_id, com_invdir), \ + .inversion_on = DT_PROP(node_id, inversion_on), \ + .bias = DT_PROP(node_id, bias), \ + .regulation_ratio = DT_PROP(node_id, regulation_ratio), \ + COND_CODE_1(DT_ON_BUS(node_id, spi), (ST7567_CONFIG_SPI(node_id)), \ + (ST7567_CONFIG_I2C(node_id))) }; \ + \ + DEVICE_DT_DEFINE(node_id, st7567_init, NULL, &data##node_id, &config##node_id, \ + POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &st7567_driver_api); + +DT_FOREACH_STATUS_OKAY(sitronix_st7567, ST7567_DEFINE) diff --git a/drivers/display/display_st7567_regs.h b/drivers/display/display_st7567_regs.h new file mode 100644 index 00000000000..e963e3cf490 --- /dev/null +++ b/drivers/display/display_st7567_regs.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 MASSDRIVER EI (massdriver.space) + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __DISPLAY_ST7567_REGS_H__ +#define __DISPLAY_ST7567_REGS_H__ + +/* All following bytes will contain commands */ +#define ST7567_CONTROL_ALL_BYTES_CMD 0x00 +/* All following bytes will contain data */ +#define ST7567_CONTROL_ALL_BYTES_DATA 0x40 +/* The next byte will contain a command, then expect another control byte */ +#define ST7567_CONTROL_CONTINUE_BYTE_CMD 0x80 + +#define ST7567_DISPLAY_OFF 0xAE +#define ST7567_DISPLAY_ON 0xAF +#define ST7567_DISPLAY_ALL_PIXEL_ON 0xA5 +#define ST7567_DISPLAY_ALL_PIXEL_NORMAL 0xA4 +#define ST7567_SET_CONTRAST_CTRL 0x81 +#define ST7567_SET_BIAS 0xA2 +#define ST7567_SET_REGULATION_RATIO 0x20 + +/* Inversion controls */ +#define ST7567_SET_NORMAL_DISPLAY 0xA6 +#define ST7567_SET_REVERSE_DISPLAY 0xA7 + +/* COM invdir */ +#define ST7567_SET_COM_OUTPUT_SCAN_FLIPPED 0xC8 +#define ST7567_SET_COM_OUTPUT_SCAN_NORMAL 0xC0 + +/* SEG invdir */ +#define ST7567_SET_SEGMENT_MAP_FLIPPED 0xA1 +#define ST7567_SET_SEGMENT_MAP_NORMAL 0xA0 + +/* Power Control */ +#define ST7567_POWER_CONTROL 0x28 +#define ST7567_POWER_CONTROL_VF 0x1 +#define ST7567_POWER_CONTROL_VR 0x2 +#define ST7567_POWER_CONTROL_VB 0x4 + +/* Offsets */ +#define ST7567_COLUMN_MSB 0x10 +#define ST7567_COLUMN_LSB 0x0 +#define ST7567_PAGE 0xB0 +#define ST7567_LINE_SCROLL 0x40 + +#define ST7567_RESET_DELAY 1 +#define ST7567_RESET 0xE2 + +#endif /* __DISPLAY_ST7567_REGS_H__ */ diff --git a/dts/bindings/display/sitronix,st7567-common.yaml b/dts/bindings/display/sitronix,st7567-common.yaml new file mode 100644 index 00000000000..1152a781759 --- /dev/null +++ b/dts/bindings/display/sitronix,st7567-common.yaml @@ -0,0 +1,44 @@ +# Copyright (c) 2025 MASSDRIVER EI (massdriver.space) +# SPDX-License-Identifier: Apache-2.0 + +include: display-controller.yaml + +properties: + column-offset: + type: int + required: true + description: 8-bits start column address + + line-offset: + type: int + required: true + description: 6-bits start line offset + + regulation-ratio: + type: int + required: true + description: Regulation resistor ratio (3-bits RR2 RR1 RR0) + + segment-invdir: + type: boolean + description: Scan direction is from last SEG output to first SEG output + + com-invdir: + type: boolean + description: Scan direction is from last COM output to first COM output + + inversion-on: + type: boolean + description: Invert display + + bias: + type: boolean + description: Bias setting (0 for 1/9, or 1 for 1/7) + + reset-gpios: + type: phandle-array + description: RESET pin. + + The RESET pin of ST7567 is active low. + If connected directly, the MCU pin should be configured + as active low. diff --git a/dts/bindings/display/sitronix,st7567-i2c.yaml b/dts/bindings/display/sitronix,st7567-i2c.yaml new file mode 100644 index 00000000000..a0554d902a6 --- /dev/null +++ b/dts/bindings/display/sitronix,st7567-i2c.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2025 MASSDRIVER EI (massdriver.space) +# SPDX-License-Identifier: Apache-2.0 + +description: ST7567 132x65 dot-matrix LCD controller on I2C bus + +compatible: "sitronix,st7567" + +include: ["sitronix,st7567-common.yaml", "i2c-device.yaml"] diff --git a/dts/bindings/display/sitronix,st7567-spi.yaml b/dts/bindings/display/sitronix,st7567-spi.yaml new file mode 100644 index 00000000000..e90722e38ef --- /dev/null +++ b/dts/bindings/display/sitronix,st7567-spi.yaml @@ -0,0 +1,14 @@ +# Copyright (c) 2025 MASSDRIVER EI (massdriver.space) +# SPDX-License-Identifier: Apache-2.0 + +description: ST7567 132x65 dot-matrix LCD controller on SPI bus + +compatible: "sitronix,st7567" + +include: ["sitronix,st7567-common.yaml", "spi-device.yaml"] + +properties: + data-cmd-gpios: + type: phandle-array + required: true + description: D/C# pin.