Beware that this post is related to an SDK in maintenance mode
More Info: Consider nRF Connect SDK for new designs
This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Can't get me' PPI channel between timer (working in counter mode) and the UART RXDRDY event working

I must be doing something obviously wrong but I just can't see it.

I'm trying to create a PPI linkage between the UART RXDRDY event and a timer working in counter mode so that I can count received characters.  However my counter never moves from zero.  I can see that the DMA has received and transferred the expected number of characters when the ENDRX event is handled if I call nrf_uarte_rx_amount_get() but my counter never increases.  My initialisation code is below, can anyone spot what I'm doing wrong?  I am reading the counter with nrfx_timer_capture_get(&gTimer, 0).

static nrfx_timer_t       gTimer = NRFX_TIMER_INSTANCE(2);
static nrf_ppi_channel_t  gPpiChannel;
static NRF_UARTE_Type    *gpReg = NRF_UARTE0;

static void counterEventHandler(nrf_timer_event_t eventType,
                                void *pContext)
{
    (void) eventType;
    (void) pContext;
}

// Set up UART and counter.
int32_t init(int32_t pinTx, int32_t pinRx,
             int32_t baudRateNrf)
{
    int32_t             errorCode = 0;
    nrfx_timer_config_t timerConfig = NRFX_TIMER_DEFAULT_CONFIG;

    timerConfig.mode = NRF_TIMER_MODE_COUNTER;
    timerConfig.bit_width = NRF_TIMER_BIT_WIDTH_32;

    if (nrfx_timer_init(&gTimer,
                        &timerConfig, counterEventHandler) == NRFX_SUCCESS) {
        if (nrfx_ppi_channel_alloc(&gPpiChannel)) == NRFX_SUCCESS) {
            if ((nrfx_ppi_channel_assign(gPpiChannel,
                                         nrf_uarte_event_address_get(gpReg,
                                                                     NRF_UARTE_EVENT_RXDRDY),
                                         nrfx_timer_task_address_get(&gTimer,
                                                                     NRF_TIMER_TASK_COUNT)) != NRFX_SUCCESS) ||
                (nrfx_ppi_channel_enable(gPpiChannel) != NRFX_SUCCESS)) {
                nrfx_ppi_channel_disable(gPpiChannel);
                nrfx_ppi_channel_free(gPpiChannel);
                errorCode = -1;
            }
        } else {
            errorCode = -1;
        }
    } else {
        errorCode = -1;
    }

    if (errorCode == 0) {

        // [ I create UART buffers, "pRxBufferWriteNext" and the like, etc. here]

        nrf_uarte_baudrate_set(gpReg, baudRateNrf);
        nrf_gpio_pin_set(pinTx);
        nrf_gpio_cfg_output(pinTx);
        nrf_uarte_txrx_pins_set(gpReg, pinTx, pinRx);

        nrf_uarte_configure(gpReg, NRF_UARTE_PARITY_EXCLUDED, NRF_UARTE_HWFC_DISABLED);

        nrf_uarte_enable(gpReg);

        nrf_uarte_event_clear(gpReg, NRF_UARTE_EVENT_ENDRX);
        nrf_uarte_event_clear(gpReg, NRF_UARTE_EVENT_ENDTX);
        nrf_uarte_event_clear(gpReg, NRF_UARTE_EVENT_ERROR);
        nrf_uarte_event_clear(gpReg, NRF_UARTE_EVENT_RXSTARTED);
        nrf_uarte_event_clear(gpReg, NRF_UARTE_EVENT_TXSTOPPED);

        nrf_uarte_shorts_enable(gpReg, NRF_UARTE_SHORT_ENDRX_STARTRX);

        nrf_uarte_rx_buffer_set(gpReg,
                                (uint8_t *) (pRxBufferWriteNext->pStart),
                                256);
        pRxBufferWriteNext = pRxBufferWriteNext->pNext;
        nrf_uarte_task_trigger(gpReg, NRF_UARTE_TASK_STARTRX);
        nrf_uarte_int_enable(gpReg, NRF_UARTE_INT_ENDRX_MASK     |
                                    NRF_UARTE_INT_ERROR_MASK     |
                                    NRF_UARTE_INT_RXSTARTED_MASK);
        NRFX_IRQ_PRIORITY_SET(getIrqNumber((void *) gpReg),
                              NRFX_UARTE_DEFAULT_CONFIG_IRQ_PRIORITY);
        NRFX_IRQ_ENABLE(getIrqNumber((void *) (gpReg)));

        nrfx_timer_enable(&gTimer);
        nrfx_timer_clear(&gTimer);
    }

    return errorCode;
}

void nrfx_uarte_0_irq_handler(void)
{
    if (nrf_uarte_event_check(gpReg, NRF_UARTE_EVENT_ENDRX)) {
        nrf_uarte_event_clear(gpReg, NRF_UARTE_EVENT_ENDRX);
    } else if (nrf_uarte_event_check(gpReg, NRF_UARTE_EVENT_RXSTARTED)) {
        nrf_uarte_event_clear(gpReg, NRF_UARTE_EVENT_RXSTARTED);
        nrf_uarte_rx_buffer_set(gpReg,
                                (uint8_t *) (pRxBufferWriteNext->pStart),
                                256);
        pRxBufferWriteNext = pRxBufferWriteNext->pNext;
    } else if (nrf_uarte_event_check(gpReg, NRF_UARTE_EVENT_ERROR)) {
        nrf_uarte_event_clear(gpReg, NRF_UARTE_EVENT_ERROR);
        nrf_uarte_errorsrc_get_and_clear(gpReg);
    }
}

Parents
  • I do something similar, though this is low-level it should still work if you choose unused PPI channels to test with. It toggles an output pin every time a character is notified as being available to DMA (prior to transfer as this is the DMA trigger) and latches the counter value in a capture register. It can also count characters of course, but I just want the elapsed time since last character as I am using a packet-based protocol so all characters are cuddled up together.

    // Define unused channels and a pin for 'scope
    #define PPI_CHANNEL        4
    #define GPIOTE_CHANNEL     3
    #define PIN_EVENT_OUTPUT  11  // Optional toggle pin
    
    void SetupForLastRxByteTime(void)
    {
       // Timer for this test
       NRF_TIMER_Type *pTimer = NRF_TIMER1;
    
       // Configure GPIO pin as output
       NRF_GPIO->DIRSET = (1UL << PIN_EVENT_OUTPUT);
    
       // Configure GPIOTE->TASKS_OUT[GPIOTE_CHANNEL] to toggle pin
       NRF_GPIOTE->CONFIG[GPIOTE_CHANNEL] = (GPIOTE_CONFIG_MODE_Task       << GPIOTE_CONFIG_MODE_Pos)
                                          | (GPIOTE_CONFIG_OUTINIT_Low     << GPIOTE_CONFIG_OUTINIT_Pos)
                                          | (GPIOTE_CONFIG_POLARITY_Toggle << GPIOTE_CONFIG_POLARITY_Pos)
                                          | (PIN_EVENT_OUTPUT              << GPIOTE_CONFIG_PSEL_Pos);
    
       // Configure PPI channel between last character received and timer Capture register
       NRF_PPI->CH[PPI_CHANNEL].EEP = (uint32_t)&pUART->EVENTS_RXDRDY;
       NRF_PPI->CH[PPI_CHANNEL].TEP = (uint32_t)&NRF_GPIOTE->TASKS_OUT[GPIOTE_CHANNEL];
       NRF_PPI->FORK[PPI_CHANNEL].TEP = (uint32_t)&pTimer->TASKS_CAPTURE[3];
    
       // Enable the PPI channel
       NRF_PPI->CHENSET = (1UL << PPI_CHANNEL);
    }

    Call this setup once at the end of the init; assumes whichever timer you use is already running ..

    Oops, just read original post and see you wanted to count characters, not get elapsed time since last character received. If I get a mo' I'll see if I have anything. Ah, Looks like replace TASKS_CAPTURE with TASKS_COUNT, but a separate timer  is required:

       // Time from last char
       NRF_PPI->FORK[PPI_CHANNEL].TEP = (uint32_t)&pTimer->TASKS_CAPTURE[3];
       // Count of received chars
       NRF_PPI->FORK[PPI_CHANNEL].TEP = (uint32_t)&pTimer->TASKS_COUNT;

  • An interesting suggestion, nice to see something that is known to work!

    I guess your timer configure/enable code is not shown above so I've left mine exactly as it is but, after it (i.e. between lines 40 and 42 of my code snippet) I've inserted my version of your lines (3 is an unused PPI channel):

                // Configure PPI channel between last character received and timer Count register
                NRF_PPI->CH[3].EEP = (uint32_t) &(NRF_UARTE0->EVENTS_RXDRDY);
                NRF_PPI->CH[3].TEP = (uint32_t) &(NRF_TIMER2->TASKS_COUNT);
    
                // Enable the PPI channel
                NRF_PPI->CHENSET = (1UL << 3);
    

    ...then I read the timer value by triggering NRF_TIMER2->TASKS_CAPTURE[0] and reading NRF_TIMER2->CC[0].  Is that correct? No change unfortunately, the value I read is still zero.  I'm confused by the acts of counting and capturing now.  I had assumed that the single PPI linkage I needed was the one above, TASKS_COUNT linked to EVENTS_RXDRDY and then the act of capturing is done when I want to read that count.  But do I also need FORK with a capture task, i.e. my lines should be:

    // Configure PPI channel between last character received and timer Count register
                NRF_PPI->CH[3].EEP = (uint32_t) &(NRF_UARTE0->EVENTS_RXDRDY);
                NRF_PPI->CH[3].TEP = (uint32_t) &(NRF_TIMER2->TASKS_COUNT);
                NRF_PPI->FORK[3].TEP = (uint32_t) &(NRF_TIMER2->TASKS_CAPTURE[0]);
    
                // Enable the PPI channel
                NRF_PPI->CHENSET = (1UL << 3);
    

    ...and then leave out my NRF_TIMER2->TASKS_CAPTURE[0] step before the read? Though that doesn't help either, I still get zero.

    I must be missing something in my configuration somewhere.  The UART is working, the DMA is working, data is arriving, what's happening to RXDRDY and my counter?!

Reply
  • An interesting suggestion, nice to see something that is known to work!

    I guess your timer configure/enable code is not shown above so I've left mine exactly as it is but, after it (i.e. between lines 40 and 42 of my code snippet) I've inserted my version of your lines (3 is an unused PPI channel):

                // Configure PPI channel between last character received and timer Count register
                NRF_PPI->CH[3].EEP = (uint32_t) &(NRF_UARTE0->EVENTS_RXDRDY);
                NRF_PPI->CH[3].TEP = (uint32_t) &(NRF_TIMER2->TASKS_COUNT);
    
                // Enable the PPI channel
                NRF_PPI->CHENSET = (1UL << 3);
    

    ...then I read the timer value by triggering NRF_TIMER2->TASKS_CAPTURE[0] and reading NRF_TIMER2->CC[0].  Is that correct? No change unfortunately, the value I read is still zero.  I'm confused by the acts of counting and capturing now.  I had assumed that the single PPI linkage I needed was the one above, TASKS_COUNT linked to EVENTS_RXDRDY and then the act of capturing is done when I want to read that count.  But do I also need FORK with a capture task, i.e. my lines should be:

    // Configure PPI channel between last character received and timer Count register
                NRF_PPI->CH[3].EEP = (uint32_t) &(NRF_UARTE0->EVENTS_RXDRDY);
                NRF_PPI->CH[3].TEP = (uint32_t) &(NRF_TIMER2->TASKS_COUNT);
                NRF_PPI->FORK[3].TEP = (uint32_t) &(NRF_TIMER2->TASKS_CAPTURE[0]);
    
                // Enable the PPI channel
                NRF_PPI->CHENSET = (1UL << 3);
    

    ...and then leave out my NRF_TIMER2->TASKS_CAPTURE[0] step before the read? Though that doesn't help either, I still get zero.

    I must be missing something in my configuration somewhere.  The UART is working, the DMA is working, data is arriving, what's happening to RXDRDY and my counter?!

Children
  • A break through: I had been following the pattern of nrf_libuarte_drv.c, which is to connect NRF_UARTE_EVENT_RXDRDY to NRF_TIMER_TASK_COUNT using PPI and then call nrfx_timer_capture_get() to read the timer/counter value when the time comes; as it says in the instructions above nrfx_timer_capture_get() "Use this function to read channel values when PPI is used for capturing".  But of course connecting NRF_UARTE_EVENT_RXDRDY to NRF_TIMER_TASK_COUNT using PPI doesn't actually do a capture, it simply drives the underlying counter, so if I leave all my code exactly as it is but read the timer/counter with nrfx_timer_capture() instead then it works, I get a non-zero value, the number of bytes received, because the read function is doing both a capture and then a read of the capture channel.

    What I don't understand is how nrf_libuarte_drv.c manages to work?  It has groups of things that it enables, maybe somehow the capture is done as a side-effect of some other PPI operation it has enabled? 

  • Hi Rob,

    Congrats that you have figured out by yourself. 

    Exactly as you said, the PPI task only increase the counter in the register. It has to be captured first and stored in one CC register before you can read it out. 

    As in the nrf_libuarte_drc.c, if you look at other PPI configurations, you can find some PPIs are configured with a fork task to capture counters from timer. For instance

        ret = ppi_channel_configure(
                &p_libuarte->ctrl_blk->ppi_channels[NRF_LIBUARTE_DRV_PPI_CH_ENDRX_STARTRX],
                nrf_uarte_event_address_get(p_libuarte->uarte, NRF_UARTE_EVENT_ENDRX),
                nrf_uarte_task_address_get(p_libuarte->uarte, NRF_UARTE_TASK_STARTRX),
                nrfx_timer_capture_task_address_get(&p_libuarte->timer, 0));

    A timer capture will be triggered when NRF_UARTE_EVENT_ENDRX event is generated.

    You can check how TIMER capture works in TIMER — Timer/counter

  • But if the capture of the counter/timer is only triggered on ENDRX, rather than when the RX is stopped, the resolution of the byte count will be exactly the same as that of the DMA count, in chunks of whatever size the DMA is set to.  There's not much point in running the count on RXDRDY, nrf_libuarte_driv.c could just ask the DMA how much it had transferred in the ENDRX interrupt.

    Don't understand!

  • Hi, 

    Please take a look of the code and there are multiple PPI tasks to capture the counter, searching "nrfx_timer_capture_task_address_get".

    You will see it captures counter when RX is stopped as well.

    ret = ppi_channel_configure(
                    &p_libuarte->ctrl_blk->ppi_channels[NRF_LIBUARTE_DRV_PPI_CH_EXT_STOP_STOPRX],
                    p_config->endrx_evt,
                    nrf_uarte_task_address_get(p_libuarte->uarte, NRF_UARTE_TASK_STOPRX),
                    nrfx_timer_capture_task_address_get(&p_libuarte->timer, 1));

    Please read through the code and if you can't understand the whole logic, the best way is to read it line by line and understand what it does.

  • To help you guys out, there is a big gap here.  I asked in this ticket for help with understanding how the UARTE HW is meant to be driven for continuous receive.  I didn't want more software, I wanted design guidance on how the hardware was intended to be used.  I was told to go look at nrf_libuarte_driv.c to find out how to do this: you have no design guidelines on how to do this, no "story" style documentation (as ESP32 has), no comprehensive example code (as STM32F4 has).

    This is a real problem: being directed to read the even more ridiculously complicated nrf_libuarte_driv.c code, with its multiple PPI channels for multiple use cases and paucity of comments, cannot be considered help or advice.  nrf_libuarte_driv.c is intended as functional code that meets your API: fine, that's not a problem but please do NOT consider it a substitute for design guidance.

    You REALLY need design guidance.  Just letting you know this as constructive customer feedback.  Your HW is a helluvalot simpler to drive directly (as evidenced by the small number of lines of code offered by @hmolesworth above) than any of the five layers of driver code above and, if it were well explained, you'd be so much better off.  The lack of such documentation means it has taken me many weeks longer to achieve running code on the NRF52840 versus the competition.

    Anyway, I do now have working code, so thank you.

Related