[PATCH 4/7] serial: core: Power the line transceiver with the port

From: Tapio Reijonen

Date: Wed Nov 11 2026 - 07:48:09 EST


A line transceiver that can be switched off stays powered for as long
as the system is up, because nothing ties it to the port: serial core
powers the port up when it is opened and down when it is closed, but
the transceiver behind it is unknown to the kernel.

Implement the transceiver properties of serial.yaml. Serial core
requests the enable GPIO when a port is added, so a driver needs no
change to support it, and from then on uart_change_pm() powers the
transceiver along with the port: on before the driver powers the port
up, waiting transceiver-startup-delay-us for the part to come up, off
after the driver has powered the port down, so the transceiver is
never switched off under a running port. That covers open and close,
suspend and resume, the probe-time power-down and the brief power-up
for /proc/tty/driver. Whether the pins stop driving while the
transceiver is off is up to the driver. A console port stays powered, as
it does today; its transceiver is switched off when the port is removed,
after the driver has powered the port down. A never-opened console gets
no shutdown() at removal, so a driver that three-states its pins only
there keeps driving until the power-off; a console on a removable UART
is left as that corner. The GPIO is requested driven inactive, so
console output between the request and the power-up in
uart_configure_port() is lost; earlycon output before probe depends on
how the bootloader left the line.

On a port with the transceiver GPIO, the rs485-select-gpios line is
set from the port's RS485 configuration at registration, before the
power-up, so the transceiver comes on in the configured mode instead
of the one the line was left in; RTS reaches its RS485 idle level in
uart_rs485_config() after that. A port without the GPIO keeps the
order of the rs485-select change.

The lookup goes through the port's device, which is the chip for a
multi-port device. A driver whose ports have a firmware node of their
own requests the GPIO from that node with uart_get_transceiver()
before adding the port; serial core looks at the port's device only
for a port that has no GPIO yet, which serves single-port devices. The
GPIO belongs to one port, so on a multi-port device it has to be
described per port.

The property is optional. A port without it gets no GPIO, and serial
core behaves as before.

Signed-off-by: Tapio Reijonen <tapio.reijonen@xxxxxxxxxxx>
---
drivers/tty/serial/serial_core.c | 99 +++++++++++++++++++++++++++++++++++++++-
include/linux/serial_core.h | 3 ++
2 files changed, 100 insertions(+), 2 deletions(-)

diff --git a/drivers/tty/serial/serial_core.c b/drivers/tty/serial/serial_core.c
index 6c0a6d9dcb851b6db0acd91143e651bcb33ce5ae..d36bffaa100b89b519e84c20df2f61f507e6e89f 100644
--- a/drivers/tty/serial/serial_core.c
+++ b/drivers/tty/serial/serial_core.c
@@ -2348,12 +2348,32 @@ uart_set_options(struct uart_port *port, struct console *co,
EXPORT_SYMBOL_GPL(uart_set_options);
#endif /* CONFIG_SERIAL_CORE_CONSOLE */

+/*
+ * Power the line transceiver along with the port: on before the driver
+ * powers the port up, off after it has powered the port down.
+ */
+static void uart_transceiver_pm(struct uart_port *port,
+ enum uart_pm_state pm_state)
+{
+ bool powered = (pm_state == UART_PM_STATE_ON);
+
+ if (!port->transceiver_enable_gpio)
+ return;
+
+ gpiod_set_value_cansleep(port->transceiver_enable_gpio, powered);
+ if (powered && port->transceiver_startup_delay_us)
+ fsleep(port->transceiver_startup_delay_us);
+}
+
/**
* uart_change_pm - set power state of the port
*
* @state: port descriptor
* @pm_state: new state
*
+ * The line transceiver follows: powered on before the driver sees the
+ * new state, off after it.
+ *
* Locking: port->mutex has to be held
*/
static void uart_change_pm(struct uart_state *state,
@@ -2362,8 +2382,12 @@ static void uart_change_pm(struct uart_state *state,
struct uart_port *port = uart_port_check(state);

if (state->pm_state != pm_state) {
+ if (port && pm_state == UART_PM_STATE_ON)
+ uart_transceiver_pm(port, pm_state);
if (port && port->ops->pm)
port->ops->pm(port, pm_state, state->pm_state);
+ if (port && pm_state != UART_PM_STATE_ON)
+ uart_transceiver_pm(port, pm_state);
state->pm_state = pm_state;
}
}
@@ -2638,6 +2662,10 @@ uart_configure_port(struct uart_driver *drv, struct uart_state *state,
if (uart_console(port))
console_lock();

+ /* Select the interface before the transceiver is powered */
+ if (port->transceiver_enable_gpio)
+ uart_set_rs485_select_gpio(port, &port->rs485);
+
/* Power up port for set_mctrl() */
uart_change_pm(state, UART_PM_STATE_ON);

@@ -3282,6 +3310,12 @@ static void serial_core_remove_one_port(struct uart_driver *drv,

if (tty_port_initialized(port))
uart_shutdown(NULL, state);
+
+ /* Only a console port is still powered here */
+ if (uport->transceiver_enable_gpio &&
+ state->pm_state == UART_PM_STATE_ON)
+ uart_change_pm(state, UART_PM_STATE_OFF);
+ uport->transceiver_enable_gpio = NULL;
}

/*
@@ -3364,10 +3398,18 @@ static int serial_core_port_device_add(struct serial_ctrl_device *ctrl_dev,
int serial_core_register_port(struct uart_driver *drv, struct uart_port *port)
{
struct serial_ctrl_device *ctrl_dev, *new_ctrl_dev = NULL;
+ bool core_transceiver = false;
int ret;

guard(mutex)(&port_mutex);

+ if (port->dev && !port->transceiver_enable_gpio) {
+ ret = uart_get_transceiver(port);
+ if (ret)
+ return ret;
+ core_transceiver = true;
+ }
+
/*
* Prevent serial_port_runtime_resume() from trying to use the port
* until serial_core_add_one_port() has completed
@@ -3378,8 +3420,10 @@ int serial_core_register_port(struct uart_driver *drv, struct uart_port *port)
ctrl_dev = serial_core_ctrl_find(drv, port->dev, port->ctrl_id);
if (!ctrl_dev) {
new_ctrl_dev = serial_core_ctrl_device_add(port);
- if (IS_ERR(new_ctrl_dev))
- return PTR_ERR(new_ctrl_dev);
+ if (IS_ERR(new_ctrl_dev)) {
+ ret = PTR_ERR(new_ctrl_dev);
+ goto err_put_transceiver;
+ }
ctrl_dev = new_ctrl_dev;
}

@@ -3409,6 +3453,14 @@ int serial_core_register_port(struct uart_driver *drv, struct uart_port *port)
err_unregister_ctrl_dev:
serial_base_ctrl_device_remove(new_ctrl_dev);

+err_put_transceiver:
+ /* The port may be registered again: forget the GPIO */
+ if (core_transceiver && port->transceiver_enable_gpio) {
+ devm_gpiod_put(port->dev, port->transceiver_enable_gpio);
+ port->transceiver_enable_gpio = NULL;
+ port->transceiver_startup_delay_us = 0;
+ }
+
return ret;
}

@@ -3710,6 +3762,49 @@ int uart_get_rs485_mode(struct uart_port *port)
}
EXPORT_SYMBOL_GPL(uart_get_rs485_mode);

+/**
+ * uart_get_transceiver() - request the line transceiver GPIO of a port
+ * @port: uart device's target port
+ *
+ * Reads the transceiver properties of serial.yaml from the firmware
+ * node of @port->dev. Serial core calls it for a port added without a
+ * GPIO; a driver whose ports have firmware nodes of their own calls it
+ * per port, with the device's node retargeted, before adding the port.
+ */
+int uart_get_transceiver(struct uart_port *port)
+{
+ struct device *dev = port->dev;
+ struct gpio_desc *desc;
+ u32 delay_us = 0;
+ int ret;
+
+ port->transceiver_enable_gpio = NULL;
+ port->transceiver_startup_delay_us = 0;
+
+ desc = devm_gpiod_get_optional(dev, "transceiver-enable", GPIOD_OUT_LOW);
+ if (IS_ERR(desc))
+ return dev_err_probe(dev, PTR_ERR(desc),
+ "Cannot get transceiver-enable-gpios\n");
+ if (!desc)
+ return 0;
+
+ if (device_property_present(dev, "transceiver-startup-delay-us")) {
+ ret = device_property_read_u32(dev, "transceiver-startup-delay-us",
+ &delay_us);
+ if (ret) {
+ devm_gpiod_put(dev, desc);
+ return dev_err_probe(dev, ret,
+ "Cannot read transceiver-startup-delay-us\n");
+ }
+ }
+
+ port->transceiver_enable_gpio = desc;
+ port->transceiver_startup_delay_us = delay_us;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(uart_get_transceiver);
+
/* Compile-time assertions for serial_rs485 layout */
static_assert(offsetof(struct serial_rs485, padding) ==
(offsetof(struct serial_rs485, delay_rts_after_send) + sizeof(__u32)));
diff --git a/include/linux/serial_core.h b/include/linux/serial_core.h
index fe90a8aada98cacf5a80f20ec2a91e249c92d0ed..419d7b46302e5627f7bfbf0b3e658fde37e06a96 100644
--- a/include/linux/serial_core.h
+++ b/include/linux/serial_core.h
@@ -580,6 +580,7 @@ struct uart_port {
unsigned int custom_divisor;
unsigned int line; /* port index */
unsigned int minor;
+ u32 transceiver_startup_delay_us;
resource_size_t mapbase; /* for ioremap */
resource_size_t mapsize;
struct device *dev; /* serial port physical parent device */
@@ -601,6 +602,7 @@ struct uart_port {
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 gpio_desc *transceiver_enable_gpio; /* turns the line transceiver on */
struct serial_iso7816 iso7816;
void *private_data; /* generic platform data pointer */
};
@@ -1341,6 +1343,7 @@ static inline int uart_handle_break(struct uart_port *port)
!((cflag) & CLOCAL))

int uart_get_rs485_mode(struct uart_port *port);
+int uart_get_transceiver(struct uart_port *port);

void uart_get_ioinfos(struct uart_port *port, char *buf, size_t size);
bool uart_iotype_mmio(enum uart_iotype iotype);

--
2.47.3