blob: b391f58212dc206eea91c27d296ca554dece7c8a [file] [log] [blame]
/*
* 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/i2c.h>
#include <zephyr/drivers/mipi_dbi.h>
#include <zephyr/kernel.h>
#include "display_st7567_regs.h"
struct st7567_dbi {
const struct device *mipi_dev;
const struct mipi_dbi_config dbi_config;
};
union st7567_bus {
struct i2c_dt_spec i2c;
struct st7567_dbi dbi;
};
typedef bool (*st7567_bus_ready_fn)(const struct device *dev);
typedef int (*st7567_write_cmd_bus_fn)(const struct device *dev, const uint8_t *buf, size_t len);
typedef int (*st7567_write_pixels_bus_fn)(const struct device *dev, const uint8_t *buf, size_t len);
typedef void (*st7567_release_bus_fn)(const struct device *dev);
typedef int (*st7567_reset_fn)(const struct device *dev);
typedef const char *(*st7567_bus_name_fn)(const struct device *dev);
struct st7567_config {
union st7567_bus bus;
st7567_bus_ready_fn bus_ready;
st7567_write_cmd_bus_fn write_cmd_bus;
st7567_write_pixels_bus_fn write_pixels_bus;
st7567_release_bus_fn release_bus;
st7567_reset_fn reset;
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_cmd_bus_i2c(const struct device *dev, const uint8_t *buf, size_t len)
{
const struct st7567_config *config = dev->config;
return i2c_burst_write_dt(&config->bus.i2c, ST7567_CONTROL_ALL_BYTES_CMD, buf, len);
}
static int st7567_write_pixels_bus_i2c(const struct device *dev, const uint8_t *buf, size_t len)
{
const struct st7567_config *config = dev->config;
return i2c_burst_write_dt(&config->bus.i2c, 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;
}
static int st7567_reset_i2c(const struct device *dev)
{
/* do nothing */
return 0;
}
static void st7567_release_bus_i2c(const struct device *dev)
{
/* do nothing */
}
#endif
#if DT_HAS_COMPAT_ON_BUS_STATUS_OKAY(sitronix_st7567, mipi_dbi)
static bool st7567_bus_ready_dbi(const struct device *dev)
{
const struct st7567_config *config = dev->config;
return device_is_ready(config->bus.dbi.mipi_dev);
}
static int st7567_write_cmd_bus_dbi(const struct device *dev, const uint8_t *buf, size_t len)
{
const struct st7567_config *config = dev->config;
int ret;
for (size_t i = 0; i < len; i++) {
ret = mipi_dbi_command_write(config->bus.dbi.mipi_dev, &config->bus.dbi.dbi_config,
buf[i], NULL, 0);
if (ret) {
return ret;
}
}
mipi_dbi_release(config->bus.dbi.mipi_dev, &config->bus.dbi.dbi_config);
return ret;
}
static int st7567_write_pixels_bus_dbi(const struct device *dev, const uint8_t *buf, size_t len)
{
const struct st7567_config *config = dev->config;
struct display_buffer_descriptor mipi_desc;
int ret;
mipi_desc.height = 8;
mipi_desc.width = len;
mipi_desc.pitch = len;
mipi_desc.buf_size = len;
ret = mipi_dbi_write_display(config->bus.dbi.mipi_dev, &config->bus.dbi.dbi_config,
buf, &mipi_desc, PIXEL_FORMAT_MONO01);
return ret;
}
static const char *st7567_bus_name_dbi(const struct device *dev)
{
const struct st7567_config *config = dev->config;
return config->bus.dbi.mipi_dev->name;
}
static int st7567_reset_dbi(const struct device *dev)
{
const struct st7567_config *config = dev->config;
int err;
err = mipi_dbi_reset(config->bus.dbi.mipi_dev, ST7567_RESET_DELAY);
if (err < 0) {
LOG_ERR("Failed to reset device!");
}
return err;
}
static void st7567_release_bus_dbi(const struct device *dev)
{
const struct st7567_config *config = dev->config;
mipi_dbi_release(config->bus.dbi.mipi_dev, &config->bus.dbi.dbi_config);
}
#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_cmd_bus(const struct device *dev, const uint8_t *buf, size_t len)
{
const struct st7567_config *config = dev->config;
return config->write_cmd_bus(dev, buf, len);
}
static inline int st7567_write_pixels_bus(const struct device *dev, const uint8_t *buf, size_t len)
{
const struct st7567_config *config = dev->config;
return config->write_pixels_bus(dev, buf, len);
}
static inline void release_bus(const struct device *dev)
{
const struct st7567_config *config = dev->config;
config->release_bus(dev);
}
static inline int st7567_hw_reset(const struct device *dev)
{
const struct st7567_config *config = dev->config;
return config->reset(dev);
}
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_cmd_bus(dev, cmd_buf, sizeof(cmd_buf));
}
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_cmd_bus(dev, cmd_buf, 1);
if (ret < 0) {
return ret;
}
cmd_buf[0] = ST7567_POWER_CONTROL | ST7567_POWER_CONTROL_VB;
ret = st7567_write_cmd_bus(dev, cmd_buf, 1);
if (ret < 0) {
return ret;
}
cmd_buf[0] = ST7567_POWER_CONTROL | ST7567_POWER_CONTROL_VB | ST7567_POWER_CONTROL_VR;
ret = st7567_write_cmd_bus(dev, cmd_buf, 1);
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_cmd_bus(dev, cmd_buf, 1);
if (ret < 0) {
return ret;
}
cmd_buf[0] = ST7567_SET_REGULATION_RATIO | (config->regulation_ratio & 0x7);
ret = st7567_write_cmd_bus(dev, cmd_buf, 1);
if (ret < 0) {
return ret;
}
cmd_buf[0] = ST7567_LINE_SCROLL | (config->line_offset & 0x3F);
ret = st7567_write_cmd_bus(dev, cmd_buf, 1);
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_cmd_bus(dev, cmd_buf, sizeof(cmd_buf));
}
static int st7567_suspend(const struct device *dev)
{
uint8_t cmd_buf[] = {
ST7567_DISPLAY_OFF,
ST7567_DISPLAY_ALL_PIXEL_ON,
};
return st7567_write_cmd_bus(dev, cmd_buf, sizeof(cmd_buf));
}
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_cmd_bus(dev, cmd_buf, sizeof(cmd_buf));
if (ret < 0) {
return ret;
}
ret = st7567_write_pixels_bus(dev, ((const uint8_t *)buf + i * desc->pitch),
desc->pitch);
if (ret < 0) {
return ret;
}
}
release_bus(dev);
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_cmd_bus(dev, cmd_buf, sizeof(cmd_buf));
}
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_cmd_bus(dev, &cmd, 1);
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;
int ret;
uint8_t cmd_buf[] = {
ST7567_DISPLAY_OFF,
(config->inversion_on ? ST7567_SET_REVERSE_DISPLAY : ST7567_SET_NORMAL_DISPLAY),
};
ret = st7567_hw_reset(dev);
if (ret < 0) {
return ret;
}
return st7567_write_cmd_bus(dev, cmd_buf, sizeof(cmd_buf));
}
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_cmd_bus(dev, cmd_buf, sizeof(cmd_buf));
if (ret < 0) {
LOG_ERR("Error clearing display");
return ret;
}
ret = st7567_write_pixels_bus(dev, (uint8_t *)&buf, 1);
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_cmd_bus(dev, cmd_buf, sizeof(cmd_buf));
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;
if (!st7567_bus_ready(dev)) {
LOG_ERR("Bus device %s not ready!", config->bus_name(dev));
return -EINVAL;
}
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,
.clear = st7567_clear,
.set_contrast = st7567_set_contrast,
.get_capabilities = st7567_get_capabilities,
.set_pixel_format = st7567_set_pixel_format,
};
#define ST7567_WORD_SIZE(inst) \
((DT_STRING_UPPER_TOKEN(inst, mipi_mode) == MIPI_DBI_MODE_SPI_4WIRE) ? SPI_WORD_SET(8) \
: SPI_WORD_SET(9))
#define ST7567_CONFIG_DBI(node_id) \
.bus = {.dbi.dbi_config = MIPI_DBI_CONFIG_DT( \
node_id, ST7567_WORD_SIZE(node_id) | SPI_OP_MODE_MASTER, 0), \
.dbi.mipi_dev = DEVICE_DT_GET(DT_PARENT(node_id)),}, \
.bus_ready = st7567_bus_ready_dbi, \
.write_cmd_bus = st7567_write_cmd_bus_dbi, .write_pixels_bus = st7567_write_pixels_bus_dbi,\
.bus_name = st7567_bus_name_dbi, .release_bus = st7567_release_bus_dbi, \
.reset = st7567_reset_dbi,
#define ST7567_CONFIG_I2C(node_id) \
.bus = {.i2c = I2C_DT_SPEC_GET(node_id)}, .bus_ready = st7567_bus_ready_i2c, \
.write_cmd_bus = st7567_write_cmd_bus_i2c, .write_pixels_bus = st7567_write_pixels_bus_i2c,\
.bus_name = st7567_bus_name_i2c, .release_bus = st7567_release_bus_i2c, \
.reset = st7567_reset_i2c,
#define ST7567_DEFINE(node_id) \
static struct st7567_data data##node_id; \
static const struct st7567_config config##node_id = { \
.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, mipi_dbi), (ST7567_CONFIG_DBI(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)