Feedback
Feedback

If you are having issues with the exercises, please create a ticket on DevZone: devzone.nordicsemi.com
Click or drag files to this area to upload. You can upload up to 2 files.

Exercise 3 – Interfacing with ADC using nrfx drivers and TIMER/PPI

v2.8.x – v2.7.0

In this exercise, we will explore the advanced mode of the SAADC driver to measure voltage source (E.g., a battery) at a high sample rate. We will use a hardware TIMER instance to trigger sampling through DPPI/PPI, without any CPU involvement.

Exercise steps

Open the code base of the exercise by navigating to Create a new application in the nRF Connect for VS Code extension, select Copy a sample, and search for Lesson 6 – Exercise 3.

Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l6/l6_e3.

1. Enable the SAADC driver.

Enable the driver by adding the following lines into the application configuration file prj.conf.

CONFIG_NRFX_SAADC=y
CONFIG_NRFX_PPI=y
CONFIG_NRFX_DPPI=y
Kconfig

We also need to enable the TIMER instance that will be used. Available TIMER instances differ between targets. For nRF52/nRF53/nRF91, we will use TIMER2, for nRF54L15, we will use TIMER22. This is done by enabling it in prj.conf:

CONFIG_NRFX_TIMER2=y
Kconfig
CONFIG_NRFX_TIMER22=y
Kconfig

Important

When building the project, you will see a warning that either PPI or DPPI was assigned the value ‘y’, but got the value ‘n’, depending on which chip you build for. You can ignore this, but it can be resolved by creating separate Kconfig fragment files for the different boards. In this exercise, we have put all Kconfigs in one common prj.conf file for simplicity.

2. Include the nrfx-related header files in the application.

We will use the nrfx SAADC, TIMER and (D)PPI drivers to configure the peripherals.

nRF52 Series chips have a PPI peripheral, while nRF54, nRF53 and nRF91 Series have a more flexible DPPI peripheral. nrfx provides a helper API (nrfx GPPI) that can be used to configure both peripheral variants through a common API. Add nrfx-related header files by including the following lines at the top (include section) of main.c:

#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
C

3. Configure the TIMER used to trigger the sampling of the SAADC.

3.1 In the define section close to the top of main.c, define the SAADC sample interval:

#define SAADC_SAMPLE_INTERVAL_US 50
C

3.2 Declaring an instance of nrfx_timer_t for the correct TIMER instance. For nRF52/nRF53/nRF91, we use TIMER2. For nRF54L15, we use TIMER22.

#define TIMER_INSTANCE_NUMBER 2
const nrfx_timer_t timer_instance = NRFX_TIMER_INSTANCE(TIMER_INSTANCE_NUMBER);
C
#define TIMER_INSTANCE_NUMBER 22
const nrfx_timer_t timer_instance = NRFX_TIMER_INSTANCE(TIMER_INSTANCE_NUMBER);
C

3.3 In the function configure_timer(), declare the timer config struct. The parameter passed to NRFX_TIMER_DEFAULT_CONFIG will set the frequency of the timer. The timer config is used to initialize the instance of the timer driver. Since we will use (D)PPI to trigger sampling, no interrupts are needed, and we pass NULL to the event handler parameter:

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;
}
C

3.4 We will use the COMPARE0 event from the TIMER to trigger sampling at the interval given by SAADC_SAMPLE_INTERVAL_US. The TIMER will be cleared every time the COMPARE0 event is hit, to create a recurring timer event. Add these lines to the end of the function configure_timer():

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);
C

4. Configure the SAADC driver.

4.1 In this example, we will use double buffering, which requires two separate buffers that will be filled with samples sequentially. One buffer will be filled while the other buffer can be processed. Since we have a short sample interval, the buffer needs to be large enough for the CPU to start up and process the previous buffer before the new buffer is filled. Start by defining the buffer size for each of the buffers by adding this line close to the top of main.c:

#define SAADC_BUFFER_SIZE   8000
C

4.2 Next, let’s declare the two buffers by adding this line below the define section in main.c

static int16_t saadc_sample_buffer[2][SAADC_BUFFER_SIZE];
C

4.3 To keep track of which of the two buffers should be assigned to the SAADC driver, we declare a variable corresponding to the current buffer index:

static uint32_t saadc_current_buffer = 0;
C

4.4 We will reference the ADC defined in the Zephyr devicetree, to make the code more portable. To connect the SAADC interrupt to SAADC interrupt handler, add these lines in configure_saadc():

IRQ_CONNECT(DT_IRQN(DT_NODELABEL(adc)),
            DT_IRQ(DT_NODELABEL(adc), priority),
            nrfx_isr, nrfx_saadc_irq_handler, 0);
C

By default, the ADC is enabled in the board DTS file for all DKs supported by this course, but for custom boards you may have to enable it in your DTS or overlay file using the following code snippet:

&adc {
    status = "okay";
};
Devicetree

4.5 Before using the SAADC driver, the driver instance must be initialized. We will again refer to the devicetree to get the configured priority of the ADC node and use this for the driver:

err = nrfx_saadc_init(DT_IRQ(DT_NODELABEL(adc), priority));
if (err != NRFX_SUCCESS) {
    LOG_ERR("nrfx_saadc_init error: %08x", err);
    return;
}
C

4.6 Declare the struct to hold the configuration for the SAADC channel used to sample the battery voltage. The macro NRFX_SAADC_DEFAULT_CHANNEL_SE() will create the default configuration struct for a single ended input for the provided analog input on channel 0.

The way of configuring pins for SAADC channels have changed for nRF54L15. Chose the tab below that matches your DK.

We will use AIN0 for all Nordic DKs , except the nRF54L15 DK.

#define SAADC_INPUT_PIN NRF_SAADC_INPUT_AIN0
static nrfx_saadc_channel_t channel = NRFX_SAADC_DEFAULT_CHANNEL_SE(SAADC_INPUT_PIN, 0);
C

On the nRF54L15 DK, we will use AIN4 instead of the AIN0 as AIN0 (P1.04) pin is used by default for UART.

#define NRF_SAADC_INPUT_AIN4 NRF_PIN_PORT_TO_PIN_NUMBER(11U, 1)
#define SAADC_INPUT_PIN NRF_SAADC_INPUT_AIN4
static nrfx_saadc_channel_t channel = NRFX_SAADC_DEFAULT_CHANNEL_SE(SAADC_INPUT_PIN, 0);
C

Connect a battery between GND and analog input (AIN0 or AIN4, depending on your target). Check the Hardware and Layout ->Pin assignment chapter in the Product specification to know which Pin is connected to the analog inputs on your choice of SoC/SiP. You can also connect a jumper wire between analog input 0 and VDD if you do not have a battery available.

4.7 Configure the SAADC channel using the previously defined channel configuration structure. The default configuration uses GAIN=1, which is too high to support supply voltage measurements. We need to change the gain config before configuring the channel:

The GAIN steps are different for nRF54L15 compared to previous SoCs. Chose the tab below that matches with your DK.

Any Nordic development kit ohter than the nRF54L15 DK

channel.channel_config.gain = NRF_SAADC_GAIN1_6;
err = nrfx_saadc_channels_config(&channel, 1);
if (err != NRFX_SUCCESS) {
    LOG_ERR("nrfx_saadc_channels_config error: %08x", err);
    return;
}
C

The GAIN step are different for nRF54L15 compared to previous SoCs.If building for nRF54L15 DK , use the below code snippet:

channel.channel_config.gain = NRF_SAADC_GAIN1_4;
err = nrfx_saadc_channels_config(&channel, 1);
if (err != NRFX_SUCCESS) {
    LOG_ERR("nrfx_saadc_channels_config error: %08x", err);
    return;
}
C

4.8 We will use the nrfx SAADC driver in advanced mode on channel 0 for this exercise. It is required to pass a configuration struct to the function, we will use the default defined configuration:

The default configuration disables OVERSAMPLING, BURST, and the internal timer, and it prevents the driver from triggering the START task when END event is generated (when a buffer have been filled). We will later configure triggering of the START task in HW through a (D)PPI channel to make sure there is no delay in buffer switching caused by other SW interrupts preempting the SAADC interrupt handler that would normally handle the buffer swapping. Passing an event handler to the last argument of the function will make the driver operate in non-blocking mode, we will implement the event handler in the next step. Add these lines to configure_saadc():

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;
}
C

4.9 The SAADC peripheral can support double buffering by providing a new buffer pointer as soon as the previous buffer has been acquired by the peripheral (STARTED event generated). The SAADC driver can support this feature by calling the buffer set function twice. Call the function once for each of the two previously declared buffers:

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;
}
C

4.10 Trigger the SAADC driver mode. This will not start sampling but will prepare a 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;
}
C

5. Implement the event handler for the SAADC driver.

We will now implement the event handler for the SAADC driver, where events passed from the driver will be processed by the application. The available event types are as follows:

In this application, we will not use the limit or calibration features, and since we will use double-buffering, the NRFX_SAADC_EVT_FINISHED event should not happen. The event handler function is declared as saadc_event_handler() in the firmware.

5.1 The NRFX_SAADC_EVT_READY event will trigger when the first buffer has been initialized in the driver and the SAADC is ready for sampling. We will start the timer in this event.

Add this line under the case before the break:

nrfx_timer_enable(&timer_instance);
C

5.2 The NRFX_SAADC_EVT_BUF_REQ event will be generated whenever a buffer is acquired by the driver, and it can accept a new buffer. We will alternate between the two previously defined buffers and provide the correct one by incrementing a variable (saadc_current_buffer). Add these lines under the case before the break:

err = nrfx_saadc_buffer_set(saadc_sample_buffer[(saadc_current_buffer++)%2], SAADC_BUFFER_SIZE);
if (err != NRFX_SUCCESS) {
    LOG_ERR("nrfx_saadc_buffer_set error: %08x", err);
    return;
}
C

5.3 The final event we will handle is the NRFX_SAADC_EVT_DONE event, which is generated when a buffer has been filled with the requested number of samples. Since we are only measuring battery voltage in this example, we will calculate the average, minimum and maximum sample value of all samples in the buffer and output this on the log:

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);
C

6. Setup the (D)PPI channels.

Finally, we will setup the (D)PPI channels that will be used to trigger actions automatically in HW, without any CPU interaction.

  • Trigger SAADC->SAMPLE task based on COMPARE event from timer
  • Trigger SAADC->START task when SAADC->END event indicates that the buffer is full.

6.1 Declare variables used to hold the (D)PPI channel number. Add these lines to configure_ppi()

uint8_t m_saadc_sample_ppi_channel;
uint8_t m_saadc_start_ppi_channel;
C

6.2 Allocate (D)PPI channels for both actions. The nrfx_gppi helper API works with both PPI and DPPI peripherals:

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;
}
C

6.3 Each (D)PPI channel is assigned to one task and one event endpoint. The endpoints are the register address of the task or event as documented in the Product Specifications of the chip. Most drivers or HAL (Hardware Abstraction Layer) implementation where (D)PPI are relevant have APIs to get the addresses. Setup the first (D)PPI channel from TIMER->COMPARE0 event to trigger SAADC->SAMPLE task:

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));
C

6.4 Setup the second (D)PPI channel from SAADC->END event to trigger SAADC->START task:

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));
C

6.5 The (D)PPI channels need to be enabled before they will have any effect. Enabled both channels by adding these lines:

nrfx_gppi_channels_enable(BIT(m_saadc_sample_ppi_channel));
nrfx_gppi_channels_enable(BIT(m_saadc_start_ppi_channel));
C

Testing

7. Build the application and flash it to your board.

8. Connect your analog input to a voltage source, just as you did in exercise 1.

This could be a dedicated power supply, a PPK2, a battery, or you can simply connect a wire between the analog input (AIN0) and VDD as shown below.

Note

ake sure that the voltage applied to the analog input does not exceed VDD. If you have a battery with a higher voltage level than VDD, you need to use a voltage divider between the battery and the input.

If you want to measure battery voltage directly from VDD, you can replace NRF_SAADC_INPUT_AIN0 or NRF_SAADC_INPUT_AIN4 (nRF54) with NRF_SAADC_INPUT_VDD.

9. Using a serial terminal, you should see the below output:

*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
[00:00:00.653,900] <inf> main: SAADC buffer at 0x20001150 filled with 8000 samples
[00:00:00.653,900] <inf> main: AVG=2064, MIN=2029, MAX=2099
[00:00:01.052,673] <inf> main: SAADC buffer at 0x20004fd0 filled with 8000 samples
[00:00:01.052,673] <inf> main: AVG=2064, MIN=2025, MAX=2097
[00:00:01.451,446] <inf> main: SAADC buffer at 0x20001150 filled with 8000 samples
[00:00:01.451,446] <inf> main: AVG=2064, MIN=2028, MAX=2094
[00:00:01.850,097] <inf> main: SAADC buffer at 0x20004fd0 filled with 8000 samples
[00:00:01.850,128] <inf> main: AVG=2062, MIN=2009, MAX=2100
[00:00:02.248,840] <inf> main: SAADC buffer at 0x20001150 filled with 8000 samples
[00:00:02.248,870] <inf> main: AVG=2064, MIN=2029, MAX=2103
[00:00:02.647,521] <inf> main: SAADC buffer at 0x20004fd0 filled with 8000 samples
[00:00:02.647,521] <inf> main: AVG=2064, MIN=2025, MAX=2098
Terminal

Observe that the filled buffer alternates between two different locations, corresponding to the two buffers we have defined.

The solution for this exercise can be found in the course repository, l6/l6_e3_sol.

v2.6.2 -v2.5.2

In this exercise, we will explore the advanced mode of the SAADC driver to measure voltage source (E.g., a battery) at a high sample rate. We will use a hardware TIMER instance to trigger sampling through DPPI/PPI, without any CPU involvement.

Exercise steps

Open the code base of the exercise by navigating to Create a new application in the nRF Connect for VS Code extension, select Copy a sample, and search for Lesson 6 – Exercise 3.

Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l6/l6_e3.

1. Enable the SAADC driver.

Enable the driver by adding the following lines into the application configuration file prj.conf.

CONFIG_NRFX_SAADC=y
CONFIG_NRFX_PPI=y
CONFIG_NRFX_DPPI=y
Kconfig

We also need to enable the TIMER instance that will be used. Available TIMER instances differ between targets. For nRF52/nRF53/nRF91, we will use TIMER2, for nRF54L15, we will use TIMER22. This is done by enabling it in prj.conf:

CONFIG_NRFX_TIMER2=y
Kconfig
CONFIG_NRFX_TIMER22=y
Kconfig

Important

When building the project, you will see a warning that either PPI or DPPI was assigned the value ‘y’, but got the value ‘n’, depending on which chip you build for. You can ignore this, but it can be resolved by creating separate Kconfig fragment files for the different boards. In this exercise, we have put all Kconfigs in one common prj.conf file for simplicity.

2. Include the nrfx-related header files in the application.

We will use the nrfx SAADC, TIMER and (D)PPI drivers to configure the peripherals.

nRF52 Series chips have a PPI peripheral, while nRF54, nRF53 and nRF91 Series have a more flexible DPPI peripheral. nrfx provides a helper API (nrfx GPPI) that can be used to configure both peripheral variants through a common API. Add nrfx-related header files by including the following lines at the top (include section) of main.c:

#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
C

3. Configure the TIMER used to trigger the sampling of the SAADC.

3.1 In the define section close to the top of main.c, define the SAADC sample interval:

#define SAADC_SAMPLE_INTERVAL_US 50
C

3.2 Declaring an instance of nrfx_timer_t for the correct TIMER instance. For nRF52/nRF53/nRF91, we use TIMER2. For nRF54L15, we use TIMER22.

#define TIMER_INSTANCE_NUMBER 2
const nrfx_timer_t timer_instance = NRFX_TIMER_INSTANCE(TIMER_INSTANCE_NUMBER);
C
#define TIMER_INSTANCE_NUMBER 22
const nrfx_timer_t timer_instance = NRFX_TIMER_INSTANCE(TIMER_INSTANCE_NUMBER);
C

3.3 In the function configure_timer(), declare the timer config struct. The parameter passed to NRFX_TIMER_DEFAULT_CONFIG will set the frequency of the timer. The timer config is used to initialize the instance of the timer driver. Since we will use (D)PPI to trigger sampling, no interrupts are needed, and we pass NULL to the event handler parameter:

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;
}
C

3.4 We will use the COMPARE0 event from the TIMER to trigger sampling at the interval given by SAADC_SAMPLE_INTERVAL_US. The TIMER will be cleared every time the COMPARE0 event is hit, to create a recurring timer event. Add these lines to the end of the function configure_timer():

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);
C

4. Configure the SAADC driver.

4.1 In this example, we will use double buffering, which requires two separate buffers that will be filled with samples sequentially. One buffer will be filled while the other buffer can be processed. Since we have a short sample interval, the buffer needs to be large enough for the CPU to start up and process the previous buffer before the new buffer is filled. Start by defining the buffer size for each of the buffers by adding this line close to the top of main.c:

#define SAADC_BUFFER_SIZE   8000
C

4.2 Next, let’s declare the two buffers by adding this line below the define section in main.c

static int16_t saadc_sample_buffer[2][SAADC_BUFFER_SIZE];
C

4.3 To keep track of which of the two buffers should be assigned to the SAADC driver, we declare a variable corresponding to the current buffer index:

static uint32_t saadc_current_buffer = 0;
C

4.4 We will reference the ADC defined in the Zephyr devicetree, to make the code more portable. To connect the SAADC interrupt to SAADC interrupt handler, add these lines in configure_saadc():

IRQ_CONNECT(DT_IRQN(DT_NODELABEL(adc)),
            DT_IRQ(DT_NODELABEL(adc), priority),
            nrfx_isr, nrfx_saadc_irq_handler, 0);
C

By default, the ADC is enabled in the board DTS file for all DKs supported by this course, but for custom boards you may have to enable it in your DTS or overlay file using the following code snippet:

&adc {
    status = "okay";
};
Devicetree

4.5 Before using the SAADC driver, the driver instance must be initialized. We will again refer to the devicetree to get the configured priority of the ADC node and use this for the driver:

err = nrfx_saadc_init(DT_IRQ(DT_NODELABEL(adc), priority));
if (err != NRFX_SUCCESS) {
    LOG_ERR("nrfx_saadc_init error: %08x", err);
    return;
}
C

4.6 Declare the struct to hold the configuration for the SAADC channel used to sample the battery voltage. The macro NRFX_SAADC_DEFAULT_CHANNEL_SE() will create the default configuration struct for a single ended input for the provided analog input on channel 0.

The way of configuring pins for SAADC channels have changed for nRF54L15. Chose the tab below that matches your DK.

We will use AIN0 for all Nordic DKs , except the nRF54L15 DK.

#define SAADC_INPUT_PIN NRF_SAADC_INPUT_AIN0
static nrfx_saadc_channel_t channel = NRFX_SAADC_DEFAULT_CHANNEL_SE(SAADC_INPUT_PIN, 0);
C

On the nRF54L15 DK, we will use AIN4 instead of the AIN0 as AIN0 (P1.04) pin is used by default for UART.

#define NRF_SAADC_INPUT_AIN4 NRF_PIN_PORT_TO_PIN_NUMBER(11U, 1)
#define SAADC_INPUT_PIN NRF_SAADC_INPUT_AIN4
static nrfx_saadc_channel_t channel = NRFX_SAADC_DEFAULT_CHANNEL_SE(SAADC_INPUT_PIN, 0);
C

Connect a battery between GND and analog input (AIN0 or AIN4, depending on your target). Check the Hardware and Layout ->Pin assignment chapter in the Product specification to know which Pin is connected to the analog inputs on your choice of SoC/SiP. You can also connect a jumper wire between analog input 0 and VDD if you do not have a battery available.

4.7 Configure the SAADC channel using the previously defined channel configuration structure. The default configuration uses GAIN=1, which is too high to support supply voltage measurements. We need to change the gain config before configuring the channel:

The GAIN steps are different for nRF54L15 compared to previous SoCs. Chose the tab below that matches with your DK.

Any Nordic development kit ohter than the nRF54L15 DK

channel.channel_config.gain = NRF_SAADC_GAIN1_6;
err = nrfx_saadc_channels_config(&channel, 1);
if (err != NRFX_SUCCESS) {
    LOG_ERR("nrfx_saadc_channels_config error: %08x", err);
    return;
}
C

The GAIN step are different for nRF54L15 compared to previous SoCs.If building for nRF54L15 DK , use the below code snippet:

channel.channel_config.gain = NRF_SAADC_GAIN1_4;
err = nrfx_saadc_channels_config(&channel, 1);
if (err != NRFX_SUCCESS) {
    LOG_ERR("nrfx_saadc_channels_config error: %08x", err);
    return;
}
C

4.8 We will use the nrfx SAADC driver in advanced mode on channel 0 for this exercise. It is required to pass a configuration struct to the function, we will use the default defined configuration:

The default configuration disables OVERSAMPLING, BURST, and the internal timer, and it prevents the driver from triggering the START task when END event is generated (when a buffer have been filled). We will later configure triggering of the START task in HW through a (D)PPI channel to make sure there is no delay in buffer switching caused by other SW interrupts preempting the SAADC interrupt handler that would normally handle the buffer swapping. Passing an event handler to the last argument of the function will make the driver operate in non-blocking mode, we will implement the event handler in the next step. Add these lines to configure_saadc():

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;
}
C

4.9 The SAADC peripheral can support double buffering by providing a new buffer pointer as soon as the previous buffer has been acquired by the peripheral (STARTED event generated). The SAADC driver can support this feature by calling the buffer set function twice. Call the function once for each of the two previously declared buffers:

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;
}
C

4.10 Trigger the SAADC driver mode. This will not start sampling but will prepare a 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;
}
C

5. Implement the event handler for the SAADC driver.

We will now implement the event handler for the SAADC driver, where events passed from the driver will be processed by the application. The available event types are as follows:

In this application, we will not use the limit or calibration features, and since we will use double-buffering, the NRFX_SAADC_EVT_FINISHED event should not happen. The event handler function is declared as saadc_event_handler() in the firmware.

5.1 The NRFX_SAADC_EVT_READY event will trigger when the first buffer has been initialized in the driver and the SAADC is ready for sampling. We will start the timer in this event.

Add this line under the case before the break:

nrfx_timer_enable(&timer_instance);
C

5.2 The NRFX_SAADC_EVT_BUF_REQ event will be generated whenever a buffer is acquired by the driver, and it can accept a new buffer. We will alternate between the two previously defined buffers and provide the correct one by incrementing a variable (saadc_current_buffer). Add these lines under the case before the break:

err = nrfx_saadc_buffer_set(saadc_sample_buffer[(saadc_current_buffer++)%2], SAADC_BUFFER_SIZE);
if (err != NRFX_SUCCESS) {
    LOG_ERR("nrfx_saadc_buffer_set error: %08x", err);
    return;
}
C

5.3 The final event we will handle is the NRFX_SAADC_EVT_DONE event, which is generated when a buffer has been filled with the requested number of samples. Since we are only measuring battery voltage in this example, we will calculate the average, minimum and maximum sample value of all samples in the buffer and output this on the log:

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);
C

6. Setup the (D)PPI channels.

Finally, we will setup the (D)PPI channels that will be used to trigger actions automatically in HW, without any CPU interaction.

  • Trigger SAADC->SAMPLE task based on COMPARE event from timer
  • Trigger SAADC->START task when SAADC->END event indicates that the buffer is full.

6.1 Declare variables used to hold the (D)PPI channel number. Add these lines to configure_ppi()

uint8_t m_saadc_sample_ppi_channel;
uint8_t m_saadc_start_ppi_channel;
C

6.2 Allocate (D)PPI channels for both actions. The nrfx_gppi helper API works with both PPI and DPPI peripherals:

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;
}
C

6.3 Each (D)PPI channel is assigned to one task and one event endpoint. The endpoints are the register address of the task or event as documented in the Product Specifications of the chip. Most drivers or HAL (Hardware Abstraction Layer) implementation where (D)PPI are relevant have APIs to get the addresses. Setup the first (D)PPI channel from TIMER->COMPARE0 event to trigger SAADC->SAMPLE task:

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));
C

6.4 Setup the second (D)PPI channel from SAADC->END event to trigger SAADC->START task:

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));
C

6.5 The (D)PPI channels need to be enabled before they will have any effect. Enabled both channels by adding these lines:

nrfx_gppi_channels_enable(BIT(m_saadc_sample_ppi_channel));
nrfx_gppi_channels_enable(BIT(m_saadc_start_ppi_channel));
C

Testing

7. Build the application and flash it to your board.

8. Connect your analog input to a voltage source, just as you did in exercise 1.

This could be a dedicated power supply, a PPK2, a battery, or you can simply connect a wire between the analog input (AIN0) and VDD as shown below.

Note

ake sure that the voltage applied to the analog input does not exceed VDD. If you have a battery with a higher voltage level than VDD, you need to use a voltage divider between the battery and the input.

If you want to measure battery voltage directly from VDD, you can replace NRF_SAADC_INPUT_AIN0 or NRF_SAADC_INPUT_AIN4 (nRF54) with NRF_SAADC_INPUT_VDD.

9. Using a serial terminal, you should see the below output:

*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
[00:00:00.653,900] <inf> main: SAADC buffer at 0x20001150 filled with 8000 samples
[00:00:00.653,900] <inf> main: AVG=2064, MIN=2029, MAX=2099
[00:00:01.052,673] <inf> main: SAADC buffer at 0x20004fd0 filled with 8000 samples
[00:00:01.052,673] <inf> main: AVG=2064, MIN=2025, MAX=2097
[00:00:01.451,446] <inf> main: SAADC buffer at 0x20001150 filled with 8000 samples
[00:00:01.451,446] <inf> main: AVG=2064, MIN=2028, MAX=2094
[00:00:01.850,097] <inf> main: SAADC buffer at 0x20004fd0 filled with 8000 samples
[00:00:01.850,128] <inf> main: AVG=2062, MIN=2009, MAX=2100
[00:00:02.248,840] <inf> main: SAADC buffer at 0x20001150 filled with 8000 samples
[00:00:02.248,870] <inf> main: AVG=2064, MIN=2029, MAX=2103
[00:00:02.647,521] <inf> main: SAADC buffer at 0x20004fd0 filled with 8000 samples
[00:00:02.647,521] <inf> main: AVG=2064, MIN=2025, MAX=2098
Terminal

Observe that the filled buffer alternates between two different locations, corresponding to the two buffers we have defined.

The solution for this exercise can be found in the course repository, l6/l6_e3_sol.

Register an account
Already have an account? Log in
(All fields are required unless specified optional)

  • 8 or more characters
  • Upper and lower case letters
  • At least one number or special character

Forgot your password?
Enter the email associated with your account, and we will send you a link to reset your password.