nRF54L15 GRTC -> GPPI -> GPIOTE pipeline

I've been working with the nRF54L15 for a bit now and it's been great, but trying to get a bit fancy now and running into problems. I'm developing with the latest NCS v3.0.2 release.

Below, find code I've written trying to set up pwm control so I can provide PWM dimming of an LED. To make it extra complicated, this is for a battery powered device and I need to maintain low power operation and keep HFClk disabled as much as possible.

I believe my 'task' is set up correctly, and calling 'nrfx_gpiote_clr_task_trigger' and 'nrfx_gpiote_clr_task_trigger' manually I'm able to toggle the output pin state, but it doesn't seem to be firing correctly when hooked up to run automatically.

Additionally, if I define a temporary 'nrfx_grtc_cc_handler_t' handler and pass that into one of the 'nrfx_grtc_syscounter_cc_absolute_set' calls with 'enable_irq' set to True, I do see that the callback is being hit, but seemingly only two times following initialization then never again.

pwm_led.c
#include <zephyr/kernel.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>

#include <nrfx_grtc.h>
#include <nrfx_gpiote.h>
#include <helpers/nrfx_gppi.h>

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(led_app, LOG_LEVEL_INF);

#define LED_NODE            DT_ALIAS(led0)
#define GPIO_PORT(node)     DT_PROP_BY_PHANDLE_IDX(node, gpios, 0, port)
#define GPIO_PIN_NUM(node)  DT_GPIO_PIN(node, gpios)
#define LED_PIN_RAW         NRF_GPIO_PIN_MAP(GPIO_PORT(LED_NODE), GPIO_PIN_NUM(LED_NODE))

/* GPIOTE instance */
static const nrfx_gpiote_t gpiote_inst = NRFX_GPIOTE_INSTANCE(20);
/* Kernel timer for 1s pulse */
static struct k_timer pulse_duration_timer;
/* GPPI channels for SET, CLR, and GRTC reset */
static uint8_t gppi_ch_set;
static uint8_t gppi_ch_clr;
static uint8_t gppi_ch_reset_timer;
/* GRTC CC channels for period and duty */
static uint8_t m_cc_period;
static uint8_t m_cc_duty;
/* GPIOTE event channel */
static uint8_t gpiote_ch;

/* PWM constants using the 1 MHz SYSCOUNTER frequency */
#define PWM_SYSCOUNTER_FREQ   1000000U //
#define PWM_FREQUENCY_HZ      256U
#define PWM_PERIOD_TICKS      (PWM_SYSCOUNTER_FREQ / PWM_FREQUENCY_HZ)
#define DUTY_CYCLE_PERCENT    80U
#define PWM_DUTY_TICKS        ((PWM_PERIOD_TICKS * DUTY_CYCLE_PERCENT) / 100U)

/**
 * Stop PWM, disable GPPI and turn LED off
 */
static void pwm_stop(struct k_timer *timer_id) {
    ARG_UNUSED(timer_id);
    // Disable only the GPPI channels controlling the GPIO pin
    nrfx_gppi_channels_disable(BIT(gppi_ch_set) | BIT(gppi_ch_clr));
    nrfx_gpiote_clr_task_trigger(&gpiote_inst, LED_PIN_RAW);
}

/**
 * Trigger a 1-second PWM pulse on the LED.
 */
void pulse_led_for_one_second(void) {
    // Enable the GPPI channels to start the PWM output
    nrfx_gppi_channels_enable(BIT(gppi_ch_set) | BIT(gppi_ch_clr));
    k_timer_start(&pulse_duration_timer, K_SECONDS(1), K_NO_WAIT);
}

/**
 * Initialize GRTC and GPPI for self-running PWM.
 */
static void pwm_generator_init(void) {
    nrfx_err_t err;

    /* Initialize GRTC */
    if (!nrfx_grtc_init_check()) {
        err = nrfx_grtc_init(NRFX_GRTC_DEFAULT_CONFIG_IRQ_PRIORITY);
        if (err != NRFX_SUCCESS) {
            LOG_ERR("nrfx_grtc_init error: %d", err);
        }
    }

    /* Initialize GPIOTE */
    if (!nrfx_gpiote_init_check(&gpiote_inst)) {
        err = nrfx_gpiote_init(&gpiote_inst, NRFX_GPIOTE_DEFAULT_CONFIG_IRQ_PRIORITY);
        if (err != NRFX_SUCCESS) {
            LOG_ERR("nrfx_gpiote_init error: %d", err);
        }
    }

    /* Allocate GRTC channels */
    err = nrfx_grtc_channel_alloc(&m_cc_period);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_grtc_channel_alloc (period) error: %d", err);
    }
    err = nrfx_grtc_channel_alloc(&m_cc_duty);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_grtc_channel_alloc (duty) error: %d", err);
    }

    /* Allocate GPIOTE channel */
    err = nrfx_gpiote_channel_alloc(&gpiote_inst, &gpiote_ch);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gpiote_channel_alloc error: %d", err);
    }

    /* Allocate GPPI channels */
    err = nrfx_gppi_channel_alloc(&gppi_ch_set);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gppi_channel_alloc (set) error: %d", err);
    }
    err = nrfx_gppi_channel_alloc(&gppi_ch_clr);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gppi_channel_alloc (clr) error: %d", err);
    }
    err = nrfx_gppi_channel_alloc(&gppi_ch_reset_timer);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gppi_channel_alloc (reset_timer) error: %d", err);
    }

    /* Configure GPIOTE task */
    nrfx_gpiote_output_config_t out_cfg = NRFX_GPIOTE_DEFAULT_OUTPUT_CONFIG;
    nrfx_gpiote_task_config_t task_cfg = {
        .task_ch  = gpiote_ch, .polarity = NRF_GPIOTE_POLARITY_TOGGLE, .init_val = NRF_GPIOTE_INITIAL_VALUE_LOW,
    };
    err = nrfx_gpiote_output_configure(&gpiote_inst, LED_PIN_RAW, &out_cfg, &task_cfg);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gpiote_output_configure error: %d", err);
    }
    nrfx_gpiote_out_task_enable(&gpiote_inst, LED_PIN_RAW);

    /* Set absolute compare values */
    nrfx_grtc_channel_t period_ch = { .channel = m_cc_period };
    err = nrfx_grtc_syscounter_cc_absolute_set(&period_ch, PWM_PERIOD_TICKS, false);
    NRFX_ASSERT(err == NRFX_SUCCESS);

    nrfx_grtc_channel_t duty_ch = { .channel = m_cc_duty };
    err = nrfx_grtc_syscounter_cc_absolute_set(&duty_ch, PWM_DUTY_TICKS, false);
    NRFX_ASSERT(err == NRFX_SUCCESS);
    
    /* Publish the GRTC events so GPPI can subscribe to them */
    nrf_grtc_event_t evt_period_enum = nrf_grtc_sys_counter_compare_event_get(m_cc_period);
    nrf_grtc_event_t evt_duty_enum   = nrf_grtc_sys_counter_compare_event_get(m_cc_duty);

    nrf_grtc_publish_set(NRF_GRTC, evt_period_enum, gppi_ch_set);
    nrf_grtc_publish_set(NRF_GRTC, evt_period_enum, gppi_ch_reset_timer);
    nrf_grtc_publish_set(NRF_GRTC, evt_duty_enum, gppi_ch_clr);

    /* Map GRTC events to tasks via GPPI endpoints */
    uint32_t evt_period_addr = nrfx_grtc_event_address_get(evt_period_enum);
    uint32_t evt_duty_addr   = nrfx_grtc_event_address_get(evt_duty_enum);
    
    uint32_t task_set   = nrfx_gpiote_set_task_address_get(&gpiote_inst, LED_PIN_RAW);
    uint32_t task_clr   = nrfx_gpiote_clr_task_address_get(&gpiote_inst, LED_PIN_RAW);
    uint32_t task_grtc_clear = nrfx_grtc_task_address_get(NRF_GRTC_TASK_CLEAR);

    nrfx_gppi_channel_endpoints_setup(gppi_ch_set, evt_period_addr, task_set);
    nrfx_gppi_channel_endpoints_setup(gppi_ch_clr, evt_duty_addr, task_clr);
    nrfx_gppi_channel_endpoints_setup(gppi_ch_reset_timer, evt_period_addr, task_grtc_clear);

    /* Enable the compare channel event generation */
    nrf_grtc_sys_counter_compare_event_enable(NRF_GRTC, m_cc_period);
    nrf_grtc_sys_counter_compare_event_enable(NRF_GRTC, m_cc_duty);    

    /* Enable the self-resetting mechanism (permanently) */
    nrfx_gppi_channels_enable(BIT(gppi_ch_reset_timer));

    /* Initialize kernel timer to stop the pulse */
    k_timer_init(&pulse_duration_timer, pwm_stop, NULL);

    /* Attempt to clear the gtrc to get a clean edge to start from */
    err = nrfx_grtc_action_perform(NRFX_GRTC_ACTION_CLEAR);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_grtc_action_perform fail: %d", err);
    }
}

/**
 * Initialize the low power PWM LED controller, then use 'pulse_led_for_one_second' to trigger it
 * to be on with the specified duty cycle for 1s.
 */
void led_init(void)
{
    pwm_generator_init();
}


I am using a custom board, but including "<nordic/nrf54l15_cpuapp.dtsi>" which enables all the 'dppic??' and 'ppib??' peripherals. I also have the following relevant parts in my dts file:
/ {
	aliases {
		led0 = &p1_7_led;
    };

    leds {
        compatible = "gpio-leds";
        p1_7_led: p1_7_led {
            gpios = <&gpio1 7 GPIO_ACTIVE_HIGH>;
            label = "GPPI-Controlled LED";
        };
    };
};

&grtc {
	owned-channels = <0 1 2 3 4 5 6 7 8 9 10 11>;
	/* Channels 7-11 reserved for Zero Latency IRQs, 3-4 for FLPR */
	child-owned-channels = <3 4 7 8 9 10 11>;
	status = "okay";
};



If anyone has any insight as to what might be going on here, I would love to figure this out.

Thanks,
Jeremy
Related