| /* |
| * Copyright (c) 2025 MASSDRIVER EI (massdriver.space) |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(display_st7567, CONFIG_DISPLAY_LOG_LEVEL); |
| |
| #include <string.h> |
| #include <zephyr/device.h> |
| #include <zephyr/init.h> |
| #include <zephyr/drivers/display.h> |
| #include <zephyr/drivers/gpio.h> |
| #include <zephyr/drivers/i2c.h> |
| #include <zephyr/drivers/spi.h> |
| #include <zephyr/kernel.h> |
| |
| #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 = 0; |
| 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) |