You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
973 lines
28 KiB
973 lines
28 KiB
/* |
|
* Copyright 2023 NXP |
|
* |
|
* SPDX-License-Identifier: Apache-2.0 |
|
*/ |
|
|
|
#define DT_DRV_COMPAT nxp_lcdic |
|
|
|
#include <zephyr/drivers/mipi_dbi.h> |
|
#include <zephyr/drivers/pinctrl.h> |
|
#include <zephyr/drivers/clock_control.h> |
|
#include <zephyr/pm/device.h> |
|
#include <zephyr/pm/policy.h> |
|
#include <zephyr/drivers/spi.h> |
|
#include <zephyr/drivers/dma.h> |
|
#include <zephyr/logging/log.h> |
|
#include <soc.h> |
|
#include <zephyr/drivers/dma/dma_mcux_lpc.h> |
|
LOG_MODULE_REGISTER(mipi_dbi_lcdic, CONFIG_MIPI_DBI_LOG_LEVEL); |
|
|
|
#include <fsl_inputmux.h> |
|
|
|
enum lcdic_data_fmt { |
|
LCDIC_DATA_FMT_BYTE = 0, |
|
LCDIC_DATA_FMT_HALFWORD = 1, /* 2 byte */ |
|
LCDIC_DATA_FMT_WORD = 2, /* 4 byte */ |
|
}; |
|
|
|
enum lcdic_cmd_dc { |
|
LCDIC_COMMAND = 0, |
|
LCDIC_DATA = 1, |
|
}; |
|
|
|
enum lcdic_cmd_type { |
|
LCDIC_RX = 0, |
|
LCDIC_TX = 1, |
|
}; |
|
|
|
enum lcdic_cmd_te { |
|
LCDIC_TE_NO_SYNC = 0, |
|
LCDIC_TE_RISING_EDGE = 1, |
|
LCDIC_TE_FALLING_EDGE = 2, |
|
}; |
|
|
|
/* Limit imposed by size of data length field in LCDIC command */ |
|
#define LCDIC_MAX_XFER 0x40000 |
|
/* Max reset width (in terms of Timer0_Period, see RST_CTRL register) */ |
|
#define LCDIC_MAX_RST_WIDTH 0x3F |
|
/* Max reset pulse count */ |
|
#define LCDIC_MAX_RST_PULSE_COUNT 0x7 |
|
|
|
/* Descriptor for LCDIC command */ |
|
union lcdic_trx_cmd { |
|
struct { |
|
/* Data length in bytes. LCDIC transfers data_len + 1 */ |
|
uint32_t data_len: 18; |
|
/* Dummy SCLK cycles between TX and RX (for SPI mode) */ |
|
uint32_t dummy_count: 3; |
|
uint32_t rsvd: 2; |
|
/* Use auto repeat mode */ |
|
uint32_t auto_repeat: 1; |
|
/* Tearing enable sync mode */ |
|
uint32_t te_sync_mode: 2; |
|
/* TRX command timeout mode */ |
|
uint32_t trx_timeout_mode: 1; |
|
/* Data format, see lcdic_data_fmt */ |
|
uint32_t data_format: 2; |
|
/* Enable command done interrupt */ |
|
uint32_t cmd_done_int: 1; |
|
/* LCD command or LCD data, see lcdic_cmd_dc */ |
|
uint32_t cmd_data: 1; |
|
/* TX or RX command, see lcdic_cmd_type */ |
|
uint32_t trx: 1; |
|
} bits; |
|
uint32_t u32; |
|
}; |
|
|
|
struct mipi_dbi_lcdic_config { |
|
LCDIC_Type *base; |
|
void (*irq_config_func)(const struct device *dev); |
|
const struct pinctrl_dev_config *pincfg; |
|
const struct device *clock_dev; |
|
clock_control_subsys_t clock_subsys; |
|
bool swap_bytes; |
|
uint8_t write_active_min; |
|
uint8_t write_inactive_min; |
|
uint8_t timer0_ratio; |
|
uint8_t timer1_ratio; |
|
}; |
|
|
|
#ifdef CONFIG_MIPI_DBI_NXP_LCDIC_DMA |
|
struct stream { |
|
const struct device *dma_dev; |
|
uint32_t channel; |
|
struct dma_config dma_cfg; |
|
struct dma_block_config blk_cfg[2]; |
|
}; |
|
#endif |
|
|
|
struct mipi_dbi_lcdic_data { |
|
/* Tracks number of bytes remaining in command */ |
|
uint32_t cmd_bytes; |
|
/* Tracks number of bytes remaining in transfer */ |
|
uint32_t xfer_bytes; |
|
/* Tracks start of transfer buffer */ |
|
const uint8_t *xfer_buf; |
|
/* When sending data that does not evenly fit into 4 byte chunks, |
|
* this is used to store the last unaligned segment of the data. |
|
*/ |
|
uint32_t unaligned_word __aligned(4); |
|
/* Tracks lcdic_data_fmt value we should use for pixel data */ |
|
uint8_t pixel_fmt; |
|
/* Tracks TE edge setting we should use for pixel data */ |
|
uint8_t te_edge; |
|
/* Tracks TE delay setting we should use */ |
|
k_timeout_t te_delay; |
|
/* Flag indicates we need to reconfigure TE signal. |
|
* This is the case when we exit low power modes where we |
|
* need to configure the hardware registers. |
|
*/ |
|
bool reconfigure_te; |
|
/* Are we starting a new display frame */ |
|
bool new_frame; |
|
const struct mipi_dbi_config *active_cfg; |
|
struct k_sem xfer_sem; |
|
struct k_sem lock; |
|
#ifdef CONFIG_MIPI_DBI_NXP_LCDIC_DMA |
|
struct stream dma_stream; |
|
#endif |
|
}; |
|
|
|
#define LCDIC_ALL_INTERRUPTS \ |
|
(LCDIC_ICR_RFIFO_THRES_INTR_CLR_MASK | \ |
|
LCDIC_ICR_RFIFO_UNDERFLOW_INTR_CLR_MASK | \ |
|
LCDIC_ICR_TFIFO_THRES_INTR_CLR_MASK | \ |
|
LCDIC_ICR_TFIFO_OVERFLOW_INTR_CLR_MASK | \ |
|
LCDIC_ICR_TE_TO_INTR_CLR_MASK | \ |
|
LCDIC_ICR_CMD_TO_INTR_CLR_MASK | \ |
|
LCDIC_ICR_CMD_DONE_INTR_CLR_MASK | \ |
|
LCDIC_ICR_RST_DONE_INTR_CLR_MASK) |
|
|
|
/* RX and TX FIFO thresholds */ |
|
#ifdef CONFIG_MIPI_DBI_NXP_LCDIC_DMA |
|
#define LCDIC_RX_FIFO_THRESH 0x0 |
|
#define LCDIC_TX_FIFO_THRESH 0x0 |
|
#else |
|
#define LCDIC_RX_FIFO_THRESH 0x0 |
|
#define LCDIC_TX_FIFO_THRESH 0x3 |
|
#endif |
|
|
|
/* After LCDIC is enabled or disabled, there should be a wait longer than |
|
* 5x the module clock before other registers are read |
|
*/ |
|
static inline void mipi_dbi_lcdic_reset_delay(void) |
|
{ |
|
k_busy_wait(1); |
|
} |
|
|
|
/* Resets state of the LCDIC TX/RX FIFO */ |
|
static inline void mipi_dbi_lcdic_reset_state(const struct device *dev) |
|
{ |
|
const struct mipi_dbi_lcdic_config *config = dev->config; |
|
LCDIC_Type *base = config->base; |
|
|
|
base->CTRL &= ~LCDIC_CTRL_LCDIC_EN_MASK; |
|
mipi_dbi_lcdic_reset_delay(); |
|
base->CTRL |= LCDIC_CTRL_LCDIC_EN_MASK; |
|
mipi_dbi_lcdic_reset_delay(); |
|
} |
|
|
|
|
|
#ifdef CONFIG_MIPI_DBI_NXP_LCDIC_DMA |
|
|
|
/* Start DMA to send data using LCDIC TX FIFO */ |
|
static int mipi_dbi_lcdic_start_dma(const struct device *dev) |
|
{ |
|
const struct mipi_dbi_lcdic_config *config = dev->config; |
|
struct mipi_dbi_lcdic_data *data = dev->data; |
|
struct stream *stream = &data->dma_stream; |
|
uint32_t aligned_len = data->cmd_bytes & (~0x3); |
|
uint32_t unaligned_len = data->cmd_bytes & 0x3; |
|
int ret; |
|
|
|
stream->dma_cfg.head_block = &stream->blk_cfg[0]; |
|
if (aligned_len == 0) { |
|
/* Only unaligned data exists, send it in the first block */ |
|
/* First DMA block configuration is used to send aligned data */ |
|
stream->blk_cfg[0].source_address = (uint32_t)&data->unaligned_word; |
|
stream->blk_cfg[0].dest_address = (uint32_t)&config->base->TFIFO_WDATA; |
|
/* Block size should be the aligned portion of the transfer */ |
|
stream->blk_cfg[0].block_size = sizeof(uint32_t); |
|
stream->dma_cfg.block_count = 1; |
|
stream->blk_cfg[0].next_block = NULL; |
|
} else { |
|
/* First DMA block configuration is used to send aligned data */ |
|
stream->blk_cfg[0].source_address = (uint32_t)data->xfer_buf; |
|
stream->blk_cfg[0].dest_address = (uint32_t)&config->base->TFIFO_WDATA; |
|
/* Block size should be the aligned portion of the transfer */ |
|
stream->blk_cfg[0].block_size = aligned_len; |
|
/* Second DMA block configuration sends unaligned block */ |
|
if (unaligned_len) { |
|
stream->dma_cfg.block_count = 2; |
|
stream->blk_cfg[0].next_block = |
|
&stream->blk_cfg[1]; |
|
stream->blk_cfg[1].source_address = |
|
(uint32_t)&data->unaligned_word; |
|
stream->blk_cfg[1].dest_address = |
|
(uint32_t)&config->base->TFIFO_WDATA; |
|
stream->blk_cfg[1].block_size = sizeof(uint32_t); |
|
} else { |
|
stream->dma_cfg.block_count = 1; |
|
stream->blk_cfg[0].next_block = NULL; |
|
} |
|
} |
|
|
|
ret = dma_config(stream->dma_dev, stream->channel, &stream->dma_cfg); |
|
if (ret) { |
|
return ret; |
|
} |
|
/* Enable DMA channel before we set up DMA request. This way, |
|
* the hardware DMA trigger does not fire until the DMA |
|
* start function has initialized the DMA. |
|
*/ |
|
ret = dma_start(stream->dma_dev, stream->channel); |
|
if (ret) { |
|
return ret; |
|
} |
|
/* Enable DMA request */ |
|
config->base->CTRL |= LCDIC_CTRL_DMA_EN_MASK; |
|
return ret; |
|
} |
|
|
|
/* DMA completion callback */ |
|
static void mipi_dbi_lcdic_dma_callback(const struct device *dma_dev, |
|
void *user_data, uint32_t channel, int status) |
|
{ |
|
if (status < 0) { |
|
LOG_ERR("DMA callback with error %d", status); |
|
} |
|
} |
|
|
|
#endif /* CONFIG_MIPI_DBI_NXP_LCDIC_DMA */ |
|
|
|
/* Configure LCDIC */ |
|
static int mipi_dbi_lcdic_configure(const struct device *dev, |
|
const struct mipi_dbi_config *dbi_config) |
|
{ |
|
const struct mipi_dbi_lcdic_config *config = dev->config; |
|
struct mipi_dbi_lcdic_data *data = dev->data; |
|
const struct spi_config *spi_cfg = &dbi_config->config; |
|
LCDIC_Type *base = config->base; |
|
int ret; |
|
uint32_t reg; |
|
|
|
if (dbi_config == data->active_cfg) { |
|
return 0; |
|
} |
|
|
|
/* Clear all interrupt flags */ |
|
base->ICR = LCDIC_ALL_INTERRUPTS; |
|
/* Mask all interrupts */ |
|
base->IMR = LCDIC_ALL_INTERRUPTS; |
|
|
|
/* Set LCDIC clock frequency */ |
|
ret = clock_control_set_rate(config->clock_dev, config->clock_subsys, |
|
(clock_control_subsys_rate_t)spi_cfg->frequency); |
|
if (ret) { |
|
LOG_ERR("Invalid clock frequency %d", spi_cfg->frequency); |
|
return ret; |
|
} |
|
if (spi_cfg->slave != 0) { |
|
/* Only one slave select line */ |
|
return -ENOTSUP; |
|
} |
|
if (SPI_WORD_SIZE_GET(spi_cfg->operation) > 8) { |
|
LOG_ERR("Unsupported word size"); |
|
return -ENOTSUP; |
|
} |
|
|
|
reg = base->CTRL; |
|
/* Disable LCD module during configuration */ |
|
reg &= ~LCDIC_CTRL_LCDIC_EN_MASK; |
|
if (dbi_config->mode == MIPI_DBI_MODE_8080_BUS_8_BIT) { |
|
/* Enable 8080 Mode */ |
|
reg |= LCDIC_CTRL_LCDIC_MD_MASK; |
|
} else if (dbi_config->mode == MIPI_DBI_MODE_SPI_4WIRE) { |
|
/* Select SPI 4 wire mode */ |
|
reg |= LCDIC_CTRL_SPI_MD_MASK; |
|
reg &= ~LCDIC_CTRL_LCDIC_MD_MASK; |
|
} else if (dbi_config->mode == MIPI_DBI_MODE_SPI_3WIRE) { |
|
/* Select SPI 3 wire mode */ |
|
reg &= ~(LCDIC_CTRL_LCDIC_MD_MASK | |
|
LCDIC_CTRL_SPI_MD_MASK); |
|
} else { |
|
/* Unsupported mode */ |
|
return -ENOTSUP; |
|
} |
|
/* If using SPI mode, validate that half-duplex was requested */ |
|
if ((!(reg & LCDIC_CTRL_LCDIC_MD_MASK)) && |
|
(!(spi_cfg->operation & SPI_HALF_DUPLEX))) { |
|
LOG_ERR("LCDIC only supports half duplex operation"); |
|
return -ENOTSUP; |
|
} |
|
/* Enable byte swapping if user requested it */ |
|
reg = (reg & ~LCDIC_CTRL_DAT_ENDIAN_MASK) | |
|
LCDIC_CTRL_DAT_ENDIAN(!config->swap_bytes); |
|
/* Disable DMA */ |
|
reg &= ~LCDIC_CTRL_DMA_EN_MASK; |
|
base->CTRL = reg; |
|
mipi_dbi_lcdic_reset_delay(); |
|
|
|
/* Setup SPI CPOL and CPHA selections */ |
|
reg = base->SPI_CTRL; |
|
reg = (reg & ~LCDIC_SPI_CTRL_SDAT_ENDIAN_MASK) | |
|
LCDIC_SPI_CTRL_SDAT_ENDIAN((spi_cfg->operation & |
|
SPI_TRANSFER_LSB) ? 1 : 0); |
|
reg = (reg & ~LCDIC_SPI_CTRL_CPHA_MASK) | |
|
LCDIC_SPI_CTRL_CPHA((spi_cfg->operation & SPI_MODE_CPHA) ? 1 : 0); |
|
reg = (reg & ~LCDIC_SPI_CTRL_CPOL_MASK) | |
|
LCDIC_SPI_CTRL_CPOL((spi_cfg->operation & SPI_MODE_CPOL) ? 1 : 0); |
|
base->SPI_CTRL = reg; |
|
|
|
/* |
|
* Set 8080 control based on module properties. TRIW and TRAW are |
|
* set to their reset values |
|
*/ |
|
base->I8080_CTRL1 = LCDIC_I8080_CTRL1_TRIW(0xf) | |
|
LCDIC_I8080_CTRL1_TRAW(0xf) | |
|
LCDIC_I8080_CTRL1_TWIW(config->write_inactive_min) | |
|
LCDIC_I8080_CTRL1_TWAW(config->write_active_min); |
|
|
|
/* Enable the module */ |
|
base->CTRL |= LCDIC_CTRL_LCDIC_EN_MASK; |
|
mipi_dbi_lcdic_reset_delay(); |
|
|
|
data->active_cfg = dbi_config; |
|
|
|
return 0; |
|
} |
|
|
|
/* Gets unaligned word data from array. Return value will be a 4 byte |
|
* value containing the last unaligned section of the array data |
|
*/ |
|
static uint32_t mipi_dbi_lcdic_get_unaligned(const uint8_t *bytes, |
|
uint32_t buf_len) |
|
{ |
|
uint32_t word = 0U; |
|
uint8_t unaligned_len = buf_len & 0x3; |
|
uint32_t aligned_len = buf_len - unaligned_len; |
|
|
|
while ((unaligned_len--)) { |
|
word <<= 8U; |
|
word |= bytes[aligned_len + unaligned_len]; |
|
} |
|
return word; |
|
} |
|
|
|
/* Fills the TX fifo with data. Returns number of bytes written. */ |
|
static int mipi_dbi_lcdic_fill_tx(LCDIC_Type *base, const uint8_t *buf, |
|
uint32_t buf_len, uint32_t last_word) |
|
{ |
|
uint32_t *word_buf = (uint32_t *)buf; |
|
uint32_t bytes_written = 0U; |
|
uint32_t write_len; |
|
|
|
/* TX FIFO consumes 4 bytes on each write, so we can write up |
|
* to buf_len / 4 times before we send all data. |
|
* Write to FIFO it overflows or we send entire buffer. |
|
*/ |
|
while (buf_len) { |
|
if (buf_len < 4) { |
|
/* Send last bytes */ |
|
base->TFIFO_WDATA = last_word; |
|
write_len = buf_len; |
|
} else { |
|
/* Otherwise, write one word */ |
|
base->TFIFO_WDATA = word_buf[bytes_written >> 2]; |
|
write_len = 4; |
|
} |
|
if (base->IRSR & LCDIC_IRSR_TFIFO_OVERFLOW_RAW_INTR_MASK) { |
|
/* TX FIFO has overflowed, last word write did not |
|
* complete. Return current number of bytes written. |
|
*/ |
|
base->ICR |= LCDIC_ICR_TFIFO_OVERFLOW_INTR_CLR_MASK; |
|
return bytes_written; |
|
} |
|
bytes_written += write_len; |
|
buf_len -= write_len; |
|
} |
|
return bytes_written; |
|
} |
|
|
|
/* Writes command word */ |
|
static void mipi_dbi_lcdic_set_cmd(LCDIC_Type *base, |
|
enum lcdic_cmd_type dir, |
|
enum lcdic_cmd_dc dc, |
|
enum lcdic_data_fmt data_fmt, |
|
enum lcdic_cmd_te te_sync, |
|
uint32_t buf_len) |
|
{ |
|
union lcdic_trx_cmd cmd = {0}; |
|
|
|
|
|
/* TX FIFO will be clear, write command word */ |
|
cmd.bits.data_len = buf_len - 1; |
|
cmd.bits.cmd_data = dc; |
|
cmd.bits.trx = dir; |
|
cmd.bits.cmd_done_int = true; |
|
cmd.bits.data_format = data_fmt; |
|
cmd.bits.te_sync_mode = te_sync; |
|
/* Write command */ |
|
base->TFIFO_WDATA = cmd.u32; |
|
} |
|
|
|
static int mipi_dbi_lcdic_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) |
|
{ |
|
const struct mipi_dbi_lcdic_config *config = dev->config; |
|
struct mipi_dbi_lcdic_data *dev_data = dev->data; |
|
LCDIC_Type *base = config->base; |
|
int ret; |
|
enum lcdic_cmd_te te_sync = LCDIC_TE_NO_SYNC; |
|
uint32_t interrupts = 0U; |
|
|
|
ret = k_sem_take(&dev_data->lock, K_FOREVER); |
|
if (ret) { |
|
goto release_sem; |
|
} |
|
|
|
pm_policy_device_power_lock_get(dev); |
|
|
|
ret = mipi_dbi_lcdic_configure(dev, dbi_config); |
|
if (ret) { |
|
goto release_power_lock; |
|
} |
|
|
|
if (dev_data->new_frame) { |
|
switch (dev_data->te_edge) { |
|
case MIPI_DBI_TE_RISING_EDGE: |
|
te_sync = LCDIC_TE_RISING_EDGE; |
|
break; |
|
case MIPI_DBI_TE_FALLING_EDGE: |
|
te_sync = LCDIC_TE_FALLING_EDGE; |
|
break; |
|
default: |
|
te_sync = LCDIC_TE_NO_SYNC; |
|
break; |
|
} |
|
dev_data->new_frame = false; |
|
} |
|
|
|
if (!desc->frame_incomplete) { |
|
/* Next frame will be a new one */ |
|
dev_data->new_frame = true; |
|
} |
|
|
|
/* State reset is required before transfer */ |
|
mipi_dbi_lcdic_reset_state(dev); |
|
|
|
if (desc->buf_size != 0) { |
|
dev_data->xfer_bytes = desc->buf_size; |
|
/* Cap command to max transfer size */ |
|
dev_data->cmd_bytes = MIN(desc->buf_size, LCDIC_MAX_XFER); |
|
dev_data->xfer_buf = framebuf; |
|
/* If the length of the transfer is not divisible by |
|
* 4, save the unaligned portion of the transfer into |
|
* a temporary buffer |
|
*/ |
|
if (dev_data->cmd_bytes & 0x3) { |
|
dev_data->unaligned_word = mipi_dbi_lcdic_get_unaligned( |
|
dev_data->xfer_buf, |
|
dev_data->cmd_bytes); |
|
} |
|
|
|
/* Save pixel format */ |
|
if (DISPLAY_BITS_PER_PIXEL(pixfmt) == 32) { |
|
dev_data->pixel_fmt = LCDIC_DATA_FMT_WORD; |
|
} else if (DISPLAY_BITS_PER_PIXEL(pixfmt) == 16) { |
|
dev_data->pixel_fmt = LCDIC_DATA_FMT_HALFWORD; |
|
} else if (DISPLAY_BITS_PER_PIXEL(pixfmt) == 8) { |
|
dev_data->pixel_fmt = LCDIC_DATA_FMT_BYTE; |
|
} else { |
|
if (config->swap_bytes) { |
|
LOG_WRN("Unsupported pixel format, byte swapping disabled"); |
|
} |
|
} |
|
/* Use pixel format data width, so we can byte swap |
|
* if needed |
|
*/ |
|
mipi_dbi_lcdic_set_cmd(base, LCDIC_TX, LCDIC_DATA, |
|
dev_data->pixel_fmt, |
|
te_sync, |
|
dev_data->cmd_bytes); |
|
#ifdef CONFIG_MIPI_DBI_NXP_LCDIC_DMA |
|
/* Enable command complete interrupt */ |
|
interrupts |= LCDIC_IMR_CMD_DONE_INTR_MSK_MASK; |
|
/* Write interrupt mask */ |
|
base->IMR &= ~interrupts; |
|
/* Configure DMA to send data */ |
|
ret = mipi_dbi_lcdic_start_dma(dev); |
|
if (ret) { |
|
LOG_ERR("Could not start DMA (%d)", ret); |
|
goto release_power_lock; |
|
} |
|
#else |
|
/* Enable TX FIFO threshold interrupt. This interrupt |
|
* should fire once enabled, which will kick off |
|
* the transfer |
|
*/ |
|
interrupts |= LCDIC_IMR_TFIFO_THRES_INTR_MSK_MASK; |
|
/* Enable command complete interrupt */ |
|
interrupts |= LCDIC_IMR_CMD_DONE_INTR_MSK_MASK; |
|
/* Write interrupt mask */ |
|
base->IMR &= ~interrupts; |
|
#endif |
|
ret = k_sem_take(&dev_data->xfer_sem, K_FOREVER); |
|
/* Do not release the lock from the power states. |
|
* This is released in the ISR after the transfer |
|
* is complete. |
|
*/ |
|
goto release_sem; |
|
} |
|
|
|
release_power_lock: |
|
pm_policy_device_power_lock_put(dev); |
|
|
|
release_sem: |
|
k_sem_give(&dev_data->lock); |
|
return ret; |
|
|
|
} |
|
|
|
static int mipi_dbi_lcdic_write_cmd(const struct device *dev, |
|
const struct mipi_dbi_config *dbi_config, |
|
uint8_t cmd, |
|
const uint8_t *data, |
|
size_t data_len) |
|
{ |
|
const struct mipi_dbi_lcdic_config *config = dev->config; |
|
struct mipi_dbi_lcdic_data *dev_data = dev->data; |
|
LCDIC_Type *base = config->base; |
|
int ret; |
|
uint32_t interrupts = 0U; |
|
|
|
ret = k_sem_take(&dev_data->lock, K_FOREVER); |
|
if (ret) { |
|
goto release_sem; |
|
} |
|
|
|
pm_policy_device_power_lock_get(dev); |
|
|
|
ret = mipi_dbi_lcdic_configure(dev, dbi_config); |
|
if (ret) { |
|
goto release_power_lock; |
|
} |
|
|
|
/* State reset is required before transfer */ |
|
mipi_dbi_lcdic_reset_state(dev); |
|
|
|
/* Write command */ |
|
mipi_dbi_lcdic_set_cmd(base, LCDIC_TX, LCDIC_COMMAND, |
|
LCDIC_DATA_FMT_BYTE, LCDIC_TE_NO_SYNC, 1); |
|
/* Use standard byte writes */ |
|
dev_data->pixel_fmt = LCDIC_DATA_FMT_BYTE; |
|
base->TFIFO_WDATA = cmd; |
|
/* Wait for command completion */ |
|
while ((base->IRSR & LCDIC_IRSR_CMD_DONE_RAW_INTR_MASK) == 0) { |
|
/* Spin */ |
|
} |
|
base->ICR |= LCDIC_ICR_CMD_DONE_INTR_CLR_MASK; |
|
|
|
if (data_len != 0) { |
|
dev_data->xfer_bytes = data_len; |
|
/* Cap command to max transfer size */ |
|
dev_data->cmd_bytes = MIN(data_len, LCDIC_MAX_XFER); |
|
dev_data->xfer_buf = data; |
|
/* If the length of the transfer is not divisible by |
|
* 4, save the unaligned portion of the transfer into |
|
* a temporary buffer |
|
*/ |
|
if (dev_data->cmd_bytes & 0x3) { |
|
dev_data->unaligned_word = mipi_dbi_lcdic_get_unaligned( |
|
dev_data->xfer_buf, |
|
dev_data->cmd_bytes); |
|
} |
|
mipi_dbi_lcdic_set_cmd(base, LCDIC_TX, LCDIC_DATA, |
|
LCDIC_DATA_FMT_BYTE, |
|
LCDIC_TE_NO_SYNC, |
|
dev_data->cmd_bytes); |
|
#ifdef CONFIG_MIPI_DBI_NXP_LCDIC_DMA |
|
if (((((uint32_t)dev_data->xfer_buf) & 0x3) == 0) || |
|
(dev_data->cmd_bytes < 4)) { |
|
/* Data is aligned, we can use DMA */ |
|
/* Enable command complete interrupt */ |
|
interrupts |= LCDIC_IMR_CMD_DONE_INTR_MSK_MASK; |
|
/* Write interrupt mask */ |
|
base->IMR &= ~interrupts; |
|
/* Configure DMA to send data */ |
|
ret = mipi_dbi_lcdic_start_dma(dev); |
|
if (ret) { |
|
LOG_ERR("Could not start DMA (%d)", ret); |
|
goto release_power_lock; |
|
} |
|
} else /* Data is not aligned */ |
|
#endif |
|
{ |
|
/* Enable TX FIFO threshold interrupt. This interrupt |
|
* should fire once enabled, which will kick off |
|
* the transfer |
|
*/ |
|
interrupts |= LCDIC_IMR_TFIFO_THRES_INTR_MSK_MASK; |
|
/* Enable command complete interrupt */ |
|
interrupts |= LCDIC_IMR_CMD_DONE_INTR_MSK_MASK; |
|
/* Write interrupt mask */ |
|
base->IMR &= ~interrupts; |
|
} |
|
ret = k_sem_take(&dev_data->xfer_sem, K_FOREVER); |
|
/* Do not release the lock from the power states. |
|
* This is released in the ISR after the transfer |
|
* is complete. |
|
*/ |
|
goto release_sem; |
|
} |
|
|
|
release_power_lock: |
|
pm_policy_device_power_lock_put(dev); |
|
|
|
release_sem: |
|
k_sem_give(&dev_data->lock); |
|
return ret; |
|
} |
|
|
|
static int mipi_dbi_lcdic_reset(const struct device *dev, k_timeout_t delay) |
|
{ |
|
const struct mipi_dbi_lcdic_config *config = dev->config; |
|
LCDIC_Type *base = config->base; |
|
uint32_t lcdic_freq; |
|
uint32_t delay_ms = k_ticks_to_ms_ceil32(delay.ticks); |
|
uint32_t rst_width, pulse_cnt; |
|
|
|
/* Calculate delay based off timer0 ratio. Formula given |
|
* by RM is as follows: |
|
* Reset pulse width = (RST_WIDTH + 1) * Timer0_Period |
|
* Timer0_Period = 2^(TIMER_RATIO0) / LCDIC_Clock_Freq |
|
*/ |
|
if (clock_control_get_rate(config->clock_dev, config->clock_subsys, |
|
&lcdic_freq)) { |
|
return -EIO; |
|
} |
|
rst_width = (delay_ms * (lcdic_freq)) / ((1 << config->timer0_ratio) * MSEC_PER_SEC); |
|
/* If rst_width is larger than max value supported by hardware, |
|
* increase the pulse count (rounding up) |
|
*/ |
|
pulse_cnt = ((rst_width + (LCDIC_MAX_RST_WIDTH - 1)) / LCDIC_MAX_RST_WIDTH); |
|
rst_width = MIN(LCDIC_MAX_RST_WIDTH, rst_width); |
|
|
|
if ((pulse_cnt - 1) > LCDIC_MAX_RST_PULSE_COUNT) { |
|
/* Still issue reset pulse, but warn user */ |
|
LOG_WRN("Reset pulse is too long for configured timer0 ratio"); |
|
pulse_cnt = LCDIC_MAX_RST_PULSE_COUNT + 1; |
|
} |
|
|
|
/* Start the reset signal */ |
|
base->RST_CTRL = LCDIC_RST_CTRL_RST_WIDTH(rst_width - 1) | |
|
LCDIC_RST_CTRL_RST_SEQ_NUM(pulse_cnt - 1) | |
|
LCDIC_RST_CTRL_RST_START_MASK; |
|
/* Wait for reset to complete */ |
|
while ((base->IRSR & LCDIC_IRSR_RST_DONE_RAW_INTR_MASK) == 0) { |
|
/* Spin */ |
|
} |
|
base->ICR |= LCDIC_ICR_RST_DONE_INTR_CLR_MASK; |
|
return 0; |
|
} |
|
|
|
static int mipi_dbi_lcdic_configure_te(const struct device *dev, |
|
uint8_t edge, |
|
k_timeout_t delay) |
|
{ |
|
const struct mipi_dbi_lcdic_config *config = dev->config; |
|
LCDIC_Type *base = config->base; |
|
struct mipi_dbi_lcdic_data *data = dev->data; |
|
uint32_t lcdic_freq, ttew, reg; |
|
uint32_t delay_us = k_ticks_to_us_ceil32(delay.ticks); |
|
|
|
/* Calculate delay based off timer0 ratio. Formula given |
|
* by RM is as follows: |
|
* TE delay = Timer1_Period * ttew |
|
* Timer1_Period = 2^(TIMER_RATIO1) * Timer0_Period |
|
* Timer0_Period = 2^(TIMER_RATIO0) / LCDIC_Clock_Freq |
|
*/ |
|
if (clock_control_get_rate(config->clock_dev, config->clock_subsys, |
|
&lcdic_freq)) { |
|
return -EIO; |
|
} |
|
|
|
/* |
|
* Calculate TTEW. Done in multiple steps to avoid overflowing |
|
* the uint32_t type. Full formula is: |
|
* (lcdic_freq * delay_us) / |
|
* ((2 ^ (TIMER_RATIO1 + TIMER_RATIO0)) * USEC_PER_SEC) |
|
*/ |
|
ttew = lcdic_freq / (1 << config->timer0_ratio); |
|
ttew *= delay_us; |
|
ttew /= (1 << config->timer1_ratio); |
|
ttew /= USEC_PER_SEC; |
|
|
|
/* Check to see if the delay is shorter than we can support */ |
|
if ((ttew == 0) && (delay_us != 0)) { |
|
LOG_ERR("Timer ratios too large to support this TE delay"); |
|
return -ENOTSUP; |
|
} |
|
reg = base->TE_CTRL; |
|
reg &= ~LCDIC_TE_CTRL_TTEW_MASK; |
|
reg |= LCDIC_TE_CTRL_TTEW(ttew); |
|
base->TE_CTRL = reg; |
|
data->te_edge = edge; |
|
data->te_delay = delay; |
|
/* We should re-configure te signal when coming out of PM mode */ |
|
data->reconfigure_te = true; |
|
return 0; |
|
} |
|
|
|
|
|
/* Initializes LCDIC peripheral */ |
|
static int mipi_dbi_lcdic_init_common(const struct device *dev) |
|
{ |
|
const struct mipi_dbi_lcdic_config *config = dev->config; |
|
struct mipi_dbi_lcdic_data *data = dev->data; |
|
LCDIC_Type *base = config->base; |
|
int ret; |
|
|
|
ret = clock_control_on(config->clock_dev, config->clock_subsys); |
|
if (ret) { |
|
return ret; |
|
} |
|
|
|
/* Set initial clock rate of 10 MHz */ |
|
ret = clock_control_set_rate(config->clock_dev, config->clock_subsys, |
|
(clock_control_subsys_rate_t)MHZ(10)); |
|
if (ret) { |
|
return ret; |
|
} |
|
|
|
ret = pinctrl_apply_state(config->pincfg, PINCTRL_STATE_DEFAULT); |
|
if (ret) { |
|
return ret; |
|
} |
|
/* Clear all interrupt flags */ |
|
base->ICR = LCDIC_ALL_INTERRUPTS; |
|
/* Mask all interrupts */ |
|
base->IMR = LCDIC_ALL_INTERRUPTS; |
|
|
|
/* Enable interrupts */ |
|
config->irq_config_func(dev); |
|
|
|
/* Setup RX and TX fifo thresholds */ |
|
base->FIFO_CTRL = LCDIC_FIFO_CTRL_RFIFO_THRES(LCDIC_RX_FIFO_THRESH) | |
|
LCDIC_FIFO_CTRL_TFIFO_THRES(LCDIC_TX_FIFO_THRESH); |
|
/* Disable command timeouts */ |
|
base->TO_CTRL &= ~(LCDIC_TO_CTRL_CMD_LONG_TO_MASK | |
|
LCDIC_TO_CTRL_CMD_SHORT_TO_MASK); |
|
|
|
/* Ensure LCDIC timer ratios are at reset values */ |
|
base->TIMER_CTRL = LCDIC_TIMER_CTRL_TIMER_RATIO1(config->timer1_ratio) | |
|
LCDIC_TIMER_CTRL_TIMER_RATIO0(config->timer0_ratio); |
|
|
|
data->te_edge = MIPI_DBI_TE_NO_EDGE; |
|
|
|
#ifdef CONFIG_MIPI_DBI_NXP_LCDIC_DMA |
|
/* Attach the LCDIC DMA request signal to the DMA channel we will |
|
* use with hardware triggering. |
|
*/ |
|
INPUTMUX_AttachSignal(INPUTMUX, data->dma_stream.channel, |
|
kINPUTMUX_LcdTxRegToDmaSingleToDma0); |
|
INPUTMUX_EnableSignal(INPUTMUX, |
|
kINPUTMUX_Dmac0InputTriggerLcdTxRegToDmaSingleEna, true); |
|
#endif |
|
|
|
return 0; |
|
} |
|
|
|
static DEVICE_API(mipi_dbi, mipi_dbi_lcdic_driver_api) = { |
|
.command_write = mipi_dbi_lcdic_write_cmd, |
|
.write_display = mipi_dbi_lcdic_write_display, |
|
.configure_te = mipi_dbi_lcdic_configure_te, |
|
.reset = mipi_dbi_lcdic_reset, |
|
}; |
|
|
|
static void mipi_dbi_lcdic_isr(const struct device *dev) |
|
{ |
|
const struct mipi_dbi_lcdic_config *config = dev->config; |
|
struct mipi_dbi_lcdic_data *data = dev->data; |
|
LCDIC_Type *base = config->base; |
|
uint32_t bytes_written, isr_status; |
|
|
|
isr_status = base->ISR; |
|
/* Clear pending interrupts */ |
|
base->ICR |= isr_status; |
|
|
|
if (isr_status & LCDIC_ISR_CMD_DONE_INTR_MASK) { |
|
if (config->base->CTRL & LCDIC_CTRL_DMA_EN_MASK) { |
|
/* DMA completed. Update buffer tracking data */ |
|
data->xfer_bytes -= data->cmd_bytes; |
|
data->xfer_buf += data->cmd_bytes; |
|
/* Disable DMA request */ |
|
config->base->CTRL &= ~LCDIC_CTRL_DMA_EN_MASK; |
|
} |
|
if (data->xfer_bytes == 0) { |
|
/* Disable interrupts */ |
|
base->IMR |= LCDIC_ALL_INTERRUPTS; |
|
/* All data has been sent. */ |
|
k_sem_give(&data->xfer_sem); |
|
pm_policy_device_power_lock_put(dev); |
|
} else { |
|
/* Command done. Queue next command */ |
|
data->cmd_bytes = MIN(data->xfer_bytes, LCDIC_MAX_XFER); |
|
mipi_dbi_lcdic_set_cmd(base, LCDIC_TX, LCDIC_DATA, |
|
data->pixel_fmt, |
|
LCDIC_TE_NO_SYNC, |
|
data->cmd_bytes); |
|
if (data->cmd_bytes & 0x3) { |
|
/* Save unaligned portion of transfer into |
|
* a temporary buffer |
|
*/ |
|
data->unaligned_word = mipi_dbi_lcdic_get_unaligned( |
|
data->xfer_buf, |
|
data->cmd_bytes); |
|
} |
|
#ifdef CONFIG_MIPI_DBI_NXP_LCDIC_DMA |
|
if (((((uint32_t)data->xfer_buf) & 0x3) == 0) || |
|
(data->cmd_bytes < 4)) { |
|
/* Data is aligned. We can use DMA */ |
|
mipi_dbi_lcdic_start_dma(dev); |
|
} else |
|
#endif |
|
{ |
|
/* We must refill the FIFO here in order to continue |
|
* the next transfer, since the TX FIFO threshold |
|
* interrupt may have already fired. |
|
*/ |
|
bytes_written = mipi_dbi_lcdic_fill_tx(base, data->xfer_buf, |
|
data->cmd_bytes, |
|
data->unaligned_word); |
|
if (bytes_written > 0) { |
|
data->xfer_buf += bytes_written; |
|
data->cmd_bytes -= bytes_written; |
|
data->xfer_bytes -= bytes_written; |
|
} |
|
} |
|
} |
|
} else if (isr_status & LCDIC_ISR_TFIFO_THRES_INTR_MASK) { |
|
/* If command is not done, continue filling TX FIFO from |
|
* current transfer buffer |
|
*/ |
|
bytes_written = mipi_dbi_lcdic_fill_tx(base, data->xfer_buf, |
|
data->cmd_bytes, |
|
data->unaligned_word); |
|
if (bytes_written > 0) { |
|
data->xfer_buf += bytes_written; |
|
data->cmd_bytes -= bytes_written; |
|
data->xfer_bytes -= bytes_written; |
|
} |
|
} |
|
} |
|
|
|
static int mipi_dbi_lcdic_pm_action(const struct device *dev, enum pm_device_action action) |
|
{ |
|
struct mipi_dbi_lcdic_data *data = dev->data; |
|
|
|
switch (action) { |
|
case PM_DEVICE_ACTION_RESUME: |
|
break; |
|
case PM_DEVICE_ACTION_SUSPEND: |
|
break; |
|
case PM_DEVICE_ACTION_TURN_OFF: |
|
break; |
|
case PM_DEVICE_ACTION_TURN_ON: |
|
mipi_dbi_lcdic_init_common(dev); |
|
data->active_cfg = NULL; |
|
if (data->reconfigure_te) { |
|
mipi_dbi_lcdic_configure_te(dev, data->te_edge, data->te_delay); |
|
} |
|
break; |
|
default: |
|
return -ENOTSUP; |
|
} |
|
return 0; |
|
} |
|
|
|
static int mipi_dbi_lcdic_init(const struct device *dev) |
|
{ |
|
struct mipi_dbi_lcdic_data *data = dev->data; |
|
int ret; |
|
|
|
ret = k_sem_init(&data->xfer_sem, 0, 1); |
|
if (ret) { |
|
return ret; |
|
} |
|
ret = k_sem_init(&data->lock, 1, 1); |
|
if (ret) { |
|
return ret; |
|
} |
|
/* Rest of the init is done from the PM_DEVICE_TURN_ON action |
|
* which is invoked by pm_device_driver_init(). |
|
*/ |
|
return pm_device_driver_init(dev, mipi_dbi_lcdic_pm_action); |
|
} |
|
|
|
#ifdef CONFIG_MIPI_DBI_NXP_LCDIC_DMA |
|
#define LCDIC_DMA_CHANNELS(n) \ |
|
.dma_stream = { \ |
|
.dma_dev = DEVICE_DT_GET(DT_INST_DMAS_CTLR(n)), \ |
|
.channel = DT_INST_DMAS_CELL_BY_IDX(n, 0, channel), \ |
|
.dma_cfg = { \ |
|
.dma_slot = LPC_DMA_HWTRIG_EN | \ |
|
LPC_DMA_TRIGPOL_HIGH_RISING | \ |
|
LPC_DMA_TRIGBURST, \ |
|
.channel_direction = MEMORY_TO_PERIPHERAL, \ |
|
.dma_callback = mipi_dbi_lcdic_dma_callback, \ |
|
.source_data_size = 4, \ |
|
.dest_data_size = 4, \ |
|
.user_data = (void *)DEVICE_DT_INST_GET(n), \ |
|
}, \ |
|
}, |
|
#else |
|
#define LCDIC_DMA_CHANNELS(n) |
|
#endif |
|
|
|
|
|
#define MIPI_DBI_LCDIC_INIT(n) \ |
|
static void mipi_dbi_lcdic_config_func_##n( \ |
|
const struct device *dev) \ |
|
{ \ |
|
IRQ_CONNECT(DT_INST_IRQN(n), DT_INST_IRQ(n, priority), \ |
|
mipi_dbi_lcdic_isr, \ |
|
DEVICE_DT_INST_GET(n), 0); \ |
|
\ |
|
irq_enable(DT_INST_IRQN(n)); \ |
|
} \ |
|
\ |
|
PINCTRL_DT_INST_DEFINE(n); \ |
|
static const struct mipi_dbi_lcdic_config \ |
|
mipi_dbi_lcdic_config_##n = { \ |
|
.base = (LCDIC_Type *)DT_INST_REG_ADDR(n), \ |
|
.pincfg = PINCTRL_DT_INST_DEV_CONFIG_GET(n), \ |
|
.clock_dev = DEVICE_DT_GET(DT_INST_CLOCKS_CTLR(n)), \ |
|
.clock_subsys = (clock_control_subsys_t) \ |
|
DT_INST_CLOCKS_CELL(n, name), \ |
|
.irq_config_func = mipi_dbi_lcdic_config_func_##n, \ |
|
.swap_bytes = DT_INST_PROP(n, nxp_swap_bytes), \ |
|
.write_active_min = \ |
|
DT_INST_PROP(n, nxp_write_active_cycles), \ |
|
.write_inactive_min = \ |
|
DT_INST_PROP(n, nxp_write_inactive_cycles), \ |
|
.timer0_ratio = DT_INST_PROP(n, nxp_timer0_ratio), \ |
|
.timer1_ratio = DT_INST_PROP(n, nxp_timer1_ratio), \ |
|
}; \ |
|
static struct mipi_dbi_lcdic_data mipi_dbi_lcdic_data_##n = { \ |
|
LCDIC_DMA_CHANNELS(n) \ |
|
}; \ |
|
PM_DEVICE_DT_INST_DEFINE(n, mipi_dbi_lcdic_pm_action); \ |
|
DEVICE_DT_INST_DEFINE(n, mipi_dbi_lcdic_init, \ |
|
PM_DEVICE_DT_INST_GET(n), \ |
|
&mipi_dbi_lcdic_data_##n, \ |
|
&mipi_dbi_lcdic_config_##n, \ |
|
POST_KERNEL, \ |
|
CONFIG_MIPI_DBI_INIT_PRIORITY, \ |
|
&mipi_dbi_lcdic_driver_api); |
|
|
|
DT_INST_FOREACH_STATUS_OKAY(MIPI_DBI_LCDIC_INIT)
|
|
|