adc-lectura-analogica-nanoframework

ADC in nanoFramework: Reading Analog Sensors

  • 5 min

An ADC is a converter that transforms an analog voltage into a numerical value that our program can use.

We live in an analog world. Temperature doesn’t jump from 20°C to 21°C instantly; it goes through infinite intermediate values. Sunlight varies smoothly, and the volume of your voice is a continuous wave.

Microcontrollers, however, are digital beings. They only understand zeros and ones. To bridge this gap, we need a translator: the ADC (Analog-to-Digital Converter).

In this post, we will learn how to use the System.Device.Adc library to read variable voltages, allowing us to use potentiometers, joysticks, light sensors (LDRs), and much more.

For this tutorial you need to install the NuGet package: nanoFramework.System.Device.Adc

How does an ADC work?

The ADC takes an input voltage (for example, between 0V and 3.3V) and converts it into an integer number.

The quality of this conversion depends on the Resolution, measured in bits.

  • On Arduino Uno (10-bit): The range is from 0 to 1023.
  • On a 12-bit ADC: the nominal range is 0 to 4095.

A 12-bit ADC offers four times more quantization levels than a 10-bit one. Actual precision also depends on noise, reference voltage, and the converter’s linearity.

  • 0V -> Value 0
  • 1.65V -> Value ~2048
  • 3.3V -> Value 4095

The AdcController Class

Following nanoFramework’s philosophy, everything starts with a controller. The flow is identical to GPIO or PWM:

  1. Instantiate the AdcController.
  2. Open a channel (AdcChannel).
  3. Read the value.

Potentiometer Connection

For our first example, we will read a Potentiometer. Connect the potentiometer as follows:

  • Pin 1: GND
  • Pin 3: 3.3V
  • Middle Pin (Wiper): GPIO 34 (On ESP32, pins 34, 35, 36, and 39 are excellent for analog input because they are input-only).

Reading the Raw Value

Let’s write a simple program that displays the numerical value on the console.

using System;
using System.Device.Adc;
using System.Threading;
using System.Diagnostics;

namespace AdcEjemplo
{
    public class Program
    {
        public static void Main()
        {
            // 1. Instantiate the controller
            AdcController adc = new AdcController();

            // 2. Open the channel.
            // NOTE: On ESP32 the Channel to GPIO mapping is not always direct.
            // Generally, for GPIO 34 the channel is 6 (ADC1_CHANNEL_6).
            // However, nanoFramework usually abstracts this and allows using the pin number
            // depending on your board's implementation.
            // If you get an error, consult your board's "ADC Channel Mapping" scheme.

            // Let's try opening the channel associated with pin 34.
            AdcChannel pot = adc.OpenChannel(34);

            Debug.WriteLine($"Max Resolution: {adc.ResolutionInBits} bits");
            Debug.WriteLine($"Maximum possible value: {adc.MaxValue}");

            while (true)
            {
                // 3. Read the value (0 to 4095)
                int valorRaw = pot.ReadValue();

                // Calculate the percentage
                double porcentaje = (double)valorRaw / adc.MaxValue * 100;

                Debug.WriteLine($"Raw: {valorRaw}  -  {porcentaje:F1}%");

                Thread.Sleep(500);
            }
        }
    }
}
Copied!

Note about ESP32 and Channels: If using an ESP32, make sure not to use ADC2 (GPIOs 0, 2, 4, 12-15, 25-27) if you are using Wi-Fi. Wi-Fi interferes with ADC2. Always use pins from ADC1 (GPIOs 32-39) for stable readings.

Converting the Reading to Voltage

Having a number “3200” doesn’t tell us much. We want to know the actual voltage. To do this, we apply a simple rule of three, considering the Reference Voltage (usually 3.3V).

Let’s create a utility class or method for this, as it is a very common practice.

// ... inside the loop
int valorRaw = pot.ReadValue();

// Convert to Voltage
// NOTE: ESP32s are not perfectly linear and the reference can vary.
// 3.3V is the theoretical ideal.
double voltaje = (valorRaw / (double)adc.MaxValue) * 3.3;

Debug.WriteLine($"Voltage: {voltaje:F2} V");
Copied!

Practical Example: LDR Light Sensor

A potentiometer is fine for playing around, but let’s read a real sensor. An LDR (Photoresistor) changes its resistance based on light.

Since the ADC only reads voltage (not resistance), we need to create a Voltage Divider.

  1. Connect a 10kΩ resistor from 3.3V to the ADC pin.
  2. Connect the LDR from the ADC pin to GND.

Now, when there is a lot of light, the LDR’s resistance will drop, pulling the voltage towards 0V. When it’s dark, the resistance will rise, and we will read near 3.3V.

// Logic for a dusk sensor
int lecturaLuz = ldrChannel.ReadValue();

// Empirical threshold (adjust according to your room)
if (lecturaLuz > 3000)
{
    Debug.WriteLine("It's getting dark. Turn on lights.");
    // Here you could activate a relay or a PWM LED
}
Copied!

The ESP32 Linearity Problem

If you are an advanced user, you might notice something odd with the ESP32: near 0V and near 3.3V, the sensor “lies”. The ESP32’s ADC is not famous for its quality; it has a non-linear response curve and a lot of noise.

For professional precision applications with ESP32, we usually recommend:

  1. Using multisampling: Read 10 times in a row and take the average to filter out noise.
  2. Using external I2C ADCs (like the ADS1115) if you need real precision.

For determining if it’s day or night, or reading a potentiometer, the internal ADC is more than sufficient.

With the ADC, our microcontroller now has “senses”. It can “see” light, “feel” temperature (thermistors), or know the position of a robotic arm.