Message ID | 20240523162207.2.I0f81a5baa37d368f291c96ee4830abca337e3c87@changeid |
---|---|
State | New |
Headers | show |
Series | serial: Fix problems when serial transfer is happening at suspend time | expand |
Hi, On Fri, May 24, 2024 at 5:17 PM Doug Anderson <dianders@chromium.org> wrote: > > 1. It appears that M_GP_LENGTH can still advance after the FIFO > becomes 0, which is extra proof that the transfer is still happening > even though the FIFO says it's done. Presumably we could keep track of > how many bytes we have enqueued into the FIFO for this command and > then compare. As I was trying to do this, though, I noticed another > option... > > 2. It appears that instead of "cancelling" the current command we can > just issue a new 0-byte transfer and wait for the 0-byte transfer to > be "done". This causes geni to give us back a "M_CMD_OVERRUN" > interrupt, but that's fine and we can ignore it. That interrupt just > says "hey, you gave me a command before the previous one was done" but > it does seem to properly accept the new command and it doesn't drop > any bytes. > > ...it turns out that we (apparently) already have been using option #2 > to interrupt a transfer without dropping bytes. When the UART is > shared between an agetty and the kernel console this happens all the > time. In qcom_geni_serial_console_write() we'll issue a new command > before finishing a current one and then re-issue the current command > with any remaining bytes. So not only should this be safe but it's > already tested to work. > > I'll need to spend a little more time on this to really confirm it > works as I expect and I'll send up a v2 using approach #2. Just to connect the dots more, I did more testing and option #2 totally didn't work. The console/kdb stuff was working mostly through ugly fragile hacks. Polling GP_LENGTH seemed like the only option, though when I dug more I found that this wasn't the right place to do it anyway... > Also note that while spending more time on this I found _yet another_ > bug, this one more serious. My original testing was done on kernel 6.6 > (with stable backports) and I just did confirmation on mainline. > That's why I didn't see this new bug originally. ...but this time I > spent more time testing on mainline. It turns out that the recent > patches for kfifo on mainline have badly broken geni serial. > Specifically, if you just do "cat /var/log/messages" and then "Ctrl-C" > the machine will hard lockup! Yikes! This is yet another side effect > of the geni "packet"-based protocol biting us (so related to the > problems in ${SUBJECT}, but not identical). Whenever we setup a TX > transfer we set it up for the number of bytes in the queue at the > time. If that number goes down then we're in trouble. Specifically, it > can be noted that: > * When we start transmitting we look at the current queue size, setup > a transfer, and store "tx_remaining". > * Whenever there's space in the FIFO we add bytes and remove them from > the queue and "tx_remaining". > * We don't ever expect bytes to disappear from the queue. I think in > the old code if this happened we're just transfer some bogus bytes. > Now we'll loop in qcom_geni_serial_send_chunk_fifo() because > uart_fifo_out() will keep returning 0. > > I'll try to take a gander at that, too... I spent _far_ too long poking at this and I've finally come up with a v2 that _I think_ fixes everything. For reference: https://lore.kernel.org/r/20240530224603.730042-1-dianders@chromium.org
diff --git a/drivers/tty/serial/qcom_geni_serial.c b/drivers/tty/serial/qcom_geni_serial.c index 2bd25afe0d92..9110ac4bdbbf 100644 --- a/drivers/tty/serial/qcom_geni_serial.c +++ b/drivers/tty/serial/qcom_geni_serial.c @@ -265,8 +265,8 @@ static bool qcom_geni_serial_secondary_active(struct uart_port *uport) return readl(uport->membase + SE_GENI_STATUS) & S_GENI_CMD_ACTIVE; } -static bool qcom_geni_serial_poll_bit(struct uart_port *uport, - int offset, int field, bool set) +static bool qcom_geni_serial_poll_bitfield(struct uart_port *uport, + int offset, int field, u32 val) { u32 reg; struct qcom_geni_serial_port *port; @@ -295,7 +295,7 @@ static bool qcom_geni_serial_poll_bit(struct uart_port *uport, timeout_us = DIV_ROUND_UP(timeout_us, 10) * 10; while (timeout_us) { reg = readl(uport->membase + offset); - if ((bool)(reg & field) == set) + if ((reg & field) == val) return true; udelay(10); timeout_us -= 10; @@ -303,6 +303,12 @@ static bool qcom_geni_serial_poll_bit(struct uart_port *uport, return false; } +static bool qcom_geni_serial_poll_bit(struct uart_port *uport, + int offset, int field, bool set) +{ + return qcom_geni_serial_poll_bitfield(uport, offset, field, set ? field : 0); +} + static void qcom_geni_serial_setup_tx(struct uart_port *uport, u32 xmit_size) { u32 m_cmd; @@ -675,6 +681,31 @@ static void qcom_geni_serial_stop_tx_fifo(struct uart_port *uport) if (!qcom_geni_serial_main_active(uport)) return; + /* + * Wait until the FIFO has been drained. We've already taken bytes out + * of the higher level queue in qcom_geni_serial_send_chunk_fifo() so + * if we don't drain the FIFO but send the "cancel" below they seem to + * get lost. + */ + qcom_geni_serial_poll_bitfield(uport, SE_GENI_TX_FIFO_STATUS, TX_FIFO_WC, 0); + + /* + * If we send the cancel immediately after the FIFO reports that it's + * empty then bytes still seem to get lost. From trial and error, it + * appears that a small delay here keeps bytes from being lost and + * there is (apparently) no bit that we can poll instead of this. + * Specifically it can be noted that the sequencer is still "active" + * if it's waiting for us to send it more bytes from the current + * transfer. + */ + mdelay(1); + + /* + * Cancel the current command. After this the main sequencer will + * stop reporting that it's active and we'll have to start a new + * transfer command. If the cancel doesn't take, we'll also send an + * abort. + */ geni_se_cancel_m_cmd(&port->se); if (!qcom_geni_serial_poll_bit(uport, SE_GENI_M_IRQ_STATUS, M_CMD_CANCEL_EN, true)) { @@ -684,6 +715,14 @@ static void qcom_geni_serial_stop_tx_fifo(struct uart_port *uport) writel(M_CMD_ABORT_EN, uport->membase + SE_GENI_M_IRQ_CLEAR); } writel(M_CMD_CANCEL_EN, uport->membase + SE_GENI_M_IRQ_CLEAR); + + /* + * We've cancelled the current command. "tx_remaining" stores how + * many bytes are left to finish in the current command so we know + * when to start a new command. Since the command was cancelled we + * need to zero "tx_remaining". + */ + port->tx_remaining = 0; } static void qcom_geni_serial_handle_rx_fifo(struct uart_port *uport, bool drop)
If qcom_geni_serial_stop_tx_fifo() was called while a transfer was happening then the UART would be effectively stuck and wouldn't transmit again. Specifically, I could reproduce these problem by logging in via an agetty on the debug serial port (which was _not_ used for kernel console) and running: cat /var/log/messages ...and then (via an SSH session) forcing a few suspend/resume cycles. Digging into this showed a number of problems. One problem was that we cancelled the current "tx" command but we forgot to zero out "tx_remaining". Another problem was that we didn't drain the buffer before cancelling the "tx" command and thus those bytes were lost. Another problem was that failing to drain the buffer meant that the hardware still reported something in the FIFO and that caused qcom_geni_serial_start_tx_fifo() to be a no-op, since it doesn't do anything if the TX FIFO is not empty. Though I haven't gone back and validated on ancient code, it appears from code inspection that many of these problems have existed since the start of the driver. In the very least, I could reproduce the problems on vanilla v5.15. The problems don't seem to reproduce when using the serial port for kernel console output and also don't seem to reproduce if nothing is being printed to the console at suspend time, so this is presumably why they were not noticed until now. Fixes: c4f528795d1a ("tty: serial: msm_geni_serial: Add serial driver support for GENI based QUP") Signed-off-by: Douglas Anderson <dianders@chromium.org> --- drivers/tty/serial/qcom_geni_serial.c | 45 +++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-)