[PATCH] serial: 8250_dw: Allow TX FIFO to drain before writing to UART_LCR An issue has been observed on the Broadcom BCM56160 serial port which appears closely related to a similar issue on the Marvell Armada 38x serial port.

From: Richard Laing
Date: Sun Oct 15 2023 - 21:33:09 EST


Writes to UART_LCR can result in characters that are currently held in the
TX FIFO being lost rather than sent, even if the userspace process has
attempted to flush them.

This is most visible when using the "resize" command (tested on Busybox),
where we have observed the escape code for restoring cursor position
becoming mangled.

Since this appears to be a more common problem add a new driver option
to flush the TX FIFO before writing to the UART_LCR.

Signed-off-by: Richard Laing <richard.laing@xxxxxxxxxxxxxxxxxxx>
---
drivers/tty/serial/8250/8250_dw.c | 18 ++++++++++++++++++
drivers/tty/serial/8250/8250_dwlib.h | 1 +
2 files changed, 19 insertions(+)

diff --git a/drivers/tty/serial/8250/8250_dw.c b/drivers/tty/serial/8250/8250_dw.c
index f4cafca1a7da..17ee824294c7 100644
--- a/drivers/tty/serial/8250/8250_dw.c
+++ b/drivers/tty/serial/8250/8250_dw.c
@@ -161,6 +161,10 @@ static void dw8250_serial_out(struct uart_port *p, int offset, int value)
{
struct dw8250_data *d = to_dw8250_data(p->private_data);

+ /* Allow the TX to drain before we reconfigure */
+ if (offset == UART_LCR && d->drain_before_lcr_change)
+ dw8250_tx_wait_empty(p);
+
writeb(value, p->membase + (offset << p->regshift));

if (offset == UART_LCR && !d->uart_16550_compatible)
@@ -197,6 +201,10 @@ static void dw8250_serial_outq(struct uart_port *p, int offset, int value)
{
struct dw8250_data *d = to_dw8250_data(p->private_data);

+ /* Allow the TX to drain before we reconfigure */
+ if (offset == UART_LCR && d->drain_before_lcr_change)
+ dw8250_tx_wait_empty(p);
+
value &= 0xff;
__raw_writeq(value, p->membase + (offset << p->regshift));
/* Read back to ensure register write ordering. */
@@ -211,6 +219,10 @@ static void dw8250_serial_out32(struct uart_port *p, int offset, int value)
{
struct dw8250_data *d = to_dw8250_data(p->private_data);

+ /* Allow the TX to drain before we reconfigure */
+ if (offset == UART_LCR && d->drain_before_lcr_change)
+ dw8250_tx_wait_empty(p);
+
writel(value, p->membase + (offset << p->regshift));

if (offset == UART_LCR && !d->uart_16550_compatible)
@@ -228,6 +240,10 @@ static void dw8250_serial_out32be(struct uart_port *p, int offset, int value)
{
struct dw8250_data *d = to_dw8250_data(p->private_data);

+ /* Allow the TX to drain before we reconfigure */
+ if (offset == UART_LCR && d->drain_before_lcr_change)
+ dw8250_tx_wait_empty(p);
+
iowrite32be(value, p->membase + (offset << p->regshift));

if (offset == UART_LCR && !d->uart_16550_compatible)
@@ -597,6 +613,8 @@ static int dw8250_probe(struct platform_device *pdev)
/* Always ask for fixed clock rate from a property. */
device_property_read_u32(dev, "clock-frequency", &p->uartclk);

+ data->drain_before_lcr_change = device_property_read_bool(dev, "drain-before-lcr-change");
+
/* If there is separate baudclk, get the rate from it. */
data->clk = devm_clk_get_optional(dev, "baudclk");
if (data->clk == NULL)
diff --git a/drivers/tty/serial/8250/8250_dwlib.h b/drivers/tty/serial/8250/8250_dwlib.h
index f13e91f2cace..f7d88fa8f058 100644
--- a/drivers/tty/serial/8250/8250_dwlib.h
+++ b/drivers/tty/serial/8250/8250_dwlib.h
@@ -45,6 +45,7 @@ struct dw8250_data {

unsigned int skip_autocfg:1;
unsigned int uart_16550_compatible:1;
+ unsigned int drain_before_lcr_change:1;
};

void dw8250_do_set_termios(struct uart_port *p, struct ktermios *termios, const struct ktermios *old);
--
2.42.0