From 25b98942ce90e77862d101ee607d227d1c93ff9b Mon Sep 17 00:00:00 2001
From: Luke Wren <wren6991@gmail.com>
Date: Wed, 8 Sep 2021 10:39:21 +0100
Subject: [PATCH] Disable UART when writing to LCR

---
 .../hardware_uart/include/hardware/uart.h     |  72 +++++++-----
 src/rp2_common/hardware_uart/uart.c           | 109 +++++++++++++++++-
 2 files changed, 147 insertions(+), 34 deletions(-)

diff --git a/src/rp2_common/hardware_uart/include/hardware/uart.h b/src/rp2_common/hardware_uart/include/hardware/uart.h
index 1599098..6d3a3fe 100644
--- a/src/rp2_common/hardware_uart/include/hardware/uart.h
+++ b/src/rp2_common/hardware_uart/include/hardware/uart.h
@@ -130,6 +130,13 @@ typedef enum {
  * Put the UART into a known state, and enable it. Must be called before other
  * functions.
  *
+ * This function always enables the FIFOs, and configures the UART for the
+ * following default line format:
+ * 
+ * - 8 data bits
+ * - No parity bit
+ * - One stop bit
+ * 
  * \note There is no guarantee that the baudrate requested will be possible, the nearest will be chosen,
  * and this function will return the configured baud rate.
  *
@@ -153,6 +160,17 @@ void uart_deinit(uart_inst_t *uart);
  *  \ingroup hardware_uart
  *
  * Set baud rate as close as possible to requested, and return actual rate selected.
+ * 
+ * The UART is paused for around two character periods whilst the settings are
+ * changed. Data received during this time may be dropped by the UART.
+ * 
+ * Any characters still in the transmit buffer will be sent using the new
+ * updated baud rate. uart_tx_wait_blocking() can be called before this
+ * function to ensure all characters at the old baud rate have been sent
+ * before the rate is changed.
+ * 
+ * This function should not be called from an interrupt context, and the UART
+ * interrupt should be disabled before calling this function.
  *
  * \param uart UART instance. \ref uart0 or \ref uart1
  * \param baudrate Baudrate in Hz
@@ -176,27 +194,25 @@ static inline void uart_set_hw_flow(uart_inst_t *uart, bool cts, bool rts) {
 /*! \brief Set UART data format
  *  \ingroup hardware_uart
  *
- * Configure the data format (bits etc() for the UART
- *
+ * Configure the data format (bits etc) for the UART.
+ * 
+ * The UART is paused for around two character periods whilst the settings are
+ * changed. Data received during this time may be dropped by the UART.
+ * 
+ * Any characters still in the transmit buffer will be sent using the new
+ * updated data format. uart_tx_wait_blocking() can be called before this
+ * function to ensure all characters needing the old format have been sent
+ * before the format is changed.
+ * 
+ * This function should not be called from an interrupt context, and the UART
+ * interrupt should be disabled before calling this function.
+ * 
  * \param uart UART instance. \ref uart0 or \ref uart1
  * \param data_bits Number of bits of data. 5..8
  * \param stop_bits Number of stop bits 1..2
  * \param parity Parity option.
  */
-static inline void uart_set_format(uart_inst_t *uart, uint data_bits, uint stop_bits, uart_parity_t parity) {
-    invalid_params_if(UART, data_bits < 5 || data_bits > 8);
-    invalid_params_if(UART, stop_bits != 1 && stop_bits != 2);
-    invalid_params_if(UART, parity != UART_PARITY_NONE && parity != UART_PARITY_EVEN && parity != UART_PARITY_ODD);
-    hw_write_masked(&uart_get_hw(uart)->lcr_h,
-                   ((data_bits - 5u) << UART_UARTLCR_H_WLEN_LSB) |
-                   ((stop_bits - 1u) << UART_UARTLCR_H_STP2_LSB) |
-                   (bool_to_bit(parity != UART_PARITY_NONE) << UART_UARTLCR_H_PEN_LSB) |
-                   (bool_to_bit(parity == UART_PARITY_EVEN) << UART_UARTLCR_H_EPS_LSB),
-                   UART_UARTLCR_H_WLEN_BITS |
-                   UART_UARTLCR_H_STP2_BITS |
-                   UART_UARTLCR_H_PEN_BITS |
-                   UART_UARTLCR_H_EPS_BITS);
-}
+void uart_set_format(uart_inst_t *uart, uint data_bits, uint stop_bits, uart_parity_t parity);
 
 /*! \brief Setup UART interrupts
  *  \ingroup hardware_uart
@@ -241,15 +257,20 @@ static inline bool uart_is_enabled(uart_inst_t *uart) {
 /*! \brief Enable/Disable the FIFOs on specified UART
  *  \ingroup hardware_uart
  *
+ * The UART is paused for around two character periods whilst the settings are
+ * changed. Data received during this time may be dropped by the UART.
+ * 
+ * Any characters still in the transmit FIFO will be lost if the FIFO is
+ * disabled. uart_tx_wait_blocking() can be called before this
+ * function to avoid this.
+ * 
+ * This function should not be called from an interrupt context, and the UART
+ * interrupt should be disabled when calling this function.
+ *
  * \param uart UART instance. \ref uart0 or \ref uart1
  * \param enabled true to enable FIFO (default), false to disable
  */
-static inline void uart_set_fifo_enabled(uart_inst_t *uart, bool enabled) {
-    hw_write_masked(&uart_get_hw(uart)->lcr_h,
-                   (bool_to_bit(enabled) << UART_UARTLCR_H_FEN_LSB),
-                   UART_UARTLCR_H_FEN_BITS);
-}
-
+void uart_set_fifo_enabled(uart_inst_t *uart, bool enabled);
 
 // ----------------------------------------------------------------------------
 // Generic input/output
@@ -397,12 +418,7 @@ static inline char uart_getc(uart_inst_t *uart) {
  * \param uart UART instance. \ref uart0 or \ref uart1
  * \param en Assert break condition (TX held low) if true. Clear break condition if false.
  */
-static inline void uart_set_break(uart_inst_t *uart, bool en) {
-    if (en)
-        hw_set_bits(&uart_get_hw(uart)->lcr_h, UART_UARTLCR_H_BRK_BITS);
-    else
-        hw_clear_bits(&uart_get_hw(uart)->lcr_h, UART_UARTLCR_H_BRK_BITS);
-}
+void uart_set_break(uart_inst_t *uart, bool en);
 
 /*! \brief Set CR/LF conversion on UART
  *  \ingroup hardware_uart
diff --git a/src/rp2_common/hardware_uart/uart.c b/src/rp2_common/hardware_uart/uart.c
index ba48e87..2a95d13 100644
--- a/src/rp2_common/hardware_uart/uart.c
+++ b/src/rp2_common/hardware_uart/uart.c
@@ -53,10 +53,10 @@ uint uart_init(uart_inst_t *uart, uint baudrate) {
     uint baud = uart_set_baudrate(uart, baudrate);
     uart_set_format(uart, 8, 1, UART_PARITY_NONE);
 
+    // Enable FIFOs (must be before setting UARTEN, as this is an LCR access)
+    hw_set_bits(&uart_get_hw(uart)->lcr_h, UART_UARTLCR_H_FEN_BITS);
     // Enable the UART, both TX and RX
     uart_get_hw(uart)->cr = UART_UARTCR_UARTEN_BITS | UART_UARTCR_TXE_BITS | UART_UARTCR_RXE_BITS;
-    // Enable FIFOs
-    hw_set_bits(&uart_get_hw(uart)->lcr_h, UART_UARTLCR_H_FEN_BITS);
     // Always enable DREQ signals -- no harm in this if DMA is not listening
     uart_get_hw(uart)->dmacr = UART_UARTDMACR_TXDMAE_BITS | UART_UARTDMACR_RXDMAE_BITS;
 
@@ -69,6 +69,42 @@ void uart_deinit(uart_inst_t *uart) {
     uart_reset(uart);
 }
 
+uint32_t uart_disable_before_lcr_write(uart_inst_t *uart) {
+    // Notes from PL011 reference manual:
+    //
+    // - Before writing LCR, must disable UART and wait for current TX + RX
+    //   char to finish
+    //
+    // - There is a BUSY flag which waits for the current TX char, but this is
+    //   OR'd with TX FIFO !FULL, so not usable when FIFOs are enabled and
+    //   potentially nonempty
+    //
+    // - FIFOs can't be set to disabled whilst a character is in progress
+    //   (else "FIFO integrity is not guaranteed")
+    //
+    // Combination of these means there is no general way to halt and poll for
+    // end of TX character, if FIFOs may be enabled. Either way, there is no
+    // way to poll for end of RX character.
+    //
+    // So... we're going to insert a 15 baud period delay before changing
+    // settings (comfortably higher than max data + start + stop + parity).
+    // Anything else would require API changes to permit a non-enabled UART
+    // state after init() where settings can be changed safely.
+
+    uint32_t cr_save = uart_get_hw(uart)->cr;
+    hw_clear_bits(&uart_get_hw(uart)->cr,
+        UART_UARTCR_UARTEN_BITS | UART_UARTCR_TXE_BITS | UART_UARTCR_RXE_BITS);
+
+    uint32_t current_ibrd = uart_get_hw(uart)->ibrd;
+    uint32_t current_fbrd = uart_get_hw(uart)->fbrd;
+    uint64_t baud_period_usec = 1u + 
+        ((uint64_t)64 * current_ibrd + current_fbrd) /
+        (4u * clock_get_hz(clk_peri));
+
+    busy_wait_us(15 * baud_period_usec);
+    return cr_save;
+}
+
 /// \tag::uart_set_baudrate[]
 uint uart_set_baudrate(uart_inst_t *uart, uint baudrate) {
     invalid_params_if(UART, baudrate == 0);
@@ -86,19 +122,80 @@ uint uart_set_baudrate(uart_inst_t *uart, uint baudrate) {
         baud_fbrd = ((baud_rate_div & 0x7f) + 1) / 2;
     }
 
-    // Load PL011's baud divisor registers
+    // Need to cleanly disable UART before touching LCR
+    bool was_enabled = uart_is_enabled(uart);
+    uint32_t cr_save;
+    if (was_enabled)
+        cr_save = uart_disable_before_lcr_write(uart);
+
     uart_get_hw(uart)->ibrd = baud_ibrd;
     uart_get_hw(uart)->fbrd = baud_fbrd;
-
-    // PL011 needs a (dummy) line control register write to latch in the
-    // divisors. We don't want to actually change LCR contents here.
+    // PL011 needs a (dummy) LCR_H write to latch in the divisors. We don't
+    // want to actually change LCR_H contents here.
     hw_set_bits(&uart_get_hw(uart)->lcr_h, 0);
 
+    // Re-enable using saved control register value
+    if (was_enabled)
+        uart_get_hw(uart)->cr = cr_save;
+
     // See datasheet
     return (4 * clock_get_hz(clk_peri)) / (64 * baud_ibrd + baud_fbrd);
 }
 /// \end::uart_set_baudrate[]
 
+void uart_set_format(uart_inst_t *uart, uint data_bits, uint stop_bits, uart_parity_t parity) {
+    invalid_params_if(UART, data_bits < 5 || data_bits > 8);
+    invalid_params_if(UART, stop_bits != 1 && stop_bits != 2);
+    invalid_params_if(UART, parity != UART_PARITY_NONE && parity != UART_PARITY_EVEN && parity != UART_PARITY_ODD);
+
+    bool was_enabled = uart_is_enabled(uart);
+    uint32_t cr_save;
+    if (was_enabled)
+        cr_save = uart_disable_before_lcr_write(uart);
+
+    hw_write_masked(&uart_get_hw(uart)->lcr_h,
+                   ((data_bits - 5u) << UART_UARTLCR_H_WLEN_LSB) |
+                   ((stop_bits - 1u) << UART_UARTLCR_H_STP2_LSB) |
+                   (bool_to_bit(parity != UART_PARITY_NONE) << UART_UARTLCR_H_PEN_LSB) |
+                   (bool_to_bit(parity == UART_PARITY_EVEN) << UART_UARTLCR_H_EPS_LSB),
+                   UART_UARTLCR_H_WLEN_BITS |
+                   UART_UARTLCR_H_STP2_BITS |
+                   UART_UARTLCR_H_PEN_BITS |
+                   UART_UARTLCR_H_EPS_BITS);
+
+    if (was_enabled)
+        uart_get_hw(uart)->cr = cr_save;
+}
+
+void uart_set_fifo_enabled(uart_inst_t *uart, bool enabled) {
+    bool was_enabled = uart_is_enabled(uart);
+    uint32_t cr_save;
+    if (was_enabled)
+        cr_save = uart_disable_before_lcr_write(uart);
+
+    hw_write_masked(&uart_get_hw(uart)->lcr_h,
+                   (bool_to_bit(enabled) << UART_UARTLCR_H_FEN_LSB),
+                   UART_UARTLCR_H_FEN_BITS);
+
+    if (was_enabled)
+        uart_get_hw(uart)->cr = cr_save;
+}
+
+void uart_set_break(uart_inst_t *uart, bool en) {
+    bool was_enabled = uart_is_enabled(uart);
+    uint32_t cr_save;
+    if (was_enabled)
+        cr_save = uart_disable_before_lcr_write(uart);
+
+    if (en)
+        hw_set_bits(&uart_get_hw(uart)->lcr_h, UART_UARTLCR_H_BRK_BITS);
+    else
+        hw_clear_bits(&uart_get_hw(uart)->lcr_h, UART_UARTLCR_H_BRK_BITS);
+
+    if (was_enabled)
+        uart_get_hw(uart)->cr = cr_save;
+}
+
 void uart_set_translate_crlf(uart_inst_t *uart, bool crlf) {
 #if PICO_UART_ENABLE_CRLF_SUPPORT
     uart_char_to_line_feed[uart_get_index(uart)] = crlf ? '\n' : 0x100;
-- 
GitLab