In this exercise, we will explore the simple mode of the nrfx SAADC driver to measure a voltage source (E.g., a battery) at a regular interval using a software timer.
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 2.
Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l6/l6_e2
of whichever version directory you are using.
1. Let’s enable the SAADC driver by adding the following line into the application configuration file prj.conf
.
CONFIG_NRFX_SAADC=y
Kconfig2. Add SAADC related header file by including the following line at the top (include section) of the main.c
#include <nrfx_saadc.h>
C3. We need to declare some objects that will be used later in the initialization process.
3.1 Declare the struct to hold the configuration for the SAADC channel used to sample the battery voltage. The macro will assign default configuration parameters for a single-ended input. The configuration can be changed later.
#define SAADC_INPUT_PIN NRF_SAADC_INPUT_AIN0
static nrfx_saadc_channel_t channel = NRFX_SAADC_DEFAULT_CHANNEL_SE(SAADC_INPUT_PIN, 0);
CThe way of configuring pins for SAADC channels has changed for nRF54L15.
#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);
CWe are using AIN4 for nRF54L15 since the GPIO shared with AIN0 by default is used for one of the UARTE instances on the nRF54L15 DK.
Connect a battery between GND and the 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 the analog input and VDD if you do not have a battery available.
3.2 Declare the buffer where the SAADC sample value will be stored during sampling. Add this line below code from the last point:
static int16_t sample;
C4. We will use a software timer (k_timer
) to trigger the sampling.
4.1 Define the sample interval by adding this line close to the top of main.c
:
#define BATTERY_SAMPLE_INTERVAL_MS 2000
C4.2 Define the timer instance that will be used for sampling:
K_TIMER_DEFINE(battery_sample_timer, battery_sample_timer_handler, NULL);
C4.3 Add forward declaration of timer callback handler right before the previous timer definition:
static void battery_sample_timer_handler(struct k_timer * timer);
C5. Configure the SAADC driver.
5.1 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:
IRQ_CONNECT(DT_IRQN(DT_NODELABEL(adc)),
DT_IRQ(DT_NODELABEL(adc), priority),
nrfx_isr, nrfx_saadc_irq_handler, 0);
CBy 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";
};
Devicetree5.2 Before using the nrfx 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:
nrfx_err_t err = nrfx_saadc_init(DT_IRQ(DT_NODELABEL(adc), priority));
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_mode_trigger error: %08x", err);
return;
}
C5.3 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.
channel.channel_config.gain = NRF_SAADC_GAIN1_6;
err = nrfx_saadc_channels_config(&channel, 1);
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_channels_config error: %08x", err);
return;
}
CThe GAIN steps are different for nRF54L15 compared to previous SoCs.
channel.channel_config.gain = NRF_SAADC_GAIN1_4;
err = nrfx_saadc_channels_config(&channel, 1);
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_channels_config error: %08x", err);
return;
}
C5.4 Configure SAADC driver in simple mode on channel 0. Passing NULL to the last argument will make the driver operate in blocking mode:
err = nrfx_saadc_simple_mode_set(BIT(0),
NRF_SAADC_RESOLUTION_12BIT,
NRF_SAADC_OVERSAMPLE_DISABLED,
NULL);
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_simple_mode_set error: %08x", err);
return;
}
CThe SAADC has a temperature-dependent offset. It is advisable to perform offset calibration before starting the first sample and then at regular intervals or whenever the ambient temperature changes by more than 5 to 10 °C; this recommendation is highly dependent on the specific application.
To start calibration, call nrfx_saadc_offset_calibrate(nrfx_saadc_event_handler_t calib_event_handler))
when the driver is in an idle state (you can call nrfx_saadc_abort()
to abort any ongoing or buffered conversions first). It will notify the application in the provided calib_event_handler
when calibration is completed. if no handler is provided, the calibration procedure will be blocking. There are some SAADC examples in the SDK where you can see how these APIs are used.
5.5 Set buffer where the sample will be stored. Since the sample interval is quite long and we only sample one channel, a buffer of one sample is sufficient:
err = nrfx_saadc_buffer_set(&sample, 1);
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_buffer_set error: %08x", err);
return;
}
C6. Start the periodic timer for battery sampling at the interval given by BATTERY_SAMPLE_INTERVAL_MS
.
Add the following line to the end of the function configure_saadc()
:
k_timer_start(&battery_sample_timer, K_NO_WAIT, K_MSEC(BATTERY_SAMPLE_INTERVAL_MS));
C7. We will now implement the timer callback handler where sampling is triggered and results are printed.
7.1 Add the empty function structure to the main.c
file
void battery_sample_timer_handler(struct k_timer *timer)
{
/* Step 7.2 - Trigger the sampling */
/* STEP 7.3 - Calculate and print voltage */
}
C7.2 Trigger the sampling. The SAADC driver was previously configured in blocking mode, so the sample will be ready when the function returns:
nrfx_err_t err = nrfx_saadc_mode_trigger();
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_mode_trigger error: %08x", err);
return;
}
C7.3 Calculate the battery voltage from the sample and print it on console. The calculation is different for nRF54L15 due to change in the internal reference voltage and GAIN steps.
For other targets than nRF54L15, the formula is based on the Digital output formula in nRF52/nRF53/nRF91 SAADC peripheral chapter and converted to millivolts:
int battery_voltage = ((600*6) * sample) / ((1<<12));
printk("SAADC sample: %d\n", sample);
printk("Battery Voltage: %d mV\n", battery_voltage);
CFor the nRF54L15 target, the formula is based on the Digital output formula in nRF54L15 SAADC peripheral chapter and converted to millivolts:
int battery_voltage = ((900*4) * sample) / ((1<<12));
printk("SAADC sample: %d\n", sample);
printk("Battery Voltage: %d mV\n", battery_voltage);
CTesting
8. Build the application and flash it to your board.
9. Connect your analog input to a voltage source, just as you did in exercise 1.
Make 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
with NRF_SAADC_INPUT_VDD
.
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.
10. Using a serial terminal, you should see the below output:
*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
SAADC sample: 3245
Battery Voltage: 2852 mV
SAADC sample: 3237
Battery Voltage: 2845 mV
SAADC sample: 3252
Battery Voltage: 2858 mV
TerminalThe solution for this exercise can be found in the course repository, l6/l6_e2_sol
.
In this exercise, we will explore the simple mode of the nrfx SAADC driver to measure a voltage source (E.g., a battery) at a regular interval using a software timer.
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 2.
Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l6/l6_e2
of whichever version directory you are using.
1. Let’s enable the SAADC driver by adding the following line into the application configuration file prj.conf
.
CONFIG_NRFX_SAADC=y
Kconfig2. Add SAADC related header file by including the following line at the top (include section) of the main.c
#include <nrfx_saadc.h>
C3. We need to declare some objects that will be used later in the initialization process.
3.1 Declare the struct to hold the configuration for the SAADC channel used to sample the battery voltage. The macro will assign default configuration parameters for a single-ended input. The configuration can be changed later.
#define SAADC_INPUT_PIN NRF_SAADC_INPUT_AIN0
static nrfx_saadc_channel_t channel = NRFX_SAADC_DEFAULT_CHANNEL_SE(SAADC_INPUT_PIN, 0);
CThe way of configuring pins for SAADC channels has changed for nRF54L15.
#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);
CWe are using AIN4 for nRF54L15 since the GPIO shared with AIN0 by default is used for one of the UARTE instances on the nRF54L15 DK.
Connect a battery between GND and the 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 the analog input and VDD if you do not have a battery available.
3.2 Declare the buffer where the SAADC sample value will be stored during sampling. Add this line below code from the last point:
static int16_t sample;
C4. We will use a software timer (k_timer
) to trigger the sampling.
4.1 Define the sample interval by adding this line close to the top of main.c
:
#define BATTERY_SAMPLE_INTERVAL_MS 2000
C4.2 Define the timer instance that will be used for sampling:
K_TIMER_DEFINE(battery_sample_timer, battery_sample_timer_handler, NULL);
C4.3 Add forward declaration of timer callback handler right before the previous timer definition:
static void battery_sample_timer_handler(struct k_timer * timer);
C5. Configure the SAADC driver.
5.1 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:
IRQ_CONNECT(DT_IRQN(DT_NODELABEL(adc)),
DT_IRQ(DT_NODELABEL(adc), priority),
nrfx_isr, nrfx_saadc_irq_handler, 0);
CBy 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";
};
Devicetree5.2 Before using the nrfx 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:
nrfx_err_t err = nrfx_saadc_init(DT_IRQ(DT_NODELABEL(adc), priority));
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_mode_trigger error: %08x", err);
return;
}
C5.3 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.
channel.channel_config.gain = NRF_SAADC_GAIN1_6;
err = nrfx_saadc_channels_config(&channel, 1);
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_channels_config error: %08x", err);
return;
}
CThe GAIN steps are different for nRF54L15 compared to previous SoCs.
channel.channel_config.gain = NRF_SAADC_GAIN1_4;
err = nrfx_saadc_channels_config(&channel, 1);
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_channels_config error: %08x", err);
return;
}
C5.4 Configure SAADC driver in simple mode on channel 0. Passing NULL to the last argument will make the driver operate in blocking mode:
err = nrfx_saadc_simple_mode_set(BIT(0),
NRF_SAADC_RESOLUTION_12BIT,
NRF_SAADC_OVERSAMPLE_DISABLED,
NULL);
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_simple_mode_set error: %08x", err);
return;
}
C5.5 Set buffer where the sample will be stored. Since the sample interval is quite long and we only sample one channel, a buffer of one sample is sufficient:
err = nrfx_saadc_buffer_set(&sample, 1);
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_buffer_set error: %08x", err);
return;
}
C6. Start the periodic timer for battery sampling at the interval given by BATTERY_SAMPLE_INTERVAL_MS
.
Add the following line to the end of the function configure_saadc()
:
k_timer_start(&battery_sample_timer, K_NO_WAIT, K_MSEC(BATTERY_SAMPLE_INTERVAL_MS));
C7. We will now implement the timer callback handler where sampling is triggered and results are printed.
7.1 Add the empty function structure to the main.c
file
void battery_sample_timer_handler(struct k_timer *timer)
{
/* Step 7.2 - Trigger the sampling */
/* STEP 7.3 - Calculate and print voltage */
}
C7.2 Trigger the sampling. The SAADC driver was previously configured in blocking mode, so the sample will be ready when the function returns:
nrfx_err_t err = nrfx_saadc_mode_trigger();
if (err != NRFX_SUCCESS) {
printk("nrfx_saadc_mode_trigger error: %08x", err);
return;
}
C7.3 Calculate the battery voltage from the sample and print it on console. The calculation is different for nRF54L15 due to change in the internal reference voltage and GAIN steps.
For other targets than nRF54L15, the formula is based on the Digital output formula in nRF52/nRF53/nRF91 SAADC peripheral chapter and converted to millivolts:
int battery_voltage = ((600*6) * sample) / ((1<<12));
printk("SAADC sample: %d\n", sample);
printk("Battery Voltage: %d mV\n", battery_voltage);
CFor the nRF54L15 target, the formula is based on the Digital output formula in nRF54L15 SAADC peripheral chapter and converted to millivolts:
int battery_voltage = ((900*4) * sample) / ((1<<12));
printk("SAADC sample: %d\n", sample);
printk("Battery Voltage: %d mV\n", battery_voltage);
CTesting
8. Build the application and flash it to your board.
9. Connect your analog input to a voltage source, just as you did in exercise 1.
Make 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
with NRF_SAADC_INPUT_VDD
.
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.
10. Using a serial terminal, you should see the below output:
*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
SAADC sample: 3245
Battery Voltage: 2852 mV
SAADC sample: 3237
Battery Voltage: 2845 mV
SAADC sample: 3252
Battery Voltage: 2858 mV
TerminalThe solution for this exercise can be found in the course repository, l6/l6_e2_sol
.