In this exercise, we will learn how to interact with an ADC (SAADC) on a Nordic device using the Zephyr ADC API.
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 1. Make sure to use the version directory that matches the SDK version you are using.
Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l6/l6_e1
of whichever version directory you are using.
1. Enable the ADC API and driver in prj.conf
CONFIG_ADC=y
Kconfig2. Define the ADC channel configration in devicetree
2.1 Create a devicetree overlay file for your board.
In the boards directory of the application, l6_e1/boards
, rename the overlay file to the name of the build target for the board you are using, for example nrf52840dk_nrf52840.overlay
.
2.2 Inside the devicetree overlay file, under the root node , create a zephyr,user
node and set its io-channels
to the ADC channel(s), you would like to use
In this exercise, we will only use one channel which is channel 0.
Add the following in your devicetree overlay file:
/ {
zephyr,user {
io-channels = <&adc 0>;
};
};
DevicetreeNote: you could specify multiple channels and have them separated by comma (Ex: io-channels = <&adc0 1>, <&adc0 3>;
)
2.3 Configure the ADC channel(s)
Add the following in your devicetree overlay file (outside the root node):
&adc {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_AIN0>; /* P0.02 for nRF52xx, P0.04 for nRF5340 */
zephyr,resolution = <12>;
};
};
DevicetreeOn the nRF54L15 SoC, a 14-bit resolution is used, and ADC_GAIN_1_4 gain.
&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>;
};
};
DevicetreeThe 0 after the @ in channel and reg signify that we are referencing channel 0 of the ADC.
We will populate the properties of the adc node which are described in here with all possible values for each property.
The gain zephyr,gain
is set to ADC_GAIN_1_6
, which means that the reading will be attenuated by (x 1/6
). For the nRF54L15 SoC, ADC_GAIN_1_4
gain is used.
The following gains are supported on the nRF SAADC: 1/6, 1/5, 1/4, 1/3, 1/2, 1, 2, 4. This is defined in the product specification.
For the reference voltage zephyr,reference
, we will use the internal +0.6, which is specified by ADC_REF_INTERNAL
.
For the acquisition time zephyr,acquisition-time
, we will use the default value set in the hardware ADC_ACQ_TIME_DEFAULT,
which equals 10us.
For the analog input zephyr,input-positive
, we will use a single-ended input by not specifying an input-negative property, and we will set it to AIN0 (NRF_SAADC_AIN0
). Since single-ended mode is just differential mode with the negative end internally connected to GND, noise can cause slightly negative measurements.
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. The mapping may differ from one nRF SoC to another, as demonstrated in the table below:
SoC/SiP | AIN0 | AIN1 | AIN2 | AIN3 | AIN4 | AIN5 | AIN6 | AIN7 |
---|---|---|---|---|---|---|---|---|
nRF52833 | P0.02 | P0.03 | P0.04 | P0.05 | P0.28 | P0.29 | P0.30 | P0.31 |
nRF5340 | P0.04 | P0.05 | P0.06 | P0.07 | P0.25 | P0.26 | P0.27 | P0.28 |
nRF9160 | P0.13 | P0.14 | P0.15 | P0.16 | P0.17 | P0.18 | P0.19 | P0.20 |
Do not confuse the Arduino shield analog input marking (A0-A5) printed on your DK PCB with the input of the SAADC (AIN0-AIN7), as these two are not the same entity.
Be aware that it is possible to use the SADDC to measure the internal voltage by specifying NRF_SAADC_VDD
or NRF_SAADC_VDDHDIV5
in zephyr,input-positive
.
For the resolution zephyr,resolution
, we will select 12 bits.(14 bits on nRF54L15 SoC).
For specifying multiple channels, see the ADC sample by Zephyr.
3. Retrieve the API-specific device structure for the ADC channel.
After we have defined the channel(s) we are interested in and its parameters (gain, reference, input mode, resolution, etc.) in the devicetree overlay, we can access it from the C code and set it up.
3.1 Include the header file of the Zephyr ADC API
#include <zephyr/drivers/adc.h>
C3.2 Define a variable of type adc_dt_spec
for each channel.
Since we are using only one channel in this exercise, we will use the ADC_DT_SPEC_GET()
macro to get the io-channels defined at index 0.
static const struct adc_dt_spec adc_channel = ADC_DT_SPEC_GET(DT_PATH(zephyr_user));
C3.3 We must validate that the ADC peripheral (SAADC) is ready before setting it up. This is done by calling adc_is_ready_dt()
.
if (!adc_is_ready_dt(&adc_channel)) {
LOG_ERR("ADC controller devivce %s not ready", adc_channel.dev->name);
return 0;
}
C3.4 Setup the ADC channel by calling adc_channel_setup_dt()
. The setup will be based on the configurations we have set in the devicetree overlay file.
err = adc_channel_setup_dt(&adc_channel);
if (err < 0) {
LOG_ERR("Could not setup channel #%d (%d)", 0, err);
return 0;
}
C4. Define and initialize a sequence to store samples captured by the ADC.
4.1 Define a variable of type adc_sequence
and a buffer of type int16_t
to specify where the samples are to be written.
int16_t buf;
struct adc_sequence sequence = {
.buffer = &buf,
/* buffer size in bytes, not number of samples */
.buffer_size = sizeof(buf),
//Optional
//.calibrate = true,
};
C4.2 Initialize the ADC sequence
err = adc_sequence_init_dt(&adc_channel, &sequence);
if (err < 0) {
LOG_ERR("Could not initalize sequnce");
return 0;
}
C5. Read a sample from the ADC by calling adc_read()
err = adc_read(adc_channel.dev, &sequence);
if (err < 0) {
LOG_ERR("Could not read (%d)", err);
continue;
}
C6. Convert raw value to mV by calling adc_raw_to_millivolts_dt()
. This function relies on the parameters set in the devicetree overlay file.
err = adc_raw_to_millivolts_dt(&adc_channel, &val_mv);
/* conversion to mV may not be supported, skip if not */
if (err < 0) {
LOG_WRN(" (value in mV not available)\n");
} else {
LOG_INF(" = %d mV", val_mv);
}
CTesting
7. Build and flash the application to your board.
8. Connect your analog input to a voltage source.
This could be a dedicated power supply, a PPK II kit, a battery, or you can simply connect a wire between the analog input (AIN0) and VDD as shown below.
As mentioned before, you don’t have to have external wiring to measure the VDD, you can simply set it in zephyr,input-positive = <NRF_SAADC_VDD>
On the nRF54L15 DK, connect P1.11 to Either (GND/VDDIO), By default, the VDDIO voltage is 1.8v but can be changed using the Board Configurator in nRF Connect for Desktop.
9. On your serial terminal, you should see the measured voltage in mV.
*** Booting nRF Connect SDK v2.8.0-a2386bfc8401 ***
*** Using Zephyr OS v3.7.99-0bc3393fb112 ***
[00:00:00.252,075] <inf> Lesson6_Exercise1: ADC reading[0]: adc@40007000, channel 0: Raw: 3401
[00:00:00.252,075] <inf> Lesson6_Exercise1: = 2989 mV
[00:00:01.252,227] <inf> Lesson6_Exercise1: ADC reading[1]: adc@40007000, channel 0: Raw: 3404
[00:00:01.252,258] <inf> Lesson6_Exercise1: = 2991 mV
[00:00:02.252,441] <inf> Lesson6_Exercise1: ADC reading[2]: adc@40007000, channel 0: Raw: 3401
[00:00:02.252,441] <inf> Lesson6_Exercise1: = 2989 mV
[00:00:03.252,593] <inf> Lesson6_Exercise1: ADC reading[3]: adc@40007000, channel 0: Raw: 3404
[00:00:03.252,624] <inf> Lesson6_Exercise1: = 2991 mV
[00:00:04.252,807] <inf> Lesson6_Exercise1: ADC reading[4]: adc@40007000, channel 0: Raw: 3399
[00:00:04.252,807] <inf> Lesson6_Exercise1: = 2987 mV
TerminalYou could connect the wire now to GND and see the measured voltage.
*** Booting nRF Connect SDK v2.8.0-a2386bfc8401 ***
*** Using Zephyr OS v3.7.99-0bc3393fb112 ***
[00:00:00.252,075] <inf> Lesson6_Exercise1: ADC reading[0]: adc@40007000, channel 0: Raw: 2
[00:00:00.252,075] <inf> Lesson6_Exercise1: = 1 mV
[00:00:01.252,227] <inf> Lesson6_Exercise1: ADC reading[1]: adc@40007000, channel 0: Raw: 8
[00:00:01.252,258] <inf> Lesson6_Exercise1: = 7 mV
[00:00:02.252,441] <inf> Lesson6_Exercise1: ADC reading[2]: adc@40007000, channel 0: Raw: 5
[00:00:02.252,441] <inf> Lesson6_Exercise1: = 4 mV
[00:00:03.252,593] <inf> Lesson6_Exercise1: ADC reading[3]: adc@40007000, channel 0: Raw: -1
[00:00:03.252,624] <inf> Lesson6_Exercise1: = -1 mV
[00:00:04.252,807] <inf> Lesson6_Exercise1: ADC reading[4]: adc@40007000, channel 0: Raw: 5
[00:00:04.252,807] <inf> Lesson6_Exercise1: = 4 mV
TerminalThe small mV is due to noise. This is common for single-ended mode.
It’s worth noting that If you have a PPK II , you could use it as a variable voltage supply. Simply connect the VOUT of the PPK II to the analog input pin , connect the GND of the PPK II to your DK GND, Open the Power Profiler App in nRF Connect for Desktop, use the Source meter, and set the supply voltage to the desired value.
The analog input measured should not exceed the internal voltage of the SoC/SiP. If you need to measure an analog input higher than the internal voltage, you must have the necessary voltage step-down circuit.
Make sure that the measured analog input has the same ground as your DK.
The solution for this exercise can be found in the GitHub repository, l6/l6_e1_sol
.
In this exercise, we will learn how to interact with an ADC (SAADC) on a Nordic device using the Zephyr ADC API.
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 1. Make sure to use the version directory that matches the SDK version you are using.
Alternatively, in the GitHub repository for this course, go to the base code for this exercise, found in l6/l6_e1
of whichever version directory you are using.
1. Enable the ADC API and driver in prj.conf
CONFIG_ADC=y
Kconfig2. Define the ADC channel configration in devicetree
2.1 Create a devicetree overlay file for your board.
In the boards directory of the application, l6_e1/boards
, rename the overlay file to the name of the build target for the board you are using, for example nrf52840dk_nrf52840.overlay
.
2.2 Inside the devicetree overlay file, under the root node , create a zephyr,user
node and set its io-channels
to the ADC channel(s), you would like to use
In this exercise, we will only use one channel which is channel 0.
Add the following in your devicetree overlay file:
/ {
zephyr,user {
io-channels = <&adc 0>;
};
};
DevicetreeNote: you could specify multiple channels and have them separated by comma (Ex: io-channels = <&adc0 1>, <&adc0 3>;
)
2.3 Configure the ADC channel(s)
Add the following in your devicetree overlay file (outside the root node):
&adc {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_AIN0>; /* P0.02 for nRF52xx, P0.04 for nRF5340 */
zephyr,resolution = <12>;
};
};
DevicetreeThe 0 after the @ in channel and reg signify that we are referencing channel 0 of the ADC.
We will populate the properties of the adc node which are described in here with all possible values for each property.
The gain zephyr,gain
is set to ADC_GAIN_1_6
, which means that the reading will be attenuated by (x 1/6
).
The following gains are supported on the nRF SAADC: 1/6, 1/5, 1/4, 1/3, 1/2, 1, 2, 4. This is defined in the product specification.
For the reference voltage zephyr,reference
, we will use the internal +0.6, which is specified by ADC_REF_INTERNAL
.
For the acquisition time zephyr,acquisition-time
, we will use the default value set in the hardware ADC_ACQ_TIME_DEFAULT,
which equals 10us.
For the analog input zephyr,input-positive
, we will use a single-ended input by not specifying an input-negative property, and we will set it to AIN0 (NRF_SAADC_AIN0
). Since single-ended mode is just differential mode with the negative end internally connected to GND, noise can cause slightly negative measurements.
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. The mapping may differ from one nRF SoC to another, as demonstrated in the table below:
SoC/SiP | AIN0 | AIN1 | AIN2 | AIN3 | AIN4 | AIN5 | AIN6 | AIN7 |
---|---|---|---|---|---|---|---|---|
nRF52833 | P0.02 | P0.03 | P0.04 | P0.05 | P0.28 | P0.29 | P0.30 | P0.31 |
nRF5340 | P0.04 | P0.05 | P0.06 | P0.07 | P0.25 | P0.26 | P0.27 | P0.28 |
nRF9160 | P0.13 | P0.14 | P0.15 | P0.16 | P0.17 | P0.18 | P0.19 | P0.20 |
Do not confuse the Arduino shield analog input marking (A0-A5) printed on your DK PCB with the input of the SAADC (AIN0-AIN7), as these two are not the same entity.
Be aware that it is possible to use the SADDC to measure the internal voltage by specifying NRF_SAADC_VDD
or NRF_SAADC_VDDHDIV5
in zephyr,input-positive
.
For the resolution zephyr,resolution
, we will select 12 bits.
For specifying multiple channels, see the ADC sample by Zephyr.
3. Retrieve the API-specific device structure for the ADC channel.
After we have defined the channel(s) we are interested in and its parameters (gain, reference, input mode, resolution, etc.) in the devicetree overlay, we can access it from the C code and set it up.
3.1 Include the header file of the Zephyr ADC API
#include <zephyr/drivers/adc.h>
C3.2 Define a variable of type adc_dt_spec
for each channel.
Since we are using only one channel in this exercise, we will use the ADC_DT_SPEC_GET()
macro to get the io-channels defined at index 0.
static const struct adc_dt_spec adc_channel = ADC_DT_SPEC_GET(DT_PATH(zephyr_user));
C3.3 We must validate that the ADC peripheral (SAADC) is ready before setting it up. This is done by calling adc_is_ready_dt()
.
if (!adc_is_ready_dt(&adc_channel)) {
LOG_ERR("ADC controller devivce %s not ready", adc_channel.dev->name);
return 0;
}
C3.4 Setup the ADC channel by calling adc_channel_setup_dt()
. The setup will be based on the configurations we have set in the devicetree overlay file.
err = adc_channel_setup_dt(&adc_channel);
if (err < 0) {
LOG_ERR("Could not setup channel #%d (%d)", 0, err);
return 0;
}
C4. Define and initialize a sequence to store samples captured by the ADC.
4.1 Define a variable of type adc_sequence
and a buffer of type int16_t
to specify where the samples are to be written.
int16_t buf;
struct adc_sequence sequence = {
.buffer = &buf,
/* buffer size in bytes, not number of samples */
.buffer_size = sizeof(buf),
//Optional
//.calibrate = true,
};
C4.2 Initialize the ADC sequence
err = adc_sequence_init_dt(&adc_channel, &sequence);
if (err < 0) {
LOG_ERR("Could not initalize sequnce");
return 0;
}
C5. Read a sample from the ADC by calling adc_read()
err = adc_read(adc_channel.dev, &sequence);
if (err < 0) {
LOG_ERR("Could not read (%d)", err);
continue;
}
C6. Convert raw value to mV by calling adc_raw_to_millivolts_dt()
. This function relies on the parameters set in the devicetree overlay file.
err = adc_raw_to_millivolts_dt(&adc_channel, &val_mv);
/* conversion to mV may not be supported, skip if not */
if (err < 0) {
LOG_WRN(" (value in mV not available)\n");
} else {
LOG_INF(" = %d mV", val_mv);
}
CTesting
7. Build and flash the application to your board.
8. Connect your analog input to a voltage source.
This could be a dedicated power supply, a PPK II kit, a battery, or you can simply connect a wire between the analog input (AIN0) and VDD as shown below.
As mentioned before, you don’t have to have external wiring to measure the VDD, you can simply set it in zephyr,input-positive = <NRF_SAADC_VDD>
9. On your serial terminal, you should see the measured voltage in mV.
*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
[00:00:00.252,075] <inf> Lesson6_Exercise1: ADC reading[0]: adc@40007000, channel 0: Raw: 3401
[00:00:00.252,075] <inf> Lesson6_Exercise1: = 2989 mV
[00:00:01.252,227] <inf> Lesson6_Exercise1: ADC reading[1]: adc@40007000, channel 0: Raw: 3404
[00:00:01.252,258] <inf> Lesson6_Exercise1: = 2991 mV
[00:00:02.252,441] <inf> Lesson6_Exercise1: ADC reading[2]: adc@40007000, channel 0: Raw: 3401
[00:00:02.252,441] <inf> Lesson6_Exercise1: = 2989 mV
[00:00:03.252,593] <inf> Lesson6_Exercise1: ADC reading[3]: adc@40007000, channel 0: Raw: 3404
[00:00:03.252,624] <inf> Lesson6_Exercise1: = 2991 mV
[00:00:04.252,807] <inf> Lesson6_Exercise1: ADC reading[4]: adc@40007000, channel 0: Raw: 3399
[00:00:04.252,807] <inf> Lesson6_Exercise1: = 2987 mV
TerminalYou could connect the wire now to GND and see the measured voltage.
*** Booting nRF Connect SDK 2.6.1-3758bcbfa5cd ***
[00:00:00.252,075] <inf> Lesson6_Exercise1: ADC reading[0]: adc@40007000, channel 0: Raw: 2
[00:00:00.252,075] <inf> Lesson6_Exercise1: = 1 mV
[00:00:01.252,227] <inf> Lesson6_Exercise1: ADC reading[1]: adc@40007000, channel 0: Raw: 8
[00:00:01.252,258] <inf> Lesson6_Exercise1: = 7 mV
[00:00:02.252,441] <inf> Lesson6_Exercise1: ADC reading[2]: adc@40007000, channel 0: Raw: 5
[00:00:02.252,441] <inf> Lesson6_Exercise1: = 4 mV
[00:00:03.252,593] <inf> Lesson6_Exercise1: ADC reading[3]: adc@40007000, channel 0: Raw: -1
[00:00:03.252,624] <inf> Lesson6_Exercise1: = -1 mV
[00:00:04.252,807] <inf> Lesson6_Exercise1: ADC reading[4]: adc@40007000, channel 0: Raw: 5
[00:00:04.252,807] <inf> Lesson6_Exercise1: = 4 mV
TerminalThe small mV is due to noise. This is common for single-ended mode.
It’s worth noting that If you have a PPK II , you could use it as a variable voltage supply. Simply connect the VOUT of the PPK II to the analog input pin , connect the GND of the PPK II to your DK GND, Open the Power Profiler App in nRF Connect for Desktop, use the Source meter, and set the supply voltage to the desired value.
The analog input measured should not exceed the internal voltage of the SoC/SiP. If you need to measure an analog input higher than the internal voltage, you must have the necessary voltage step-down circuit.
Make sure that the measured analog input has the same ground as your DK.
The solution for this exercise can be found in the GitHub repository, l6/l6_e1_sol
.