Nordic nRF54L15-DK Zephyr SAADC using nrfx drivers and TIMER/PPI in Linux

Please refer to the previous article Nordic nRF54L15-DK Zephyr Linux Installation Steps to install the nRF54L15-DK Nordic Zephyr Dev installation needed to complete this. This article will describe how to interface to the Nordic nRF54L15-DK SAADC using nrfx drivers and TIMER/PPI. This is an advanced mode of the SAADC driver to measure an external voltage source (e.g., a battery, solar cell, voltage driven photoresistor dividers, wind speed sensor, etc) at a high sampling rate. Here a hardware TIMER instance is used to trigger sampling through DPPI/PPI, without any Central Processing Unit involvement. In order to do this, please create the following files in a project folder.

First, create the prj.conf

#
# Copyright (c) 2024 Nordic Semiconductor
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
#

CONFIG_LOG=y
# STEP 1 - Enable the ADC API and driver
CONFIG_ADC=y

The main.c application is the following,

/*
 * Copyright (c) 2024 Nordic Semiconductor ASA
 *
 * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
 */

#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(Output, LOG_LEVEL_DBG);

/* STEP 2 - Include header for nrfx drivers */
#include <nrfx_saadc.h>
#include <nrfx_timer.h>
#include <helpers/nrfx_gppi.h>
#if defined(DPPI_PRESENT)
#include <nrfx_dppi.h>
#else
#include <nrfx_ppi.h>
#endif

/* STEP 3.1 - Define the SAADC sample interval in microseconds */
#define SAADC_SAMPLE_INTERVAL_US 50

/* STEP 4.1 - Define the buffer size for the SAADC */
#define SAADC_BUFFER_SIZE   8000

/* STEP 4.6 - Declare the struct to hold the configuration for the SAADC channel used to sample the battery voltage */
#if NRF_SAADC_HAS_AIN_AS_PIN

#if defined(CONFIG_SOC_NRF54L15)
#define NRF_SAADC_INPUT_AIN4 NRF_PIN_PORT_TO_PIN_NUMBER(11U, 1)
#define SAADC_INPUT_PIN NRF_SAADC_INPUT_AIN4
#else
BUILD_ASSERT(0, "Unsupported device family");
#endif
#else 
#define SAADC_INPUT_PIN NRF_SAADC_INPUT_AIN0
#endif
static nrfx_saadc_channel_t channel = NRFX_SAADC_DEFAULT_CHANNEL_SE(SAADC_INPUT_PIN, 0);


/* STEP 3.2 - Declaring an instance of nrfx_timer for TIMER2. */
#if defined(CONFIG_SOC_NRF54L15)
#define TIMER_INSTANCE_NUMBER 22
#else
#define TIMER_INSTANCE_NUMBER 2
#endif
const nrfx_timer_t timer_instance = NRFX_TIMER_INSTANCE(TIMER_INSTANCE_NUMBER);

/* STEP 4.2 - Declare the buffers for the SAADC */
static int16_t saadc_sample_buffer[2][SAADC_BUFFER_SIZE];

/* STEP 4.3 - Declare variable used to keep track of which buffer was last assigned to the SAADC driver */
static uint32_t saadc_current_buffer = 0;

static void configure_timer(void)
{
    nrfx_err_t err;

    /* STEP 3.3 - Declaring timer config and intialize nrfx_timer instance. */
    nrfx_timer_config_t timer_config = NRFX_TIMER_DEFAULT_CONFIG(1000000);
    err = nrfx_timer_init(&timer_instance, &timer_config, NULL);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_timer_init error: %08x", err);
        return;
    }

    /* STEP 3.4 - Set compare channel 0 to generate event every SAADC_SAMPLE_INTERVAL_US. */
    uint32_t timer_ticks = nrfx_timer_us_to_ticks(&timer_instance, SAADC_SAMPLE_INTERVAL_US);
    nrfx_timer_extended_compare(&timer_instance, NRF_TIMER_CC_CHANNEL0, timer_ticks, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);

}

static void saadc_event_handler(nrfx_saadc_evt_t const * p_event)
{
    nrfx_err_t err;
    switch (p_event->type)
    {
        case NRFX_SAADC_EVT_READY:
        
           /* STEP 5.1 - Buffer is ready, timer (and sampling) can be started. */
            nrfx_timer_enable(&timer_instance);
            break;                        
            
        case NRFX_SAADC_EVT_BUF_REQ:
        
            /* STEP 5.2 - Set up the next available buffer. Alternate between buffer 0 and 1 */
            err = nrfx_saadc_buffer_set(saadc_sample_buffer[(saadc_current_buffer++)%2], SAADC_BUFFER_SIZE);
            //err = nrfx_saadc_buffer_set(saadc_sample_buffer[((saadc_current_buffer == 0 )? saadc_current_buffer++ : 0)], SAADC_BUFFER_SIZE);
            if (err != NRFX_SUCCESS) {
                LOG_ERR("nrfx_saadc_buffer_set error: %08x", err);
                return;
            }
            break;

        case NRFX_SAADC_EVT_DONE:

            /* STEP 5.3 - Buffer has been filled. Do something with the data and proceed */
            int64_t average = 0;
            int16_t max = INT16_MIN;
            int16_t min = INT16_MAX;
            int16_t current_value; 
            for(int i=0; i < p_event->data.done.size; i++){
                current_value = ((int16_t *)(p_event->data.done.p_buffer))[i];
                average += current_value;
                if(current_value > max){
                    max = current_value;
                }
                if(current_value < min){
                    min = current_value;
                }
            }
            average = average/p_event->data.done.size;
            LOG_INF("SAADC buffer at 0x%x filled with %d samples", (uint32_t)p_event->data.done.p_buffer, p_event->data.done.size);
            LOG_INF("AVG=%d, MIN=%d, MAX=%d", (int16_t)average, min, max);
            break;
        default:
            LOG_INF("Unhandled SAADC evt %d", p_event->type);
            break;
    }
}

static void configure_saadc(void)
{
    nrfx_err_t err;

    /* STEP 4.4 - Connect ADC interrupt to nrfx interrupt handler */
    IRQ_CONNECT(DT_IRQN(DT_NODELABEL(adc)),
                DT_IRQ(DT_NODELABEL(adc), priority),
                nrfx_isr, nrfx_saadc_irq_handler, 0);

    
    /* STEP 4.5 - Initialize the nrfx_SAADC driver */
    err = nrfx_saadc_init(DT_IRQ(DT_NODELABEL(adc), priority));
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_saadc_init error: %08x", err);
        return;
    }

    /* STEP 4.7 - Change gain config in default config and apply channel configuration */
#if defined(CONFIG_SOC_NRF54L15)
    channel.channel_config.gain = NRF_SAADC_GAIN1_4;
#else
    channel.channel_config.gain = NRF_SAADC_GAIN1_6;
#endif
    err = nrfx_saadc_channels_config(&channel, 1);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_saadc_channels_config error: %08x", err);
        return;
    }

    /* STEP 4.8 - Configure channel 0 in advanced mode with event handler (non-blocking mode) */
    nrfx_saadc_adv_config_t saadc_adv_config = NRFX_SAADC_DEFAULT_ADV_CONFIG;
    err = nrfx_saadc_advanced_mode_set(BIT(0),
                                        NRF_SAADC_RESOLUTION_12BIT,
                                        &saadc_adv_config,
                                        saadc_event_handler);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_saadc_advanced_mode_set error: %08x", err);
        return;
    }
                                            
    /* STEP 4.9 - Configure two buffers to make use of double-buffering feature of SAADC */
    err = nrfx_saadc_buffer_set(saadc_sample_buffer[0], SAADC_BUFFER_SIZE);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_saadc_buffer_set error: %08x", err);
        return;
    }
    err = nrfx_saadc_buffer_set(saadc_sample_buffer[1], SAADC_BUFFER_SIZE);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_saadc_buffer_set error: %08x", err);
        return;
    }

    /* STEP 4.10 - Trigger the SAADC. This will not start sampling, but will prepare buffer for sampling triggered through PPI */
    err = nrfx_saadc_mode_trigger();
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_saadc_mode_trigger error: %08x", err);
        return;
    }

}

static void configure_ppi(void)
{
    nrfx_err_t err;
    /* STEP 6.1 - Declare variables used to hold the (D)PPI channel number */
    uint8_t m_saadc_sample_ppi_channel;
    uint8_t m_saadc_start_ppi_channel;

    /* STEP 6.2 - Trigger task sample from timer */
    err = nrfx_gppi_channel_alloc(&m_saadc_sample_ppi_channel);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gppi_channel_alloc error: %08x", err);
        return;
    }

    err = nrfx_gppi_channel_alloc(&m_saadc_start_ppi_channel);
    if (err != NRFX_SUCCESS) {
        LOG_ERR("nrfx_gppi_channel_alloc error: %08x", err);
        return;
    }

    /* STEP 6.3 - Trigger task sample from timer */
    nrfx_gppi_channel_endpoints_setup(m_saadc_sample_ppi_channel, 
                                      nrfx_timer_compare_event_address_get(&timer_instance, NRF_TIMER_CC_CHANNEL0),
                                      nrf_saadc_task_address_get(NRF_SAADC, NRF_SAADC_TASK_SAMPLE));

    /* STEP 6.4 - Trigger task start from end event */
    nrfx_gppi_channel_endpoints_setup(m_saadc_start_ppi_channel, 
                                      nrf_saadc_event_address_get(NRF_SAADC, NRF_SAADC_EVENT_END),
                                      nrf_saadc_task_address_get(NRF_SAADC, NRF_SAADC_TASK_START));

    /* STEP 6.5 - Enable both (D)PPI channels */ 
    nrfx_gppi_channels_enable(BIT(m_saadc_sample_ppi_channel));
    nrfx_gppi_channels_enable(BIT(m_saadc_start_ppi_channel));
}


int main(void)
{
    configure_timer();
    configure_saadc();  
    configure_ppi();
    k_sleep(K_FOREVER);
}

Finally create the nrf54l15dk_nrf54l15.overlay overlay file is as follows,

/ {
	zephyr,user {
		io-channels = <&adc 0>;
	};
};

&adc {
	#address-cells = <1>;
	#size-cells = <0>;
	status = "okay";
	channel@0 {
		reg = <0>;
		zephyr,gain = "ADC_GAIN_1_4";
		zephyr,reference = "ADC_REF_INTERNAL";
		zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
		zephyr,input-positive = <NRF_SAADC_AIN4>; /* P1.11 for the nRF54L15 DK */
		zephyr,resolution = <14>;
	};
};

The resulting directory structure in your project folder as follows

├── CMakeLists.txt
├── nrf54l15dk_nrf54l15.overlay
├── prj.conf
└── src
    └── main.c

Now proceed to build this project as follows in the python virtual environment,

digikey_coffee_cup (venv) $  west build -p always -b nrf54l15dk/nrf54l15/cpuapp -- -DEXTRA_DTC_OVERLAY_FILE=nrf54l15dk_nrf54l15.overlay

Finally connect the nRF54L15-DK Nordic Development kit via the USB interface to flash it,

digikey_coffee_cup (venv) $  west flash
-- west flash: rebuilding
ninja: no work to do.
-- west flash: using runner nrfutil
-- runners.nrfutil: reset after flashing requested
Using board 001057777221
-- runners.nrfutil: Flashing file: /home/engineer/Digikey/Projects/Workspace/zephyr/zephyrproject/zephyr/adc/build/zephyr/zephyr.hex
-- runners.nrfutil: Connecting to probe
-- runners.nrfutil: Programming image
-- runners.nrfutil: Verifying image
-- runners.nrfutil: Reset
-- runners.nrfutil: Board(s) with serial number(s) 1057777221 flashed successfully.

To see the results of this project open a minicom terminal as follows,

digikey_coffee_cup $ minicom -D /dev/ttyACM1



Welcome to minicom 2.9

OPTIONS: I18n 
Port /dev/ttyACM1, 20:30:04

Press CTRL-A Z for help on special keys
*** Booting Zephyr OS build v4.2.0-5624-gfb7a74ebbd0a ***
[00:00:00.400,672] <inf> Output: SAADC buffer at 0x20000c28 filled with 8000 samples
[00:00:00.400,688] <inf> Output: AVG=-72, MIN=-100, MAX=148
[00:00:00.798,458] <inf> Output: SAADC buffer at 0x20004aa8 filled with 8000 samples
[00:00:00.798,464] <inf> Output: AVG=-72, MIN=-108, MAX=-56
[00:00:01.196,330] <inf> Output: SAADC buffer at 0x20000c28 filled with 8000 samples
[00:00:01.196,336] <inf> Output: AVG=-72, MIN=-100, MAX=-56
[00:00:01.594,142] <inf> Output: SAADC buffer at 0x20004aa8 filled with 8000 samples
[00:00:01.594,157] <inf> Output: AVG=-72, MIN=-100, MAX=-56
[00:00:01.991,977] <inf> Output: SAADC buffer at 0x20000c28 filled with 8000 samples
[00:00:01.991,983] <inf> Output: AVG=-72, MIN=-104, MAX=-52
[00:00:02.389,785] <inf> Output: SAADC buffer at 0x20004aa8 filled with 8000 samples
[00:00:02.389,791] <inf> Output: AVG=-72, MIN=-100, MAX=-56
[00:00:02.787,606] <inf> Output: SAADC buffer at 0x20000c28 filled with 8000 samples
[00:00:02.787,622] <inf> Output: AVG=-72, MIN=-104, MAX=-56
[00:00:03.185,387] <inf> Output: SAADC buffer at 0x20004aa8 filled with 8000 samples
[00:00:03.185,393] <inf> Output: AVG=-72, MIN=-96, MAX=-56
[00:00:03.583,139] <inf> Output: SAADC buffer at 0x20000c28 filled with 8000 samples
[00:00:03.583,145] <inf> Output: AVG=-72, MIN=-100, MAX=-56
[00:00:03.980,978] <inf> Output: SAADC buffer at 0x20004aa8 filled with 8000 samples
[00:00:03.980,993] <inf> Output: AVG=-72, MIN=-104, MAX=-52
[00:00:04.378,823] <inf> Output: SAADC buffer at 0x20000c28 filled with 8000 samples
[00:00:04.378,829] <inf> Output: AVG=-72, MIN=-100, MAX=-60
[00:00:04.776,526] <inf> Output: SAADC buffer at 0x20004aa8 filled with 8000 samples
[00:00:04.776,532] <inf> Output: AVG=-72, MIN=-100, MAX=-56
[00:00:05.174,313] <inf> Output: SAADC buffer at 0x20000c28 filled with 8000 samples
[00:00:05.174,328] <inf> Output: AVG=-72, MIN=-104, MAX=-56


Observe that the filled buffer used in the nRF54L15-DK program alternates between two different locations, each corresponding to the two buffers defined. The maximum, average and minima are displayed for each collection of samples in the buffers. The nRF54L15-DK available at DigiKey

image

is an excellent development platform for many demanding IoT applications.

Have a great day!

Este artículo esta disponible en español aquí,

This article is available in spanish here.

1 Like