12  Reading digital and analog

12.1 Turn on LED upon button press

Download code
// Define the pin numbers
const int buttonPin = D4;
const int ledPin = A4;

void setup() {
  // Set the pin modes
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);

  // Initialize Serial communication
  Serial.begin(115200);
}

void loop() {
  // Read the state of the button
  int buttonState = digitalRead(buttonPin);

  // If the button is not pressed (pin is pulled high)
  if (buttonState == HIGH) {
    // Turn on the LED
    digitalWrite(ledPin, LOW);

    // Print the button state to the Serial Monitor
    Serial.println("Button is not pressed.");
  } else {
    // Turn off the LED
    digitalWrite(ledPin, HIGH);

    // Print the button state to the Serial Monitor
    Serial.println("Button is pressed.");
  }

  // Delay for a short period to debounce the button
  delay(50);
}

12.2 Toggle LED upon button press

Download code
// Define the pin numbers
const int buttonPin = D4;
const int ledPin = A4;

// Initialize the LED state to off
bool ledState = false;

void setup() {
  // Set the pin modes
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);

  // Initialize Serial communication
  Serial.begin(115200);
}

void loop() {
  // Read the state of the button
  int buttonState = digitalRead(buttonPin);

  // If the button is pressed
  if (buttonState == LOW) {
    // Toggle the LED state
    ledState = !ledState;
    digitalWrite(ledPin, ledState);

    // Print the LED state to the Serial Monitor
    if (ledState) {
      Serial.println("LED is on.");
    } else {
      Serial.println("LED is off.");
    }

    // Wait a short period to debounce the button
    delay(500);
  }
}

12.3 Digital read

  • Goal: Read button press and take an action (flash external LED).

Use the following code.

Download code
const int buttonPin = A4;        // Pin where the button is connected
const int ledPin = D9;   // Pin where the LED is connected

// Variable to store the button state
int buttonState = 0;

void setup() {
  // Initialize the LED pin as an output
  pinMode(ledPin, OUTPUT);

  // Initialize the button pin as an input
  pinMode(buttonPin, INPUT_PULLUP);  // Use the internal pull-up resistor
  
  // Start serial communication for debugging (optional)
  Serial.begin(115200);
}

void loop() {
  // Read the state of the button
  buttonState = digitalRead(buttonPin);
  
  // Check if the button is pressed
  if (buttonState == LOW) {  // Assuming the button connects to ground when pressed
    Serial.println("Button pressed! Blinking LED...");
    
    // Blink the LED rapidly for 2 seconds (20 times per second)
    unsigned long startMillis = millis();
    while (millis() - startMillis < 2000) { // Run for 2000ms (2 seconds)
      digitalWrite(ledPin, HIGH);   // Turn the LED on
      delay(50);                    // Wait for 50ms
      digitalWrite(ledPin, LOW);    // Turn the LED off
      delay(50);                    // Wait for 50ms
    }
    
    // Optional: You can add a small delay here to debounce the button
    delay(200);  // Debounce the button for 200ms
  }

  // Add a small delay to avoid excessive readings
  delay(50);
}
see diagram
see wiring scheme

12.4 Touch sensor

Download code
#include <Arduino.h>

// Define the touch sensor pin
#define TOUCH_PIN D12  // You can change this to another touch pin if needed

void setup() {
  // Initialize serial communication at 115200 baud
  Serial.begin(115200);
  
  // Wait for the serial monitor to connect
  while (!Serial) {
    ; // Wait for serial port to connect. Needed for native USB port only
  }
}

void loop() {
  // Read the value from the touch sensor
  int touchValue = touchRead(TOUCH_PIN);

  // Print the touch sensor value
  Serial.print("Touch value: ");
  Serial.println(touchValue);
  
  // Check if the touch sensor is activated (value will be lower when touched)
  if (touchValue < 30) {  // You might need to adjust this threshold value
    Serial.println("Touch detected!");
  }

  // Add a small delay to avoid spamming the serial monitor
  delay(100);
}

12.5 Control LED with touch sensor

Download code
#include <Arduino.h>

// Define the touch sensor pin
#define TOUCH_PIN T0  // You can change this to another touch pin if needed

// Define the built-in LED pin
#define LED_PIN LED_BUILTIN  // Most ESP32 boards use GPIO 2 for the built-in LED

void setup() {
  // Initialize serial communication at 115200 baud
  Serial.begin(115200);
  
  // Wait for the serial monitor to connect
  while (!Serial) {
    ; // Wait for serial port to connect. Needed for native USB port only
  }

  // Set the LED pin as an output
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  // Read the value from the touch sensor
  int touchValue = touchRead(TOUCH_PIN);

  // Print the touch sensor value
  Serial.print("Touch value: ");
  Serial.println(touchValue);
  
  // Check if the touch sensor is activated (value will be lower when touched)
  if (touchValue < 30) {  // You might need to adjust this threshold value
    Serial.println("Touch detected!");
    digitalWrite(LED_PIN, HIGH);  // Turn on the LED
  } else {
    digitalWrite(LED_PIN, LOW);  // Turn off the LED
  }

  // Add a small delay to avoid spamming the serial monitor
  delay(100);
}

12.6 Reading an Analog Pin, convert to digital with built-in ADC

We will read the signal (middle) pin of the potentiometer, and use the 12-bit built-in Analog to Digital Converter.

Download code
const int analog_pin = A4; // Pin into which we connect the input voltage
int val ;  // variable to store the value read


void setup() {
  Serial.begin(115200); //
}

void loop() {
  val = analogRead(analog_pin);  // read the input pin
  Serial.println(val);          // print value
  delay(100);
}
see diagram
see wiring scheme

Why is the maximum number 4095? What does this have to do with 12-bits?

Try the interactive ADC explorer to compare voltage, bit depth and digital codes.

12.7 Reading an Analog Pin, convert to voltage

Use the same wiring as above. The 12-bit ADC returns a code from 0 to 4095. The example below uses the approximate endpoint conversion \(V = \text{code} \times 3.3 / 4095\). For calibrated ESP32 measurements, use analogReadMilliVolts().

Download code
const int analog_pin = A4; // Pin into which we connect the input voltage
int val ;  // variable to store the value read
float voltage;

void setup() {
  Serial.begin(115200); //
}

void loop() {
  val = analogRead(analog_pin);  // read the input pin
  voltage = (3.3/4095)*val;  // convert to voltage according to ADC
  Serial.print("Voltage: ");
  Serial.print(voltage);          // print value
  Serial.println(" V");
  delay(100);
}

12.8 Reading Potentiometer with Analog Pin, convert to angle

Put a piece of cardboard in your potentiometer’s slit. Your goal is to build a calibration curve, that is, a function that converts reading from something you don’t care about (increments from 0 to 4095) to something that you do care about (angle).

The simplest thing you can do is to measure the increments (\(i\)) that you get for two given angles (say, \(\theta\) equals zero and 90 degrees). Let’s say you measured \((i_1,\theta_1)\) and \((i_2,\theta_2)\). If the potentiometer is linear, we will get a linear relationship between angle and increment:

\[ \theta = a i + b \]

Substitute these two points in the equation above:

\[ \theta_1 = a i_1 + b \tag{12.1}\]

\[ \theta_2 = a i_2 + b \tag{12.2}\]

Now let’s subtract Equation 12.2 from Equation 12.1:

\[ \theta_1 - \theta_2 = a (i_1 - i_2) \tag{12.3}\]

Now solve for the slope \(a\):

\[ a = (\theta_1 - \theta_2) / (i_1 - i_2) \tag{12.4}\]

To find the intercept \(b\), just substitute \(a\) into Equation 12.1 and solve for \(b\):

\[ b = \theta_1 - a i_1 \tag{12.5}\]

Try it · From a reading to an angle

Two points define the conversion

Enter the ADC codes measured at two known angles. Then move the current reading and follow its estimated angle on the calibration line.

Point 1
Point 2
04095

Interpolation: the current code is between the two measured codes.

θ = 0.030000 × i − 15.00

Estimated angle
45.0°
Slope a (°/code)
0.030000
Intercept b (°)
−15.00
ADC code to angle calibrationTwo reference points define a line. ADC code 2000 gives 45 degrees with the example values.
● Points 1 and 2: measurements◆ Current estimateDashed: beyond the measured interval
What can two points tell us?

They set the slope and offset of a straight-line model: a = (θ₂ − θ₁) / (i₂ − i₁), then b = θ₁ − a × i₁. Reversing both point labels leaves the same line. A sensor whose reading decreases as the angle increases has a negative slope.

The two ADC codes must differ. Two points alone cannot prove that the response is linear: check additional known angles. Estimates outside the measured interval are extrapolations. See Adafruit's two-point calibration guide.

Example measurements, not a calibration for your potentiometer. Use your own readings. The graph rescales vertically; calculated angles are not clamped to the reference angles. Displayed coefficients are rounded, but the calculation uses full precision.

put the lines below in the code we ran before, they are not in the correct place, it is here just so you have an idea how to update the previous code.

float angle, increment, a, b;

a = 12345;  // put here what you calculated
b = 12345;  // put here what you calculated

increment = analogRead(analog_pin);
angle = a * increment + b;

12.9 Voltage divider

Many sensors have calibration curves that relate their resistance changes to the measured variable. For example, the resistance of photoresistors and thermistors changes with light and temperature, respectively. To handle this, we need to understand Voltage Dividers.

Check out this great tutorial

How to derive the equation for the voltage divider?

First, write Ohm’s law for the two components in series:

\[ V_{in} = (R_1 + R_2) I \tag{12.6}\]

Now write Ohm’s law just for the second component

\[ V_{out} = R_2 I \tag{12.7}\]

Now solve Equation 12.7 for \(I\)

\[ I = \frac{V_{out}}{R_2} \tag{12.8}\]

… and substitute it into Equation 12.6:

\[ V_{in} = (R_1 + R_2) \frac{V_{out}}{R_2} \tag{12.9}\]

Solving for \(V_{out}\) yields the famous equation for the voltage divider:

\[ V_{out} = \frac{R_2}{R_1 + R_2} V_{in} \tag{12.10}\]

We usually need to find the value of the resistance \(R_2\):

\[ R_2 = R_1 \frac{V_{out}}{V_{in} - V_{out}} \]

This rearrangement assumes \(R_1 > 0\) and \(0 < V_{out} < V_{in}\), with no load drawing current from the output.

Try it · Resistance becomes voltage

The ratio sets the reading

R1 connects the supply to the output. R2 connects the output to ground. Change either resistor and follow the voltage at their junction.

1 kΩ = 1,000 Ω. Imagine R2 is the photoresistor or thermistor in the next exercise.

Output voltage
1.650 V
Series current
0.165 mA
Ideal 12-bit ADC
2048
Two-resistor voltage dividerTwo 10 kilo-ohm resistors divide 3.3 volts into a 1.65 volt output.Vin = 3.3 VR1 = 10 kΩR2 = 10 kΩ1.650 VVoutGND = 0 VDrop: 1.650 VDrop: 1.650 V
R2 is 50.0% of the total resistance, so Vout is 50.0% of Vin.

Vout = 3.3 × 10 / (10 + 10) = 1.650 V

The ideal ADC model uses 4,096 equal intervals across 0–3.3 V.

Why does doubling both resistors keep Vout the same?

The fraction R2 / (R1 + R2) stays the same, but total resistance doubles. The series current is therefore halved. If a sensor takes the place of R2, increasing its resistance raises Vout; placing it at R1 reverses that response.

This model assumes no current is drawn from the output. A load changes the effective lower resistance and the output voltage. A divider is therefore useful for sensing, but it is not a regulated power supply.

The ADC readout uses the same ideal 0–3.3 V model as the ADC explorer. Real ESP32 input range and accuracy depend on hardware, attenuation and calibration. The model's range is not a GPIO voltage rating.

12.10 Photoresistor

Play with your photoresistor. Measure its resistance when it receives bright light and when it doesn’t receive any light at all. This will give you an idea of what value of resistor to use together with your photoresistor when building your voltage divider. Build the following circuit:
3V3 → resistor → * photoresistor → GND
Connect pin A4 to the asterisk, this is your \(V_\text{out}\) in the voltage divider.

We will read the increments from the photoresistor, using a code from before:

Download code
const int analog_pin = A4; // Pin into which we connect the input voltage
int val ;  // variable to store the value read


void setup() {
  Serial.begin(115200); //
}

void loop() {
  val = analogRead(analog_pin);  // read the input pin
  Serial.println(val);          // print value
  delay(100);
}

12.11 Thermistor

We will use the \(\beta\) model

\[ R = R_{25} \exp\left[ \beta \left(\frac{1}{T} - \frac{1}{298.15} \right) \right], \]

where

  • \(R\) is given in ohms,
  • \(R_{25}\) is the resistance for 25 °C,
  • \(T\) is temperature in Kelvin.

Of course, we want the temperature (°C) as a function of the resistance:

\[ T = \left[ \frac{1}{\beta} \ln\left(\frac{R}{R_{25}}\right) + \frac{1}{298.15} \right]^{-1} - 273.15 \]

For our thermistor, \[ \beta = 4150 \]

Download Data Sheet

Download code
// Thermistor 
// reading the temperature via the ESP32's ADC

const int analog_pin = A4; // Pin into which we connect the input voltage
int val ;  // variable to store the value read
float v2; // variable to store the voltage of v2
int r1 = 10000; // R1 value (in the voltage divider)
float v1 = 3.3; // V1 value
int b_val = 4150; // B value of the resistor (from the data sheet)
float r2; // variable to store the resistance of r2 (thermistor resistance)
float t; // variable to store the temperature
int adc_steps = 4095; // the number of increments our ADC has (ESP32 = 12bit)
float r_25 = 10000; // the thermistor resistance at 25C (from the data sheet)

// function to convert resistance to temperature according to the b value
// equation in this link:
// https://www.thinksrs.com/downloads/programs/therm%20calc/ntccalibrator/ntccalculator.html
float resistance_to_temperature(int b, float r, float r_25) {
  return b/(log(r/r_25)+(b/298.15))-273.15;
}

void setup() {
  Serial.begin(115200); //
}

void loop() {
  val = analogRead(analog_pin);  // read the input pin (reading v2)
  v2 = (v1/adc_steps)*val;  // convert to voltage of v2 according to ADC
  r2 = (v2*r1)/(v1-v2);
  t = resistance_to_temperature(b_val, r2, r_25);
  Serial.print("Temperature: ");
  Serial.print(t);          // print temperature
  Serial.println(" C");
  delay(100);
}