How to get more than 2 UART's

From the nRF52840 specification it seems that the CPU only has 2 UART peripherals. For my particular project, I need to have atleast 4 UART peripherals:

1. UART0 - for logging and debugging

2. UART1 - For communicating with LTE modem

3. UART2 - For communicating with the GNSS

4. UART3 - For communicating with the CO2 sensor

I would like to know what are the most optimal solutions if we need to use more than 2 UART on the nRF52840?

  • As Kenneth says, bit-banging is an option but not very reliable for faster baud rates. You can also use PWM for output Tx, and hardware timers for input Rx.

    // PWM Half-Duplex UART - nrfx drivers
    // ===================================
    //
    // Requirements
    //  10 bit bytes (Start - 8xData - Stop)
    //  single char or burst (packet) transmission
    //  Differential output using 2 pins
    //  No level change on pins outside transmitted data to avoid spurious character detection
    
    // Standard Uart character 0x21
    //
    //        |     |     |     |     |     |     |     |     |     |     |
    //  Idle   Start   0     1     2     3     4     5     6     7   Stop    Idle
    // -------+     +-----+                       +-----+           +-----+-------
    //        |     |     |                       |     |           |
    //        |     |     |                       |     |           |       Normal
    //        +-----+     +-----+-----+-----+-----+     +-----+-----+
    //
    //        +-----+     +-----+-----+-----+-----+     +-----+-----+
    //        |     |     |                       |     |           |       Invert
    //        |     |     |                       |     |           |
    // -------+     +-----+                       +-----+           +-----+-------
    

    There was some discussion here pwm-waveform which might be useful. PWM output and timer input works well, but it's quite a bit of code. I can share some PWM code for you to try, Tx allows a very high baud rate.

    This is a build message function:

    #define FEATURE_NRFX_DRIVERS               // Comment out this line for low-level drivers
    #define LL_BAUDRATE      1200              // Tested ok up to 9600 baud with voting inside interrupt (30uSec); faster if emcoding outside
    #define F_CLK        16000000              // nRF52832/nRF52840 fixed at 16MHz
    #define F_PRESCALER  (PWM_PRESCALER_PRESCALER_DIV_1)  // Divide by 1, 16MHz clock
    #define COUNTER_TOP  ((F_CLK+(LL_BAUDRATE/2)) / LL_BAUDRATE)
    STATIC_ASSERT(COUNTER_TOP < 32768, "COUNTER_TOP value too large for 15-bit register");
    STATIC_ASSERT(COUNTER_TOP >= 3, "COUNTER_TOP value too small for correct operation");
    #define BIT_LOW               0           // Normal encoding
    #define BIT_HIGH    (COUNTER_TOP)         // Normal encoding
    //#define BIT_LOW   ((2*COUNTER_TOP)/3)   // Manchester encoding 2/3 bit
    //#define BIT_HIGH  (COUNTER_TOP/3)       // Manchester encoding 1/3 bit
    #define PWM_NEG (PIN_FEATHER_D6)          // P0.07 0x00000080 Low level outside transmission
    #define PWM_POS (PIN_FEATHER_D11)          // P0.26 0x04000000 High level outside transmission
    #define DEBUG_PIN (PIN_FEATHER_D9)       // P0.06 0x00000080 Low level outside transmission
    #define STX_BYTE 0x02 // Example start character: STX (^B)
    #define ETX_BYTE 0x03 // Example start character: ETX (^C)
    
    typedef struct TX_BYTE_T {
        nrf_pwm_values_grouped_t StartBit;
        nrf_pwm_values_grouped_t DataBits[8];
        nrf_pwm_values_grouped_t StopBit;
    } TxByte_t;
    
    TxByte_t TxPacket[4];
    const uint16_t TxPacketSize = (sizeof(TxPacket)/sizeof(uint16_t));
    
    void encodeByte(TxByte_t* pBuffer, const uint8_t ch)
    {
        // Encode byte in little-endian format with start and stop bits
        pBuffer->StartBit.group_0 = pBuffer->StartBit.group_1 = (BIT_LOW);  pBuffer->StartBit.group_0 |= 0x8000; // 0 Start bit
        pBuffer->StopBit.group_0  = pBuffer->StopBit.group_1  = (BIT_HIGH); pBuffer->StopBit.group_0  |= 0x8000; // 0 Stop bit
        for(uint32_t i=0; i<8; i++)
        {
            pBuffer->DataBits[i].group_0 = pBuffer->DataBits[i].group_1 = (((ch>>i) & 0x01) ? BIT_HIGH : BIT_LOW);
            pBuffer->DataBits[i].group_0 |= 0x8000; // invert
        }
    }
    
    void buildMessage(void)
    {
        TxByte_t *pTx = TxPacket;
        uint8_t STX         = 0x82;
        uint8_t signal_id   = 0x64 | 0x80;
        uint8_t data_byte_0 = 0x03 | 0x80;
        uint8_t checksum    = (signal_id) + (data_byte_0);
        checksum &= 0x7F;
    #if 0
        encodeByte(pTx++, STX);
        encodeByte(pTx++, signal_id);
        encodeByte(pTx++, data_byte_0);
        encodeByte(pTx++, checksum);
    #else
        encodeByte(pTx++, 'A');
        encodeByte(pTx++, 'B');
        encodeByte(pTx++, 'C');
        encodeByte(pTx++, 'D');
    #endif
    }

    This is the PWM function to transmit the message built above:

    // Timer for this module, not all timers have 6 CC registers
    static NRF_TIMER_Type *pTimer = NRF_TIMER3;
    #define PWM_TIMER_IRQn TIMER3_IRQn
    void RxTimerInit(void);
    
    void TestPWM_Uart(void)
    {
        // Start accurate HFCLK (XOSC)
        NRF_CLOCK->TASKS_HFCLKSTART = 1;
        while (NRF_CLOCK->EVENTS_HFCLKSTARTED == 0)
            ;
        NRF_CLOCK->EVENTS_HFCLKSTARTED = 0;
        static nrfx_pwm_t m_pwm0 = NRFX_PWM_INSTANCE(0);
        uint32_t err_code;
        // Declare a configuration structure and use a macro to instantiate it with default parameters.
        nrfx_pwm_config_t pwm_config = NRFX_PWM_DEFAULT_CONFIG;
        //    .irq_priority = NRFX_PWM_DEFAULT_CONFIG_IRQ_PRIORITY,                  \
        //    .base_clock   = (nrf_pwm_clk_t)NRFX_PWM_DEFAULT_CONFIG_BASE_CLOCK,     \
        //    .count_mode   = (nrf_pwm_mode_t)NRFX_PWM_DEFAULT_CONFIG_COUNT_MODE,    \
        //    .top_value    = NRFX_PWM_DEFAULT_CONFIG_TOP_VALUE,                     \
        //    .load_mode    = (nrf_pwm_dec_load_t)NRFX_PWM_DEFAULT_CONFIG_LOAD_MODE, \
        //    .step_mode    = (nrf_pwm_dec_step_t)NRFX_PWM_DEFAULT_CONFIG_STEP_MODE
        static nrf_pwm_sequence_t pwm_sequence;
    
        // Override some of the default parameters:
        pwm_config.output_pins[0] = PWM_NEG | NRFX_PWM_PIN_INVERTED;
        pwm_config.output_pins[1] = NRFX_PWM_PIN_NOT_USED;
        pwm_config.output_pins[2] = PWM_POS;
        pwm_config.output_pins[3] = NRFX_PWM_PIN_NOT_USED;
    
        pwm_config.load_mode      = NRF_PWM_LOAD_GROUPED;   // 1 == 1st half word (16-bit) used in channels 0 and 1; 2nd word in channels 2 and 3
        pwm_config.base_clock     = F_PRESCALER;            // 0 == divide by 1 for 16MHz clock
        pwm_config.step_mode      = NRFX_PWM_DEFAULT_CONFIG_STEP_MODE; // 0 == Count Up
        pwm_config.top_value      = COUNTER_TOP;
    
        // Pass config structure into driver init() function
        err_code = nrfx_pwm_init(&m_pwm0, &pwm_config, NULL);
        APP_ERROR_CHECK(err_code);
        // Boost differential pwm driver pins to high drive - this is optional
        nrf_gpio_cfg(PWM_NEG, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_CONNECT, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
        nrf_gpio_cfg(PWM_POS, NRF_GPIO_PIN_DIR_OUTPUT, NRF_GPIO_PIN_INPUT_CONNECT, NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_H0H1, NRF_GPIO_PIN_NOSENSE);
    
        buildMessage();
        pwm_sequence.values.p_grouped = (uint32_t)TxPacket;
        pwm_sequence.length           = TxPacketSize;
        pwm_sequence.repeats          = 0;
        // Test Rx using hardware timer
        RxTimerInit();
        pTimer->TASKS_START = 1;
        nrfx_pwm_simple_playback(&m_pwm0, &pwm_sequence, 1, NRFX_PWM_FLAG_STOP); // LOOP or STOP
        while (1)
        {
            __WFE();
        }
    }

    This all works, hopefully not too hard to follow. This technique allows stuff like Manchester Encoding and non-standard character lengths.

    Edit: Here's a post using the SAADC for Rx: capturing-gpio-transitions-of-a-4khz-signal-with-ble-enabled

Related