[PATCH 2/2] serial: core: Drive the transceiver interface select GPIO
From: Tapio Reijonen
Date: Wed Oct 21 2026 - 01:21:19 EST
Multiprotocol transceivers such as the LTC2873 provide both an RS232
and an RS485 interface on the same bus pins and choose between them
with an interface select input. There is no generic way to drive that
input today, so a port wired to such a transceiver is stuck in
whichever interface the pin is strapped to.
Add the optional rs485-select-gpios property to the GPIOs acquired by
uart_get_rs485_mode() and drive it from the RS485 configuration state:
the GPIO is active while RS485 is enabled, which selects the RS485
interface, and inactive otherwise, which selects the alternative
interface, typically RS232. RS422 is a mode of SER_RS485_ENABLED and
therefore selects the RS485 interface as well.
The GPIO is requested with an initial state derived from
linux,rs485-enabled-at-boot-time, so the transceiver presents the right
interface from probe on. The state is applied in uart_rs485_config() for
the boot-time and resume paths and in uart_set_rs485_config() for the
ioctl path. Unlike the termination and RX-during-TX GPIOs, which only
matter while RS485 is enabled, this GPIO must also be driven when RS485
is disabled to return the transceiver to its alternative interface. It
is deasserted when the driver rejects the boot-time configuration and
restored to the previous setting when the driver rejects an ioctl, as is
done for the other GPIOs.
In RS485 mode RTS is the driver enable of the transceiver. The GPIO is
therefore driven after the driver callback when RS485 is enabled, so
RTS already sits at its RS485 idle level when the transceiver switches
over, and before the driver callback when RS485 is disabled, so the
transceiver has left RS485 mode before RTS reverts to modem control.
Drivers calling uart_get_rs485_mode() need no change to support such
transceivers.
Assisted-by: LLM
Signed-off-by: Tapio Reijonen <tapio.reijonen@xxxxxxxxxxx>
---
drivers/tty/serial/serial_core.c | 29 ++++++++++++++++++++++++++++-
include/linux/serial_core.h | 1 +
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/drivers/tty/serial/serial_core.c b/drivers/tty/serial/serial_core.c
index c0deec0e6af41db10de15ccca4169c603d9cebf6..57b022d0cf01b00d0d383d833d13afcfa6084760 100644
--- a/drivers/tty/serial/serial_core.c
+++ b/drivers/tty/serial/serial_core.c
@@ -1435,13 +1435,22 @@ static void uart_set_rs485_rx_during_tx(struct uart_port *port,
!!(rs485->flags & SER_RS485_RX_DURING_TX));
}
+static void uart_set_rs485_select_gpio(struct uart_port *port,
+ const struct serial_rs485 *rs485)
+{
+ gpiod_set_value_cansleep(port->rs485_select_gpio,
+ !!(rs485->flags & SER_RS485_ENABLED));
+}
+
static int uart_rs485_config(struct uart_port *port)
{
struct serial_rs485 *rs485 = &port->rs485;
int ret;
- if (!(rs485->flags & SER_RS485_ENABLED))
+ if (!(rs485->flags & SER_RS485_ENABLED)) {
+ uart_set_rs485_select_gpio(port, rs485);
return 0;
+ }
uart_sanitize_serial_rs485(port, rs485);
uart_set_rs485_termination(port, rs485);
@@ -1456,6 +1465,8 @@ static int uart_rs485_config(struct uart_port *port)
gpiod_set_value_cansleep(port->rs485_rx_during_tx_gpio, 0);
}
+ uart_set_rs485_select_gpio(port, rs485);
+
return ret;
}
@@ -1492,6 +1503,10 @@ static int uart_set_rs485_config(struct tty_struct *tty, struct uart_port *port,
uart_set_rs485_termination(port, &rs485);
uart_set_rs485_rx_during_tx(port, &rs485);
+ /* Leave RS485 before RTS reverts to modem control below */
+ if (!(rs485.flags & SER_RS485_ENABLED))
+ uart_set_rs485_select_gpio(port, &rs485);
+
scoped_guard(uart_port_lock_irqsave, port) {
ret = port->rs485_config(port, &tty->termios, &rs485);
if (!ret) {
@@ -1508,9 +1523,14 @@ static int uart_set_rs485_config(struct tty_struct *tty, struct uart_port *port,
!!(port->rs485.flags & SER_RS485_TERMINATE_BUS));
gpiod_set_value_cansleep(port->rs485_rx_during_tx_gpio,
!!(port->rs485.flags & SER_RS485_RX_DURING_TX));
+ uart_set_rs485_select_gpio(port, &port->rs485);
return ret;
}
+ /* Enter RS485 only once the driver has parked RTS */
+ if (rs485.flags & SER_RS485_ENABLED)
+ uart_set_rs485_select_gpio(port, &rs485);
+
if (copy_to_user(rs485_user, &port->rs485, sizeof(port->rs485)))
return -EFAULT;
@@ -3643,6 +3663,13 @@ int uart_get_rs485_mode(struct uart_port *port)
if (port->rs485_rx_during_tx_gpio)
port->rs485_supported.flags |= SER_RS485_RX_DURING_TX;
+ dflags = (rs485conf->flags & SER_RS485_ENABLED) ?
+ GPIOD_OUT_HIGH : GPIOD_OUT_LOW;
+ desc = devm_gpiod_get_optional(dev, "rs485-select", dflags);
+ if (IS_ERR(desc))
+ return dev_err_probe(dev, PTR_ERR(desc), "Cannot get rs485-select-gpios\n");
+ port->rs485_select_gpio = desc;
+
return 0;
}
EXPORT_SYMBOL_GPL(uart_get_rs485_mode);
diff --git a/include/linux/serial_core.h b/include/linux/serial_core.h
index 4eeb8253752aa08b27829e80a259c7db65728e8e..fe90a8aada98cacf5a80f20ec2a91e249c92d0ed 100644
--- a/include/linux/serial_core.h
+++ b/include/linux/serial_core.h
@@ -600,6 +600,7 @@ struct uart_port {
struct serial_rs485 rs485_supported; /* Supported mask for serial_rs485 */
struct gpio_desc *rs485_term_gpio; /* enable RS485 bus termination */
struct gpio_desc *rs485_rx_during_tx_gpio; /* Output GPIO that sets the state of RS485 RX during TX */
+ struct gpio_desc *rs485_select_gpio; /* select the RS485 interface */
struct serial_iso7816 iso7816;
void *private_data; /* generic platform data pointer */
};
--
2.47.3