This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Understand UART, UARTE and getting it to work with PPI

Hi all,

Using UART is confusing for me because I see various implementations in the documentation. There is a Serial library, then there is the UART driver and HAL. The HAL is further divided to UART and UARTE. I read that the difference between UART and UARTE is that the UARTE uses EasyDMA.

I like to try a few things:

1. Use PPI to setup a timer event to trigger a UART TX task. Eg. Send out "Hello" every 1 second.

2. Use PPI to setup a timer event to trigger a UART RX task. Eg. Read from UART every 1 second.

For (1), I have setup the ppi channel as follows:

err_code = nrf_drv_ppi_channel_assign(m_ppi_channel1, nrf_drv_timer_event_address_get(&m_timer1, NRF_TIMER_EVENT_COMPARE0), nrf_drv_uart_task_address_get(&uart_driver_instance, NRF_UART_TASK_STARTTX));

 

In this case, should I be using nrf_uart_task_address_get or nrf_uarte_task_address_get or nrf_drv_uart_task_address_get?

I also noticed that there is a NRF_UART_TASK_STOPTX. Does it mean that I also need to have an event to trigger a stop task? Otherwise, it will never stop transmitting?

How does the UART tx happen? Should I be placing the string in some register or ram address so that when the task is triggered, the string is automatically send from the tx pin? If I am using UARTE, I presume i should initalise the tx buffer with this function nrf_uarte_tx_buffer_set.?

For(2), the ppi channel is setup as follows:

err_code = nrf_drv_ppi_channel_assign(m_ppi_channel1, nrf_drv_timer_event_address_get(&m_timer1, NRF_TIMER_EVENT_COMPARE0), nrf_drv_uart_task_address_get(&uart_driver_instance, NRF_UART_TASK_STARTRX));

Similarly, how does the RX happen? What do I need to configure for the RX data to be stored to memory? How can I access it later when I wake up the CPU? Do I also need another event to stop the RX task?

This is the code that I have now.

#include <stdint.h>

#include "nrf_delay.h"
#include "app_error.h"

#include "nrf_drv_ppi.h"
#include "nrf_drv_timer.h"
#include "nrf_drv_uart.h"


#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_log_default_backends.h"

#define UART0_USE_EASY_DMA        true
#define UART_EASY_DMA_SUPPORT     1
//#define UART_LEGACY_SUPPORT       1

#define PPI_EXAMPLE_TIMER1_INTERVAL             (1000)  // Timer interval in milliseconds

static const nrf_drv_uart_t uart_driver_instance = NRF_DRV_UART_INSTANCE(UART0_INSTANCE_INDEX);
static const nrf_drv_timer_t m_timer1 = NRF_DRV_TIMER_INSTANCE(1);
static nrf_ppi_channel_t m_ppi_channel1;

static volatile uint32_t m_counter;

/* Timer event handler. Not used since Timer1 is used only for PPI. */
static void empty_timer_handler(nrf_timer_event_t event_type, void * p_context)
{
}

/** @brief Function for initializing the UART peripheral.
*/
static void uart_init(void)
{
    uint32_t err_code = NRF_SUCCESS;
	
		nrf_drv_uart_config_t config = NRF_DRV_UART_DEFAULT_CONFIG;
		config.use_easy_dma = true;
		
		err_code = nrf_drv_uart_init(&uart_driver_instance, &config, NULL);
		APP_ERROR_CHECK(err_code);
	
}
	
/** @brief Function for initializing the PPI peripheral.
*/
static void ppi_init(void)
{
    uint32_t err_code = NRF_SUCCESS;

    err_code = nrf_drv_ppi_init();
    APP_ERROR_CHECK(err_code);

    /* Configure 1st available PPI channel to start UART RX event on TIMER1 COMPARE[0] match,
     * which is every even number of seconds.
     */
    err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channel1);
    APP_ERROR_CHECK(err_code);
    err_code = nrf_drv_ppi_channel_assign(m_ppi_channel1,
                                          nrf_drv_timer_event_address_get(&m_timer1,
                                                                          NRF_TIMER_EVENT_COMPARE0),
                                          nrf_drv_uart_task_address_get(&uart_driver_instance,
                                                                         NRF_UART_TASK_STARTTX));
    APP_ERROR_CHECK(err_code);

    // Enable both configured PPI channels
    err_code = nrf_drv_ppi_channel_enable(m_ppi_channel1);
    APP_ERROR_CHECK(err_code);
}


/** @brief Function for Timer 1 initialization.
 *  @details Initializes TIMER1 peripheral to generate an event every 1 seconds. The events 
 *           are used to start UART TX or RX via PPI: TIMER1->EVENT_COMPARE[0] triggers UART->TASK_STARTTX/TASK_STARTRX.
 */
static void timer1_init(void)
{
    // Check TIMER1 configuration for details.
    nrf_drv_timer_config_t timer_cfg = NRF_DRV_TIMER_DEFAULT_CONFIG;
    timer_cfg.frequency = NRF_TIMER_FREQ_31250Hz;
    ret_code_t err_code = nrf_drv_timer_init(&m_timer1, &timer_cfg, empty_timer_handler);
    APP_ERROR_CHECK(err_code);

    nrf_drv_timer_extended_compare(&m_timer1,
                                   NRF_TIMER_CC_CHANNEL0,
                                   nrf_drv_timer_ms_to_ticks(&m_timer1,
                                                             PPI_EXAMPLE_TIMER1_INTERVAL),
                                   NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK,
                                   false);
}


/**
 * @brief Function for application main entry.
 */
int main(void)
{
    uint32_t old_val = 0;
    uint32_t err_code;

    err_code = NRF_LOG_INIT(NULL);
    APP_ERROR_CHECK(err_code);

    NRF_LOG_DEFAULT_BACKENDS_INIT();

		uart_init();
    ppi_init();
    timer1_init(); // Timer to generate events

    nrf_drv_timer_enable(&m_timer1);

    while (true)
    {
			
    }
}

Thanks in advance!

Parents
  • Hi,

    You are correct that there are multiple implementations of UART drivers and libraries in the SDK. Which implementation to choose depends on much of the low-level stuff you want abstracted away. At the bottom, the UART peripheral is controlled by writing to and reading from the registers, to configure, start tasks, and receive events. The Hardware Abstraction Layer (HAL) is a set of functions that simplyfy reading and writing the registers. The HAL does not provide any additional functionality. Next, the UART driver provides APIs on a higher level than the HAL. The driver implements an interrupt handler that can detect events for you and pass these to the application. As you realized, the nRF52 series have two UART peripherals, legacy peripherals (UART) and peripherals using EasyDMA (UARTE). The UART driver provide a common API for two types of peripherals, and can be configured to use either one of them.

    On an even higher abstraction layer, you find the UART library (app_uart) and the Serial Port library. These are libraries built on top of the UART driver, providing additional abstraction and functionality, as described in the documentation. The serial port library is the latest addition to the SDK, and provide better support for multiple UART instances (available on nRF52840 IC), in addition to some new nice features.

    What you describe does only make sense if using the UARTE peripheral, as the UART peripheral can only hold a single byte in the TXD register, meaning you will have to update the register in software for each byte you want to transmitt. In your question your are doing a lot of mixing of the HAL functions with the driver functions. You should stick to using one of the them, as calling HAL functions when the driver is active can cause the driver to enter an unexpected state. I would recommend that you study the transmission and reception sections in the UARTE peripheral documentation, and use the UARTE HAL to setup the transfers. You can later use the PPI driver, as shown in the PPI example, to connect the timer compare event to the STARTTX/STARTRX tasks in the UARTE peripheral.

    Both TX and RX should be stopped automatically when the number of bytes given in the TXD.MAXCNT register have been transmitted, or the RX buffer have been filled with RXD.MAXCNT bytes. You might have to stop the receiption if you do not receive the number of bytes available in your buffer, but you should try to write your application in a way that avoids this.

    Hope this helps clearing things up!

    Best regards,
    Jørgen

Reply
  • Hi,

    You are correct that there are multiple implementations of UART drivers and libraries in the SDK. Which implementation to choose depends on much of the low-level stuff you want abstracted away. At the bottom, the UART peripheral is controlled by writing to and reading from the registers, to configure, start tasks, and receive events. The Hardware Abstraction Layer (HAL) is a set of functions that simplyfy reading and writing the registers. The HAL does not provide any additional functionality. Next, the UART driver provides APIs on a higher level than the HAL. The driver implements an interrupt handler that can detect events for you and pass these to the application. As you realized, the nRF52 series have two UART peripherals, legacy peripherals (UART) and peripherals using EasyDMA (UARTE). The UART driver provide a common API for two types of peripherals, and can be configured to use either one of them.

    On an even higher abstraction layer, you find the UART library (app_uart) and the Serial Port library. These are libraries built on top of the UART driver, providing additional abstraction and functionality, as described in the documentation. The serial port library is the latest addition to the SDK, and provide better support for multiple UART instances (available on nRF52840 IC), in addition to some new nice features.

    What you describe does only make sense if using the UARTE peripheral, as the UART peripheral can only hold a single byte in the TXD register, meaning you will have to update the register in software for each byte you want to transmitt. In your question your are doing a lot of mixing of the HAL functions with the driver functions. You should stick to using one of the them, as calling HAL functions when the driver is active can cause the driver to enter an unexpected state. I would recommend that you study the transmission and reception sections in the UARTE peripheral documentation, and use the UARTE HAL to setup the transfers. You can later use the PPI driver, as shown in the PPI example, to connect the timer compare event to the STARTTX/STARTRX tasks in the UARTE peripheral.

    Both TX and RX should be stopped automatically when the number of bytes given in the TXD.MAXCNT register have been transmitted, or the RX buffer have been filled with RXD.MAXCNT bytes. You might have to stop the receiption if you do not receive the number of bytes available in your buffer, but you should try to write your application in a way that avoids this.

    Hope this helps clearing things up!

    Best regards,
    Jørgen

Children
  • Hi Jorgen,

    Thank you for the reply. I managed to get the PPI to trigger a UARTE TX task successfully.

    I am however still stuck with the UARTE RX task. This is what I want to do. An external MCU is sending data to the nrf52832 via serial at one second intervals. The amount of data sent in each second varies. After 10 seconds, I want to wake up the CPU and process the data in DMA buffer.

    I have thought of 2 possible implementations:

    1. Set up the PPI for a NRF_UARTE_EVENT_ENDRX  event to wake up the CPU and start processing the data in RAM.

    2. Since I know the data is coming in at a regular interval, I can setup  2 PPIs. The first one is to setup a one second timer event to trigger a NRF_UARTE_TASK_STARTRX task. Then I have another PPI that uses the same timer event to trigger a counter task. When the counter reaches a value of 10, it wakes the CPU and process the data in RAM.

    I don't think method (1) works. The NRF_UARTE_EVENT_ENDRX  happens when receive buffer is filled up. I have declared a RX buffer for 500 bytes. But because the amount data coming in every second is variable, I cannot know when this buffer of 500 bytes is filled up. Unless the amount of data coming in every second is constant at 50 bytes, then this method might work?

    I have implemented method (2) in code, but I am getting an error. Before that, I have a question too. The external MCU sends data to the nrf52832 every second, but I don't know when it will start sending. So after I setup the PPI, and one second is up, the event triggers the NRF_UARTE_TASK_STARTRX  task. But the external MCU hasn't started sending any data yet so there is actually nothing to receive.

    In my code implementation, I can compile without errors. However, when I try to debug the code, after the program is executed, it come to a halt in app_error_weak.c and stops at the line NRF_BREAKPOINT_COND. I don't really know what is wrong and how to test the code. 

    My codes are given below for SDK_14.2.0

    #include <stdint.h>
    #include "boards.h"
    #include "nrf_delay.h"
    #include "app_error.h"
    #include "nrf_uarte.h"
    #include "nrf_gpio.h"
    
    #include "nrf_drv_ppi.h"
    #include "nrf_drv_timer.h"
    
    #include "nrf_log.h"
    #include "nrf_log_ctrl.h"
    #include "nrf_log_default_backends.h"
    
    #define PPI_EXAMPLE_TIMER1_INTERVAL             (1000)  // Timer interval in milliseconds
    
    #define RX_COUNTER  10
    
    //#define RX_PIN_NUMBER  NRF_GPIO_PIN_MAP(1, 8)
    //#define TX_PIN_NUMBER  NRF_GPIO_PIN_MAP(1, 6)
    
    static const nrf_drv_timer_t m_timer1 = NRF_DRV_TIMER_INSTANCE(1);
    
    /* Counter instance to count number of times NRF_UARTE_TASK_STARTRX has been called, and trigger an interrupt and wake up CPU */
    static nrf_drv_timer_t serial_counter = NRF_DRV_TIMER_INSTANCE(2);
    
    static nrf_ppi_channel_t m_ppi_channel1;
    static nrf_ppi_channel_t m_ppi_channel2;
    
    static volatile uint32_t m_counter;
    
    static uint8_t rx_buff[500];
    
    volatile bool serial_transfers_complete = false;
    
    /* Timer event handler. Not used since Timer1 is used only for PPI. */
    static void empty_timer_handler(nrf_timer_event_t event_type, void * p_context)
    {
    	
    }
    
    // Serial counter handler. 
    void serial_counter_handler(nrf_timer_event_t event_type, void * p_context)
    {   
    		ret_code_t err_code;	
        serial_transfers_complete = true;
           
        APP_ERROR_CHECK(err_code);
    }
    
    /** @brief Function for initializing the UART peripheral.
    */
    static void uarte_init(void)
    {
        uint32_t err_code = NRF_SUCCESS;
    	
    		nrf_uarte_txrx_pins_set(NRF_UARTE0, TX_PIN_NUMBER, RX_PIN_NUMBER);
    		nrf_uarte_baudrate_set(NRF_UARTE0, NRF_UARTE_BAUDRATE_115200);
    		nrf_uarte_configure(NRF_UARTE0, NRF_UARTE_PARITY_EXCLUDED, NRF_UARTE_HWFC_DISABLED);
    		nrf_uarte_enable(NRF_UARTE0 );
    		nrf_uarte_rx_buffer_set(NRF_UARTE0, rx_buff, 60);
    		nrf_uarte_task_trigger(NRF_UARTE0, NRF_UARTE_TASK_STARTRX );
    
    		APP_ERROR_CHECK(err_code);
    }
    	
    /** @brief Function for initializing the PPI peripheral.
    */
    static void ppi_init(void)
    {
        uint32_t err_code = NRF_SUCCESS;
    
        err_code = nrf_drv_ppi_init();
        APP_ERROR_CHECK(err_code);
    
        /* Configure 1st available PPI channel to start UART RX event on TIMER1 COMPARE[0] match,
         * which is every even number of seconds.
         */
        err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channel1);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_assign(m_ppi_channel1,
                                              nrf_drv_timer_event_address_get(&m_timer1,
                                                                              NRF_TIMER_EVENT_COMPARE0),
                                              nrf_uarte_task_address_get(NRF_UARTE0,
                                                                             NRF_UARTE_TASK_STARTRX));
        APP_ERROR_CHECK(err_code);
    
        /* Configure 2nd available PPI channel to count number of RX events
         */
        err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channel2);
        APP_ERROR_CHECK(err_code);
        err_code = nrf_drv_ppi_channel_assign(m_ppi_channel2,
                                              nrf_drv_timer_event_address_get(&m_timer1,
                                                                              NRF_TIMER_EVENT_COMPARE0),
                                              nrf_drv_timer_task_address_get(&serial_counter, 
    																																					NRF_TIMER_TASK_COUNT));
        APP_ERROR_CHECK(err_code);
    	
        // Enable both configured PPI channels
        err_code = nrf_drv_ppi_channel_enable(m_ppi_channel1);
        APP_ERROR_CHECK(err_code);
    		err_code = nrf_drv_ppi_channel_enable(m_ppi_channel2);
        APP_ERROR_CHECK(err_code);
    }
    
    
    /** @brief Function for Timer 1 initialization.
     *  @details Initializes TIMER1 peripheral to generate an event every 1 seconds. The events 
     *           are used to start UART TX or RX via PPI: TIMER1->EVENT_COMPARE[0] triggers UART->TASK_STARTTX/TASK_STARTRX.
     */
    static void timer1_init(void)
    {
        // Check TIMER1 configuration for details.
        nrf_drv_timer_config_t timer_cfg = NRF_DRV_TIMER_DEFAULT_CONFIG;
        timer_cfg.frequency = NRF_TIMER_FREQ_31250Hz;
        ret_code_t err_code = nrf_drv_timer_init(&m_timer1, &timer_cfg, empty_timer_handler);
        APP_ERROR_CHECK(err_code);
    
        nrf_drv_timer_extended_compare(&m_timer1,
                                       NRF_TIMER_CC_CHANNEL0,
                                       nrf_drv_timer_ms_to_ticks(&m_timer1,
                                                                 PPI_EXAMPLE_TIMER1_INTERVAL),
                                       NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK,
                                       false);
    }
    
    static void timer2_init(void)
    {
    	  uint32_t err_code;
    	
        // Set up counter with default configuration
        nrf_drv_timer_config_t counter_config = NRF_DRV_TIMER_DEFAULT_CONFIG;
        counter_config.mode = NRF_TIMER_MODE_COUNTER;	
    
        err_code = nrf_drv_timer_init(&serial_counter, &counter_config, serial_counter_handler);
        APP_ERROR_CHECK(err_code);	
    	
        nrf_drv_timer_extended_compare(&serial_counter, (nrf_timer_cc_channel_t)0,
    																	 RX_COUNTER, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, true);	
    }
    
    /**
     * @brief Function for application main entry.
     */
    int main(void)
    {
        uint32_t old_val = 0;
        uint32_t err_code;
    
        err_code = NRF_LOG_INIT(NULL);
        APP_ERROR_CHECK(err_code);
    
        NRF_LOG_DEFAULT_BACKENDS_INIT();
    
    		uarte_init();
        ppi_init();
        timer1_init(); // Timer to generate events
    		timer2_init();
    	
        nrf_drv_timer_enable(&m_timer1);
    		nrf_drv_timer_enable(&serial_counter);
    	
        while (true)
        {
            if(NRF_LOG_PROCESS() == false)
            {
                nrf_gpio_pin_set(LED_4); // Turn LED OFF when CPU is sleeping
                
    						while(serial_transfers_complete == false)
                {
                    // Make sure any pending events are cleared
                    __SEV();
                    __WFE();
                    // Enter System ON sleep mode
                    __WFE();           
                }
                nrf_gpio_pin_clear(LED_4); // Turn LED ON when CPU is working		
    
    						
    						// Do processing here
    
    						
    						// Reset data ready flag
                serial_transfers_complete = false;
    				}						
        }
    }
    

  • Note that the EasyDMA feature of UARTE peripheral on nRF52832 have a maximum size of 255 bytes.

    You should debug your application using this method, to determine which function is returning an error, and what the error code is. This should help you get the application working with the second solution.

Related