blob: a0e487c0689aec1d5f8c14729ed50a204b366b6f [file]
/*
* Copyright (c) 2025 JUMO GmbH & Co. KG
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT levetop_lt7680
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/types.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(display_lt7680, CONFIG_DISPLAY_LOG_LEVEL);
/*
* Levetop LT768x Display Controller Driver - Register definitions
*
* LT768 SPI wire protocol - bits[7:6] of the FIRST byte of each transaction
* encode the access mode (lower 6 bits are irrelevant for the access type):
*
* 0x00 (bits[7:6]=00) : select register address -> 2nd byte = reg addr
* 0x40 (bits[7:6]=01) : read status -> 2nd byte clocked back = status
* 0x80 (bits[7:6]=10) : write data -> 2nd+ bytes = data
* 0xC0 (bits[7:6]=11) : read data -> 2nd byte clocked back = value
*
* Every register write is TWO separate CS transactions:
* TX1: [0x00, reg_addr] (select register)
* TX2: [0x80, data_value] (write data)
*
* Every register read is TWO separate CS transactions:
* TX1: [0x00, reg_addr] (select register)
* TX2: [0xC0, 0xFF], RX: [_, val] (read data; second RX byte = value)
*
* Status read is ONE transaction:
* TX: [0x40, 0xFF], RX: [_, status] (second RX byte = status byte)
*
* Memory (pixel) burst write is TWO transaction:
* TX1: [0x00, LT7680_REG_MRWDP] CONFIG_EMUL (select memory R/W data port)
* TX2: [0x80, p0, p1, p2, ...] (data-write mode + continuous pixel stream)
*/
/* SPI access-mode bytes */
#define LT7680_SPI_SEL_REG 0x00u
#define LT7680_SPI_STATUS_READ 0x40u
#define LT7680_SPI_DATA_WRITE 0x80u
#define LT7680_SPI_DATA_READ 0xC0u
/* Registers. */
#define LT7680_SRR 0x00
#define LT7680_CCR 0x01
#define LT7680_MCR 0x02
#define LT7680_ICR 0x03
#define LT7680_MRWDP 0x04 /* Memory R/W Data Port */
#define LT7680_PPLLC1 0x05 /* PCLK (pixel clock) PLL control 1 */
#define LT7680_PPLLC2 0x06 /* PCLK (pixel clock) PLL control 2 */
#define LT7680_MPLLC1 0x07 /* MCLK (SDRAM clock) PLL control 1 */
#define LT7680_MPLLC2 0x08 /* MCLK (SDRAM clock) PLL control 2 */
#define LT7680_CPLLC1 0x09 /* CCLK (core clock) PLL control 1 */
#define LT7680_CPLLC2 0x0A /* CCLK (core clock) PLL control 2 */
#define LT7680_DPCR 0x12
#define LT7680_HDCR 0x13
#define LT7680_HDWR 0x14 /* Horizontal display width = (HDWR + 1) * 8 + HDWFTR pixels */
#define LT7680_HDWFTR 0x15 /* fine tuning */
#define LT7680_HNDR 0x16 /* Horizontal non-display period (back porch) */
#define LT7680_HNDFTR 0x17 /* fine tuning */
#define LT7680_HSTR 0x18 /* HSYNC start position (front porch) */
#define LT7680_HPW 0x19 /* HSYNC pulse width */
/* Vertical display height */
#define LT7680_VDHR0 0x1A
#define LT7680_VDHR1 0x1B
/* Vertical non-display period (back porch) */
#define LT7680_VNDR0 0x1C
#define LT7680_VNDR1 0x1D
#define LT7680_VSTR 0x1E /* VSYNC start position (front porch) */
#define LT7680_VPWR 0x1F /* VSYNC pulse width */
/* Main Image Start Address */
#define LT7680_MISA0 0x20
#define LT7680_MISA1 0x21
#define LT7680_MISA2 0x22
#define LT7680_MISA3 0x23
/* Main Image Width (pixels, must be divisible by 4) */
#define LT7680_MIW0 0x24
#define LT7680_MIW1 0x25
/* Main Window Upper-Left corner XY */
#define LT7680_MWULX0 0x26
#define LT7680_MWULX1 0x27
#define LT7680_MWULY0 0x28
#define LT7680_MWULY1 0x29
/* Canvas Image Start Address */
#define LT7680_CVSSA0 0x50
#define LT7680_CVSSA1 0x51
#define LT7680_CVSSA2 0x52
#define LT7680_CVSSA3 0x53
/* Canvas Image Width */
#define LT7680_CVS_IMWTH0 0x54
#define LT7680_CVS_IMWTH1 0x55
/* Active Window Upper-Left XY */
#define LT7680_AWUL_X0 0x56
#define LT7680_AWUL_X1 0x57
#define LT7680_AWUL_Y0 0x58
#define LT7680_AWUL_Y1 0x59
/* Active Window Width / Height */
#define LT7680_AW_WTH0 0x5A
#define LT7680_AW_WTH1 0x5B
#define LT7680_AW_HT0 0x5C
#define LT7680_AW_HT1 0x5D
#define LT7680_MPWCTR 0x5Eu
/* Graphic Write Cursor (XY in block mode) */
#define LT7680_CURH0 0x5F
#define LT7680_CURH1 0x60
#define LT7680_CURV0 0x61
#define LT7680_CURV1 0x62
/* SDRAM registers */
#define LT7680_SDRC 0xE0u /* SDRAM Control */
#define LT7680_SDRCAS 0xE1u /* SDRAM CAS latency (0x02=CAS2, 0x03=CAS3) */
#define LT7680_SDRITV0 0xE2u /* SDRAM refresh interval [7:0] */
#define LT7680_SDRITV1 0xE3u /* SDRAM refresh interval [9:8] */
#define LT7680_SDRINI 0xE4u /* SDRAM initialise: write 0x01 */
/* Software Reset Register */
#define LT7680_SRR_SWRESET BIT(1)
#define LT7680_SRR_RECONFIG_PLL BIT(7)
/* Chip Configuration Register */
#define LT7680_CCR_HOST BIT(0)
#define LT7680_CCR_HOST_8BIT 0
#define LT7680_CCR_HOST_16BIT 1
#define LT7680_CCR_SPI_MASTER BIT(1)
#define LT7680_CCR_I2C_MASTER BIT(2)
#define LT7680_CCR_TFT GENMASK(4, 3)
#define LT7680_CCR_TFT_24BIT 0
#define LT7680_CCR_TFT_18BIT 1
#define LT7680_CCR_TFT_16BIT 2
#define LT7680_CCR_TFT_LVDS 3
#define LT7680_CCR_KEYPAD_SCAN BIT(5)
#define LT7680_CCR_SLEEP BIT(6)
#define LT7680_CCR_PLL_ENABLE BIT(7)
/* Memory Configuration Register */
/* Memory write direction */
#define LT7680_MCR_MWRITE GENMASK(2, 1)
#define LT7680_MCR_MWRITE_LR_TB 0 /* left->right, top->bottom */
#define LT7680_MCR_MWRITE_RL_TB 1 /* right->left, top->bottom */
#define LT7680_MCR_MWRITE_TB_LR 2 /* top->bottom, left->right */
#define LT7680_MCR_MWRITE_BT_LR 3 /* bottom->top, left->right */
#define LT7680_MCR_IMGFMT GENMASK(7, 6)
#define LT7680_MCR_IMGFMT_DIRECT_WRITE 0
#define LT7680_MCR_IMGFMT_MASK_HIGH_EACH 1
#define LT7680_MCR_IMGFMT_MASK_HIGH_EVEN 2
/* Interface Control Register */
#define LT7680_ICR_MEM GENMASK(1, 0)
#define LT7680_ICR_MEM_SDRAM 0
#define LT7680_ICR_MEM_GAMMA 1
#define LT7680_ICR_MEM_CURSOR_RAM 2
#define LT7680_ICR_MEM_PALETTE_RAM 3
#define LT7680_ICR_MODE BIT(2)
#define LT7680_ICR_MODE_GRAPHIC 0
#define LT7680_ICR_MODE_TEXT 1
/* [05h-0Ah] PLL Registers
* Format for registers 0x05, 0x07, 0x09:
* bits[7:6] = lpllOD (output divider, actual_div = 2^(OD-1), OD=1..3)
* bits[4:1] = lpllR (input divider R, >=1)
* bit[0] = lpllN[8] (MSB of 9-bit multiplier N)
* Registers 0x06, 0x08, 0x0A hold lpllN[7:0].
* PLL output frequency: FOUT = XI_freq * N / (R * 2^(OD-1))
* where XI_freq is the crystal frequency, N = 9-bit value,
* R is the 4-bit input divider.
*/
#define LT7680_XPLLC1_PLL_DIV_MSB BIT(0)
#define LT7680_XPLLC1_INPUT_DIV GENMASK(5, 1)
#define LT7680_XPLLC1_OUTPUT_DIV GENMASK(7, 6)
#define LT7680_XPLLC2_PLLDIV GENMASK(7, 0)
/* Display Panel Control Register */
#define LT7680_DPCR_PDATA GENMASK(2, 0)
#define LT7680_DPCR_PDATA_RGB 0
#define LT7680_DPCR_PDATA_BGR 5
#define LT7680_DPCR_VSCAN BIT(3)
#define LT7680_DPCR_VSCAN_T_TO_B 0 /* vertical scan top->bottom */
#define LT7680_DPCR_VSCAN_B_TO_T 1 /* vertical scan bottom->top */
#define LT7680_DPCR_DISPLAY_TEST_COLOR_BAR BIT(5)
#define LT7680_DPCR_DISPLAY_ON BIT(6)
#define LT7680_DPCR_PCLK BIT(7)
#define LT7680_DPCR_PCLK_RISING 0
#define LT7680_DPCR_PCLK_FALLING 1
/* Horizontal/Display Control Register */
#define LT7680_HDCR_PDE BIT(5)
#define LT7680_HDCR_PDE_HIGH_ACT 0
#define LT7680_HDCR_PDE_LOW_ACT 1
#define LT7680_HDCR_VSYNC BIT(6)
#define LT7680_HDCR_VSYNC_LOW_ACT 0
#define LT7680_HDCR_VSYNC_HIGH_ACT 1
#define LT7680_HDCR_HSYNC BIT(7)
#define LT7680_HDCR_HSYNC_LOW_ACT 0
#define LT7680_HDCR_HSYNC_HIGH_ACT 1
/* Cursor and addressing mode control */
#define LT7680_MPWCTR_BPP GENMASK(1, 0)
#define LT7680_MPWCTR_BPP_8 0
#define LT7680_MPWCTR_BPP_16 1
#define LT7680_MPWCTR_BPP_24 2
#define LT7680_MPWCTR_MODE BIT(2)
#define LT7680_MPWCTR_MODE_XY 0
#define LT7680_MPWCTR_MODE_LINEAR 1
/* SDRC default value (64 Mbit, 4 banks, row=13, col=9, 16-bit) */
#define LT7680_SDRC_DEFAULT 0x29 /* 32MB SDRAM at 143 MHz */
#define LT7680_SDRCAS_CAS3 0x03
/* Status register bit masks (read from SPI status byte) */
#define LT7680_STATUS_INTERRUPT BIT(0)
#define LT7680_STATUS_POWER_SAVING BIT(1)
#define LT7680_STATUS_SDRAM_READY BIT(2)
#define LT7680_STATUS_2D_BUSY BIT(3)
#define LT7680_STATUS_MEM_WR_FIFO_EMPTY BIT(6)
#define LT7680_STATUS_MEM_WR_FIFO_FULL BIT(7)
struct lt7680_config {
struct spi_dt_spec spi_spec;
struct gpio_dt_spec reset_gpio;
uint16_t width;
uint16_t height;
uint16_t reset_delay_ms;
uint32_t clock_frequency;
uint8_t rotation;
uint16_t hsync_len;
uint16_t hback_porch;
uint16_t hfront_porch;
uint16_t vsync_len;
uint16_t vback_porch;
uint16_t vfront_porch;
bool hsync_active;
bool vsync_active;
bool de_active;
bool pixelclk_active;
};
struct lt7680_data {
enum display_pixel_format current_pixel_format;
bool display_on;
bool blanking;
enum display_orientation orientation;
};
static int lt7680_read_status(const struct device *dev, uint8_t *status)
{
const struct lt7680_config *cfg = dev->config;
int ret;
uint8_t tx[] = {LT7680_SPI_STATUS_READ, 0xFF};
uint8_t rx[] = {0, 0};
const struct spi_buf tx_buf = {.buf = tx, .len = sizeof(tx)};
const struct spi_buf rx_buf = {.buf = rx, .len = sizeof(rx)};
const struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
const struct spi_buf_set rx_set = {.buffers = &rx_buf, .count = 1};
ret = spi_transceive_dt(&cfg->spi_spec, &tx_set, &rx_set);
if (ret >= 0) {
*status = rx[1];
}
return ret;
}
static int lt7680_read_reg(const struct device *dev, uint8_t reg, uint8_t *val)
{
const struct lt7680_config *cfg = dev->config;
int ret;
uint8_t sel[] = {LT7680_SPI_SEL_REG, reg};
const struct spi_buf sel_buf = {.buf = sel, .len = sizeof(sel)};
const struct spi_buf_set sel_set = {.buffers = &sel_buf, .count = 1};
uint8_t tx[] = {LT7680_SPI_DATA_READ, 0xFF};
uint8_t rx[] = {0, 0};
const struct spi_buf tx_buf = {.buf = tx, .len = sizeof(tx)};
const struct spi_buf rx_buf = {.buf = rx, .len = sizeof(rx)};
const struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
const struct spi_buf_set rx_set = {.buffers = &rx_buf, .count = 1};
ret = spi_write_dt(&cfg->spi_spec, &sel_set);
if (ret < 0) {
return ret;
}
ret = spi_transceive_dt(&cfg->spi_spec, &tx_set, &rx_set);
if (ret == 0) {
*val = rx[1];
}
return ret;
}
static int lt7680_write_reg(const struct device *dev, uint8_t reg, uint8_t val)
{
const struct lt7680_config *cfg = dev->config;
int ret;
uint8_t sel[] = {LT7680_SPI_SEL_REG, reg};
uint8_t wr[] = {LT7680_SPI_DATA_WRITE, val};
const struct spi_buf sel_buf = {.buf = sel, .len = sizeof(sel)};
const struct spi_buf wr_buf = {.buf = wr, .len = sizeof(wr)};
const struct spi_buf_set sel_set = {.buffers = &sel_buf, .count = 1};
const struct spi_buf_set wr_set = {.buffers = &wr_buf, .count = 1};
ret = spi_write_dt(&cfg->spi_spec, &sel_set);
if (ret < 0) {
return ret;
}
return spi_write_dt(&cfg->spi_spec, &wr_set);
}
/** @brief Write two consecutive registers (16-bit value, LSB first). */
static int lt7680_write_reg16(const struct device *dev, uint8_t reg, uint16_t val)
{
int ret;
ret = lt7680_write_reg(dev, reg, val & 0xFF);
if (ret < 0) {
return ret;
}
return lt7680_write_reg(dev, reg + 1, val >> 8);
}
/** @brief Write four consecutive registers (32-bit value, LSB first). */
static int lt7680_write_reg32(const struct device *dev, uint8_t reg, uint32_t val)
{
int ret;
ret = lt7680_write_reg(dev, reg, val & 0xFF);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg(dev, reg + 1, (val >> 8) & 0xFF);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg(dev, reg + 2, (val >> 16) & 0xFF);
if (ret < 0) {
return ret;
}
return lt7680_write_reg(dev, reg + 3, (val >> 24) & 0xFF);
}
static int lt7680_write_reg_div8sub1(const struct device *dev, uint8_t reg, uint16_t val)
{
if (val >= 8) {
return lt7680_write_reg(dev, reg, val / 8 - 1);
} else {
return lt7680_write_reg(dev, reg, 0);
}
}
static int lt7680_write_reg16_div8sub1(const struct device *dev, uint8_t reg, uint16_t val)
{
int ret;
ret = lt7680_write_reg_div8sub1(dev, reg, val);
if (ret) {
return ret;
}
return lt7680_write_reg(dev, reg + 1, val % 8);
}
static int lt7680_wait_for_status(const struct device *dev, uint8_t mask, bool present)
{
uint8_t status;
int ret;
for (size_t i = 0; i < 200; i++) {
ret = lt7680_read_status(dev, &status);
if (ret < 0) {
return ret;
}
if ((!!(status & mask)) == present) {
return 0;
}
k_msleep(1);
}
return -ETIMEDOUT;
}
/** @brief Set active window + move memory-write cursor to (x, y). */
static int lt7680_set_window(const struct device *dev, uint16_t x, uint16_t y, uint16_t w,
uint16_t h)
{
int ret;
const struct lt7680_config *cfg = dev->config;
struct lt7680_data *data = dev->data;
uint16_t windowx = x;
uint16_t windowy = y;
uint16_t cursorx = x;
uint16_t cursory = y;
if (data->orientation == DISPLAY_ORIENTATION_ROTATED_180) {
windowx = cfg->width - x - w;
cursorx = cfg->width - 1 - x;
}
/* Active window origin */
ret = lt7680_write_reg16(dev, LT7680_AWUL_X0, windowx);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg16(dev, LT7680_AWUL_Y0, windowy);
if (ret < 0) {
return ret;
}
/* Active window size */
ret = lt7680_write_reg16(dev, LT7680_AW_WTH0, w);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg16(dev, LT7680_AW_HT0, h);
if (ret < 0) {
return ret;
}
/* Move write cursor to (x, y) */
ret = lt7680_write_reg16(dev, LT7680_CURH0, cursorx);
if (ret < 0) {
return ret;
}
return lt7680_write_reg16(dev, LT7680_CURV0, cursory);
}
/**
* @brief Push raw pixel bytes to the LT768 frame-buffer.
*
* SPI TX: [0x80 (data-write mode), pixel_data...] - single burst transaction.
* The LT768 auto-increments the write address within the active window.
*/
static int lt7680_push_pixels(const struct device *dev, const uint8_t *data, size_t len)
{
const struct lt7680_config *cfg = dev->config;
int ret;
uint8_t sel[] = {LT7680_SPI_SEL_REG, LT7680_MRWDP};
const struct spi_buf sel_buf = {.buf = sel, .len = sizeof(sel)};
const struct spi_buf_set sel_set = {.buffers = &sel_buf, .count = 1};
/* Transaction 1: select register */
ret = spi_write_dt(&cfg->spi_spec, &sel_set);
if (ret < 0) {
return ret;
}
if (IS_ENABLED(CONFIG_LT7680_TRANSMIT_DATA_AS_BURST)) {
/* Transaction 2: write pixel bytes as burst */
uint8_t cmd = LT7680_SPI_DATA_WRITE;
const struct spi_buf tx_bufs[] = {
{.buf = &cmd, .len = 1},
{.buf = (void *)data, .len = len},
};
const struct spi_buf_set tx_set = {
.buffers = tx_bufs,
.count = ARRAY_SIZE(tx_bufs),
};
ret = spi_write_dt(&cfg->spi_spec, &tx_set);
if (ret < 0) {
return ret;
}
} else {
/* Transaction 2: write pixel pytes as single bytes with fifo-Check */
for (size_t i = 0; i < len; i++) {
uint8_t tx[] = {LT7680_SPI_DATA_WRITE, data[i]};
const struct spi_buf tx_buf = {.buf = tx, .len = sizeof(tx)};
const struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
/* Wait for MEM_WR_FIFO to have space for at least one more byte */
ret = lt7680_wait_for_status(dev, LT7680_STATUS_MEM_WR_FIFO_FULL, false);
if (ret < 0) {
return ret;
}
/* Transaction 2: write pixel byte */
ret = spi_write_dt(&cfg->spi_spec, &tx_set);
if (ret < 0) {
return ret;
}
}
}
return 0;
}
/** @brief Write a rectangular region of pixels. */
static int lt7680_write(const struct device *dev, const uint16_t x, const uint16_t y,
const struct display_buffer_descriptor *desc, const void *buf)
{
const struct lt7680_config *cfg = dev->config;
struct lt7680_data *data = dev->data;
int ret;
if ((buf == NULL) || (desc == NULL)) {
return -EINVAL;
}
if (((x + desc->width) > cfg->width) || ((y + desc->height) > cfg->height)) {
LOG_ERR("Write (%u,%u)+(%ux%u) exceeds display (%ux%u)", x, y, desc->width,
desc->height, cfg->width, cfg->height);
return -EINVAL;
}
if (data->blanking) {
/* discard while blanked */
return 0;
}
ret = lt7680_set_window(dev, x, y, desc->width, desc->height);
if (ret < 0) {
LOG_ERR("set_window failed: %d", ret);
return ret;
}
size_t px_size = (data->current_pixel_format == PIXEL_FORMAT_RGB_888) ? 3 : 2;
size_t byte_len = (size_t)desc->width * (size_t)desc->height * px_size;
ret = lt7680_push_pixels(dev, buf, byte_len);
if (ret < 0) {
LOG_ERR("pixel push failed: %d", ret);
}
return ret;
}
/**
* @brief Read a rectangular region of pixels from the frame-buffer.
*
* @note Read-back requires the SPI bus to support half-duplex / SDI.
* Verify your MIPI DBI host controller supports command_read.
*/
static int lt7680_read(const struct device *dev, const uint16_t x, const uint16_t y,
const struct display_buffer_descriptor *desc, void *buf)
{
const struct lt7680_config *cfg = dev->config;
struct lt7680_data *data = dev->data;
size_t px_size;
size_t byte_len;
uint8_t *out;
int ret;
uint8_t sel[] = {LT7680_SPI_SEL_REG, LT7680_MRWDP};
const struct spi_buf sel_buf = {.buf = sel, .len = sizeof(sel)};
const struct spi_buf_set sel_set = {.buffers = &sel_buf, .count = 1};
if ((buf == NULL) || (desc == NULL)) {
return -EINVAL;
}
if (((x + desc->width) > cfg->width) || ((y + desc->height) > cfg->height)) {
return -EINVAL;
}
ret = lt7680_set_window(dev, x, y, desc->width, desc->height);
if (ret < 0) {
LOG_ERR("set_window failed: %d", ret);
return ret;
}
px_size = (data->current_pixel_format == PIXEL_FORMAT_RGB_888) ? 3 : 2;
byte_len = (size_t)desc->width * (size_t)desc->height * px_size;
/* Transaction 1: select MRWDP register, identical to lt7680_push_pixels */
ret = spi_write_dt(&cfg->spi_spec, &sel_set);
if (ret < 0) {
return ret;
}
if (IS_ENABLED(CONFIG_LT7680_READ_DATA_AS_BURST)) {
uint8_t tx_cmd[] = {LT7680_SPI_DATA_READ, 0xFFu};
uint8_t rx_dummy[] = {0, 0};
const struct spi_buf tx_bufs[] = {
{.buf = tx_cmd, .len = sizeof(tx_cmd)},
{.buf = buf, .len = byte_len},
};
const struct spi_buf rx_bufs[] = {
{.buf = rx_dummy, .len = sizeof(rx_dummy)},
{.buf = buf, .len = byte_len},
};
const struct spi_buf_set tx_set = {
.buffers = tx_bufs,
.count = ARRAY_SIZE(tx_bufs),
};
const struct spi_buf_set rx_set = {
.buffers = rx_bufs,
.count = ARRAY_SIZE(rx_bufs),
};
/* Transaction 2: read buffer as burst. */
ret = spi_transceive_dt(&cfg->spi_spec, &tx_set, &rx_set);
if (ret < 0) {
return ret;
}
} else {
uint8_t tx[] = {LT7680_SPI_DATA_READ, 0xFFu};
uint8_t rx[] = {0, 0};
const struct spi_buf tx_buf = {.buf = tx, .len = sizeof(tx)};
const struct spi_buf rx_buf = {.buf = rx, .len = sizeof(rx)};
const struct spi_buf_set tx_set = {.buffers = &tx_buf, .count = 1};
const struct spi_buf_set rx_set = {.buffers = &rx_buf, .count = 1};
/* Transaction 2..N: read one byte per 2-byte transceive - identical to
* lt7680_read_reg
*/
out = (uint8_t *)buf;
for (size_t i = 0; i < byte_len; i++) {
ret = spi_transceive_dt(&cfg->spi_spec, &tx_set, &rx_set);
if (ret < 0) {
return ret;
}
out[i] = rx[1];
}
}
return 0;
}
static void *lt7680_get_framebuffer(const struct device *dev)
{
return NULL;
}
static int lt7680_set_brightness(const struct device *dev, const uint8_t brightness)
{
return -ENOTSUP;
}
static int lt7680_set_contrast(const struct device *dev, const uint8_t contrast)
{
return -ENOTSUP;
}
static int lt7680_blanking_on(const struct device *dev)
{
struct lt7680_data *data = dev->data;
uint8_t val;
int ret;
ret = lt7680_read_reg(dev, LT7680_DPCR, &val);
if (ret < 0) {
return ret;
}
val &= ~LT7680_DPCR_DISPLAY_ON;
ret = lt7680_write_reg(dev, LT7680_DPCR, val);
if (ret == 0) {
data->blanking = true;
}
return ret;
}
static int lt7680_blanking_off(const struct device *dev)
{
struct lt7680_data *data = dev->data;
uint8_t val;
int ret;
ret = lt7680_read_reg(dev, LT7680_DPCR, &val);
if (ret < 0) {
return ret;
}
val |= LT7680_DPCR_DISPLAY_ON;
ret = lt7680_write_reg(dev, LT7680_DPCR, val);
if (ret == 0) {
data->blanking = false;
}
return ret;
}
static void lt7680_get_capabilities(const struct device *dev, struct display_capabilities *caps)
{
const struct lt7680_config *cfg = dev->config;
const struct lt7680_data *data = dev->data;
*caps = (struct display_capabilities){
.x_resolution = cfg->width,
.y_resolution = cfg->height,
.supported_pixel_formats = PIXEL_FORMAT_RGB_565 | PIXEL_FORMAT_RGB_888,
.current_pixel_format = data->current_pixel_format,
.current_orientation = data->orientation,
};
}
static int lt7680_set_pixel_format(const struct device *dev, const enum display_pixel_format fmt)
{
struct lt7680_data *data = dev->data;
uint8_t mode_bpp;
uint8_t val;
int ret;
switch (fmt) {
case PIXEL_FORMAT_RGB_565:
mode_bpp = LT7680_MPWCTR_BPP_16;
break;
case PIXEL_FORMAT_RGB_888:
mode_bpp = LT7680_MPWCTR_BPP_24;
break;
default:
LOG_ERR("Unsupported pixel format: %d", fmt);
return -ENOTSUP;
}
ret = lt7680_read_reg(dev, LT7680_MPWCTR, &val);
if (ret < 0) {
return ret;
}
val &= ~LT7680_MPWCTR_BPP;
val |= FIELD_PREP(LT7680_MPWCTR_BPP, mode_bpp);
ret = lt7680_write_reg(dev, LT7680_MPWCTR, val);
if (ret == 0) {
data->current_pixel_format = fmt;
}
return ret;
}
static int lt7680_set_orientation(const struct device *dev,
const enum display_orientation orientation)
{
struct lt7680_data *data = dev->data;
uint8_t mcr;
uint8_t dpcr;
uint8_t memory_direction = 0;
uint8_t vscan_direction = 0;
int ret;
switch (orientation) {
case DISPLAY_ORIENTATION_NORMAL:
memory_direction = LT7680_MCR_MWRITE_LR_TB;
vscan_direction = LT7680_DPCR_VSCAN_T_TO_B;
break;
case DISPLAY_ORIENTATION_ROTATED_180:
memory_direction = LT7680_MCR_MWRITE_RL_TB;
vscan_direction = LT7680_DPCR_VSCAN_B_TO_T;
break;
default:
return -ENOTSUP;
}
ret = lt7680_read_reg(dev, LT7680_MCR, &mcr);
if (ret < 0) {
return ret;
}
mcr &= ~LT7680_MCR_MWRITE;
mcr |= FIELD_PREP(LT7680_MCR_MWRITE, memory_direction);
ret = lt7680_write_reg(dev, LT7680_MCR, mcr);
if (ret < 0) {
return ret;
}
ret = lt7680_read_reg(dev, LT7680_DPCR, &dpcr);
if (ret < 0) {
return ret;
}
dpcr &= ~LT7680_DPCR_VSCAN;
dpcr |= FIELD_PREP(LT7680_DPCR_VSCAN, vscan_direction);
ret = lt7680_write_reg(dev, LT7680_DPCR, dpcr);
if (ret < 0) {
return ret;
}
data->orientation = orientation;
return 0;
}
/**
* @brief Toggle the optional hardware reset GPIO.
*
* When no reset GPIO is configured, a power-on stabilisation delay is used.
*/
static void lt7680_hw_reset(const struct device *dev)
{
const struct lt7680_config *cfg = dev->config;
if (cfg->reset_gpio.port) {
gpio_pin_set_dt(&cfg->reset_gpio, 0);
k_msleep(cfg->reset_delay_ms);
gpio_pin_set_dt(&cfg->reset_gpio, 1);
k_msleep(cfg->reset_delay_ms);
gpio_pin_set_dt(&cfg->reset_gpio, 0);
k_msleep(cfg->reset_delay_ms);
}
}
static uint32_t lt7680_calculate_sclk_mhz(const struct device *dev)
{
const struct lt7680_config *cfg = dev->config;
uint32_t htotal =
(uint32_t)cfg->width + cfg->hback_porch + cfg->hfront_porch + cfg->hsync_len;
uint32_t vtotal =
(uint32_t)cfg->height + cfg->vback_porch + cfg->vfront_porch + cfg->vsync_len;
uint32_t sclk_mhz = 0;
uint32_t sclk_hz = htotal * vtotal * 60;
uint32_t round_tmp = (sclk_hz % 1000000) / 100000;
if (round_tmp > 5) {
sclk_mhz = sclk_hz / 1000000 + 1;
} else {
sclk_mhz = sclk_hz / 1000000;
}
return sclk_mhz;
}
static int lt7680_set_pll_control(const struct device *dev, uint8_t reg, uint8_t od, uint8_t r,
uint16_t n)
{
int ret;
uint8_t c1;
uint8_t c2;
c1 = FIELD_PREP(LT7680_XPLLC1_OUTPUT_DIV, od);
c1 |= FIELD_PREP(LT7680_XPLLC1_INPUT_DIV, r);
c1 |= FIELD_PREP(LT7680_XPLLC1_PLL_DIV_MSB, (n >> 8));
ret = lt7680_write_reg(dev, reg, c1);
if (ret < 0) {
return ret;
}
c2 = FIELD_PREP(LT7680_XPLLC2_PLLDIV, (n & 0xFF));
ret = lt7680_write_reg(dev, reg + 1, c2);
if (ret < 0) {
return ret;
}
return 0;
}
/**
* @brief Configure the three on-chip PLLs (SCLK, MCLK, CCLK).
*
* Follows the algorithm from LT768_Lib.c::LT768_PLL_Initial():
* SCLK = pixel clock derived from panel timing * 60 Hz
* MCLK = CCLK = 3 * SCLK (capped at 100 MHz)
* SCLK capped at 65 MHz
*/
static int lt7680_pll_init(const struct device *dev)
{
const struct lt7680_config *cfg = dev->config;
uint32_t xi_mhz;
uint32_t sclk_mhz;
uint32_t mclk_mhz;
uint32_t cclk_mhz;
int ret;
xi_mhz = cfg->clock_frequency / 1000000u;
if (xi_mhz != 10) {
LOG_ERR("Unsupported clock frequency: %u kHz (only 10 MHz supported)",
cfg->clock_frequency);
return -ENOTSUP;
}
sclk_mhz = lt7680_calculate_sclk_mhz(dev);
/* MCLK = CCLK = 3 * SCLK, capped at 100 MHz */
mclk_mhz = sclk_mhz * 3;
if (mclk_mhz > 100) {
mclk_mhz = 100;
}
cclk_mhz = mclk_mhz;
if (sclk_mhz > 65) {
sclk_mhz = 65;
}
LOG_DBG("LT768 PLL: XI=%u MHz, SCLK=%u MHz, MCLK=%u MHz, CCLK=%u MHz", xi_mhz, sclk_mhz,
mclk_mhz, cclk_mhz);
ret = lt7680_set_pll_control(dev, LT7680_PPLLC1, 3, 5, 2 * sclk_mhz);
if (ret < 0) {
return ret;
}
ret = lt7680_set_pll_control(dev, LT7680_MPLLC1, 2, 5, mclk_mhz);
if (ret < 0) {
return ret;
}
ret = lt7680_set_pll_control(dev, LT7680_CPLLC1, 2, 5, cclk_mhz);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg(dev, LT7680_SRR, LT7680_SRR_RECONFIG_PLL);
if (ret < 0) {
return ret;
}
/* wait for PLLs to lock */
k_msleep(1);
return 0;
}
/**
* @brief Initialise the on-chip SDRAM.
*
* SDRAM refresh interval formula (from LT768_Lib.c):
* sdram_itv = (64_000_000 / 8192) / (1000 / MCLK_MHz) - 2
* = MCLK_kHz * 64 / 8192 - 2
*/
static int lt7680_sdram_init(const struct device *dev)
{
uint16_t sdram_itv;
uint32_t mclk_mhz;
int ret;
uint32_t sclk_mhz = lt7680_calculate_sclk_mhz(dev);
mclk_mhz = MIN(sclk_mhz * 3, 100);
if (sclk_mhz > 65) {
sclk_mhz = 65;
}
sdram_itv = ((sclk_mhz * 1000u * 1000000u) / (8192u * 64u)) - 2u;
ret = lt7680_write_reg(dev, LT7680_SDRC, LT7680_SDRC_DEFAULT);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg(dev, LT7680_SDRCAS, LT7680_SDRCAS_CAS3);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg16(dev, LT7680_SDRITV0, sdram_itv);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg(dev, LT7680_SDRINI, 1);
if (ret < 0) {
return ret;
}
ret = lt7680_wait_for_status(dev, LT7680_STATUS_SDRAM_READY, true);
if (ret < 0) {
LOG_ERR("LT768: SDRAM init timeout");
}
return ret;
}
/**
* @brief Configure the panel control registers from the DT configuration.
*
* All horizontal values are in units of 8 pixels; vertical in lines.
*
* @note If hsync_len == 0, detailed timing setup
* is skipped and the LT768 retains its power-on defaults.
*/
static int lt7680_panel_init(const struct device *dev)
{
const struct lt7680_config *cfg = dev->config;
const struct lt7680_data *data = dev->data;
uint8_t reg_val = 0;
uint8_t mcr;
uint8_t icr;
uint8_t dpcr;
uint8_t hdcr;
int ret;
ret = lt7680_read_reg(dev, LT7680_CCR, &reg_val);
if (ret < 0) {
return ret;
}
reg_val &= ~LT7680_CCR_SPI_MASTER;
reg_val &= ~LT7680_CCR_I2C_MASTER;
reg_val &= ~LT7680_CCR_HOST;
reg_val |= FIELD_PREP(LT7680_CCR_HOST, LT7680_CCR_HOST_8BIT);
reg_val &= ~LT7680_CCR_TFT;
reg_val |= FIELD_PREP(LT7680_CCR_TFT, LT7680_CCR_TFT_16BIT);
ret = lt7680_write_reg(dev, LT7680_CCR, reg_val);
if (ret < 0) {
return ret;
}
mcr = FIELD_PREP(LT7680_MCR_MWRITE, LT7680_MCR_MWRITE_LR_TB);
if (data->current_pixel_format == PIXEL_FORMAT_RGB_888) {
mcr |= LT7680_MCR_IMGFMT_DIRECT_WRITE;
} else {
mcr |= LT7680_MCR_IMGFMT_MASK_HIGH_EACH;
}
ret = lt7680_write_reg(dev, LT7680_MCR, mcr);
if (ret < 0) {
return ret;
}
icr = FIELD_PREP(LT7680_ICR_MEM, LT7680_ICR_MEM_SDRAM);
icr |= FIELD_PREP(LT7680_ICR_MODE, LT7680_ICR_MODE_GRAPHIC);
ret = lt7680_write_reg(dev, LT7680_ICR, icr);
if (ret < 0) {
return ret;
}
/* PCLK edge, display OFF (enabled later), scan dir, RGB */
dpcr = FIELD_PREP(LT7680_DPCR_VSCAN, LT7680_DPCR_VSCAN_T_TO_B);
dpcr |= FIELD_PREP(LT7680_DPCR_PDATA, LT7680_DPCR_PDATA_RGB);
if (!cfg->pixelclk_active) {
dpcr |= FIELD_PREP(LT7680_DPCR_PCLK, LT7680_DPCR_PCLK_FALLING);
}
ret = lt7680_write_reg(dev, LT7680_DPCR, dpcr);
if (ret < 0) {
return ret;
}
hdcr = 0;
if (cfg->de_active) {
hdcr |= FIELD_PREP(LT7680_HDCR_PDE, LT7680_HDCR_PDE_HIGH_ACT);
} else {
hdcr |= FIELD_PREP(LT7680_HDCR_PDE, LT7680_HDCR_PDE_LOW_ACT);
}
if (cfg->vsync_active) {
hdcr |= FIELD_PREP(LT7680_HDCR_VSYNC, LT7680_HDCR_VSYNC_HIGH_ACT);
} else {
hdcr |= FIELD_PREP(LT7680_HDCR_VSYNC, LT7680_HDCR_VSYNC_LOW_ACT);
}
if (cfg->hsync_active) {
hdcr |= FIELD_PREP(LT7680_HDCR_HSYNC, LT7680_HDCR_HSYNC_HIGH_ACT);
} else {
hdcr |= FIELD_PREP(LT7680_HDCR_HSYNC, LT7680_HDCR_HSYNC_LOW_ACT);
}
ret = lt7680_write_reg(dev, LT7680_HDCR, hdcr);
if (ret < 0) {
return ret;
}
/* Skip detailed timing if not provided in DT */
if (cfg->hsync_len == 0 || cfg->vsync_len == 0) {
LOG_DBG("LT768: panel timing not configured, skipping");
return 0;
}
/* Horizontal display width */
ret = lt7680_write_reg16_div8sub1(dev, LT7680_HDWR, cfg->width);
if (ret < 0) {
return ret;
}
/* Vertical display height */
ret = lt7680_write_reg16(dev, LT7680_VDHR0, cfg->height - 1);
if (ret < 0) {
return ret;
}
/* Horizontal non-display / back porch */
ret = lt7680_write_reg16_div8sub1(dev, LT7680_HNDR, cfg->hback_porch);
if (ret < 0) {
return ret;
}
/* HSYNC start position / front porch */
ret = lt7680_write_reg_div8sub1(dev, LT7680_HSTR, cfg->hfront_porch);
if (ret < 0) {
return ret;
}
/* HSYNC pulse width */
ret = lt7680_write_reg_div8sub1(dev, LT7680_HPW, cfg->hsync_len);
if (ret < 0) {
return ret;
}
/* Vertical non-display / back porch */
ret = lt7680_write_reg16(dev, LT7680_VNDR0, cfg->vback_porch - 1);
if (ret < 0) {
return ret;
}
/* VSYNC start position / front porch */
ret = lt7680_write_reg(dev, LT7680_VSTR, cfg->vfront_porch - 1);
if (ret < 0) {
return ret;
}
/* VSYNC pulse width */
return lt7680_write_reg(dev, LT7680_VPWR, cfg->vsync_len - 1);
}
/** @brief Configure the main image layer and canvas for full-screen use. */
static int lt7680_layer_init(const struct device *dev)
{
const struct lt7680_config *cfg = dev->config;
const struct lt7680_data *data = dev->data;
uint8_t mpw;
int ret;
/* Main image: origin at SDRAM address 0 */
ret = lt7680_write_reg32(dev, LT7680_MISA0, 0);
if (ret < 0) {
return ret;
}
/* Main image width */
ret = lt7680_write_reg16(dev, LT7680_MIW0, cfg->width);
if (ret < 0) {
return ret;
}
/* Main window upper-left at (0, 0) */
ret = lt7680_write_reg16(dev, LT7680_MWULX0, 0);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg16(dev, LT7680_MWULY0, 0);
if (ret < 0) {
return ret;
}
/* Canvas: same frame-buffer, same width */
ret = lt7680_write_reg32(dev, LT7680_CVSSA0, 0);
if (ret < 0) {
return ret;
}
ret = lt7680_write_reg16(dev, LT7680_CVS_IMWTH0, cfg->width);
if (ret < 0) {
return ret;
}
/* Active window = full display */
ret = lt7680_set_window(dev, 0, 0, cfg->width, cfg->height);
if (ret < 0) {
return ret;
}
mpw = FIELD_PREP(LT7680_MPWCTR_MODE, LT7680_MPWCTR_MODE_XY);
if (data->current_pixel_format == PIXEL_FORMAT_RGB_888) {
mpw |= FIELD_PREP(LT7680_MPWCTR_BPP, LT7680_MPWCTR_BPP_24);
} else {
mpw |= FIELD_PREP(LT7680_MPWCTR_BPP, LT7680_MPWCTR_BPP_16);
}
return lt7680_write_reg(dev, LT7680_MPWCTR, mpw);
}
static int lt7680_init(const struct device *dev)
{
const struct lt7680_config *cfg = dev->config;
struct lt7680_data *data = dev->data;
enum display_orientation orientation;
int ret;
if (cfg->rotation == 0) {
orientation = DISPLAY_ORIENTATION_NORMAL;
} else if (cfg->rotation == 180) {
orientation = DISPLAY_ORIENTATION_ROTATED_180;
} else {
return -ENOTSUP;
}
if (!spi_is_ready_dt(&cfg->spi_spec)) {
LOG_ERR("SPI bus not ready");
return -ENODEV;
}
if (cfg->reset_gpio.port) {
if (!gpio_is_ready_dt(&cfg->reset_gpio)) {
LOG_ERR("Reset GPIO not ready");
return -ENODEV;
}
ret = gpio_pin_configure_dt(&cfg->reset_gpio, GPIO_OUTPUT_ACTIVE);
if (ret < 0) {
LOG_ERR("Reset GPIO config failed: %d", ret);
return ret;
}
}
lt7680_hw_reset(dev);
ret = lt7680_wait_for_status(dev, LT7680_STATUS_POWER_SAVING, false);
if (ret < 0) {
LOG_ERR("Power-saving wait failed: %d", ret);
return ret;
}
ret = lt7680_pll_init(dev);
if (ret < 0) {
LOG_ERR("PLL init failed: %d", ret);
return ret;
}
ret = lt7680_sdram_init(dev);
if (ret < 0) {
LOG_ERR("SDRAM init failed: %d", ret);
return ret;
}
ret = lt7680_panel_init(dev);
if (ret < 0) {
LOG_ERR("Panel init failed: %d", ret);
return ret;
}
ret = lt7680_layer_init(dev);
if (ret < 0) {
LOG_ERR("Layer init failed: %d", ret);
return ret;
}
ret = lt7680_blanking_off(dev);
if (ret < 0) {
LOG_ERR("Display enable failed: %d", ret);
return ret;
}
ret = lt7680_set_orientation(dev, orientation);
if (ret < 0) {
return ret;
}
data->display_on = true;
data->blanking = false;
LOG_INF("LT768 %ux%u initialised (SPI)", cfg->width, cfg->height);
return 0;
}
static DEVICE_API(display, lt7680_api) = {
.blanking_on = lt7680_blanking_on,
.blanking_off = lt7680_blanking_off,
.write = lt7680_write,
.read = lt7680_read,
.get_framebuffer = lt7680_get_framebuffer,
.set_brightness = lt7680_set_brightness,
.set_contrast = lt7680_set_contrast,
.get_capabilities = lt7680_get_capabilities,
.set_pixel_format = lt7680_set_pixel_format,
.set_orientation = lt7680_set_orientation,
};
#define LT7680_INIT(inst) \
static struct lt7680_data lt7680_data_##inst = { \
.current_pixel_format = PIXEL_FORMAT_RGB_565, \
.display_on = false, \
.blanking = false, \
.orientation = DISPLAY_ORIENTATION_NORMAL, \
}; \
\
static const struct lt7680_config lt7680_config_##inst = { \
.spi_spec = \
SPI_DT_SPEC_GET(DT_INST(inst, levetop_lt7680), \
SPI_OP_MODE_CONTROLLER | SPI_TRANSFER_MSB | \
SPI_WORD_SET(8)), \
.reset_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, reset_gpios, {0}), \
.width = DT_INST_PROP(inst, width), \
.height = DT_INST_PROP(inst, height), \
.reset_delay_ms = DT_INST_PROP(inst, reset_delay_ms), \
.clock_frequency = DT_PROP_OR(DT_INST_CHILD(inst, display_timings), \
clock_frequency, 10000000), \
.rotation = DT_INST_PROP(inst, rotation), \
.hsync_len = DT_PROP(DT_INST_CHILD(inst, display_timings), hsync_len), \
.hback_porch = DT_PROP(DT_INST_CHILD(inst, display_timings), hback_porch), \
.hfront_porch = DT_PROP(DT_INST_CHILD(inst, display_timings), hfront_porch), \
.vsync_len = DT_PROP(DT_INST_CHILD(inst, display_timings), vsync_len), \
.vback_porch = DT_PROP(DT_INST_CHILD(inst, display_timings), vback_porch), \
.vfront_porch = DT_PROP(DT_INST_CHILD(inst, display_timings), vfront_porch), \
.hsync_active = DT_PROP(DT_INST_CHILD(inst, display_timings), hsync_active), \
.vsync_active = DT_PROP(DT_INST_CHILD(inst, display_timings), vsync_active), \
.de_active = DT_PROP(DT_INST_CHILD(inst, display_timings), de_active), \
.pixelclk_active = DT_PROP(DT_INST_CHILD(inst, display_timings), pixelclk_active), \
}; \
\
DEVICE_DT_INST_DEFINE(inst, lt7680_init, NULL, &lt7680_data_##inst, &lt7680_config_##inst, \
POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &lt7680_api);
DT_INST_FOREACH_STATUS_OKAY(LT7680_INIT)