blob: a62bb416c5f34f2269c49c5489ecedf646a1ce39 [file] [log] [blame]
/*
* Copyright 2023 NXP
* Copyright 2024-2025 TiaC Systems
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT zephyr_mipi_dbi_spi
#include <zephyr/drivers/mipi_dbi.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(mipi_dbi_spi, CONFIG_MIPI_DBI_LOG_LEVEL);
/* Expands to 1 if the node does not have the `write-only` property */
#define MIPI_DBI_SPI_WRITE_ONLY_ABSENT(n) (!DT_INST_PROP(n, write_only)) |
/* This macro will evaluate to 1 if any of the nodes with zephyr,mipi-dbi-spi
* lack a `write-only` property. The intention here is to allow the entire
* command_read function to be optimized out when it is not needed.
*/
#define MIPI_DBI_SPI_READ_REQUIRED DT_INST_FOREACH_STATUS_OKAY(MIPI_DBI_SPI_WRITE_ONLY_ABSENT) 0
uint32_t var = MIPI_DBI_SPI_READ_REQUIRED;
/* Expands to 1 if the node configures a gpio in the `te-gpios` property */
#define MIPI_DBI_SPI_TE_GPIOS_PRESENT(n) DT_INST_NODE_HAS_PROP(n, te_gpios) |
/* This macro will evaluate to 1 if any of the nodes with zephyr,mipi-dbi-spi
* has a `te-gpios` property. The intention here is to allow the entire
* configure_te and mipi_dbi_spi_te_cb functions to be optimized out when it
* is not needed.
*/
#define MIPI_DBI_SPI_TE_REQUIRED DT_INST_FOREACH_STATUS_OKAY(MIPI_DBI_SPI_TE_GPIOS_PRESENT) 0
/* Expands to 1 if the node does reflect the enum in `xfr-min-bits` property */
#define MIPI_DBI_SPI_XFR_8BITS(n) (DT_INST_STRING_UPPER_TOKEN(n, xfr_min_bits) \
== MIPI_DBI_SPI_XFR_8BIT) |
#define MIPI_DBI_SPI_XFR_16BITS(n) (DT_INST_STRING_UPPER_TOKEN(n, xfr_min_bits) \
== MIPI_DBI_SPI_XFR_16BIT) |
/* This macros will evaluate to 1 if any of the nodes with zephyr,mipi-dbi-spi
* have the `xfr-min-bits` property to corresponding enum value. The intention
* here is to allow the write helper functions to be optimized out when not all
* minimum transfer bits will be needed.
*/
#define MIPI_DBI_SPI_WRITE_8BIT_REQUIRED DT_INST_FOREACH_STATUS_OKAY(MIPI_DBI_SPI_XFR_8BITS) 0
#define MIPI_DBI_SPI_WRITE_16BIT_REQUIRED DT_INST_FOREACH_STATUS_OKAY(MIPI_DBI_SPI_XFR_16BITS) 0
/* In Type C mode 1 MIPI BIT communication, the 9th bit of the word
* (first bit sent in each word) indicates if the word is a command or
* data. Typically 0 indicates a command and 1 indicates data, but some
* displays may vary.
* Index starts from 0 so that BIT(8) means 9th bit.
*/
#define MIPI_DBI_DC_BIT BIT(8)
struct mipi_dbi_spi_config {
/* SPI hardware used to send data */
const struct device *spi_dev;
/* Command/Data gpio */
const struct gpio_dt_spec cmd_data;
/* Tearing Effect GPIO */
const struct gpio_dt_spec tearing_effect;
/* Reset GPIO */
const struct gpio_dt_spec reset;
/* Minimum transfer bits */
const uint8_t xfr_min_bits;
};
struct mipi_dbi_spi_data {
struct k_mutex lock;
#if MIPI_DBI_SPI_TE_REQUIRED
struct k_sem te_signal;
k_timeout_t te_delay;
atomic_t in_active_area;
struct gpio_callback te_cb_data;
#endif
/* Used for 3 wire mode */
uint16_t spi_byte;
};
#if MIPI_DBI_SPI_TE_REQUIRED
static void mipi_dbi_spi_te_cb(const struct device *dev,
struct gpio_callback *cb,
uint32_t pins)
{
ARG_UNUSED(dev);
ARG_UNUSED(pins);
struct mipi_dbi_spi_data *data = CONTAINER_OF(cb,
struct mipi_dbi_spi_data, te_cb_data);
/* Open frame window */
if (!atomic_cas(&data->in_active_area, 0, 1)) {
return;
}
k_sem_give(&data->te_signal);
}
#endif /* MIPI_DBI_SPI_TE_REQUIRED */
static inline int
mipi_dbi_spi_write_helper_3wire(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
bool cmd_present, uint8_t cmd,
const uint8_t *data_buf, size_t len)
{
const struct mipi_dbi_spi_config *config = dev->config;
struct mipi_dbi_spi_data *data = dev->data;
struct spi_buf buffer;
struct spi_buf_set buf_set = {
.buffers = &buffer,
.count = 1,
};
int ret = 0;
/*
* 9 bit word mode must be used, as the command/data bit
* is stored before the data word.
*/
if ((dbi_config->config.operation & SPI_WORD_SIZE_MASK)
!= SPI_WORD_SET(9)) {
return -ENOTSUP;
}
buffer.buf = &data->spi_byte;
buffer.len = 2;
/* Send command */
if (cmd_present) {
data->spi_byte = cmd;
ret = spi_write(config->spi_dev, &dbi_config->config, &buf_set);
if (ret < 0) {
goto out;
}
}
/* Write data, byte by byte */
for (size_t i = 0; i < len; i++) {
data->spi_byte = MIPI_DBI_DC_BIT | data_buf[i];
ret = spi_write(config->spi_dev, &dbi_config->config, &buf_set);
if (ret < 0) {
goto out;
}
}
out:
return ret;
}
#if MIPI_DBI_SPI_WRITE_8BIT_REQUIRED
static inline int
mipi_dbi_spi_write_helper_4wire_8bit(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
bool cmd_present, uint8_t cmd,
const uint8_t *data_buf, size_t len)
{
const struct mipi_dbi_spi_config *config = dev->config;
struct spi_buf buffer;
struct spi_buf_set buf_set = {
.buffers = &buffer,
.count = 1,
};
int ret = 0;
/*
* 4 wire mode is much simpler. We just toggle the
* command/data GPIO to indicate if we are sending
* a command or data
*/
buffer.buf = &cmd;
buffer.len = sizeof(cmd);
if (cmd_present) {
/* Set CD pin low for command */
gpio_pin_set_dt(&config->cmd_data, 0);
ret = spi_write(config->spi_dev, &dbi_config->config, &buf_set);
if (ret < 0) {
goto out;
}
}
if (len > 0) {
buffer.buf = (void *)data_buf;
buffer.len = len;
/* Set CD pin high for data */
gpio_pin_set_dt(&config->cmd_data, 1);
ret = spi_write(config->spi_dev, &dbi_config->config, &buf_set);
if (ret < 0) {
goto out;
}
}
out:
return ret;
}
#endif /* MIPI_DBI_SPI_WRITE_8BIT_REQUIRED */
#if MIPI_DBI_SPI_WRITE_16BIT_REQUIRED
static inline int
mipi_dbi_spi_write_helper_4wire_16bit(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
bool cmd_present, uint8_t cmd,
const uint8_t *data_buf, size_t len)
{
const struct mipi_dbi_spi_config *config = dev->config;
struct spi_buf buffer;
struct spi_buf_set buf_set = {
.buffers = &buffer,
.count = 1,
};
uint16_t data16;
int ret = 0;
/*
* 4 wire mode with toggle the command/data GPIO
* to indicate if we are sending a command or data
* but send 16-bit blocks (with bit stuffing).
*/
if (cmd_present) {
data16 = sys_cpu_to_be16(cmd);
buffer.buf = &data16;
buffer.len = sizeof(data16);
/* Set CD pin low for command */
gpio_pin_set_dt(&config->cmd_data, 0);
ret = spi_write(config->spi_dev, &dbi_config->config,
&buf_set);
if (ret < 0) {
goto out;
}
/* Set CD pin high for data, if there are any */
if (len > 0) {
gpio_pin_set_dt(&config->cmd_data, 1);
}
/* iterate command data */
for (int i = 0; i < len; i++) {
data16 = sys_cpu_to_be16(data_buf[i]);
ret = spi_write(config->spi_dev, &dbi_config->config,
&buf_set);
if (ret < 0) {
goto out;
}
}
} else {
int stuffing = len % sizeof(data16);
/* Set CD pin high for data, if there are any */
if (len > 0) {
gpio_pin_set_dt(&config->cmd_data, 1);
}
/* pass through generic device data */
if (len - stuffing > 0) {
buffer.buf = (void *)data_buf;
buffer.len = len - stuffing;
ret = spi_write(config->spi_dev, &dbi_config->config,
&buf_set);
if (ret < 0) {
goto out;
}
}
/* iterate remaining data with stuffing */
for (int i = len - stuffing; i < len; i++) {
data16 = sys_cpu_to_be16(data_buf[i]);
buffer.buf = &data16;
buffer.len = sizeof(data16);
ret = spi_write(config->spi_dev, &dbi_config->config,
&buf_set);
if (ret < 0) {
goto out;
}
}
}
out:
return ret;
}
#endif /* MIPI_DBI_SPI_WRITE_16BIT_REQUIRED */
static int mipi_dbi_spi_write_helper(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
bool cmd_present, uint8_t cmd,
const uint8_t *data_buf, size_t len)
{
const struct mipi_dbi_spi_config *config = dev->config;
struct mipi_dbi_spi_data *data = dev->data;
int ret = 0;
ret = k_mutex_lock(&data->lock, K_FOREVER);
if (ret < 0) {
return ret;
}
if (dbi_config->mode == MIPI_DBI_MODE_SPI_3WIRE &&
IS_ENABLED(CONFIG_MIPI_DBI_SPI_3WIRE)) {
ret = mipi_dbi_spi_write_helper_3wire(dev, dbi_config,
cmd_present, cmd,
data_buf, len);
goto out;
}
if (dbi_config->mode == MIPI_DBI_MODE_SPI_4WIRE) {
#if MIPI_DBI_SPI_WRITE_8BIT_REQUIRED
if (config->xfr_min_bits == MIPI_DBI_SPI_XFR_8BIT) {
ret = mipi_dbi_spi_write_helper_4wire_8bit(
dev, dbi_config,
cmd_present, cmd,
data_buf, len);
goto out;
}
#endif
#if MIPI_DBI_SPI_WRITE_16BIT_REQUIRED
if (config->xfr_min_bits == MIPI_DBI_SPI_XFR_16BIT) {
ret = mipi_dbi_spi_write_helper_4wire_16bit(
dev, dbi_config,
cmd_present, cmd,
data_buf, len);
goto out;
}
#endif
}
/* Otherwise, unsupported mode */
ret = -ENOTSUP;
out:
k_mutex_unlock(&data->lock);
return ret;
}
static int mipi_dbi_spi_command_write(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
uint8_t cmd, const uint8_t *data_buf,
size_t len)
{
return mipi_dbi_spi_write_helper(dev, dbi_config, true, cmd,
data_buf, len);
}
static int mipi_dbi_spi_write_display(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
const uint8_t *framebuf,
struct display_buffer_descriptor *desc,
enum display_pixel_format pixfmt)
{
ARG_UNUSED(pixfmt);
int ret = 0;
#if MIPI_DBI_SPI_TE_REQUIRED
struct mipi_dbi_spi_data *data = dev->data;
/* Wait for TE signal, otherwise transferring can begin */
if (!atomic_get(&data->in_active_area)) {
ret = k_sem_take(&data->te_signal, K_FOREVER);
if (ret < 0) {
return ret;
}
k_sleep(data->te_delay);
}
#endif
ret = mipi_dbi_spi_write_helper(dev, dbi_config, false, 0x0,
framebuf, desc->buf_size);
#if MIPI_DBI_SPI_TE_REQUIRED
/* End of frame reset */
if (!desc->frame_incomplete) {
atomic_set(&data->in_active_area, 0);
}
#endif
return ret;
}
#if MIPI_DBI_SPI_READ_REQUIRED
static inline int
mipi_dbi_spi_read_helper_3wire(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
uint8_t *cmds, size_t num_cmds,
uint8_t *response, size_t len)
{
const struct mipi_dbi_spi_config *config = dev->config;
struct mipi_dbi_spi_data *data = dev->data;
struct spi_config tmp_config;
struct spi_buf buffer;
struct spi_buf_set buf_set = {
.buffers = &buffer,
.count = 1,
};
int ret = 0;
/*
* We have to emulate 3 wire mode by packing the data/command
* bit into the upper bit of the SPI transfer, switch SPI to
* 9 bit mode, and write the transfer.
*/
if ((dbi_config->config.operation & SPI_WORD_SIZE_MASK)
!= SPI_WORD_SET(9)) {
return -ENOTSUP;
}
memcpy(&tmp_config, &dbi_config->config, sizeof(tmp_config));
tmp_config.operation &= ~SPI_WORD_SIZE_MASK;
tmp_config.operation |= SPI_WORD_SET(9);
buffer.buf = &data->spi_byte;
buffer.len = 1;
/* Send each command */
for (size_t i = 0; i < num_cmds; i++) {
data->spi_byte = cmds[i];
ret = spi_write(config->spi_dev, &tmp_config, &buf_set);
if (ret < 0) {
goto out;
}
}
/* Now, we can switch to 8 bit mode, and read data */
buffer.buf = (void *)response;
buffer.len = len;
ret = spi_read(config->spi_dev, &dbi_config->config, &buf_set);
out:
spi_release(config->spi_dev, &tmp_config); /* Really necessary here? */
return ret;
}
static inline int
mipi_dbi_spi_read_helper_4wire(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
uint8_t *cmds, size_t num_cmds,
uint8_t *response, size_t len)
{
const struct mipi_dbi_spi_config *config = dev->config;
struct spi_config tmp_config;
struct spi_buf buffer;
struct spi_buf_set buf_set = {
.buffers = &buffer,
.count = 1,
};
int ret = 0;
/*
* 4 wire mode is much simpler. We just toggle the
* command/data GPIO to indicate if we are sending
* a command or data. Note that since some SPI displays
* require CS to be held low for the entire read sequence,
* we set SPI_HOLD_ON_CS
*/
memcpy(&tmp_config, &dbi_config->config, sizeof(tmp_config));
tmp_config.operation |= SPI_HOLD_ON_CS;
if (num_cmds > 0) {
buffer.buf = cmds;
buffer.len = num_cmds;
/* Set CD pin low for command */
gpio_pin_set_dt(&config->cmd_data, 0);
ret = spi_write(config->spi_dev, &tmp_config, &buf_set);
if (ret < 0) {
goto out;
}
}
if (len > 0) {
buffer.buf = (void *)response;
buffer.len = len;
/* Set CD pin high for data */
gpio_pin_set_dt(&config->cmd_data, 1);
ret = spi_read(config->spi_dev, &tmp_config, &buf_set);
if (ret < 0) {
goto out;
}
}
out:
spi_release(config->spi_dev, &tmp_config);
return ret;
}
static int mipi_dbi_spi_command_read(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
uint8_t *cmds, size_t num_cmds,
uint8_t *response, size_t len)
{
struct mipi_dbi_spi_data *data = dev->data;
int ret = 0;
ret = k_mutex_lock(&data->lock, K_FOREVER);
if (ret < 0) {
return ret;
}
if (dbi_config->mode == MIPI_DBI_MODE_SPI_3WIRE &&
IS_ENABLED(CONFIG_MIPI_DBI_SPI_3WIRE)) {
ret = mipi_dbi_spi_read_helper_3wire(dev, dbi_config,
cmds, num_cmds,
response, len);
if (ret < 0) {
goto out;
}
} else if (dbi_config->mode == MIPI_DBI_MODE_SPI_4WIRE) {
ret = mipi_dbi_spi_read_helper_4wire(dev, dbi_config,
cmds, num_cmds,
response, len);
if (ret < 0) {
goto out;
}
} else {
/* Otherwise, unsupported mode */
ret = -ENOTSUP;
}
out:
k_mutex_unlock(&data->lock);
return ret;
}
#endif /* MIPI_DBI_SPI_READ_REQUIRED */
static inline bool mipi_dbi_has_pin(const struct gpio_dt_spec *spec)
{
return spec->port != NULL;
}
static int mipi_dbi_spi_reset(const struct device *dev, k_timeout_t delay)
{
const struct mipi_dbi_spi_config *config = dev->config;
int ret;
if (!mipi_dbi_has_pin(&config->reset)) {
return -ENOTSUP;
}
ret = gpio_pin_set_dt(&config->reset, 1);
if (ret < 0) {
return ret;
}
k_sleep(delay);
return gpio_pin_set_dt(&config->reset, 0);
}
static int mipi_dbi_spi_release(const struct device *dev,
const struct mipi_dbi_config *dbi_config)
{
const struct mipi_dbi_spi_config *config = dev->config;
return spi_release(config->spi_dev, &dbi_config->config);
}
#if MIPI_DBI_SPI_TE_REQUIRED
static int mipi_dbi_spi_configure_te(const struct device *dev,
uint8_t edge,
k_timeout_t delay_us)
{
const struct mipi_dbi_spi_config *config = dev->config;
struct mipi_dbi_spi_data *data = dev->data;
int ret = 0;
if (edge == MIPI_DBI_TE_NO_EDGE) {
/* No configuration */
return 0;
}
if (!mipi_dbi_has_pin(&config->tearing_effect)) {
return -ENOTSUP;
}
if (!gpio_is_ready_dt(&config->tearing_effect)) {
return -ENODEV;
}
ret = gpio_pin_configure_dt(&config->tearing_effect, GPIO_INPUT);
if (ret < 0) {
LOG_ERR("Could not configure Tearing Effect GPIO (%d)", ret);
return ret;
}
if (edge == MIPI_DBI_TE_RISING_EDGE) {
ret = gpio_pin_interrupt_configure_dt(&config->tearing_effect,
GPIO_INT_EDGE_RISING);
} else if (edge == MIPI_DBI_TE_FALLING_EDGE) {
ret = gpio_pin_interrupt_configure_dt(&config->tearing_effect,
GPIO_INT_EDGE_FALLING);
}
if (ret < 0) {
LOG_ERR("Could not configure Tearing Effect GPIO EXT interrupt (%d)", ret);
return ret;
}
gpio_init_callback(&data->te_cb_data, mipi_dbi_spi_te_cb,
BIT(config->tearing_effect.pin));
ret = gpio_add_callback(config->tearing_effect.port,
&data->te_cb_data);
if (ret < 0) {
LOG_ERR("Could not add Tearing Effect GPIO callback (%d)", ret);
return ret;
}
data->te_delay = delay_us;
atomic_set(&data->in_active_area, 0);
k_sem_init(&data->te_signal, 0, 1);
return ret;
}
#endif /* MIPI_DBI_SPI_TE_REQUIRED */
static int mipi_dbi_spi_init(const struct device *dev)
{
const struct mipi_dbi_spi_config *config = dev->config;
struct mipi_dbi_spi_data *data = dev->data;
int ret;
if (!device_is_ready(config->spi_dev)) {
LOG_ERR("SPI device is not ready");
return -ENODEV;
}
if (mipi_dbi_has_pin(&config->cmd_data)) {
if (!gpio_is_ready_dt(&config->cmd_data)) {
return -ENODEV;
}
ret = gpio_pin_configure_dt(&config->cmd_data, GPIO_OUTPUT);
if (ret < 0) {
LOG_ERR("Could not configure command/data GPIO (%d)", ret);
return ret;
}
}
if (mipi_dbi_has_pin(&config->reset)) {
if (!gpio_is_ready_dt(&config->reset)) {
return -ENODEV;
}
ret = gpio_pin_configure_dt(&config->reset, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
LOG_ERR("Could not configure reset GPIO (%d)", ret);
return ret;
}
}
k_mutex_init(&data->lock);
return 0;
}
static DEVICE_API(mipi_dbi, mipi_dbi_spi_driver_api) = {
.reset = mipi_dbi_spi_reset,
.command_write = mipi_dbi_spi_command_write,
.write_display = mipi_dbi_spi_write_display,
.release = mipi_dbi_spi_release,
#if MIPI_DBI_SPI_READ_REQUIRED
.command_read = mipi_dbi_spi_command_read,
#endif
#if MIPI_DBI_SPI_TE_REQUIRED
.configure_te = mipi_dbi_spi_configure_te,
#endif
};
#define MIPI_DBI_SPI_INIT(n) \
static const struct mipi_dbi_spi_config \
mipi_dbi_spi_config_##n = { \
.spi_dev = DEVICE_DT_GET( \
DT_INST_PHANDLE(n, spi_dev)), \
.cmd_data = GPIO_DT_SPEC_INST_GET_OR(n, dc_gpios, {}), \
.tearing_effect = GPIO_DT_SPEC_INST_GET_OR(n, te_gpios, {}), \
.reset = GPIO_DT_SPEC_INST_GET_OR(n, reset_gpios, {}), \
.xfr_min_bits = DT_INST_STRING_UPPER_TOKEN(n, xfr_min_bits) \
}; \
static struct mipi_dbi_spi_data mipi_dbi_spi_data_##n; \
\
DEVICE_DT_INST_DEFINE(n, mipi_dbi_spi_init, NULL, \
&mipi_dbi_spi_data_##n, \
&mipi_dbi_spi_config_##n, \
POST_KERNEL, \
CONFIG_MIPI_DBI_INIT_PRIORITY, \
&mipi_dbi_spi_driver_api);
DT_INST_FOREACH_STATUS_OKAY(MIPI_DBI_SPI_INIT)