How to implement GPIOTE + Timer + PPI with NRF54L15 for pulse width capture and RF data pin input?

Hello, I am struggling to find resources or up-to-date information on how to implement the above with NRF54DK (specifically Ezurio BL54L15 DK). Reference material I have found seems to have become outdated over time due to new SDK versions, etc. Similarly, I'm struggling with many of the concepts as I'm relatively new to Nordic and Zephyr.

I am interfacing with the DATA pin on a MAX41470 RF receiver, which is toggling high & low based on RF data received from (in this case) TPMS sensors of a car.

There are multiple types of sensors I am using - one of which transmits data in 167us pulses. Using an interrupt handler on the DATA pin, I have been able to decode the data no problem. The timing seems well within the realms of what could be handled using a normal ISR.

Another type of sensor is transmitting at 25us, and this is causing problems. The ISR is missing bit changes, and the data isn't being received in full, thus I can't decode it properly. Similarly, the ISR is triggering so fast and often that any other computations are slow - the core is basically hogged by the ISR (I believe).

Again, new to this, but this is my understanding at the moment. To try and fix this, I've been researching moving to using GPIOTE and a Timer + PPI to avoid using the CPU and get accurate timing data for the highs and lows, and thus computing the data in the loop. I'm not sure if this is the ultimate solution, as the toggling is happening so quickly, but I've been trying to attempt it (any advice here is appreciated).

What I can't figure out is how to actually do this in code. I understand that I need to use NRFX as this is NRF-specific functionality outside of Zephyr's abstractions. What I can't figure out is how to get the various functions loaded and how to write code to do this. Any pointing in the right direction would be really helpful. I've just about grasped the Zephyr abstractions, so it's hard to now wrap my head around the nrfx_ versions of similar functions.

Can anyone point me in the right direction to some reference material that could be relevant to me here?

Parents
  • Ok, to update this thread, I have attached the code I've been working with, which is as far as I've gotten figuring this out. As an example (to get an understanding of this) I've been trying to measure the duration of a button press on the Ezurio BL54L15DK. It's triggering the event correctly but the times seem wrong - the time when button is released is often less than when the button is pressed. So I'm quite confused.

    I'm also confused about initializing both a Timer and the GPIOTE instances - its seems Zephyr/NRF auto-initialises based on the Devicetree when the prj.conf file has `CONFIG_GPIO=y`. Setting `CONFIG_GPIO=n` to allow me to manually initialise doesn't work, because then I can't get the project to build - it throws errors for the timer and GPIOTE instances not existing at all. Which confuses me, as other examples I've found have `CONFIG_GPIO=n` set but still seem to be able to build without custom .dtsi files or similar. Bit over my head here, all quite confusing.

    #include <zephyr/kernel.h>
    #include <zephyr/device.h>
    #include <zephyr/drivers/gpio.h>
    #include <zephyr/logging/log.h>
    #include <nrfx_gpiote.h>
    #include <nrfx_timer.h>
    #include <helpers/nrfx_gppi.h>
    #include <hal/nrf_gpiote.h>
    #include <hal/nrf_timer.h>
    #include <hal/nrf_gpio.h>
    
    LOG_MODULE_REGISTER(nrfx_example, LOG_LEVEL_INF);
    
    #define INPUT_PIN	NRF_DT_GPIOS_TO_PSEL(DT_ALIAS(sw0), gpios)
      
    static const nrfx_gpiote_t gpiote = NRFX_GPIOTE_INSTANCE(20);
    static const nrfx_timer_t m_timer = NRFX_TIMER_INSTANCE(00);
    static uint32_t state = 0;
    
    void timer_handler(nrf_timer_event_t event_type, void * p_context) {
      // Timer capture events do not generate interrupts when DPPI triggered.
      // But you can put debug here if needed.
    }
    
    static void gpiote_evt_handler(nrfx_gpiote_pin_t pin, nrfx_gpiote_trigger_t trigger, void *context) {
      const uint32_t timer1 = NRF_TIMER00->CC[0];
      if (state == 1) {
        state = 0;
        LOG_INF("Pressed. Times %u base: %p", timer1, m_timer.p_reg);
      } else {
        state = 1;
        LOG_INF("Released. Times %u base: %p", timer1, m_timer.p_reg);
      }
    }
    
    void pulse_width_init(void) {
      nrfx_err_t err;
      uint8_t in_channel;
      uint8_t ppi_channel;
    
      // ---------------------------------------
      // 1. INIT TIMER
      // ---------------------------------------
      nrfx_timer_config_t timer_cfg = {
        .frequency = NRF_TIMER_FREQ_1MHz,
        .mode = NRF_TIMER_TASK_COUNT,
        .bit_width = NRF_TIMER_BIT_WIDTH_32,
        .interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY,
        .p_context = NULL
      };
    
      nrfx_timer_enable(&m_timer);
      if ( ! nrfx_timer_is_enabled(&m_timer)) {
        LOG_ERR("Timer00 is not enabled");
        return;
      }
    
      // ---------------------------------------
      // 2. INIT GPIOTE
      // ---------------------------------------
      // Init not required as is handled by SDK
      err = nrfx_gpiote_channel_alloc(&gpiote, &in_channel);
      if (err != NRFX_SUCCESS) {
        LOG_ERR("Channel alloc fail: %x", err);
        return;
      }
    
      // Edge detection on both edges
    	static const nrf_gpio_pin_pull_t pull_config = NRF_GPIO_PIN_PULLUP;
      nrfx_gpiote_trigger_config_t trigger_config = {
        .trigger = NRFX_GPIOTE_TRIGGER_TOGGLE,
        .p_in_channel = &in_channel
      };
      static const nrfx_gpiote_handler_config_t handler_config = {
        .handler = gpiote_evt_handler
      };
      nrfx_gpiote_input_pin_config_t gpiote_cfg = {
    		.p_pull_config = &pull_config,
        .p_trigger_config = &trigger_config,
        .p_handler_config = &handler_config
      };
    
      err = nrfx_gpiote_input_configure(&gpiote, INPUT_PIN, &gpiote_cfg);
      if (err != NRFX_SUCCESS) {
        LOG_ERR("GPIOTE configure fail: %x", err);
        return;
      }
    
      nrfx_gpiote_trigger_enable(&gpiote, INPUT_PIN, true);
    
      // ---------------------------------------
      // 3. ALLOCATE DPPI channel
      // ---------------------------------------
      err = nrfx_gppi_channel_alloc(&ppi_channel);
      if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gppi_channel_alloc fail: %x", err);
        return;
      }
    
      // ---------------------------------------
      // 4. Connect GPIOTE.event → TIMER.capture task
      // ---------------------------------------
    
      // Bind both ends on DPPI
      nrfx_gppi_channel_endpoints_setup(ppi_channel, 
        nrfx_gpiote_in_event_address_get(&gpiote, INPUT_PIN),
        nrfx_timer_capture_task_address_get(&m_timer, 0)
      );
    
      // Enable channel
      nrfx_gppi_channels_enable(BIT(ppi_channel));
    
      LOG_INF("Pulse measurement started. Watching pin %d", INPUT_PIN);
    }
    
    int main(void) {
      pulse_width_init();
    
      while (1) {
        k_msleep(100);
      }
    
      return 0;
    }
    



    Here is sample logs from the code attached, showing button presses and timings look incorrect (not the datestamps - those are perfect).

    *** Booting nRF Connect SDK v3.1.1-e2a97fe2578a ***
    *** Using Zephyr OS v4.1.99-ff8f0c579eeb ***
    [00:00:00.001,209] <inf> nrfx_example: Pulse measurement started. Watching pin 45
    [00:00:00.001,325] <inf> nrfx_example: Released. Times 1176 base: 0x50055000
    [00:00:01.180,969] <inf> nrfx_example: Pressed. Times 3521 base: 0x50055000
    [00:00:01.320,792] <inf> nrfx_example: Released. Times 24419 base: 0x50055000
    [00:00:02.578,216] <inf> nrfx_example: Pressed. Times 7438 base: 0x50055000
    [00:00:02.697,775] <inf> nrfx_example: Released. Times 60640 base: 0x50055000
    [00:00:03.830,894] <inf> nrfx_example: Pressed. Times 17061 base: 0x50055000
    [00:00:03.938,171] <inf> nrfx_example: Released. Times 35699 base: 0x50055000
    [00:00:06.055,178] <inf> nrfx_example: Pressed. Times 48404 base: 0x50055000
    [00:00:06.206,043] <inf> nrfx_example: Released. Times 27602 base: 0x50055000
    [00:00:07.296,722] <inf> nrfx_example: Pressed. Times 31350 base: 0x50055000
    [00:00:07.424,409] <inf> nrfx_example: Released. Times 18876 base: 0x50055000
    [00:00:08.544,403] <inf> nrfx_example: Pressed. Times 61163 base: 0x50055000
    [00:00:08.659,503] <inf> nrfx_example: Released. Times 12448 base: 0x50055000
    [00:00:09.634,200] <inf> nrfx_example: Pressed. Times 58274 base: 0x50055000
    [00:00:09.756,236] <inf> nrfx_example: Released. Times 174 base: 0x50055000
    [00:00:10.729,384] <inf> nrfx_example: Pressed. Times 33053 base: 0x50055000
    [00:00:10.842,590] <inf> nrfx_example: Released. Times 34340 base: 0x50055000
    [00:00:11.944,797] <inf> nrfx_example: Pressed. Times 1296 base: 0x50055000
    [00:00:12.035,288] <inf> nrfx_example: Released. Times 14991 base: 0x50055000
    [00:00:13.360,826] <inf> nrfx_example: Pressed. Times 24773 base: 0x50055000
    [00:00:13.451,968] <inf> nrfx_example: Released. Times 43799 base: 0x50055000
    [00:00:14.676,443] <inf> nrfx_example: Pressed. Times 19919 base: 0x50055000
    [00:00:14.786,534] <inf> nrfx_example: Released. Times 61642 base: 0x50055000
    [00:00:15.852,600] <inf> nrfx_example: Pressed. Times 61795 base: 0x50055000
    [00:00:15.958,466] <inf> nrfx_example: Released. Times 3566 base: 0x50055000
    [00:00:17.703,178] <inf> nrfx_example: Pressed. Times 3073 base: 0x50055000
    [00:00:17.777,741] <inf> nrfx_example: Released. Times 18508 base: 0x50055000
    [00:00:19.314,699] <inf> nrfx_example: Pressed. Times 42638 base: 0x50055000
    [00:00:19.400,757] <inf> nrfx_example: Released. Times 20168 base: 0x50055000


    Any guidance or further direction here is really appreciated.

    Attached code:
    gpiote_basic.zip

Reply
  • Ok, to update this thread, I have attached the code I've been working with, which is as far as I've gotten figuring this out. As an example (to get an understanding of this) I've been trying to measure the duration of a button press on the Ezurio BL54L15DK. It's triggering the event correctly but the times seem wrong - the time when button is released is often less than when the button is pressed. So I'm quite confused.

    I'm also confused about initializing both a Timer and the GPIOTE instances - its seems Zephyr/NRF auto-initialises based on the Devicetree when the prj.conf file has `CONFIG_GPIO=y`. Setting `CONFIG_GPIO=n` to allow me to manually initialise doesn't work, because then I can't get the project to build - it throws errors for the timer and GPIOTE instances not existing at all. Which confuses me, as other examples I've found have `CONFIG_GPIO=n` set but still seem to be able to build without custom .dtsi files or similar. Bit over my head here, all quite confusing.

    #include <zephyr/kernel.h>
    #include <zephyr/device.h>
    #include <zephyr/drivers/gpio.h>
    #include <zephyr/logging/log.h>
    #include <nrfx_gpiote.h>
    #include <nrfx_timer.h>
    #include <helpers/nrfx_gppi.h>
    #include <hal/nrf_gpiote.h>
    #include <hal/nrf_timer.h>
    #include <hal/nrf_gpio.h>
    
    LOG_MODULE_REGISTER(nrfx_example, LOG_LEVEL_INF);
    
    #define INPUT_PIN	NRF_DT_GPIOS_TO_PSEL(DT_ALIAS(sw0), gpios)
      
    static const nrfx_gpiote_t gpiote = NRFX_GPIOTE_INSTANCE(20);
    static const nrfx_timer_t m_timer = NRFX_TIMER_INSTANCE(00);
    static uint32_t state = 0;
    
    void timer_handler(nrf_timer_event_t event_type, void * p_context) {
      // Timer capture events do not generate interrupts when DPPI triggered.
      // But you can put debug here if needed.
    }
    
    static void gpiote_evt_handler(nrfx_gpiote_pin_t pin, nrfx_gpiote_trigger_t trigger, void *context) {
      const uint32_t timer1 = NRF_TIMER00->CC[0];
      if (state == 1) {
        state = 0;
        LOG_INF("Pressed. Times %u base: %p", timer1, m_timer.p_reg);
      } else {
        state = 1;
        LOG_INF("Released. Times %u base: %p", timer1, m_timer.p_reg);
      }
    }
    
    void pulse_width_init(void) {
      nrfx_err_t err;
      uint8_t in_channel;
      uint8_t ppi_channel;
    
      // ---------------------------------------
      // 1. INIT TIMER
      // ---------------------------------------
      nrfx_timer_config_t timer_cfg = {
        .frequency = NRF_TIMER_FREQ_1MHz,
        .mode = NRF_TIMER_TASK_COUNT,
        .bit_width = NRF_TIMER_BIT_WIDTH_32,
        .interrupt_priority = NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY,
        .p_context = NULL
      };
    
      nrfx_timer_enable(&m_timer);
      if ( ! nrfx_timer_is_enabled(&m_timer)) {
        LOG_ERR("Timer00 is not enabled");
        return;
      }
    
      // ---------------------------------------
      // 2. INIT GPIOTE
      // ---------------------------------------
      // Init not required as is handled by SDK
      err = nrfx_gpiote_channel_alloc(&gpiote, &in_channel);
      if (err != NRFX_SUCCESS) {
        LOG_ERR("Channel alloc fail: %x", err);
        return;
      }
    
      // Edge detection on both edges
    	static const nrf_gpio_pin_pull_t pull_config = NRF_GPIO_PIN_PULLUP;
      nrfx_gpiote_trigger_config_t trigger_config = {
        .trigger = NRFX_GPIOTE_TRIGGER_TOGGLE,
        .p_in_channel = &in_channel
      };
      static const nrfx_gpiote_handler_config_t handler_config = {
        .handler = gpiote_evt_handler
      };
      nrfx_gpiote_input_pin_config_t gpiote_cfg = {
    		.p_pull_config = &pull_config,
        .p_trigger_config = &trigger_config,
        .p_handler_config = &handler_config
      };
    
      err = nrfx_gpiote_input_configure(&gpiote, INPUT_PIN, &gpiote_cfg);
      if (err != NRFX_SUCCESS) {
        LOG_ERR("GPIOTE configure fail: %x", err);
        return;
      }
    
      nrfx_gpiote_trigger_enable(&gpiote, INPUT_PIN, true);
    
      // ---------------------------------------
      // 3. ALLOCATE DPPI channel
      // ---------------------------------------
      err = nrfx_gppi_channel_alloc(&ppi_channel);
      if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gppi_channel_alloc fail: %x", err);
        return;
      }
    
      // ---------------------------------------
      // 4. Connect GPIOTE.event → TIMER.capture task
      // ---------------------------------------
    
      // Bind both ends on DPPI
      nrfx_gppi_channel_endpoints_setup(ppi_channel, 
        nrfx_gpiote_in_event_address_get(&gpiote, INPUT_PIN),
        nrfx_timer_capture_task_address_get(&m_timer, 0)
      );
    
      // Enable channel
      nrfx_gppi_channels_enable(BIT(ppi_channel));
    
      LOG_INF("Pulse measurement started. Watching pin %d", INPUT_PIN);
    }
    
    int main(void) {
      pulse_width_init();
    
      while (1) {
        k_msleep(100);
      }
    
      return 0;
    }
    



    Here is sample logs from the code attached, showing button presses and timings look incorrect (not the datestamps - those are perfect).

    *** Booting nRF Connect SDK v3.1.1-e2a97fe2578a ***
    *** Using Zephyr OS v4.1.99-ff8f0c579eeb ***
    [00:00:00.001,209] <inf> nrfx_example: Pulse measurement started. Watching pin 45
    [00:00:00.001,325] <inf> nrfx_example: Released. Times 1176 base: 0x50055000
    [00:00:01.180,969] <inf> nrfx_example: Pressed. Times 3521 base: 0x50055000
    [00:00:01.320,792] <inf> nrfx_example: Released. Times 24419 base: 0x50055000
    [00:00:02.578,216] <inf> nrfx_example: Pressed. Times 7438 base: 0x50055000
    [00:00:02.697,775] <inf> nrfx_example: Released. Times 60640 base: 0x50055000
    [00:00:03.830,894] <inf> nrfx_example: Pressed. Times 17061 base: 0x50055000
    [00:00:03.938,171] <inf> nrfx_example: Released. Times 35699 base: 0x50055000
    [00:00:06.055,178] <inf> nrfx_example: Pressed. Times 48404 base: 0x50055000
    [00:00:06.206,043] <inf> nrfx_example: Released. Times 27602 base: 0x50055000
    [00:00:07.296,722] <inf> nrfx_example: Pressed. Times 31350 base: 0x50055000
    [00:00:07.424,409] <inf> nrfx_example: Released. Times 18876 base: 0x50055000
    [00:00:08.544,403] <inf> nrfx_example: Pressed. Times 61163 base: 0x50055000
    [00:00:08.659,503] <inf> nrfx_example: Released. Times 12448 base: 0x50055000
    [00:00:09.634,200] <inf> nrfx_example: Pressed. Times 58274 base: 0x50055000
    [00:00:09.756,236] <inf> nrfx_example: Released. Times 174 base: 0x50055000
    [00:00:10.729,384] <inf> nrfx_example: Pressed. Times 33053 base: 0x50055000
    [00:00:10.842,590] <inf> nrfx_example: Released. Times 34340 base: 0x50055000
    [00:00:11.944,797] <inf> nrfx_example: Pressed. Times 1296 base: 0x50055000
    [00:00:12.035,288] <inf> nrfx_example: Released. Times 14991 base: 0x50055000
    [00:00:13.360,826] <inf> nrfx_example: Pressed. Times 24773 base: 0x50055000
    [00:00:13.451,968] <inf> nrfx_example: Released. Times 43799 base: 0x50055000
    [00:00:14.676,443] <inf> nrfx_example: Pressed. Times 19919 base: 0x50055000
    [00:00:14.786,534] <inf> nrfx_example: Released. Times 61642 base: 0x50055000
    [00:00:15.852,600] <inf> nrfx_example: Pressed. Times 61795 base: 0x50055000
    [00:00:15.958,466] <inf> nrfx_example: Released. Times 3566 base: 0x50055000
    [00:00:17.703,178] <inf> nrfx_example: Pressed. Times 3073 base: 0x50055000
    [00:00:17.777,741] <inf> nrfx_example: Released. Times 18508 base: 0x50055000
    [00:00:19.314,699] <inf> nrfx_example: Pressed. Times 42638 base: 0x50055000
    [00:00:19.400,757] <inf> nrfx_example: Released. Times 20168 base: 0x50055000


    Any guidance or further direction here is really appreciated.

    Attached code:
    gpiote_basic.zip

Children
  • Hi,

     

    Here is a similar setup shared here, for the nrf53/91:

     RE: Efficient nrfx-based Quadrature Decoder for High-Speed Encoders on NRF5340 

    Which is based on this thread:

     Best way to measure time between falling/rising edges (NCS, Zephyr, NRF52840) 

     

    You should setup two GPIOTE IN channels with falling and rising edge detection, as per the description in the above thread.

     

    MR93 said:
    I'm also confused about initializing both a Timer and the GPIOTE instances - its seems Zephyr/NRF auto-initialises based on the Devicetree when the prj.conf file has `CONFIG_GPIO=y`. Setting `CONFIG_GPIO=n` to allow me to manually initialise doesn't work, because then I can't get the project to build - it throws errors for the timer and GPIOTE instances not existing at all. Which confuses me, as other examples I've found have `CONFIG_GPIO=n` set but still seem to be able to build without custom .dtsi files or similar. Bit over my head here, all quite confusing.

    You can void the return, or guard it with a "#if defined(CONFIG_GPIO)", to avoid this error being returned.

     

    Per now, your timer is in counter mode:

    .mode = NRF_TIMER_TASK_COUNT,

    You highly likely want it in NRF_TIMER_MODE_TIMER if you want to read out the amount of ticks/CC between press and release.

     

    Kind regards,

    Håkon

  • Thank you for your reply! It has been very helpful. The suggestion in these topics of using two channels for rising/falling events - is it possible to use `NRFX_GPIOTE_TRIGGER_TOGGLE` instead to have one event handler? I'm looking to capture the duration of each high and low, as opposed to just high, so I don't need to discern between rising/falling, but instead just capture the time between each.

    I got the timer working, and it's now incrementing correctly, but the calculations are still inaccurate. I am using a 1MHz resolution, and given that (assuming) 1 tick = 1us, I could calculate the microseconds between each edge.

    However, the numbers were wrong when cross-checked with my logic analyser. It's getting microsecond values, but they're incorrect, so I must be doing something wrong.

    At this moment in time, I'm trying to get a timer capture each time the pin changes and do math to get how long it remained high/low for before it changed again (time between each edge). Sometimes this value can be as small as 5us. I'm wondering if this is too accurate to be possible (however, I was able to achieve this using ESP32 S3 and RMT before porting the project to NRF54).


  • Hi,

     

    Could you try this?

    timed_signals_nrf54l_ncs3.1.0.zip

     

    MR93 said:
    However, the numbers were wrong when cross-checked with my logic analyser. It's getting microsecond values, but they're incorrect, so I must be doing something wrong.

    How incorrect are they? Can you share a couple of examples?

    Are they always off?

    MR93 said:
    Sometimes this value can be as small as 5us. I'm wondering if this is too accurate to be possible (however, I was able to achieve this using ESP32 S3 and RMT before porting the project to NRF54).

    DPPI + GPIOTE should be able to successfully detect this, but please note that if you have many pulses in a row in the low micro-second range, your CPU might be busy doing something else.

     

    Kind regards,

    Håkon

  • Thanks again for the code sample. I ran it on my dev kit and it built and uploaded fine. When initiatlising the timer, it shows the status "bad0000" but continues to work when toggling the buttons. Sometimes the match seems to be incorrect and it shows -1 for us, but most times it's working as intended.

    I ported this code to my own project (like for like) to see if it fared better measuring edges from the RF IC. I get the same bad0000 status, and this time the timer is constantly returning -1 for the us value (because the actual cc timer value is returning 0)

    It does seem that the quicker the pulse, it's not working correctly.

    I had totally forgotten, but I had to cut the solder jumpers to disable the external crystal to disable it for another (previous) project. So I'm using the internal crystal for this project, and have added the following to the prj.conf

    ```
    CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y
    CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=n
    CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC_CALIBRATION=y
    CONFIG_CLOCK_CONTROL_NRF_K32SRC_500PPM=y

    ```
    I didn't think this would have any affect, but maybe it does? I'm not sure. It's weird that I'm getting 0 for all the pulse width values. Again, the pin is strobbing various amounts between 5us to 500us on between each edge

    Update: I was tinkering further and manually calling nrfx_timer_capture seems to capture a timestamp, but it's not happening automatically it seems. At least, not as these speeds


  • Hi,

    MR93 said:

    I ported this code to my own project (like for like) to see if it fared better measuring edges from the RF IC. I get the same bad0000 status, and this time the timer is constantly returning -1 for the us value (because the actual cc timer value is returning 0)

    It does seem that the quicker the pulse, it's not working correctly.

    bad0000 is NRFX_SUCCESS, meaning that everything went as expected.

    Can you please try the .zip that I shared with you?

    Try setting a pulse of 5 us, then 100 ms, etc, and see if this is detected as expected.

    MR93 said:
    Update: I was tinkering further and manually calling nrfx_timer_capture seems to capture a timestamp, but it's not happening automatically it seems. At least, not as these speeds

    It is happening automatically in the code that I shared with you, but the CPU needs to fetch the register content in the ISR handler. Please note that the sample assumes active low triggering.

     

    Kind regards,

    Håkon

Related