blob: 6187d38d16e8f485eea14ec764e936b99d8eb9bb [file] [log] [blame]
/*
* Copyright (c) 2023 Renesas Electronics Corporation
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT renesas_smartbond_mipi_dbi
#include <zephyr/drivers/mipi_dbi.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/irq.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/device_runtime.h>
#include <DA1469xAB.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/clock_control/smartbond_clock_control.h>
#include <zephyr/drivers/display.h>
#include <zephyr/sys/util.h>
#include <zephyr/drivers/spi.h>
#include <da1469x_lcdc.h>
#include <da1469x_pd.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(smartbond_mipi_dbi, CONFIG_MIPI_DBI_LOG_LEVEL);
#define SMARTBOND_IRQN DT_INST_IRQN(0)
#define SMARTBOND_IRQ_PRIO DT_INST_IRQ(0, priority)
#define PINCTRL_STATE_READ PINCTRL_STATE_PRIV_START
#define MIPI_DBI_SMARTBOND_IS_READ_SUPPORTED \
DT_INST_NODE_HAS_PROP(0, spi_dev)
#define LCDC_SMARTBOND_CLK_DIV(_freq) \
((32000000U % (_freq)) ? (96000000U / (_freq)) : (32000000U / (_freq)))
#define MIPI_DBI_SMARTBOND_IS_PLL_REQUIRED \
!!(32000000U % DT_PROP(DT_CHOSEN(zephyr_display), mipi_max_frequency))
#define MIPI_DBI_SMARTBOND_IS_TE_ENABLED \
DT_INST_PROP_OR(0, te_enable, 0)
#define MIPI_DBI_SMARTBOND_IS_DMA_PREFETCH_ENABLED \
DT_INST_ENUM_IDX_OR(0, dma_prefetch, 0)
#define MIPI_DBI_SMARTBOND_IS_RESET_AVAILABLE \
DT_INST_NODE_HAS_PROP(0, reset_gpios)
#define LCDC_LAYER0_OFFSETX_REG_SET_FIELD(_field, _var, _val) \
((_var)) = \
((_var) & ~(LCDC_LCDC_LAYER0_OFFSETX_REG_ ## _field ## _Msk)) | \
(((_var) << LCDC_LCDC_LAYER0_OFFSETX_REG_ ## _field ## _Pos) & \
LCDC_LCDC_LAYER0_OFFSETX_REG_ ## _field ## _Msk)
struct mipi_dbi_smartbond_data {
/* Provide mutual exclusion when a display operation is requested. */
struct k_sem device_sem;
/* Provide synchronization between task return and ISR firing */
struct k_sem sync_sem;
/* Flag indicating whether or not an underflow took place */
volatile bool underflow_flag;
/* Layer settings */
lcdc_smartbond_layer_cfg layer;
};
struct mipi_dbi_smartbond_config {
/* Reference to device instance's pinctrl configurations */
const struct pinctrl_dev_config *pcfg;
/* Reset GPIO */
const struct gpio_dt_spec reset;
/* Host controller's timing settings */
lcdc_smartbond_timing_cfg timing_cfg;
/* Background default color configuration */
lcdc_smartbond_bgcolor_cfg bgcolor_cfg;
};
/* Mark the device is is progress and so it's not allowed to enter the sleep state. */
static void mipi_dbi_pm_get(const struct device *dev)
{
#if CONFIG_PM_DEVICE
/*
* By marking the device as busy, PM will not communicate PM events
* to the device via mipi_dbi_smartbond_pm_action. It's OK if PM is
* not used at all. Executing a single frame requires waiting for
* the frame to be completed. As such, the system might enter the idle
* state if no other tasks are pending and for as long as the frame is
* being sent.
*
* XXX: Another option would be to use a flag and mark when the controller
* is in progress so a negative value other than -ENOSYS, -ENOTSUP or
* -EALREADY is returned in mipi_dbi_smartbond_pm_action. Sideffect
* of this approach is that the sleep state will be aborted at all,
* though the system could enter a low-power state, instead (see comment
* below on how to achieve a low-power state).
*/
pm_device_busy_set(dev);
#endif
#if CONFIG_PM
/*
* LCDC controller resides in PD_SYS which is turned off when the system enters
* the extended sleep state. By calling this API, a reference counter is
* incremented, designating that the specific power domain should not be turned off.
* As a result, a low-power state (i.e. ARM WFI) will be selected, instead (when the
* system is idle).
*/
(void)da1469x_pd_acquire_noconf(MCU_PD_DOMAIN_SYS);
#endif
}
/* Mark that device is inactive and so it's allowed to enter the sleep state */
static void mipi_dbi_pm_put(const struct device *dev)
{
#if CONFIG_PM_DEVICE
(void)pm_device_busy_clear(dev);
#endif
#if CONFIG_PM
(void)da1469x_pd_release_nowait(MCU_PD_DOMAIN_SYS);
#endif
}
/* Helper function to trigger the LCDC fetching data from frame buffer to the connected display */
static void mipi_dbi_smartbond_send_single_frame(const struct device *dev)
{
struct mipi_dbi_smartbond_data *data = dev->data;
#if MIPI_DBI_SMARTBOND_IS_TE_ENABLED
da1469x_lcdc_te_set_status(true, DT_INST_PROP_OR(0, te_polarity, false));
/*
* Wait for the TE signal to be asserted so display's refresh status can be synchronized
* with the current frame update.
*/
k_sem_take(&data->sync_sem, K_FOREVER);
#endif
LCDC->LCDC_INTERRUPT_REG |= LCDC_LCDC_INTERRUPT_REG_LCDC_VSYNC_IRQ_EN_Msk;
/* Setting this bit will enable the host to start outputing pixel data */
LCDC->LCDC_MODE_REG |= LCDC_LCDC_MODE_REG_LCDC_SFRAME_UPD_Msk;
/* Wait for frame update to complete */
k_sem_take(&data->sync_sem, K_FOREVER);
if (data->underflow_flag) {
LOG_WRN("Underflow took place");
data->underflow_flag = false;
}
}
#if MIPI_DBI_SMARTBOND_IS_RESET_AVAILABLE
static int mipi_dbi_smartbond_reset(const struct device *dev, uint32_t delay)
{
const struct mipi_dbi_smartbond_config *config = dev->config;
int ret;
if (!gpio_is_ready_dt(&config->reset)) {
LOG_ERR("Reset signal not available");
return -ENODEV;
}
ret = gpio_pin_set_dt(&config->reset, 1);
if (ret < 0) {
LOG_ERR("Cannot drive reset signal");
return ret;
}
k_msleep(delay);
return gpio_pin_set_dt(&config->reset, 0);
}
#endif
/* Display pixel to output color format translation */
static inline uint8_t lcdc_smartbond_pixel_to_ocm(enum display_pixel_format pixfmt)
{
switch (pixfmt) {
case PIXEL_FORMAT_RGB_565:
return (uint8_t)LCDC_SMARTBOND_OCM_RGB565;
case PIXEL_FORMAT_RGB_888:
return (uint8_t)LCDC_SMARTBOND_OCM_RGB888;
case PIXEL_FORMAT_MONO10:
return (uint8_t)LCDC_SMARTBOND_L0_L1;
default:
LOG_ERR("Unsupported pixel format");
return 0;
};
}
static inline uint8_t lcdc_smartbond_line_mode_translation(uint8_t mode)
{
switch (mode) {
case MIPI_DBI_MODE_SPI_3WIRE:
return (uint8_t)LCDC_SMARTBOND_MODE_SPI3;
case MIPI_DBI_MODE_SPI_4WIRE:
return (uint8_t)LCDC_SMARTBOND_MODE_SPI4;
default:
LOG_ERR("Unsupported SPI mode");
return 0;
}
}
static inline uint8_t lcdc_smartbond_pixel_to_lcm(enum display_pixel_format pixfmt)
{
switch (pixfmt) {
case PIXEL_FORMAT_RGB_565:
return (uint8_t)LCDC_SMARTBOND_L0_RGB565;
case PIXEL_FORMAT_ARGB_8888:
return (uint8_t)LCDC_SMARTBOND_L0_ARGB8888;
default:
LOG_ERR("Unsupported pixel format");
return 0;
};
}
static void lcdc_smartbond_mipi_dbi_translation(const struct mipi_dbi_config *dbi_config,
lcdc_smartbond_mipi_dbi_cfg *mipi_dbi_cfg,
enum display_pixel_format pixfmt)
{
mipi_dbi_cfg->cpha = dbi_config->config.operation & SPI_MODE_CPHA;
mipi_dbi_cfg->cpol = dbi_config->config.operation & SPI_MODE_CPOL;
mipi_dbi_cfg->cs_active_high = dbi_config->config.operation & SPI_CS_ACTIVE_HIGH;
mipi_dbi_cfg->line_mode = lcdc_smartbond_line_mode_translation(dbi_config->mode);
mipi_dbi_cfg->color_mode = lcdc_smartbond_pixel_to_ocm(pixfmt);
}
#if MIPI_DBI_SMARTBOND_IS_READ_SUPPORTED
static int mipi_dbi_smartbond_command_read(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
uint8_t *cmd, size_t num_cmds,
uint8_t *response, size_t len)
{
struct mipi_dbi_smartbond_data *data = dev->data;
const struct mipi_dbi_smartbond_config *config = dev->config;
int ret = 0;
lcdc_smartbond_mipi_dbi_cfg mipi_dbi_cfg;
k_sem_take(&data->device_sem, K_FOREVER);
/*
* Add an arbitrary valid color format to satisfy subroutine. The MIPI DBI command/data
* engine should not be affected.
*/
lcdc_smartbond_mipi_dbi_translation(dbi_config, &mipi_dbi_cfg, PIXEL_FORMAT_RGB_565);
ret = da1469x_lcdc_mipi_dbi_interface_configure(&mipi_dbi_cfg);
if (ret < 0) {
goto _mipi_dbi_read_exit;
}
/* Check if the cmd/data engine is busy since the #CS line will be overruled. */
if (da1469x_lcdc_is_busy()) {
LOG_WRN("MIPI DBI host is busy");
ret = -EBUSY;
goto _mipi_dbi_read_exit;
}
/* Force CS line to low. Typically, command and data are bound in the same #CS assertion */
da1469x_lcdc_force_cs_line(true, mipi_dbi_cfg.cs_active_high);
da1469x_lcdc_send_cmd_data(true, cmd, num_cmds);
if (len) {
const struct device *spi_dev = DEVICE_DT_GET(DT_INST_PHANDLE(0, spi_dev));
struct spi_buf buffer = {
.buf = (void *)response,
.len = len,
};
struct spi_buf_set buf_set = {
.buffers = &buffer,
.count = 1,
};
if (!device_is_ready(spi_dev)) {
LOG_ERR("SPI device is not ready");
ret = -ENODEV;
goto _mipi_dbi_read_exit;
}
/* Overwrite CLK and enable DI lines. CS is driven forcefully. */
ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_READ);
if (ret < 0) {
LOG_ERR("Could not apply MIPI DBI pins' SPI read state (%d)", ret);
goto _mipi_dbi_read_exit;
}
/* Get response */
ret = spi_read(spi_dev, &dbi_config->config, &buf_set);
if (ret < 0) {
LOG_ERR("Could not read data from SPI");
goto _mipi_dbi_read_exit;
}
}
_mipi_dbi_read_exit:
/* Restore #CS line */
da1469x_lcdc_force_cs_line(false, mipi_dbi_cfg.cs_active_high);
/* Make sure default LCDC pins are applied upon exit */
ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);
if (ret < 0) {
LOG_ERR("Could not apply MIPI DBI pins' default state (%d)", ret);
}
k_sem_give(&data->device_sem);
return ret;
}
#endif
static int mipi_dbi_smartbond_command_write(const struct device *dev,
const struct mipi_dbi_config *dbi_config,
uint8_t cmd, const uint8_t *data_buf,
size_t len)
{
struct mipi_dbi_smartbond_data *data = dev->data;
int ret;
lcdc_smartbond_mipi_dbi_cfg mipi_dbi_cfg;
k_sem_take(&data->device_sem, K_FOREVER);
mipi_dbi_pm_get(dev);
/*
* Add an arbitrary valid color format to satisfy subroutine. The MIPI DBI command/data
* engine should not be affected.
*/
lcdc_smartbond_mipi_dbi_translation(dbi_config, &mipi_dbi_cfg, PIXEL_FORMAT_RGB_565);
ret = da1469x_lcdc_mipi_dbi_interface_configure(&mipi_dbi_cfg);
if (ret < 0) {
k_sem_give(&data->device_sem);
return ret;
}
/* Command and accompanied data should be transmitted via the DBIB interface */
da1469x_lcdc_send_cmd_data(true, &cmd, 1);
if (len) {
/* Data should be transmitted via the DBIB interface */
da1469x_lcdc_send_cmd_data(false, data_buf, len);
}
mipi_dbi_pm_put(dev);
k_sem_give(&data->device_sem);
return 0;
}
static int mipi_dbi_smartbond_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)
{
struct mipi_dbi_smartbond_data *data = dev->data;
const struct mipi_dbi_smartbond_config *config = dev->config;
lcdc_smartbond_layer_cfg *layer = &data->layer;
int ret = 0;
lcdc_smartbond_mipi_dbi_cfg mipi_dbi_cfg;
uint8_t layer_color = lcdc_smartbond_pixel_to_lcm(pixfmt);
if (desc->width * desc->height * (DISPLAY_BITS_PER_PIXEL(pixfmt) / 8) !=
desc->buf_size) {
LOG_ERR("Incorrect buffer size for given width and height");
return -EINVAL;
}
k_sem_take(&data->device_sem, K_FOREVER);
mipi_dbi_pm_get(dev);
/*
* Mainly check if the frame generator is busy with a pending frame update (might happen
* when two frame updates take place one after the other and the display interface is
* quite slow). VSYNC interrupt line should be asserted when the last line is being
* outputed.
*/
if (da1469x_lcdc_is_busy()) {
LOG_WRN("MIPI DBI host is busy");
ret = -EBUSY;
goto _mipi_dbi_write_exit;
}
lcdc_smartbond_mipi_dbi_translation(dbi_config, &mipi_dbi_cfg, pixfmt);
ret = da1469x_lcdc_mipi_dbi_interface_configure(&mipi_dbi_cfg);
if (ret < 0) {
goto _mipi_dbi_write_exit;
}
ret = da1469x_lcdc_timings_configure(desc->width, desc->height,
(lcdc_smartbond_timing_cfg *)&config->timing_cfg);
if (ret < 0) {
goto _mipi_dbi_write_exit;
}
LCDC_SMARTBOND_LAYER_CONFIG(layer, framebuf, 0, 0, desc->width, desc->height,
layer_color,
da1469x_lcdc_stride_calculation(layer_color, desc->width));
ret = da1469x_lcdc_layer_configure(layer);
if (ret < 0) {
goto _mipi_dbi_write_exit;
}
/* Trigger single frame update via the LCDC-DMA engine */
mipi_dbi_smartbond_send_single_frame(dev);
_mipi_dbi_write_exit:
mipi_dbi_pm_put(dev);
k_sem_give(&data->device_sem);
return ret;
}
static int mipi_dbi_smartbond_configure(const struct device *dev)
{
uint8_t clk_div =
LCDC_SMARTBOND_CLK_DIV(DT_PROP(DT_CHOSEN(zephyr_display), mipi_max_frequency));
const struct mipi_dbi_smartbond_config *config = dev->config;
/*
* First enable the controller so registers can be written. In serial interfaces
* clock divider is further divided by 2.
*/
da1469x_lcdc_set_status(true, MIPI_DBI_SMARTBOND_IS_PLL_REQUIRED,
(clk_div >= 2 ? clk_div / 2 : clk_div));
if (!da1469x_lcdc_check_id()) {
LOG_ERR("Mismatching LCDC ID");
da1469x_lcdc_set_status(false, 0, 0);
return -EINVAL;
}
da1469x_lcdc_te_set_status(false, DT_INST_PROP_OR(0, te_polarity, false));
da1469x_lcdc_bgcolor_configure((lcdc_smartbond_bgcolor_cfg *)&config->bgcolor_cfg);
LCDC_LAYER0_OFFSETX_REG_SET_FIELD(LCDC_L0_DMA_PREFETCH,
LCDC->LCDC_LAYER0_OFFSETX_REG, MIPI_DBI_SMARTBOND_IS_DMA_PREFETCH_ENABLED);
return 0;
}
static void smartbond_mipi_dbi_isr(const void *arg)
{
struct mipi_dbi_smartbond_data *data = ((const struct device *)arg)->data;
/*
* Underflow sticky bit will remain high until cleared by writing
* any value to LCDC_INTERRUPT_REG.
*/
data->underflow_flag = LCDC_STATUS_REG_GET_FIELD(LCDC_STICKY_UNDERFLOW);
/* Default interrupt mode is level triggering so interrupt should be cleared */
da1469x_lcdc_te_set_status(false, DT_INST_PROP_OR(0, te_polarity, false));
k_sem_give(&data->sync_sem);
}
static int mipi_dbi_smartbond_resume(const struct device *dev)
{
const struct mipi_dbi_smartbond_config *config = dev->config;
int ret;
/* Select default state */
ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);
if (ret < 0) {
LOG_ERR("Could not apply LCDC pins' default state (%d)", ret);
return -EIO;
}
#if MIPI_DBI_SMARTBOND_IS_PLL_REQUIRED
const struct device *clock_dev = DEVICE_DT_GET(DT_NODELABEL(osc));
if (!device_is_ready(clock_dev)) {
LOG_WRN("Clock device is not available; PLL cannot be used");
} else {
ret = z_smartbond_select_sys_clk(SMARTBOND_CLK_PLL96M);
if (ret < 0) {
LOG_WRN("Could not switch to PLL. Requested speed should not be achieved.");
}
}
#endif
return mipi_dbi_smartbond_configure(dev);
}
#ifdef CONFIG_PM_DEVICE
static int mipi_dbi_smartbond_suspend(const struct device *dev)
{
const struct mipi_dbi_smartbond_config *config = dev->config;
int ret;
/* Select sleep state; it's OK if settings fails for any reason. */
ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_SLEEP);
if (ret < 0) {
LOG_WRN("Could not apply MIPI DBI pins' sleep state");
}
/* Disable host controller to minimize power consumption. */
da1469x_lcdc_set_status(false, false, 0);
return 0;
}
static int mipi_dbi_smartbond_pm_action(const struct device *dev, enum pm_device_action action)
{
int ret = 0;
switch (action) {
case PM_DEVICE_ACTION_SUSPEND:
(void)mipi_dbi_smartbond_suspend(dev);
#if CONFIG_PM_DEVICE_RUNTIME && CONFIG_PM
da1469x_pd_release_nowait(MCU_PD_DOMAIN_SYS);
#endif
break;
case PM_DEVICE_ACTION_RESUME:
ret = mipi_dbi_smartbond_resume(dev);
#if CONFIG_PM_DEVICE_RUNTIME && CONFIG_PM
/*
* If resume succeeded, prevent the system from entering the
* extended sleep state (Instead, use __WFI). If not, do not
* as users should not suspend the device and thus releasing
* PD_SYS.
*/
if (ret == 0) {
da1469x_pd_acquire_noconf(MCU_PD_DOMAIN_SYS);
}
#endif
break;
default:
return -ENOTSUP;
}
return ret;
}
#endif
static int mipi_dbi_smartbond_init(const struct device *dev)
{
const struct mipi_dbi_smartbond_config *config = dev->config;
struct mipi_dbi_smartbond_data *data = dev->data;
int ret;
/* Device should be ready to be acquired */
k_sem_init(&data->device_sem, 1, 1);
/* Event should be signaled by LCDC ISR */
k_sem_init(&data->sync_sem, 0, 1);
#if MIPI_DBI_SMARTBOND_IS_RESET_AVAILABLE
if (gpio_is_ready_dt(&config->reset)) {
ret = gpio_pin_configure_dt(&config->reset, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
LOG_ERR("Could not configure reset line (%d)", ret);
return -EIO;
}
}
#endif
IRQ_CONNECT(SMARTBOND_IRQN, SMARTBOND_IRQ_PRIO, smartbond_mipi_dbi_isr,
DEVICE_DT_INST_GET(0), 0);
#ifdef CONFIG_PM_DEVICE_RUNTIME
/* Make sure device state is marked as suspended */
pm_device_init_suspended(dev);
ret = pm_device_runtime_enable(dev);
if ((ret < 0) && (ret != -ENOSYS)) {
return ret;
}
#else
/* Resme if either PM is not used at all or if PM without runtime is used. */
ret = mipi_dbi_smartbond_resume(dev);
if (ret < 0) {
return ret;
}
#endif
return 0;
}
static struct mipi_dbi_driver_api mipi_dbi_smartbond_driver_api = {
#if MIPI_DBI_SMARTBOND_IS_RESET_AVAILABLE
.reset = mipi_dbi_smartbond_reset,
#endif
.command_write = mipi_dbi_smartbond_command_write,
.write_display = mipi_dbi_smartbond_write_display,
#if MIPI_DBI_SMARTBOND_IS_READ_SUPPORTED
.command_read = mipi_dbi_smartbond_command_read,
#endif
};
#define SMARTBOND_MIPI_DBI_INIT(inst) \
PINCTRL_DT_INST_DEFINE(inst); \
\
static const struct mipi_dbi_smartbond_config mipi_dbi_smartbond_config_## inst = { \
.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(inst), \
.reset = GPIO_DT_SPEC_INST_GET_OR(inst, reset_gpios, {}), \
.timing_cfg = { 0 }, \
.bgcolor_cfg = { 0xFF, 0xFF, 0xFF, 0 }, \
}; \
\
static struct mipi_dbi_smartbond_data mipi_dbi_smartbond_data_## inst; \
\
PM_DEVICE_DT_INST_DEFINE(inst, mipi_dbi_smartbond_pm_action); \
\
DEVICE_DT_INST_DEFINE(inst, mipi_dbi_smartbond_init, \
PM_DEVICE_DT_INST_GET(inst), \
&mipi_dbi_smartbond_data_## inst, \
&mipi_dbi_smartbond_config_## inst, \
POST_KERNEL, \
CONFIG_MIPI_DBI_INIT_PRIORITY, \
&mipi_dbi_smartbond_driver_api);
SMARTBOND_MIPI_DBI_INIT(0);