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
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.
