zephyr-adc-lectura-analogica

ADC in Zephyr: Reading Analog Signals

  • 3 min

A ADC (Analog-to-Digital Converter) is the peripheral that converts an analog signal into a digital number.

Temperature, light, or sound pressure vary continuously. To process them, the ADC takes samples and represents them using discrete values.

It’s worth warning from the start: the ADC in Zephyr requires more configuration than in other simpler environments.

In Arduino you do analogRead(A0) and that’s it. In Zephyr, the ADC driver is designed to be ultra-flexible: it supports reading by DMA, differential sampling, programmable gains, circular buffers…

This flexibility means that reading a value requires some boilerplate or configuration code. Let’s break it down step by step.

Configure the hardware in the overlay

The first thing is to define in the Devicetree that we want to use an analog channel. Unlike GPIOs, here we use the io-channels property.

Let’s say we want to read a potentiometer on channel 0 of the ADC.

/* app.overlay */
/ {
    zephyr,user {
        /* Define a virtual "property" to access the ADC from C */
        io-channels = <&adc0 0>; // Channel 0 of ADC0
    };
};

/* Important! Enable the ADC */
&adc0 {
    status = "okay";
};
Copied!

The adc_sequence structure

In Zephyr you don’t “read a pin.” You launch a read sequence.

To read, we need to configure an adc_sequence structure that tells the hardware:

  1. Which channels to read (bit mask).
  2. Where to store the data (buffer).
  3. What resolution to use (10 bits, 12 bits…).
  4. Oversampling (if we want to average by hardware).

Example: reading a potentiometer

#include <zephyr/kernel.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/devicetree.h>

/* Channel configuration data */
static const struct adc_dt_spec adc_channel = ADC_DT_SPEC_GET(DT_PATH(zephyr_user));

/* Buffer to store the result (int16_t because it can be differential) */
int16_t sample_buffer[1];

/* Sequence structure */
struct adc_sequence sequence = {
    .buffer = sample_buffer,
    .buffer_size = sizeof(sample_buffer),
    // Optional: resolution, etc. Usually taken from DT
};

int main(void)
{
    int err;

    if (!adc_is_ready_dt(&adc_channel)) {
        printk("ADC is not ready\n");
        return 0;
    }

    /* 1. Configure the channel (Gain, Reference, Acquisition time) */
    /* Zephyr configures this automatically if we use ADC_DT_SPEC_GET
       and define it correctly in the overlay, or we can do it manually: */
    err = adc_channel_setup_dt(&adc_channel);
    if (err < 0) {
        printk("Error configuring channel (%d)\n", err);
        return 0;
    }

    /* 2. Prepare the sequence */
    /* We tell it which specific channel to read using the Devicetree info */
    err = adc_sequence_init_dt(&adc_channel, &sequence);
    if (err < 0) {
         printk("Error initializing sequence\n");
         return 0;
    }

    while (1) {
        /* 3. Read (Blocking) */
        err = adc_read(adc_channel.dev, &sequence);
        if (err < 0) {
            printk("Error reading ADC (%d)\n", err);
        } else {
            /* 4. Convert to millivolts (Optional but useful) */
            int32_t val_mv = sample_buffer[0];
            
            /* This function uses the reference and gain configuration
               to give us real millivolts */
            if (adc_raw_to_millivolts_dt(&adc_channel, &val_mv) == 0) {
                printk("Raw reading: %d, voltage: %d mV\n", sample_buffer[0], val_mv);
            } else {
                printk("Raw reading: %d; reference not convertible to mV\n",
                       sample_buffer[0]);
            }
        }

        k_msleep(1000);
    }
    return 0;
}
Copied!

Be careful with the buffer: Even if you only read one value, adc_read expects to write to an array. If your buffer is too small, you can cause memory corruption.