[PATCH 1/1] serial: 8250_bcm2835aux: Add support for RTS/CTS auto flow control
Jan Čermák <[email protected]> Thu, 6 Aug 2026 17:42:04 +0200
| Newsgroups | org.kernel.vger.linux-serial,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <7e5ed3fb37ee4e113706c258d792121b868861a9.1786030659.git.sairon@sairon.cz> |
The bcm2835aux mini UART can automatically de-assert RTS based on the RX FIFO fill level and gate the transmitter on CTS, but the driver never made use of it. Wire it up: - Enable AUTORTS/AUTOCTS (with the RTS4 threshold and inverted, i.e. conventional active-low, polarity) whenever CRTSCTS is set on ports declaring the "uart-has-rtscts" property, advertised via UPF_HARD_FLOW. - Gate the AUTORTS function on TIOCM_RTS in set_mctrl() so that clearing RTS (port close, TIOCMBIC, B0) actually de-asserts the pin while auto flow control is active. - Implement throttle()/unthrottle() by disabling the RX interrupts and letting the FIFO fill up, making the hardware de-assert RTS - the same approach as in 8250_omap. Re-sync this state on termios and rs485 changes, as those paths may re-enable the RX interrupts (or leave them disabled) behind the throttle's back. - Never enable auto flow control together with rs485, which uses RTS as the transceiver direction control. As the software CTS fallback of the serial core cannot work without a modem status interrupt, strip CRTSCTS whenever it cannot be handled in hardware. - Restore RXEN when rs485 is disabled, as the emulation may be torn down without the rs485_stop_tx() callback ever being called, which would leave the receiver disabled. Assisted-by: Claude:claude-fable-5 Signed-off-by: Jan Čermák <[email protected]> --- drivers/tty/serial/8250/8250_bcm2835aux.c | 160 +++++++++++++++++++++- 1 file changed, 156 insertions(+), 4 deletions(-) diff --git a/drivers/tty/serial/8250/8250_bcm2835aux.c b/drivers/tty/serial/8250/8250_bcm2835aux.c index 0609582a62f7..cf9cf08ddb17 100644 --- a/drivers/tty/serial/8250/8250_bcm2835aux.c +++ b/drivers/tty/serial/8250/8250_bcm2835aux.c @@ -7,9 +7,9 @@ * Based on 8250_lpc18xx.c: * Copyright (C) 2015 Joachim Eastwood <[email protected]> * - * The bcm2835aux is capable of RTS auto flow-control, but this driver doesn't - * take advantage of it yet. When adding support, be sure not to enable it - * simultaneously to rs485. + * The bcm2835aux's RTS/CTS auto flow-control is enabled for ports declaring + * the "uart-has-rtscts" property, but never simultaneously to rs485, which + * repurposes RTS as the transceiver direction control. */ #include <linux/clk.h> @@ -19,6 +19,7 @@ #include <linux/of.h> #include <linux/platform_device.h> #include <linux/property.h> +#include <linux/tty.h> #include "8250.h" @@ -34,6 +35,18 @@ #define BCM2835_AUX_UART_CNTL_RTSINV 0x40 /* Invert auto RTS polarity */ #define BCM2835_AUX_UART_CNTL_CTSINV 0x80 /* Invert auto CTS polarity */ +/* + * Set auto RTS to de-assert with 4 FIFO slots left (RTS4). + * + * The RTSINV/CTSINV bits select the auto flow assert level, which out of + * reset is active-high. Set them to get the conventional active-low RTS/CTS. + */ +#define BCM2835_AUX_UART_CNTL_AUTORTS_MASK \ + (BCM2835_AUX_UART_CNTL_AUTORTS | BCM2835_AUX_UART_CNTL_RTS4 | \ + BCM2835_AUX_UART_CNTL_RTSINV) +#define BCM2835_AUX_UART_CNTL_AUTOCTS_MASK \ + (BCM2835_AUX_UART_CNTL_AUTOCTS | BCM2835_AUX_UART_CNTL_CTSINV) + /** * struct bcm2835aux_data - driver private data of BCM2835 auxiliary UART * @clk: clock producer of the port's uartclk @@ -46,6 +59,139 @@ struct bcm2835aux_data { u32 cntl; }; +static bool bcm2835aux_tty_throttled(struct uart_port *port) +{ + struct tty_struct *tty = port->state ? port->state->port.tty : NULL; + + return tty && tty_throttled(tty); +} + +/* Program the CNTL auto flow bits from port->status and mctrl */ +static void bcm2835aux_update_flow(struct uart_port *port, unsigned int mctrl) +{ + struct bcm2835aux_data *data = dev_get_drvdata(port->dev); + struct uart_8250_port *up = up_to_u8250p(port); + + data->cntl &= ~(BCM2835_AUX_UART_CNTL_AUTORTS_MASK | + BCM2835_AUX_UART_CNTL_AUTOCTS_MASK); + if ((port->status & UPSTAT_AUTORTS) && (mctrl & TIOCM_RTS)) + data->cntl |= BCM2835_AUX_UART_CNTL_AUTORTS_MASK; + if (port->status & UPSTAT_AUTOCTS) + data->cntl |= BCM2835_AUX_UART_CNTL_AUTOCTS_MASK; + + serial_out(up, BCM2835_AUX_UART_CNTL, data->cntl); +} + +static void bcm2835aux_enable_rx_irq(struct uart_port *port) +{ + struct uart_8250_port *up = up_to_u8250p(port); + + up->ier |= UART_IER_RLSI | UART_IER_RDI; + serial_out(up, UART_IER, up->ier); +} + +static void bcm2835aux_set_autoflow(struct uart_port *port, tcflag_t cflag, + bool rs485_enabled) +{ + bool was_enabled = port->status & UPSTAT_AUTORTS; + bool enable; + + /* + * rs485 uses RTS as the transceiver direction control, which is + * mutually exclusive with the auto flow-control of the pin. + */ + enable = (cflag & CRTSCTS) && (port->flags & UPF_HARD_FLOW) && + !rs485_enabled; + + if (enable) { + port->status |= UPSTAT_AUTORTS | UPSTAT_AUTOCTS; + /* Restore the throttle state lost in startup/resume/rs485 paths */ + if (bcm2835aux_tty_throttled(port)) { + /* let the FIFO fill up and de-assert RTS in hardware */ + port->ops->stop_rx(port); + /* reclaim an RTS lowered by software flow control */ + if ((cflag & CBAUD) != B0) + port->mctrl |= TIOCM_RTS; + } + } else { + port->status &= ~(UPSTAT_AUTORTS | UPSTAT_AUTOCTS); + /* unthrottle() will no longer be called after leaving auto-RTS mode */ + if (was_enabled) + bcm2835aux_enable_rx_irq(port); + } + + bcm2835aux_update_flow(port, port->mctrl); +} + +static void bcm2835aux_set_mctrl(struct uart_port *port, unsigned int mctrl) +{ + serial8250_do_set_mctrl(port, mctrl); + bcm2835aux_update_flow(port, mctrl); +} + +static void bcm2835aux_set_termios(struct uart_port *port, + struct ktermios *termios, + const struct ktermios *old) +{ + /* + * Strip CRTSCTS when the hardware auto flow-control cannot be + * used, as the serial core's software CTS fallback cannot work + * without a modem status interrupt. + */ + if (!(port->flags & UPF_HARD_FLOW) || + (port->rs485.flags & SER_RS485_ENABLED)) + termios->c_cflag &= ~CRTSCTS; + + serial8250_do_set_termios(port, termios, old); + + guard(uart_port_lock_irqsave)(port); + + bcm2835aux_set_autoflow(port, termios->c_cflag, + port->rs485.flags & SER_RS485_ENABLED); +} + +static void bcm2835aux_throttle(struct uart_port *port) +{ + guard(uart_port_lock_irqsave)(port); + + port->ops->stop_rx(port); +} + +static void bcm2835aux_unthrottle(struct uart_port *port) +{ + guard(uart_port_lock_irqsave)(port); + + bcm2835aux_enable_rx_irq(port); +} + +static int bcm2835aux_rs485_config(struct uart_port *port, + struct ktermios *termios, + struct serial_rs485 *rs485) +{ + tcflag_t cflag = termios ? termios->c_cflag : 0; + bool rs485_enabled; + int ret; + + ret = serial8250_em485_config(port, termios, rs485); + if (ret) + return ret; + + rs485_enabled = rs485->flags & SER_RS485_ENABLED; + if (!rs485_enabled) { + struct bcm2835aux_data *data = dev_get_drvdata(port->dev); + + data->cntl |= BCM2835_AUX_UART_CNTL_RXEN; + } + + /* + * termios is NULL when rs485 is set up from firmware properties at + * port registration, before the port has been opened. + */ + bcm2835aux_set_autoflow(port, cflag, rs485_enabled); + + return 0; +} + static void bcm2835aux_rs485_start_tx(struct uart_8250_port *up, bool toggle_ier) { if (!(up->port.rs485.flags & SER_RS485_RX_DURING_TX)) { @@ -99,7 +245,11 @@ static int bcm2835aux_serial_probe(struct platform_device *pdev) up.port.dev = &pdev->dev; up.port.type = PORT_16550; up.port.flags = UPF_FIXED_PORT | UPF_FIXED_TYPE | UPF_SKIP_TEST | UPF_IOREMAP; - up.port.rs485_config = serial8250_em485_config; + up.port.set_termios = bcm2835aux_set_termios; + up.port.set_mctrl = bcm2835aux_set_mctrl; + up.port.throttle = bcm2835aux_throttle; + up.port.unthrottle = bcm2835aux_unthrottle; + up.port.rs485_config = bcm2835aux_rs485_config; up.port.rs485_supported = serial8250_em485_supported; up.rs485_start_tx = bcm2835aux_rs485_start_tx; up.rs485_stop_tx = bcm2835aux_rs485_stop_tx; @@ -134,6 +284,8 @@ static int bcm2835aux_serial_probe(struct platform_device *pdev) ret = uart_read_port_properties(&up.port); if (ret) goto rm_swnode; + if (device_property_read_bool(&pdev->dev, "uart-has-rtscts")) + up.port.flags |= UPF_HARD_FLOW; up.port.regshift = 2; up.port.fifosize = 8; -- 2.55.0