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
Parents
  • Hi Jeremy, 

    Unlike RTC all GRTC compare channels are one shot. Once they are fired, then it seems that there is no automatic (no CPU involved) way to reload them. You wired

    CC1 → PPI → GPIOTE.TASKS_SET
    CC2 → PPI → GPIOTE.TASKS_CLR

    and expected the CCs to keep firing. But since CC1/CC2 are one-shot, they only generate their events the first time the counter passes those values. They don’t reset or reload, so your LED toggles just once up and once down, then nothing.

    One option could be that you could use the inbuilt PWM inside GRTC and configure the PWMCONFIG with the period and duty and then issue a TASKS_START, hardware should be able to now autotoggle CC0/CC1 for you yielding single periodic PWM channel with no CPU intervention.

    One less confident trial that you can do is Use one PPI channel to link EVENTS_COMPARE[n] → TASKS_CLEAR. Use another PPI to link EVENTS_COMPARE[n] → GPIOTE.TASKS_SET/CLR and hoping that clearing the counter restarts the cycle and lets the same CC[n] fire again.

    Otherwise I cannot think of anyway where we can do this without CPU involvement.

Reply
  • Hi Jeremy, 

    Unlike RTC all GRTC compare channels are one shot. Once they are fired, then it seems that there is no automatic (no CPU involved) way to reload them. You wired

    CC1 → PPI → GPIOTE.TASKS_SET
    CC2 → PPI → GPIOTE.TASKS_CLR

    and expected the CCs to keep firing. But since CC1/CC2 are one-shot, they only generate their events the first time the counter passes those values. They don’t reset or reload, so your LED toggles just once up and once down, then nothing.

    One option could be that you could use the inbuilt PWM inside GRTC and configure the PWMCONFIG with the period and duty and then issue a TASKS_START, hardware should be able to now autotoggle CC0/CC1 for you yielding single periodic PWM channel with no CPU intervention.

    One less confident trial that you can do is Use one PPI channel to link EVENTS_COMPARE[n] → TASKS_CLEAR. Use another PPI to link EVENTS_COMPARE[n] → GPIOTE.TASKS_SET/CLR and hoping that clearing the counter restarts the cycle and lets the same CC[n] fire again.

    Otherwise I cannot think of anyway where we can do this without CPU involvement.

Children
No Data
Related