DPPI and GPIOTE BLE events on nRF54L15

Hi all,

I'm working on BLE time synchronization between nodes using hardware timestamps via GRTC + DPPI + PPIB cross-domain. 
CRCOK and PHYEND work perfectly, but adding TXREADY/RXREADY or GPIOTE toggle fails.

My working paths are now: 
CRCOK_DPPIC10_CH(9) → PPIB11→PPIB21 → DPPIC20_CH(6) → GRTC CC
PHYEND_DPPIC20_CH(5) → GRTC CC

When TXReady is added, both PHYEND and TXReady show the same timestamp. Because of this i'm not sure if the correct event is timestamped.

Here is the code for timestamping:

#include "dppi_ts.h"

#include <hal/nrf_dppi.h>
#include <hal/nrf_grtc.h>
#include <hal/nrf_ppib.h>
#include <hal/nrf_radio.h>
#include <nrfx_grtc.h>
#include <zephyr/drivers/timer/nrf_grtc_timer.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(dppi_ts, LOG_LEVEL_INF);

#define GRTC_CC_RX 10U
#define GRTC_CC_TX 11U
#define GRTC_CC_RXREADY 8U
#define GRTC_CC_TXREADY 9U

#define APP_CRCOK_DPPIC10_CH 9U
#define APP_PHYEND_DPPIC20_CH 5U
#define APP_CRCOK_DPPIC20_CH 6U

#define APP_RXREADY_DPPIC10_CH 7U
#define APP_RXREADY_DPPIC20_CH 8U
#define APP_TXREADY_DPPIC10_CH 10U
#define APP_TXREADY_DPPIC20_CH 11U

static uint8_t s_mpsl_phyend_ch;

static void tx_bridge_init(void) {
  s_mpsl_phyend_ch =
      (uint8_t)(NRF_RADIO->PUBLISH_PHYEND & DPPIC_SUBSCRIBE_CHG_EN_CHIDX_Msk);

  LOG_DBG("TX bridge: MPSL PHYEND on DPPIC10 ch%u (runtime discovered)",
          s_mpsl_phyend_ch);

  NRF_DPPI_ENDPOINT_SETUP(
      nrf_ppib_task_address_get(NRF_PPIB11,
                                nrf_ppib_send_task_get(s_mpsl_phyend_ch)),
      s_mpsl_phyend_ch);

  NRF_DPPI_ENDPOINT_SETUP(
      nrf_ppib_event_address_get(NRF_PPIB21,
                                 nrf_ppib_receive_event_get(s_mpsl_phyend_ch)),
      APP_PHYEND_DPPIC20_CH);

  nrf_grtc_subscribe_set(NRF_GRTC, NRF_GRTC_TASK_CAPTURE_11,
                         APP_PHYEND_DPPIC20_CH);

  nrf_dppi_channels_enable(NRF_DPPIC10, BIT(s_mpsl_phyend_ch));
  nrf_dppi_channels_enable(NRF_DPPIC20, BIT(APP_PHYEND_DPPIC20_CH));

  LOG_DBG("TX bridge armed: DPPIC10 ch%u -> PPIB11->PPIB21 -> DPPIC20 ch%u"
          " -> GRTC CC[%u]",
          s_mpsl_phyend_ch, APP_PHYEND_DPPIC20_CH, GRTC_CC_TX);
}

static void rx_bridge_init(void) {
  nrf_radio_publish_set(NRF_RADIO, NRF_RADIO_EVENT_CRCOK, APP_CRCOK_DPPIC10_CH);

  NRF_DPPI_ENDPOINT_SETUP(
      nrf_ppib_task_address_get(NRF_PPIB11,
                                nrf_ppib_send_task_get(APP_CRCOK_DPPIC10_CH)),
      APP_CRCOK_DPPIC10_CH);

  NRF_DPPI_ENDPOINT_SETUP(
      nrf_ppib_event_address_get(
          NRF_PPIB21, nrf_ppib_receive_event_get(APP_CRCOK_DPPIC10_CH)),
      APP_CRCOK_DPPIC20_CH);

  nrf_grtc_subscribe_set(NRF_GRTC, NRF_GRTC_TASK_CAPTURE_10,
                         APP_CRCOK_DPPIC20_CH);

  LOG_DBG("RX bridge configured: DPPIC10 ch%u -> PPIB11->PPIB21 ->"
          " DPPIC20 ch%u -> GRTC CC[%u] (disarmed)",
          APP_CRCOK_DPPIC10_CH, APP_CRCOK_DPPIC20_CH, GRTC_CC_RX);
}

static void rxready_bridge_init(void) {
  nrf_radio_publish_set(NRF_RADIO, NRF_RADIO_EVENT_RXREADY,
                        APP_RXREADY_DPPIC10_CH);

  NRF_DPPI_ENDPOINT_SETUP(
      nrf_ppib_task_address_get(NRF_PPIB11,
                                nrf_ppib_send_task_get(APP_RXREADY_DPPIC10_CH)),
      APP_RXREADY_DPPIC10_CH);

  NRF_DPPI_ENDPOINT_SETUP(
      nrf_ppib_event_address_get(
          NRF_PPIB21, nrf_ppib_receive_event_get(APP_RXREADY_DPPIC10_CH)),
      APP_RXREADY_DPPIC20_CH);

  nrf_grtc_subscribe_set(NRF_GRTC, NRF_GRTC_TASK_CAPTURE_8,
                         APP_RXREADY_DPPIC20_CH);

  LOG_DBG("RXREADY bridge configured: DPPIC10 ch%u -> PPIB11->PPIB21 -> "
          "DPPIC20 ch%u -> GRTC CC[%u]",
          APP_RXREADY_DPPIC10_CH, APP_RXREADY_DPPIC20_CH, GRTC_CC_RXREADY);
}

static uint8_t s_mpsl_txready_ch;
static bool s_txready_from_mpsl;

static void txready_bridge_init(void) {
  uint32_t pub = NRF_RADIO->PUBLISH_TXREADY;

  if (pub & DPPIC_SUBSCRIBE_CHG_EN_EN_Msk) {
    s_mpsl_txready_ch = (uint8_t)(pub & DPPIC_SUBSCRIBE_CHG_EN_CHIDX_Msk);
    s_txready_from_mpsl = true;

    NRF_DPPI_ENDPOINT_SETUP(
        nrf_ppib_task_address_get(NRF_PPIB11,
                                  nrf_ppib_send_task_get(s_mpsl_txready_ch)),
        s_mpsl_txready_ch);

    NRF_DPPI_ENDPOINT_SETUP(
        nrf_ppib_event_address_get(
            NRF_PPIB21, nrf_ppib_receive_event_get(s_mpsl_txready_ch)),
        APP_TXREADY_DPPIC20_CH);

    nrf_grtc_subscribe_set(NRF_GRTC, NRF_GRTC_TASK_CAPTURE_9,
                           APP_TXREADY_DPPIC20_CH);

    nrf_dppi_channels_enable(NRF_DPPIC10, BIT(s_mpsl_txready_ch));
    nrf_dppi_channels_enable(NRF_DPPIC20, BIT(APP_TXREADY_DPPIC20_CH));
  } else {
    s_txready_from_mpsl = false;

    nrf_radio_publish_set(NRF_RADIO, NRF_RADIO_EVENT_TXREADY,
                          APP_TXREADY_DPPIC10_CH);

    NRF_DPPI_ENDPOINT_SETUP(
        nrf_ppib_task_address_get(
            NRF_PPIB11, nrf_ppib_send_task_get(APP_TXREADY_DPPIC10_CH)),
        APP_TXREADY_DPPIC10_CH);

    NRF_DPPI_ENDPOINT_SETUP(
        nrf_ppib_event_address_get(
            NRF_PPIB21, nrf_ppib_receive_event_get(APP_TXREADY_DPPIC10_CH)),
        APP_TXREADY_DPPIC20_CH);

    nrf_grtc_subscribe_set(NRF_GRTC, NRF_GRTC_TASK_CAPTURE_9,
                           APP_TXREADY_DPPIC20_CH);
  }

  LOG_DBG("TXREADY bridge configured -> GRTC CC[%u]", GRTC_CC_TXREADY);
}

/* ─── Public API ─────────────────────────────────────────────────────────── */

int dppi_ts_init(void) {
  NRF_GRTC->CC[GRTC_CC_RX].CCEN = 0UL;
  NRF_GRTC->CC[GRTC_CC_TX].CCEN = 0UL;
  NRF_GRTC->CC[GRTC_CC_RXREADY].CCEN = 0UL;
  NRF_GRTC->CC[GRTC_CC_TXREADY].CCEN = 0UL;

  tx_bridge_init();
  rx_bridge_init();
  rxready_bridge_init();
  txready_bridge_init();

  LOG_DBG("DPPI timestamp init OK  CRCOK->CC[%u] (RX)  PHYEND->CC[%u] (TX)  "
          "RXREADY->CC[%u] (RXREADY)  TXREADY->CC[%u] (TXREADY)",
          GRTC_CC_RX, GRTC_CC_TX, GRTC_CC_RXREADY, GRTC_CC_TXREADY);
  return 0;
}

void dppi_ts_arm_rx(void) {
  NRF_GRTC->CC[GRTC_CC_RX].CCEN = 0UL;
  NRF_GRTC->CC[GRTC_CC_RXREADY].CCEN = 0UL;

  nrf_dppi_channels_disable(NRF_DPPIC10, BIT(APP_CRCOK_DPPIC10_CH) |
                                             BIT(APP_RXREADY_DPPIC10_CH));
  nrf_dppi_channels_disable(NRF_DPPIC20, BIT(APP_CRCOK_DPPIC20_CH) |
                                             BIT(APP_RXREADY_DPPIC20_CH));
  nrf_dppi_channels_enable(NRF_DPPIC10, BIT(APP_CRCOK_DPPIC10_CH) |
                                            BIT(APP_RXREADY_DPPIC10_CH));
  nrf_dppi_channels_enable(NRF_DPPIC20, BIT(APP_CRCOK_DPPIC20_CH) |
                                            BIT(APP_RXREADY_DPPIC20_CH));
}

void dppi_ts_reset_tx(void) {
  NRF_GRTC->CC[GRTC_CC_TX].CCEN = 0UL;
  NRF_GRTC->CC[GRTC_CC_TXREADY].CCEN = 0UL;
}

void dppi_ts_disarm_rx(void) {
  nrf_dppi_channels_disable(NRF_DPPIC10, BIT(APP_CRCOK_DPPIC10_CH));
  nrf_dppi_channels_disable(NRF_DPPIC20, BIT(APP_CRCOK_DPPIC20_CH));
}

uint64_t dppi_ts_get_rx_us(void) {
  uint64_t ts = nrf_grtc_sys_counter_cc_get(NRF_GRTC, GRTC_CC_RX);
  if (ts == 0) {
    LOG_WRN("RX CC[%u] read failed — channel may not have captured yet",
            GRTC_CC_RX);
  }
  return ts;
}

uint64_t dppi_ts_get_tx_us(void) {
  uint64_t ts = nrf_grtc_sys_counter_cc_get(NRF_GRTC, GRTC_CC_TX);
  if (ts == 0) {
    LOG_WRN("TX CC[%u] read failed — channel may not have captured yet",
            GRTC_CC_TX);
  }
  return ts;
}

uint64_t dppi_ts_get_rxready_us(void) {
  return nrf_grtc_sys_counter_cc_get(NRF_GRTC, GRTC_CC_RXREADY);
}

uint64_t dppi_ts_get_txready_us(void) {
  return nrf_grtc_sys_counter_cc_get(NRF_GRTC, GRTC_CC_TXREADY);
}

uint64_t dppi_ts_now_us(void) { return nrfx_grtc_syscounter_get(); }

Here is an example of how txready and phyend are called:

dppi_ts_reset_tx();
  bt_le_ext_adv_start(adv_set, BT_LE_EXT_ADV_START_DEFAULT);

  uint64_t txready = 0;
  uint64_t first_tx = 0;
  for (int i = 0; i < 5000; i++) {
    txready = dppi_ts_get_txready_us();
    first_tx = dppi_ts_get_tx_us();
    if (first_tx > t1_start) {
      break;
    }
    k_busy_wait(10);
  }
  bt_le_ext_adv_stop(adv_set);
  bt_le_ext_adv_delete(adv_set);

Questions:

  1. Why do TXREADY/RXREADY timestamps collide with PHYEND?

  2. Are the DPPI paths correctly set up?

  3. Are there any examples of BLE + GRTC on nRF54L15?


Additionally i want to add GPIO toggling to check timings with an analog analyser. Are there any examples on this?

Parents Reply Children
No Data
Related