From 194153f31fc790a8cd385a478547fa5f38013ce7 Mon Sep 17 00:00:00 2001 From: Alan Carvalho de Assis Date: Sun, 23 Aug 2026 13:34:17 -0300 Subject: [PATCH 1/4] ioexpander/ch422g: add a driver for the WCH CH422G I/O The CH422G offers eight bi-directional pins, IO0-IO7, and four open-drain outputs, OC0-OC3. It appears on boards that have run out of usable GPIOs once a parallel RGB panel has taken its share, the Waveshare ESP32-S3-Touch-LCD-7 among them, where it holds the panel and touch controller in and out of reset and switches the backlight. Two things about the device do not fit the shape a register-per-address I2C driver usually takes, and both are handled here rather than pushed on to board logic: - A register is selected by the I2C address the transfer is addressed to, not by a register address written ahead of the data. Each access carries a single byte to one of four addresses. - None of the write-only registers can be read back, so the driver keeps a shadow copy of each and updates it in step with the device. IO0-IO7 have no individual direction control; one bit of the system parameter register drives the whole group. The driver records the direction asked of each pin and puts the group in output mode once at least one of them is an output, which is what a board that drives some of the pins would expect. Reading a pin of a group held in output mode reports the value last written, because the hardware cannot report the level, and that is documented rather than hidden. The four open-drain outputs are presented as pins 8-11 of the same ioexpander_dev_s so that one instance covers the chip, which means CONFIG_IOEXPANDER_NPINS must be at least 12. Builds clean with no new warnings on esp32s3-touch-lcd7:usbnsh and passes nxstyle. Signed-off-by: Alan Carvalho de Assis Assisted-by: Claude Code --- .../components/drivers/special/ioexpander.rst | 44 +- drivers/ioexpander/CMakeLists.txt | 4 + drivers/ioexpander/Kconfig | 12 + drivers/ioexpander/Make.defs | 4 + drivers/ioexpander/ch422g.c | 750 ++++++++++++++++++ include/nuttx/ioexpander/ch422g.h | 116 +++ 6 files changed, 915 insertions(+), 15 deletions(-) create mode 100644 drivers/ioexpander/ch422g.c create mode 100644 include/nuttx/ioexpander/ch422g.h diff --git a/Documentation/components/drivers/special/ioexpander.rst b/Documentation/components/drivers/special/ioexpander.rst index 2f2fa32f1b1b5..5a4e19af89641 100644 --- a/Documentation/components/drivers/special/ioexpander.rst +++ b/Documentation/components/drivers/special/ioexpander.rst @@ -62,64 +62,78 @@ routine in the header of the same name under * - Awinic AW9523B - I2C - 16 - - ``IOEXPANDER_AW9523B`` + - ``CONFIG_IOEXPANDER_AW9523B`` + * - WCH CH422G + - I2C + - 12 + - ``CONFIG_IOEXPANDER_CH422G`` * - iC-Haus iC-JX - SPI - 16 - - ``IOEXPANDER_ICJX`` + - ``CONFIG_IOEXPANDER_ICJX`` * - ISO1H812G - SPI - 8 - - ``IOEXPANDER_ISO1H812G`` + - ``CONFIG_IOEXPANDER_ISO1H812G`` * - ISO1I813T - SPI - 8 - - ``IOEXPANDER_ISO1I813T`` + - ``CONFIG_IOEXPANDER_ISO1I813T`` * - Microchip MCP23008 / MCP23S08 - I2C - 8 - - ``IOEXPANDER_MCP23X08`` + - ``CONFIG_IOEXPANDER_MCP23X08`` * - Microchip MCP23017 / MCP23S17 - I2C - 16 - - ``IOEXPANDER_MCP23X17`` + - ``CONFIG_IOEXPANDER_MCP23X17`` * - NXP PCA9538 - I2C - 8 - - ``IOEXPANDER_PCA9538`` + - ``CONFIG_IOEXPANDER_PCA9538`` * - NXP PCA9555 - I2C - 16 - - ``IOEXPANDER_PCA9555`` + - ``CONFIG_IOEXPANDER_PCA9555`` * - NXP PCA9557 - I2C - 8 - - ``IOEXPANDER_PCA9557`` + - ``CONFIG_IOEXPANDER_PCA9557`` * - PCF8574 - I2C - 8 - - ``IOEXPANDER_PCF8574`` + - ``CONFIG_IOEXPANDER_PCF8574`` * - PCF8575 - I2C - 16 - - ``IOEXPANDER_PCF8575`` + - ``CONFIG_IOEXPANDER_PCF8575`` * - Diodes PI4IOE5V6408 - I2C - 8 - - ``IOEXPANDER_PI4IOE5V6408`` + - ``CONFIG_IOEXPANDER_PI4IOE5V6408`` * - Semtech SX1509 - I2C - 16 - - ``IOEXPANDER_SX1509`` + - ``CONFIG_IOEXPANDER_SX1509`` * - TCA6408 / TCA6416 / TCA6424 / PCAL6416A - I2C - 8 / 16 / 24 / 16 - - ``IOEXPANDER_TCA64XX`` + - ``CONFIG_IOEXPANDER_TCA64XX`` Notes on individual drivers: -- ``IOEXPANDER_TCA64XX`` and ``IOEXPANDER_PCF8574`` additionally depend on +- ``CONFIG_IOEXPANDER_TCA64XX`` and ``CONFIG_IOEXPANDER_PCF8574`` additionally depend on ``CONFIG_EXPERIMENTAL``. +- ``CONFIG_IOEXPANDER_CH422G`` presents the eight bi-directional pins, IO0-IO7, as + pins 0-7 and the four open-drain outputs, OC0-OC3, as pins 8-11, so + ``CONFIG_IOEXPANDER_NPINS`` must be at least 12. The device selects a + register by the I2C address a transfer is addressed to rather than by a + register address written ahead of the data, and none of its write-only + registers can be read back, so the driver shadows them. IO0-IO7 share a + single direction control in the hardware: the driver records the direction + asked of each pin and puts the group in output mode once at least one of + them is an output. Reading a pin of a group held in output mode reports + the value last written, because the device cannot report the pin level. - Drivers with a ``_MULTIPLE`` option support more than one instance of the same chip on a board. - Drivers with a ``_SHADOW_MODE`` option keep the output and diff --git a/drivers/ioexpander/CMakeLists.txt b/drivers/ioexpander/CMakeLists.txt index 34bde646335f2..62d6660492866 100644 --- a/drivers/ioexpander/CMakeLists.txt +++ b/drivers/ioexpander/CMakeLists.txt @@ -59,6 +59,10 @@ if(CONFIG_IOEXPANDER) list(APPEND SRCS pi4ioe5v6408.c) endif() + if(CONFIG_IOEXPANDER_CH422G) + list(APPEND SRCS ch422g.c) + endif() + if(CONFIG_IOEXPANDER_PCA9557) list(APPEND SRCS pca9557.c) endif() diff --git a/drivers/ioexpander/Kconfig b/drivers/ioexpander/Kconfig index 489ca1b0c7c69..a0ff52ec85b95 100644 --- a/drivers/ioexpander/Kconfig +++ b/drivers/ioexpander/Kconfig @@ -56,6 +56,18 @@ config IOEXPANDER_DUMMY_INT_POLLDELAY endif # IOEXPANDER_DUMMY +config IOEXPANDER_CH422G + bool "CH422G I2C IO expander" + default n + depends on I2C + ---help--- + Enable support for the WCH CH422G IO Expander, which offers eight + bi-directional pins, IO0-IO7, and four open-drain outputs, OC0-OC3. + + IO0-IO7 share a single direction control in the hardware. The + driver puts the group in output mode when at least one of them has + been configured as an output. + config IOEXPANDER_ICJX bool "iC-JX SPI IO expander" default n diff --git a/drivers/ioexpander/Make.defs b/drivers/ioexpander/Make.defs index 2c949558bb29b..7ca38196d81e6 100644 --- a/drivers/ioexpander/Make.defs +++ b/drivers/ioexpander/Make.defs @@ -58,6 +58,10 @@ ifeq ($(CONFIG_IOEXPANDER_PI4IOE5V6408),y) CSRCS += pi4ioe5v6408.c endif +ifeq ($(CONFIG_IOEXPANDER_CH422G),y) + CSRCS += ch422g.c +endif + ifeq ($(CONFIG_IOEXPANDER_PCA9557),y) CSRCS += pca9557.c endif diff --git a/drivers/ioexpander/ch422g.c b/drivers/ioexpander/ch422g.c new file mode 100644 index 0000000000000..97f75895ca4c6 --- /dev/null +++ b/drivers/ioexpander/ch422g.c @@ -0,0 +1,750 @@ +/**************************************************************************** + * drivers/ioexpander/ch422g.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/* The WCH CH422G is an I2C I/O expander offering eight bi-directional + * pins, IO0-IO7, and four open-drain outputs, OC0-OC3. + * + * It does not follow the usual convention of a register address written + * ahead of the data. Each register is reached through an I2C address of + * its own and every access carries a single data byte: + * + * 0x24 write System parameter register + * 0x23 write OC0-OC3 output register + * 0x38 write IO0-IO7 output register + * 0x26 read IO0-IO7 input register + * + * None of the write-only registers can be read back, so the driver keeps a + * shadow copy of each and updates it in step with the device. + * + * IO0-IO7 do not have individual direction control. A single bit of the + * system parameter register, IO_OE, drives the whole group. The driver + * records the direction requested for each pin and puts the group in output + * mode when at least one of them is an output, which is what a board that + * mixes the two would expect of the pins it drives. Reading a pin of a + * group held in output mode returns the shadowed output value rather than + * the level on the pin, because the hardware cannot report it. + */ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* I2C addresses that select a register. See the comment above. */ + +#define CH422G_ADDR_WR_SET 0x24 /* System parameter register */ +#define CH422G_ADDR_WR_OC 0x23 /* OC0-OC3 output register */ +#define CH422G_ADDR_WR_IO 0x38 /* IO0-IO7 output register */ +#define CH422G_ADDR_RD_IO 0x26 /* IO0-IO7 input register */ + +/* System parameter register bits */ + +#define CH422G_SET_IO_OE (1 << 0) /* IO0-IO7 drive their pins */ +#define CH422G_SET_A_SCAN (1 << 1) /* Digital tube scan enable */ +#define CH422G_SET_OD_EN (1 << 2) /* OC0-OC3 open-drain enable */ +#define CH422G_SET_SLEEP (1 << 3) /* Low power mode */ + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +struct ch422g_dev_s +{ + /* Must appear first so the structure can be cast to the public type */ + + struct ioexpander_dev_s dev; + + FAR struct i2c_master_s *i2c; /* Saved I2C driver instance */ + FAR struct ch422g_config_s *config; /* Board configuration data */ + mutex_t lock; /* Mutual exclusion */ + + uint8_t sysparam; /* Shadow of system parameter reg */ + uint8_t outio; /* Shadow of IO0-IO7 output reg */ + uint8_t outoc; /* Shadow of OC0-OC3 output reg */ + uint8_t outmask; /* IO0-IO7 configured as outputs */ +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static int ch422g_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin, + int dir); +static int ch422g_option(FAR struct ioexpander_dev_s *dev, uint8_t pin, + int opt, FAR void *val); +static int ch422g_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin, + bool value); +static int ch422g_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin, + FAR bool *value); +static int ch422g_readbuf(FAR struct ioexpander_dev_s *dev, uint8_t pin, + FAR bool *value); +#ifdef CONFIG_IOEXPANDER_MULTIPIN +static int ch422g_multiwritepin(FAR struct ioexpander_dev_s *dev, + FAR const uint8_t *pins, + FAR const bool *values, int count); +static int ch422g_multireadpin(FAR struct ioexpander_dev_s *dev, + FAR const uint8_t *pins, FAR bool *values, + int count); +static int ch422g_multireadbuf(FAR struct ioexpander_dev_s *dev, + FAR const uint8_t *pins, FAR bool *values, + int count); +#endif + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static const struct ioexpander_ops_s g_ch422g_ops = +{ + .ioe_direction = ch422g_direction, + .ioe_option = ch422g_option, + .ioe_writepin = ch422g_writepin, + .ioe_readpin = ch422g_readpin, + .ioe_readbuf = ch422g_readbuf, +#ifdef CONFIG_IOEXPANDER_MULTIPIN + .ioe_multiwritepin = ch422g_multiwritepin, + .ioe_multireadpin = ch422g_multireadpin, + .ioe_multireadbuf = ch422g_multireadbuf, +#endif +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: ch422g_write_reg + * + * Description: + * Write the single data byte of one register. The register is selected + * by the I2C address the transfer is addressed to. + * + ****************************************************************************/ + +static int ch422g_write_reg(FAR struct ch422g_dev_s *priv, uint8_t addr, + uint8_t value) +{ + struct i2c_msg_s msg; + + msg.frequency = priv->config->frequency; + msg.addr = addr; + msg.flags = 0; + msg.buffer = &value; + msg.length = 1; + + return I2C_TRANSFER(priv->i2c, &msg, 1); +} + +/**************************************************************************** + * Name: ch422g_read_io + * + * Description: + * Read the level of IO0-IO7. Only meaningful while the group is in + * input mode. + * + ****************************************************************************/ + +static int ch422g_read_io(FAR struct ch422g_dev_s *priv, FAR uint8_t *value) +{ + struct i2c_msg_s msg; + + msg.frequency = priv->config->frequency; + msg.addr = CH422G_ADDR_RD_IO; + msg.flags = I2C_M_READ; + msg.buffer = value; + msg.length = 1; + + return I2C_TRANSFER(priv->i2c, &msg, 1); +} + +/**************************************************************************** + * Name: ch422g_setdir + * + * Description: + * Bring the IO_OE bit into agreement with the recorded per-pin + * directions. Must be called with the lock held. + * + ****************************************************************************/ + +static int ch422g_setdir(FAR struct ch422g_dev_s *priv) +{ + uint8_t sysparam = priv->sysparam; + + if (priv->outmask != 0) + { + sysparam |= CH422G_SET_IO_OE; + } + else + { + sysparam &= ~CH422G_SET_IO_OE; + } + + if (sysparam == priv->sysparam) + { + return OK; + } + + priv->sysparam = sysparam; + return ch422g_write_reg(priv, CH422G_ADDR_WR_SET, sysparam); +} + +/**************************************************************************** + * Name: ch422g_direction + * + * Description: + * Set the direction of an I/O pin. + * + ****************************************************************************/ + +static int ch422g_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin, + int dir) +{ + FAR struct ch422g_dev_s *priv = (FAR struct ch422g_dev_s *)dev; + int ret; + + if (pin >= CH422G_NPINS) + { + return -ENXIO; + } + + if (dir != IOEXPANDER_DIRECTION_IN && dir != IOEXPANDER_DIRECTION_OUT) + { + return -EINVAL; + } + + /* OC0-OC3 are open-drain outputs and cannot be inputs */ + + if (pin >= CH422G_NIO) + { + return dir == IOEXPANDER_DIRECTION_OUT ? OK : -EINVAL; + } + + ret = nxmutex_lock(&priv->lock); + if (ret < 0) + { + return ret; + } + + if (dir == IOEXPANDER_DIRECTION_OUT) + { + priv->outmask |= (1 << pin); + } + else + { + priv->outmask &= ~(1 << pin); + } + + ret = ch422g_setdir(priv); + + nxmutex_unlock(&priv->lock); + return ret; +} + +/**************************************************************************** + * Name: ch422g_option + * + * Description: + * Set pin options. The CH422G offers none of the options the interface + * defines, so only a request that asks for nothing succeeds. + * + ****************************************************************************/ + +static int ch422g_option(FAR struct ioexpander_dev_s *dev, uint8_t pin, + int opt, FAR void *val) +{ + FAR struct ch422g_dev_s *priv = (FAR struct ch422g_dev_s *)dev; + int ret = -ENOSYS; + + if (pin >= CH422G_NPINS) + { + return -ENXIO; + } + + if (opt == IOEXPANDER_OPTION_INVERT) + { + ret = ((uintptr_t)val == IOEXPANDER_VAL_NORMAL) ? OK : -ENOSYS; + } + else if (opt == IOEXPANDER_OPTION_INTCFG) + { + ret = ((uintptr_t)val == IOEXPANDER_VAL_DISABLE) ? OK : -ENOSYS; + } + + UNUSED(priv); + return ret; +} + +/**************************************************************************** + * Name: ch422g_writepin + * + * Description: + * Set the level of an output pin. + * + ****************************************************************************/ + +static int ch422g_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin, + bool value) +{ + FAR struct ch422g_dev_s *priv = (FAR struct ch422g_dev_s *)dev; + FAR uint8_t *shadow; + uint8_t addr; + uint8_t bit; + int ret; + + if (pin >= CH422G_NPINS) + { + return -ENXIO; + } + + ret = nxmutex_lock(&priv->lock); + if (ret < 0) + { + return ret; + } + + if (pin < CH422G_NIO) + { + shadow = &priv->outio; + addr = CH422G_ADDR_WR_IO; + bit = 1 << pin; + } + else + { + shadow = &priv->outoc; + addr = CH422G_ADDR_WR_OC; + bit = 1 << (pin - CH422G_NIO); + } + + if (value) + { + *shadow |= bit; + } + else + { + *shadow &= ~bit; + } + + ret = ch422g_write_reg(priv, addr, *shadow); + + nxmutex_unlock(&priv->lock); + return ret; +} + +/**************************************************************************** + * Name: ch422g_readpin + * + * Description: + * Read the level of a pin. A pin of a group held in output mode, and any + * of the write-only open-drain outputs, reports the value last written. + * + ****************************************************************************/ + +static int ch422g_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin, + FAR bool *value) +{ + FAR struct ch422g_dev_s *priv = (FAR struct ch422g_dev_s *)dev; + uint8_t regval; + int ret; + + if (pin >= CH422G_NPINS) + { + return -ENXIO; + } + + DEBUGASSERT(value != NULL); + + ret = nxmutex_lock(&priv->lock); + if (ret < 0) + { + return ret; + } + + if (pin >= CH422G_NIO) + { + *value = ((priv->outoc >> (pin - CH422G_NIO)) & 1) != 0; + ret = OK; + } + else if ((priv->sysparam & CH422G_SET_IO_OE) != 0) + { + *value = ((priv->outio >> pin) & 1) != 0; + ret = OK; + } + else + { + ret = ch422g_read_io(priv, ®val); + if (ret >= 0) + { + *value = ((regval >> pin) & 1) != 0; + } + } + + nxmutex_unlock(&priv->lock); + return ret; +} + +/**************************************************************************** + * Name: ch422g_readbuf + * + * Description: + * Read the last value written to a pin. The CH422G does not buffer a + * separately readable copy, so this is the shadowed output value. + * + ****************************************************************************/ + +static int ch422g_readbuf(FAR struct ioexpander_dev_s *dev, uint8_t pin, + FAR bool *value) +{ + FAR struct ch422g_dev_s *priv = (FAR struct ch422g_dev_s *)dev; + int ret; + + if (pin >= CH422G_NPINS) + { + return -ENXIO; + } + + DEBUGASSERT(value != NULL); + + ret = nxmutex_lock(&priv->lock); + if (ret < 0) + { + return ret; + } + + if (pin < CH422G_NIO) + { + *value = ((priv->outio >> pin) & 1) != 0; + } + else + { + *value = ((priv->outoc >> (pin - CH422G_NIO)) & 1) != 0; + } + + nxmutex_unlock(&priv->lock); + return OK; +} + +#ifdef CONFIG_IOEXPANDER_MULTIPIN + +/**************************************************************************** + * Name: ch422g_multiwritepin + * + * Description: + * Set the level of several pins. Pins that share a register are gathered + * so that the register is written once. + * + ****************************************************************************/ + +static int ch422g_multiwritepin(FAR struct ioexpander_dev_s *dev, + FAR const uint8_t *pins, + FAR const bool *values, int count) +{ + FAR struct ch422g_dev_s *priv = (FAR struct ch422g_dev_s *)dev; + bool touchio = false; + bool touchoc = false; + int ret; + int i; + + DEBUGASSERT(pins != NULL && values != NULL); + + for (i = 0; i < count; i++) + { + if (pins[i] >= CH422G_NPINS) + { + return -ENXIO; + } + } + + ret = nxmutex_lock(&priv->lock); + if (ret < 0) + { + return ret; + } + + for (i = 0; i < count; i++) + { + uint8_t pin = pins[i]; + + if (pin < CH422G_NIO) + { + if (values[i]) + { + priv->outio |= (1 << pin); + } + else + { + priv->outio &= ~(1 << pin); + } + + touchio = true; + } + else + { + if (values[i]) + { + priv->outoc |= (1 << (pin - CH422G_NIO)); + } + else + { + priv->outoc &= ~(1 << (pin - CH422G_NIO)); + } + + touchoc = true; + } + } + + ret = OK; + + if (touchio) + { + ret = ch422g_write_reg(priv, CH422G_ADDR_WR_IO, priv->outio); + } + + if (ret >= 0 && touchoc) + { + ret = ch422g_write_reg(priv, CH422G_ADDR_WR_OC, priv->outoc); + } + + nxmutex_unlock(&priv->lock); + return ret; +} + +/**************************************************************************** + * Name: ch422g_multireadpin + * + * Description: + * Read the level of several pins with a single read of the device. + * + ****************************************************************************/ + +static int ch422g_multireadpin(FAR struct ioexpander_dev_s *dev, + FAR const uint8_t *pins, FAR bool *values, + int count) +{ + FAR struct ch422g_dev_s *priv = (FAR struct ch422g_dev_s *)dev; + uint8_t regval = 0; + bool haveinput = false; + int ret; + int i; + + DEBUGASSERT(pins != NULL && values != NULL); + + for (i = 0; i < count; i++) + { + if (pins[i] >= CH422G_NPINS) + { + return -ENXIO; + } + } + + ret = nxmutex_lock(&priv->lock); + if (ret < 0) + { + return ret; + } + + if ((priv->sysparam & CH422G_SET_IO_OE) == 0) + { + for (i = 0; i < count; i++) + { + if (pins[i] < CH422G_NIO) + { + haveinput = true; + break; + } + } + } + + if (haveinput) + { + ret = ch422g_read_io(priv, ®val); + if (ret < 0) + { + nxmutex_unlock(&priv->lock); + return ret; + } + } + + for (i = 0; i < count; i++) + { + uint8_t pin = pins[i]; + + if (pin >= CH422G_NIO) + { + values[i] = ((priv->outoc >> (pin - CH422G_NIO)) & 1) != 0; + } + else if (haveinput) + { + values[i] = ((regval >> pin) & 1) != 0; + } + else + { + values[i] = ((priv->outio >> pin) & 1) != 0; + } + } + + nxmutex_unlock(&priv->lock); + return OK; +} + +/**************************************************************************** + * Name: ch422g_multireadbuf + * + * Description: + * Read the last value written to several pins. + * + ****************************************************************************/ + +static int ch422g_multireadbuf(FAR struct ioexpander_dev_s *dev, + FAR const uint8_t *pins, FAR bool *values, + int count) +{ + FAR struct ch422g_dev_s *priv = (FAR struct ch422g_dev_s *)dev; + int ret; + int i; + + DEBUGASSERT(pins != NULL && values != NULL); + + for (i = 0; i < count; i++) + { + if (pins[i] >= CH422G_NPINS) + { + return -ENXIO; + } + } + + ret = nxmutex_lock(&priv->lock); + if (ret < 0) + { + return ret; + } + + for (i = 0; i < count; i++) + { + uint8_t pin = pins[i]; + + if (pin < CH422G_NIO) + { + values[i] = ((priv->outio >> pin) & 1) != 0; + } + else + { + values[i] = ((priv->outoc >> (pin - CH422G_NIO)) & 1) != 0; + } + } + + nxmutex_unlock(&priv->lock); + return OK; +} + +#endif /* CONFIG_IOEXPANDER_MULTIPIN */ + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: ch422g_initialize + * + * Description: + * Instantiate and configure the CH422G device driver to use the provided + * I2C device instance. + * + * Input Parameters: + * i2c - An I2C driver instance + * config - Persistent board configuration data + * + * Returned Value: + * An ioexpander_dev_s instance on success, NULL on failure. + * + ****************************************************************************/ + +FAR struct ioexpander_dev_s * +ch422g_initialize(FAR struct i2c_master_s *i2c, + FAR struct ch422g_config_s *config) +{ + FAR struct ch422g_dev_s *priv; + int ret; + + DEBUGASSERT(i2c != NULL && config != NULL); + + priv = kmm_zalloc(sizeof(struct ch422g_dev_s)); + if (priv == NULL) + { + gpioerr("ERROR: Failed to allocate driver instance\n"); + return NULL; + } + + priv->dev.ops = &g_ch422g_ops; + priv->i2c = i2c; + priv->config = config; + + nxmutex_init(&priv->lock); + + /* Leave the device in a known state: the open-drain outputs off, the + * bi-directional pins low, and the group in input mode until a board + * asks for an output. Writing the system parameter register first also + * confirms that the device is answering. + */ + + ret = ch422g_write_reg(priv, CH422G_ADDR_WR_SET, priv->sysparam); + if (ret < 0) + { + gpioerr("ERROR: CH422G not responding: %d\n", ret); + goto errout; + } + + ret = ch422g_write_reg(priv, CH422G_ADDR_WR_OC, priv->outoc); + if (ret < 0) + { + goto errout; + } + + ret = ch422g_write_reg(priv, CH422G_ADDR_WR_IO, priv->outio); + if (ret < 0) + { + goto errout; + } + + return &priv->dev; + +errout: + nxmutex_destroy(&priv->lock); + kmm_free(priv); + return NULL; +} + diff --git a/include/nuttx/ioexpander/ch422g.h b/include/nuttx/ioexpander/ch422g.h new file mode 100644 index 0000000000000..2d48bb308b884 --- /dev/null +++ b/include/nuttx/ioexpander/ch422g.h @@ -0,0 +1,116 @@ +/**************************************************************************** + * include/nuttx/ioexpander/ch422g.h + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTTX_IOEXPANDER_CH422G_H +#define __INCLUDE_NUTTX_IOEXPANDER_CH422G_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include +#include + +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* Pin numbering used by the ioexpander interface. + * + * The CH422G offers eight bi-directional pins, IO0-IO7, and four + * open-drain outputs, OC0-OC3. They are presented as one contiguous pin + * space so that a single ioexpander_dev_s covers the whole chip. + */ + +#define CH422G_IO0 0 +#define CH422G_IO1 1 +#define CH422G_IO2 2 +#define CH422G_IO3 3 +#define CH422G_IO4 4 +#define CH422G_IO5 5 +#define CH422G_IO6 6 +#define CH422G_IO7 7 +#define CH422G_OC0 8 +#define CH422G_OC1 9 +#define CH422G_OC2 10 +#define CH422G_OC3 11 + +#define CH422G_NPINS 12 +#define CH422G_NIO 8 /* IO0-IO7 */ +#define CH422G_NOC 4 /* OC0-OC3 */ + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +/* A reference to a structure of this type must be passed to the CH422G + * driver when the driver is instantiated. Memory for this structure is + * provided by the caller. It is not copied by the driver and is presumed + * to persist while the driver is active. + */ + +struct ch422g_config_s +{ + uint32_t frequency; /* I2C frequency */ +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef __cplusplus +#define EXTERN extern "C" +extern "C" +{ +#else +#define EXTERN extern +#endif + +/**************************************************************************** + * Name: ch422g_initialize + * + * Description: + * Instantiate and configure the CH422G device driver to use the provided + * I2C device instance. + * + * Input Parameters: + * i2c - An I2C driver instance + * config - Persistent board configuration data + * + * Returned Value: + * An ioexpander_dev_s instance on success, NULL on failure. + * + ****************************************************************************/ + +FAR struct ioexpander_dev_s * +ch422g_initialize(FAR struct i2c_master_s *i2c, + FAR struct ch422g_config_s *config); + +#undef EXTERN +#ifdef __cplusplus +} +#endif + +#endif /* __INCLUDE_NUTTX_IOEXPANDER_CH422G_H */ From 55e8e83a60c8c425e3e844963ceb590e1aeee2fc Mon Sep 17 00:00:00 2001 From: Alan Carvalho de Assis Date: Sun, 23 Aug 2026 12:29:04 -0300 Subject: [PATCH 2/4] esp32s3/lcd: Fix rolling issue on high resolution LCDs This patch get some fixes from ESP-IDF to fix a rolling issue that happens on big resolution LCDs such as 800x480 LCDs. Signed-off-by: Alan C. Assis Assisted-by: Claude Code --- arch/xtensa/src/esp32s3/Kconfig | 42 +++++ arch/xtensa/src/esp32s3/esp32s3_lcd.c | 222 +++++++++++++++++++++++++- 2 files changed, 263 insertions(+), 1 deletion(-) diff --git a/arch/xtensa/src/esp32s3/Kconfig b/arch/xtensa/src/esp32s3/Kconfig index 23c017e1acd70..4d189c7baa56b 100644 --- a/arch/xtensa/src/esp32s3/Kconfig +++ b/arch/xtensa/src/esp32s3/Kconfig @@ -2650,6 +2650,48 @@ config ESP32S3_LCD_CLOCK_MHZ int "LCD Pixel Clock Frequency in MHz" default 2 +config ESP32S3_LCD_BOUNCE_LINES + int "Bounce buffer height in lines (0 to disable)" + default 0 + ---help--- + Scan the panel out of two small buffers in internal RAM, each this + many lines tall, refilled from the framebuffer as they are consumed, + rather than having the DMA read the framebuffer directly. + + This matters when the framebuffer is in PSRAM. The DMA then has to + fetch every pixel from PSRAM in real time, and it is competing with + whatever else uses that memory; a CPU writing to the framebuffer, or + flushing it out of the cache, can stall those fetches for long + enough to empty the LCD FIFO. The panel sees the data stream + falter, and what shows up is an image that shakes and then rolls. + + With a bounce buffer the only thing on the critical path is internal + RAM, which nothing else can stall. The copy from PSRAM happens in + the interrupt that follows each buffer being consumed and has a + whole buffer's worth of time to complete. This is the arrangement + the ESP-IDF RGB panel driver calls a bounce buffer. + + The framebuffer must be a whole number of these buffers, so the + value has to divide the vertical resolution. Two buffers of this + size are taken out of internal RAM. Ten lines of an 800 pixel wide + panel at 16bpp costs 32000 bytes. + +config ESP32S3_LCD_PCLK_ACTIVE_NEG + bool "Latch data on the falling edge of the pixel clock" + default n + ---help--- + By default the pixel clock is low during the first half of a cycle, + so the panel latches the data lines, and DE and the sync signals + with them, on the rising edge. Enable this for a panel that + expects the falling edge instead. + + A panel driven on the wrong edge samples the sync signals at the + moment they change, which usually shows up as an image that is + recognisable but will not hold vertical lock. + + This is the equivalent of pclk_active_neg in the ESP-IDF RGB panel + driver. + config ESP32S3_LCD_VFRONTPORCH int "LCD Vertical Front Porch" default 40 diff --git a/arch/xtensa/src/esp32s3/esp32s3_lcd.c b/arch/xtensa/src/esp32s3/esp32s3_lcd.c index c3f12193e8858..a047e6a2d31b7 100644 --- a/arch/xtensa/src/esp32s3/esp32s3_lcd.c +++ b/arch/xtensa/src/esp32s3/esp32s3_lcd.c @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #include "esp_clk.h" #include "esp_gpio.h" #include "esp32s3_dma.h" +#include "hal/gdma_ll.h" #include "esp_irq.h" #include "xtensa.h" @@ -150,6 +152,28 @@ #define ESP32S3_LCD_DMADESC_NUM (ESP32S3_LCD_FB_SIZE / \ ESP32S3_DMA_BUFLEN_MAX + 1) +#if CONFIG_ESP32S3_LCD_BOUNCE_LINES > 0 + +/* Scan out of two buffers in internal RAM, refilled from the framebuffer as + * they are consumed, so that nothing on the panel's critical path depends on + * PSRAM. + */ + +# define ESP32S3_LCD_HAVE_BOUNCE 1 + +# define ESP32S3_LCD_BB_SIZE (CONFIG_ESP32S3_LCD_HRES * \ + CONFIG_ESP32S3_LCD_BOUNCE_LINES * \ + ESP32S3_LCD_DATA_WIDTH) + +# define ESP32S3_LCD_BB_DESCS ((ESP32S3_LCD_BB_SIZE + \ + ESP32S3_DMA_BUFLEN_MAX - 1) / \ + ESP32S3_DMA_BUFLEN_MAX) + +# if (ESP32S3_LCD_FB_SIZE % ESP32S3_LCD_BB_SIZE) != 0 +# error "CONFIG_ESP32S3_LCD_BOUNCE_LINES must divide the vertical resolution" +# endif +#endif + #define ESP32S3_LCD_LAYERS CONFIG_ESP32S3_LCD_BUFFER_LAYERS /* Get current layer pointer */ @@ -202,6 +226,18 @@ struct esp32s3_lcd_s uint32_t yoffset; /* The current pan offset */ +#ifdef ESP32S3_LCD_HAVE_BOUNCE + /* Descriptors for the two bounce buffers, linked into a ring so that the + * transfer never ends and the controller never has to be restarted. + */ + + struct esp32s3_dmadesc_s bbdesc[2][ESP32S3_LCD_BB_DESCS]; + + uint32_t bouncepos; /* Offset of the next chunk to copy */ + uint32_t bbeof; /* Buffers consumed so far */ + int bbcpuint; /* CPU interrupt for the DMA channel */ +#endif + int cpuint; /* CPU interrupt assigned to this LCD */ uint8_t cpu; /* CPU ID */ int32_t dma_channel; /* DMA channel */ @@ -331,6 +367,16 @@ static struct fb_vtable_s g_base_vtable = #endif }; +#ifdef ESP32S3_LCD_HAVE_BOUNCE +/* The bounce buffers. Ordinary static data, so they are in internal RAM, + * which is the whole point of them. + */ + +static uint8_t g_bounce[2][ESP32S3_LCD_BB_SIZE] + aligned_data(64); + +#endif + /**************************************************************************** * Private Functions ****************************************************************************/ @@ -731,6 +777,16 @@ static int IRAM_ATTR lcd_interrupt(int irq, void *context, void *arg) ESP32S3_LCD_FB_SIZE); #endif +#ifdef ESP32S3_LCD_HAVE_BOUNCE + /* Anchor the ring at the first bounce buffer again. The read + * position needs no correction: the framebuffer is a whole number of + * bounce buffers, so by the time a frame has been sent the buffers + * already hold the first two pieces of the next one. + */ + + esp32s3_dma_load(priv->bbdesc[0], priv->dma_channel, true); +#endif + /* Enable DMA TX */ esp32s3_dma_enable(priv->dma_channel, true); @@ -749,6 +805,74 @@ static int IRAM_ATTR lcd_interrupt(int irq, void *context, void *arg) return 0; } +#ifdef ESP32S3_LCD_HAVE_BOUNCE +/**************************************************************************** + * Name: esp32s3_lcd_bounce_fill + * + * Description: + * Copy the next piece of the framebuffer into a bounce buffer that has + * just been sent, and step the read position on. The position wraps at + * the end of the framebuffer, which is also the end of a frame because + * the framebuffer is a whole number of bounce buffers, so the two stay in + * step without anything having to resynchronise them. + * + * Input Parameters: + * priv - The LCD driver state. + * bb - Which of the two buffers to refill. + * + ****************************************************************************/ + +static void IRAM_ATTR esp32s3_lcd_bounce_fill(struct esp32s3_lcd_s *priv, + int bb) +{ + struct esp32s3_layer_s *layer = CURRENT_LAYER(priv); + + memcpy(g_bounce[bb], + &layer->framebuffer[priv->yoffset * ESP32S3_LCD_STRIDE + + priv->bouncepos], + ESP32S3_LCD_BB_SIZE); + + priv->bouncepos += ESP32S3_LCD_BB_SIZE; + if (priv->bouncepos >= ESP32S3_LCD_FB_SIZE) + { + priv->bouncepos = 0; + } +} + +/**************************************************************************** + * Name: esp32s3_lcd_dma_interrupt + * + * Description: + * One bounce buffer has been sent to the panel. Refill it while the + * other one is being sent. + * + ****************************************************************************/ + +static int IRAM_ATTR esp32s3_lcd_dma_interrupt(int irq, void *context, + void *arg) +{ + struct esp32s3_lcd_s *priv = &g_lcd_priv; + int status = esp32s3_dma_get_interrupt(priv->dma_channel, true); + + if (status <= 0) + { + return 0; + } + + esp32s3_dma_clear_interrupt(priv->dma_channel, true, status); + + if ((status & GDMA_LL_EVENT_TX_EOF) != 0) + { + /* The buffer that finished is the one this transfer started from */ + + esp32s3_lcd_bounce_fill(priv, priv->bbeof & 1); + priv->bbeof++; + } + + return 0; +} +#endif + /**************************************************************************** * Name: esp32s3_lcd_dmasetup * @@ -783,8 +907,20 @@ static int esp32s3_lcd_dmasetup(void) { struct esp32s3_layer_s *layer = &priv->layer[i]; + /* A framebuffer is large: at 800x480 in 16bpp one is 768000 bytes, + * twice that when double buffered, so it does not come from internal + * RAM and the allocation is one that can realistically fail. Say so + * rather than faulting on the memset that follows. + */ + layer->framebuffer = memalign(64, ESP32S3_LCD_FB_MEM_SIZE); - DEBUGASSERT(layer->framebuffer != NULL); + if (layer->framebuffer == NULL) + { + lcderr("Failed to allocate %d bytes for framebuffer %d\n", + ESP32S3_LCD_FB_MEM_SIZE, i); + goto errout; + } + memset(layer->framebuffer, 0, ESP32S3_LCD_FB_MEM_SIZE); esp32s3_dma_setup(layer->dmadesc, @@ -794,7 +930,63 @@ static int esp32s3_lcd_dmasetup(void) true, priv->dma_channel); } +#ifdef ESP32S3_LCD_HAVE_BOUNCE + { + struct esp32s3_dmadesc_s *tail; + int i; + + /* One chain per bounce buffer, each ending in the end of frame mark + * that raises the interrupt this driver refills on, and the two of + * them joined into a ring. The transfer then never finishes, so the + * controller runs continuously and the panel sees an unbroken stream. + */ + + for (i = 0; i < 2; i++) + { + esp32s3_dma_setup(priv->bbdesc[i], + ESP32S3_LCD_BB_DESCS, + g_bounce[i], + ESP32S3_LCD_BB_SIZE, + true, priv->dma_channel); + } + + for (i = 0; i < 2; i++) + { + for (tail = priv->bbdesc[i]; tail->next != NULL; tail = tail->next) + { + } + + tail->next = priv->bbdesc[i ^ 1]; + } + + /* Prime both buffers with the start of the first frame */ + + priv->bouncepos = 0; + priv->bbeof = 0; + + esp32s3_lcd_bounce_fill(priv, 0); + esp32s3_lcd_bounce_fill(priv, 1); + } +#endif + return OK; + +errout: + for (int i = 0; i < ESP32S3_LCD_LAYERS; i++) + { + struct esp32s3_layer_s *layer = &priv->layer[i]; + + if (layer->framebuffer != NULL) + { + free(layer->framebuffer); + layer->framebuffer = NULL; + } + } + + esp32s3_dma_release(priv->dma_channel); + priv->dma_channel = -1; + + return -ENOMEM; } /**************************************************************************** @@ -871,6 +1063,16 @@ static void esp32s3_lcd_enableclk(void) (ESP32S3_LCD_CLK_N << LCD_CAM_LCD_CLKM_DIV_NUM_S) | (clk_a << LCD_CAM_LCD_CLKM_DIV_A_S) | (clk_b << LCD_CAM_LCD_CLKM_DIV_B_S); + +#ifdef CONFIG_ESP32S3_LCD_PCLK_ACTIVE_NEG + /* Drive the pixel clock high for the first half of the cycle, so that the + * data lines, and DE and the sync signals with them, are stable across the + * falling edge the panel latches on. + */ + + regval |= LCD_CAM_LCD_CK_OUT_EDGE_M; +#endif + esp32s3_lcd_putreg(LCD_CAM_LCD_CLOCK_REG, regval); } @@ -1006,7 +1208,25 @@ static void esp32s3_lcd_enable(void) struct esp32s3_lcd_s *priv = &g_lcd_priv; struct esp32s3_layer_s *layer = CURRENT_LAYER(priv); +#ifdef ESP32S3_LCD_HAVE_BOUNCE + priv->bbcpuint = esp_setup_irq(ESP32S3_PERIPH_DMA_OUT_CH0 + + priv->dma_channel, + 1, ESP_IRQ_TRIGGER_LEVEL, + esp32s3_lcd_dma_interrupt, NULL); + if (priv->bbcpuint < 0) + { + lcderr("Failed to attach the DMA interrupt: %d\n", priv->bbcpuint); + return; + } + + esp32s3_dma_enable_interrupt(priv->dma_channel, true, + GDMA_LL_EVENT_TX_EOF, true); + up_enable_irq(ESP32S3_IRQ_DMA_OUT_CH0 + priv->dma_channel); + + esp32s3_dma_load(priv->bbdesc[0], priv->dma_channel, true); +#else esp32s3_dma_load(layer->dmadesc, priv->dma_channel, true); +#endif esp32s3_dma_enable(priv->dma_channel, true); /* Delay 1 microsecond to wait the DMA start */ From dd2a60765b1a2aad014103d04b786f81e6972e9c Mon Sep 17 00:00:00 2001 From: Alan Carvalho de Assis Date: Sun, 23 Aug 2026 12:31:48 -0300 Subject: [PATCH 3/4] boards/esp32s3-touch-lcd7: Add support to LCD This patch adds support to 7" 800x480 LCD on esp32s3-touch-lcd7 board. Signed-off-by: Alan C. Assis Assisted-by: Claude Code --- .../xtensa/esp32s3/esp32s3-touch-lcd7/Kconfig | 12 ++ .../esp32s3-touch-lcd7/configs/lcd/defconfig | 86 +++++++++++ .../esp32s3/esp32s3-touch-lcd7/src/Make.defs | 8 + .../src/esp32s3-touch-lcd7.h | 58 ++++++++ .../src/esp32s3_board_ioexpander.c | 98 +++++++++++++ .../esp32s3-touch-lcd7/src/esp32s3_bringup.c | 10 ++ .../esp32s3-touch-lcd7/src/esp32s3_lcd.c | 138 ++++++++++++++++++ 7 files changed, 410 insertions(+) create mode 100644 boards/xtensa/esp32s3/esp32s3-touch-lcd7/configs/lcd/defconfig create mode 100644 boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_board_ioexpander.c create mode 100644 boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_lcd.c diff --git a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/Kconfig b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/Kconfig index e763d2ebc50b3..cc5875291ef9e 100644 --- a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/Kconfig +++ b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/Kconfig @@ -5,4 +5,16 @@ if ARCH_BOARD_ESP32S3_TOUCH_LCD_7 +config ESP32S3_BOARD_LCD + bool "Enable Board LCD" + default n + depends on ESP32S3_LCD + select ESP32S3_I2C0 + select IOEXPANDER + select IOEXPANDER_CH422G + ---help--- + Enable the onboard 7 inch 800x480 RGB panel. The panel reset and + the display enable are driven through the CH422G I/O expander on + I2C0, so both are selected here. + endif # ARCH_BOARD_ESP32S3_TOUCH_LCD_7 diff --git a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/configs/lcd/defconfig b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/configs/lcd/defconfig new file mode 100644 index 0000000000000..9e6adb92e17f3 --- /dev/null +++ b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/configs/lcd/defconfig @@ -0,0 +1,86 @@ +# +# This file is autogenerated: PLEASE DO NOT EDIT IT. +# +# You can use "make menuconfig" to make any modifications to the installed .config file. +# You can then do "make savedefconfig" to generate a new defconfig file that includes your +# modifications. +# +# CONFIG_NSH_ARGCAT is not set +# CONFIG_NSH_CMDOPT_HEXDUMP is not set +CONFIG_ARCH="xtensa" +CONFIG_ARCH_BOARD="esp32s3-touch-lcd7" +CONFIG_ARCH_BOARD_COMMON=y +CONFIG_ARCH_BOARD_ESP32S3_TOUCH_LCD_7=y +CONFIG_ARCH_CHIP="esp32s3" +CONFIG_ARCH_CHIP_ESP32S3=y +CONFIG_ARCH_CHIP_ESP32S3WROOM1N16R8=y +CONFIG_ARCH_INTERRUPTSTACK=2048 +CONFIG_ARCH_IRQ_TO_NDX=y +CONFIG_ARCH_MINIMAL_VECTORTABLE_DYNAMIC=y +CONFIG_ARCH_NUSER_INTERRUPTS=2 +CONFIG_ARCH_STACKDUMP=y +CONFIG_ARCH_XTENSA=y +CONFIG_BOARD_LOOPSPERMSEC=16717 +CONFIG_BUILTIN=y +CONFIG_DEBUG_FULLOPT=y +CONFIG_DEBUG_SYMBOLS=y +CONFIG_ESP32S3_BOARD_LCD=y +CONFIG_ESP32S3_DMA=y +CONFIG_ESP32S3_I2C0_SCLPIN=9 +CONFIG_ESP32S3_I2C0_SDAPIN=8 +CONFIG_ESP32S3_LCD=y +CONFIG_ESP32S3_LCD_BOUNCE_LINES=10 +CONFIG_ESP32S3_LCD_CLOCK_MHZ=16 +CONFIG_ESP32S3_LCD_DATA0_PIN=14 +CONFIG_ESP32S3_LCD_DATA10_PIN=21 +CONFIG_ESP32S3_LCD_DATA11_PIN=1 +CONFIG_ESP32S3_LCD_DATA12_PIN=2 +CONFIG_ESP32S3_LCD_DATA14_PIN=41 +CONFIG_ESP32S3_LCD_DATA15_PIN=40 +CONFIG_ESP32S3_LCD_DATA1_PIN=38 +CONFIG_ESP32S3_LCD_DATA2_PIN=18 +CONFIG_ESP32S3_LCD_DATA3_PIN=17 +CONFIG_ESP32S3_LCD_DATA4_PIN=10 +CONFIG_ESP32S3_LCD_DATA5_PIN=39 +CONFIG_ESP32S3_LCD_DATA6_PIN=0 +CONFIG_ESP32S3_LCD_DATA7_PIN=45 +CONFIG_ESP32S3_LCD_DATA8_PIN=48 +CONFIG_ESP32S3_LCD_DATA9_PIN=47 +CONFIG_ESP32S3_LCD_HBACKPORCH=8 +CONFIG_ESP32S3_LCD_HE_PIN=5 +CONFIG_ESP32S3_LCD_HFRONTPORCH=8 +CONFIG_ESP32S3_LCD_HPULSEWIDTH=4 +CONFIG_ESP32S3_LCD_HRES=800 +CONFIG_ESP32S3_LCD_PCLK_ACTIVE_NEG=y +CONFIG_ESP32S3_LCD_PCLK_PIN=7 +CONFIG_ESP32S3_LCD_VBACKPORCH=8 +CONFIG_ESP32S3_LCD_VFRONTPORCH=8 +CONFIG_ESP32S3_LCD_VPULSEWIDTH=4 +CONFIG_ESP32S3_SPIRAM=y +CONFIG_ESP32S3_SPIRAM_MODE_OCT=y +CONFIG_ESP32S3_SPIRAM_SPEED_80M=y +CONFIG_ESP32S3_UART0=y +CONFIG_EXAMPLES_FB=y +CONFIG_FS_PROCFS=y +CONFIG_HAVE_CXX=y +CONFIG_HAVE_CXXINITIALIZE=y +CONFIG_IDLETHREAD_STACKSIZE=3072 +CONFIG_INIT_ENTRYPOINT="nsh_main" +CONFIG_INIT_STACKSIZE=4096 +CONFIG_INTELHEX_BINARY=y +CONFIG_LINE_MAX=64 +CONFIG_MM_REGIONS=2 +CONFIG_NSH_BUILTIN_APPS=y +CONFIG_NSH_FILEIOSIZE=512 +CONFIG_NSH_READLINE=y +CONFIG_PREALLOC_TIMERS=4 +CONFIG_RAM_SIZE=114688 +CONFIG_RAM_START=0x20000000 +CONFIG_RR_INTERVAL=200 +CONFIG_SCHED_WAITPID=y +CONFIG_START_DAY=6 +CONFIG_START_MONTH=12 +CONFIG_START_YEAR=2011 +CONFIG_SYSLOG_BUFFER=y +CONFIG_SYSTEM_NSH=y +CONFIG_UART0_SERIAL_CONSOLE=y diff --git a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/Make.defs b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/Make.defs index 152d8d79276df..db59a4ed4a4a9 100644 --- a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/Make.defs +++ b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/Make.defs @@ -26,6 +26,14 @@ ifeq ($(CONFIG_BOARDCTL_RESET),y) CSRCS += esp32s3_reset.c endif +ifeq ($(CONFIG_IOEXPANDER_CH422G),y) +CSRCS += esp32s3_board_ioexpander.c +endif + +ifeq ($(CONFIG_ESP32S3_BOARD_LCD),y) +CSRCS += esp32s3_lcd.c +endif + DEPPATH += --dep-path board VPATH += :board CFLAGS += ${INCDIR_PREFIX}$(TOPDIR)$(DELIM)arch$(DELIM)$(CONFIG_ARCH)$(DELIM)src$(DELIM)board$(DELIM)board diff --git a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3-touch-lcd7.h b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3-touch-lcd7.h index 7f63afbf247cd..3d02d955f7dbf 100644 --- a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3-touch-lcd7.h +++ b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3-touch-lcd7.h @@ -31,10 +31,35 @@ #include #include +#ifdef CONFIG_IOEXPANDER_CH422G +# include +#endif + /**************************************************************************** * Pre-processor Definitions ****************************************************************************/ +/* I2C bus ******************************************************************/ + +/* One I2C bus, GPIO8 (SDA) and GPIO9 (SCL), is shared by the CH422G I/O + * expander, the GT911 touch controller and the external I2C header. + */ + +#define BOARD_I2C_BUS 0 + +/* CH422G I/O expander ******************************************************/ + +/* The board labels the expander pins EXIO0-EXIO7, and they are the CH422G + * pins IO0-IO7 in order, so the ioexpander pin number is the EXIO number. + * EXIO0, EXIO6 and EXIO7 are not connected to anything on this board. + */ + +#define BOARD_EXIO_TP_RST CH422G_IO1 /* GT911 touch controller reset */ +#define BOARD_EXIO_DISP CH422G_IO2 /* LCD display and backlight enable */ +#define BOARD_EXIO_LCD_RST CH422G_IO3 /* LCD panel reset */ +#define BOARD_EXIO_SD_CS CH422G_IO4 /* TF card chip select */ +#define BOARD_EXIO_USB_SEL CH422G_IO5 /* USB / CAN transceiver select */ + /**************************************************************************** * Public Types ****************************************************************************/ @@ -62,5 +87,38 @@ int esp32s3_bringup(void); +#ifdef CONFIG_IOEXPANDER_CH422G +/**************************************************************************** + * Name: board_ioexpander_initialize + * + * Description: + * Bring up the CH422G I/O expander, if it has not been brought up + * already, and return it. Several parts of the board hang off the + * expander, so the first caller initialises it and the rest share it. + * + * Returned Value: + * The expander instance on success, NULL on failure. + * + ****************************************************************************/ + +FAR struct ioexpander_dev_s *board_ioexpander_initialize(void); +#endif + +#ifdef CONFIG_ESP32S3_BOARD_LCD +/**************************************************************************** + * Name: board_lcd_initialize + * + * Description: + * Bring the RGB panel out of reset, switch the backlight on and register + * the framebuffer character driver. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int board_lcd_initialize(void); +#endif + #endif /* __ASSEMBLY__ */ #endif /* __BOARDS_XTENSA_ESP32S3_ESP32S3_TOUCH_LCD7_SRC_ESP32S3_TOUCH_LCD7_H */ diff --git a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_board_ioexpander.c b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_board_ioexpander.c new file mode 100644 index 0000000000000..2d056bb1c3b8d --- /dev/null +++ b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_board_ioexpander.c @@ -0,0 +1,98 @@ +/**************************************************************************** + * boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_board_ioexpander.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include + +#include +#include +#include + +#include "esp32s3_i2c.h" +#include "esp32s3-touch-lcd7.h" + +#ifdef CONFIG_IOEXPANDER_CH422G + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static struct ch422g_config_s g_ch422g_config = +{ + .frequency = 400000, +}; + +/* The expander is shared, so it is brought up once and handed out. */ + +static FAR struct ioexpander_dev_s *g_ioe; + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: board_ioexpander_initialize + * + * Description: + * Bring up the CH422G I/O expander, if it has not been brought up + * already, and return it. Several parts of the board hang off the + * expander, so the first caller initialises it and the rest share it. + * + * Returned Value: + * The expander instance on success, NULL on failure. + * + ****************************************************************************/ + +FAR struct ioexpander_dev_s *board_ioexpander_initialize(void) +{ + FAR struct i2c_master_s *i2c; + + if (g_ioe != NULL) + { + return g_ioe; + } + + i2c = esp32s3_i2cbus_initialize(BOARD_I2C_BUS); + if (i2c == NULL) + { + syslog(LOG_ERR, "ERROR: Failed to initialize I2C%d\n", BOARD_I2C_BUS); + return NULL; + } + + g_ioe = ch422g_initialize(i2c, &g_ch422g_config); + if (g_ioe == NULL) + { + syslog(LOG_ERR, "ERROR: Failed to initialize CH422G\n"); + esp32s3_i2cbus_uninitialize(i2c); + return NULL; + } + + return g_ioe; +} + +#endif /* CONFIG_IOEXPANDER_CH422G */ diff --git a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_bringup.c b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_bringup.c index 63caf1f6a7f72..b7da58a11f648 100644 --- a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_bringup.c +++ b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_bringup.c @@ -97,6 +97,16 @@ int esp32s3_bringup(void) } #endif +#ifdef CONFIG_ESP32S3_BOARD_LCD + /* Bring up the RGB panel and register the framebuffer */ + + ret = board_lcd_initialize(); + if (ret < 0) + { + syslog(LOG_ERR, "ERROR: board_lcd_initialize() failed: %d\n", ret); + } +#endif + /* If we got here then perhaps not all initialization was successful, but * at least enough succeeded to bring-up NSH with perhaps reduced * capabilities. diff --git a/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_lcd.c b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_lcd.c new file mode 100644 index 0000000000000..75d7f8599e099 --- /dev/null +++ b/boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_lcd.c @@ -0,0 +1,138 @@ +/**************************************************************************** + * boards/xtensa/esp32s3/esp32s3-touch-lcd7/src/esp32s3_lcd.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/* The 7" 800x480 panel is driven by the LCD_CAM peripheral over a 16-bit + * parallel RGB565 bus, which arch/xtensa/src/esp32s3/esp32s3_lcd.c owns. + * Everything that is specific to this board is here: the panel reset and + * the display enable are not wired to GPIOs but to the CH422G I/O expander, + * because the RGB bus has taken most of the usable pins. + */ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "esp32s3-touch-lcd7.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* The EK9716 wants its reset held for at least 10us and needs time to come + * up before it is clocked. These are comfortably longer than that. + */ + +#define LCD_RESET_HOLD_US (20 * 1000) +#define LCD_RESET_WAIT_US (120 * 1000) + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: board_lcd_initialize + * + * Description: + * Bring the RGB panel out of reset, switch the backlight on and register + * the framebuffer character driver. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int board_lcd_initialize(void) +{ + FAR struct ioexpander_dev_s *ioe; + int ret; + + ioe = board_ioexpander_initialize(); + if (ioe == NULL) + { + return -ENODEV; + } + + /* The panel reset and the display enable are outputs of the expander. + * Drive the panel into reset and keep the display off while it settles. + */ + + ret = IOEXP_SETDIRECTION(ioe, BOARD_EXIO_LCD_RST, + IOEXPANDER_DIRECTION_OUT); + if (ret < 0) + { + syslog(LOG_ERR, "ERROR: Failed to configure LCD reset: %d\n", ret); + return ret; + } + + ret = IOEXP_SETDIRECTION(ioe, BOARD_EXIO_DISP, IOEXPANDER_DIRECTION_OUT); + if (ret < 0) + { + syslog(LOG_ERR, "ERROR: Failed to configure display enable: %d\n", + ret); + return ret; + } + + IOEXP_WRITEPIN(ioe, BOARD_EXIO_DISP, false); + IOEXP_WRITEPIN(ioe, BOARD_EXIO_LCD_RST, false); + up_udelay(LCD_RESET_HOLD_US); + + ret = IOEXP_WRITEPIN(ioe, BOARD_EXIO_LCD_RST, true); + if (ret < 0) + { + syslog(LOG_ERR, "ERROR: Failed to release LCD reset: %d\n", ret); + return ret; + } + + up_udelay(LCD_RESET_WAIT_US); + + /* The framebuffer has to exist, and so be scanning out a defined buffer, + * before the display is enabled. Enabling it first shows whatever the + * panel happens to latch until the first frame arrives. + */ + + ret = fb_register(0, 0); + if (ret < 0) + { + syslog(LOG_ERR, "ERROR: fb_register() failed: %d\n", ret); + return ret; + } + + ret = IOEXP_WRITEPIN(ioe, BOARD_EXIO_DISP, true); + if (ret < 0) + { + syslog(LOG_ERR, "ERROR: Failed to enable the display: %d\n", ret); + return ret; + } + + return OK; +} From dcd6abca4c77cc76e65df33377e09f6e8b25d18e Mon Sep 17 00:00:00 2001 From: Alan Carvalho de Assis Date: Sun, 23 Aug 2026 12:53:19 -0300 Subject: [PATCH 4/4] doc/esp32s3: Update esp32s3-touch-lcd7 documentation This commit updates the board documentation. Signed-off-by: Alan C. Assis Assisted-by: Claude Code --- .../boards/esp32s3-touch-lcd7/index.rst | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/Documentation/platforms/xtensa/esp32s3/boards/esp32s3-touch-lcd7/index.rst b/Documentation/platforms/xtensa/esp32s3/boards/esp32s3-touch-lcd7/index.rst index 8d9231c79f771..b0f5068333273 100644 --- a/Documentation/platforms/xtensa/esp32s3/boards/esp32s3-touch-lcd7/index.rst +++ b/Documentation/platforms/xtensa/esp32s3/boards/esp32s3-touch-lcd7/index.rst @@ -78,6 +78,42 @@ The board has a 7-inch LCD: EXIO2 DISP Backlight enable pin ========= ======== ========================= +The panel is an EK9716 driven over a 16-bit parallel RGB565 bus by the +LCD_CAM peripheral. Its timing is: + + ==================== ======= + Parameter Value + ==================== ======= + Resolution 800x480 + Pixel clock 16 MHz + HSYNC pulse width 4 + HSYNC back porch 8 + HSYNC front porch 8 + VSYNC pulse width 4 + VSYNC back porch 8 + VSYNC front porch 8 + ==================== ======= + +The RGB bus takes most of the usable pins, so the panel reset and the +display enable are not wired to GPIOs. They hang off a CH422G I/O +expander, reached over the I2C bus shared with the touch controller: + + ========= ============ ================================== + Signal Connection Description + ========= ============ ================================== + SDA GPIO8 I2C data, expander and touch + SCL GPIO9 I2C clock, expander and touch + TP_RST CH422G EXIO1 GT911 touch controller reset + DISP CH422G EXIO2 Display and backlight enable + LCD_RST CH422G EXIO3 Panel reset + SD_CS CH422G EXIO4 TF card chip select + USB_SEL CH422G EXIO5 USB / CAN transceiver select + ========= ============ ================================== + +A 800x480 RGB565 framebuffer is 768000 bytes and does not fit in internal +RAM, so any configuration that drives the panel must also enable the +onboard 8MB octal PSRAM and leave it in the common heap. + Configurations ============== @@ -92,6 +128,50 @@ All of the configurations presented below can be tested by running the following Configuration Directories ------------------------- +lcd +--- + +Brings up the onboard 7 inch 800x480 panel as a framebuffer character +driver at ``/dev/fb0``, on top of the ``usbnsh`` console. The CH422G I/O +expander is used to take the panel out of reset and to switch the display +on once the framebuffer exists, and the PSRAM the framebuffer is allocated +from is enabled. + +Note that the console is on UART0, GPIO43 and GPIO44, reached through the +USB-to-UART bridge on the UART USB-C connector. It is deliberately not on +the native USB peripheral, because that is the connector a USB host +configuration drives. + +``apps/examples/fb`` is included to prove the panel out. It reports the +geometry of the framebuffer it found and then draws a series of nested +rectangles over the whole display:: + + nsh> fb + VideoInfo: + fmt: 11 + xres: 800 + yres: 480 + nplanes: 1 + PlaneInfo (plane 0): + fbmem: 0x3c050040 + fblen: 1536000 + stride: 1600 + display: 0 + bpp: 16 + Mapped FB: 0x3c050040 + Use consecutive fbmem2 = 0x3c10b840, yoffset = 480 + 0: ( 0, 0) (800,480) + 1: ( 72, 43) (656,394) + 2: (144, 86) (512,308) + 3: (216,129) (368,222) + 4: (288,172) (224,136) + 5: (360,215) ( 80, 50) + Test finished + +The framebuffer is in PSRAM, which is why ``fbmem`` is in the 0x3c000000 +range, and ``fblen`` is 1536000 rather than 768000 because the driver is +double buffered by default. + usbnsh ------