6.12. Reading analog with the ADC

So far the camera has been reading digital signals – a pin is either 0 or 1, a switch is open or closed. Most signals that come off real-world sensors are analog: a continuous voltage that varies smoothly over some range. A photoresistor sweeps through every voltage between the rails as ambient brightness changes. A temperature sensor’s output drifts a few millivolts as a room heats up. A microphone’s output rises and falls with the sound around it.

An analog-to-digital converter (ADC) is the bridge. It samples the voltage on a pin and returns an integer that Python can read like any other value.

6.12.1. Quantization

A digital value cannot represent a continuous voltage exactly. The ADC’s job is to quantize – snap each sample to the nearest of a fixed set of levels. An N-bit ADC has 2^N levels; a 12-bit converter has 4096 of them spread across its input range.

A smooth analog curve plotted against time, overlaid with a stepped digital approximation. Dashed horizontal lines mark the quantization levels; the stepped curve snaps to whichever level is nearest the analog signal at each sample point.

Quantization: each sample of the analog signal (solid) is rounded to one of a finite set of digital levels (stepped dashed line).

The voltage between two adjacent levels is the step size of the ADC; anything smaller than that vanishes into rounding. A 12-bit ADC over a 3.3 V range has a step size of about 3.3 / 4096 0.8 mV – fine enough that most signals look effectively continuous in software.

6.12.2. The machine.ADC class

machine.ADC wraps one analog input channel. Construct it with the pin you want to read, then call read_u16():

from machine import ADC

adc = ADC("P6")
value = adc.read_u16()
print(value)

read_u16() always returns an unsigned 16-bit integer between 0 and 65535. The native ADC resolution varies by board (12-bit on STM32, port-specific elsewhere); the result is left-aligned into 16 bits so the hardware detail does not leak into Python – a value of 65535 is full-scale regardless of the chip.

The reference voltage – the input that corresponds to full-scale – depends on the board. Check the OpenMV Boards for the value on your cam. Anything above the reference reads as full-scale (and may damage the pin if it exceeds the absolute-maximum input voltage).

6.12.2.1. Converting counts to voltage

The mapping from counts to voltage is linear, with full-scale counts mapping exactly to Vref:

voltage = counts × Vref / 65535

In code:

VREF = 3.3  # cam-dependent; see the quickref
counts = adc.read_u16()
voltage = counts * VREF / 65535
print(voltage, "V")

6.12.3. Voltage dividers

Two resistors in series between a voltage rail and ground form a voltage divider. The node between them sits at a voltage set by the ratio of the two resistors:

A voltage divider. Vin at the top connects through R1 to a node tapped off as V_out, which then connects through R2 to ground.

A voltage divider: R1 and R2 in series scale Vin down to V_out.

V_out = Vin × R2 / (R1 + R2)

Equal resistors give half the rail voltage; R2 much smaller than R1 puts the tap close to ground; R2 much larger puts it close to the rail.

The formula assumes nothing else draws appreciable current from V_out. An ADC pin is high-impedance (megohms, nanoamps) and easily satisfies that, so a divider feeding an ADC behaves as the formula predicts.

6.12.4. Potentiometers

A potentiometer is a single physical component that is exactly a voltage divider, with a sliding wiper that moves the tap between the two ends. Turning the knob changes R1 and R2 together while keeping their sum (the total resistance of the pot) constant.

A potentiometer wired between 3.3 V and ground. The wiper is tapped off to an ADC pin.

A potentiometer wired as a manual voltage source for the ADC: 3.3 V on one end, ground on the other, wiper to the pin.

A pot is the canonical input device for trying out the ADC. Wire one end to 3.3 V, the other to ground, and the wiper to an ADC-capable pin; turning the knob sweeps the wiper through every voltage between the rails.

import time
from machine import ADC

pot = ADC("P6")
VREF = 3.3

while True:
    counts = pot.read_u16()
    voltage = counts * VREF / 65535
    print(voltage, "V")
    time.sleep_ms(100)

6.12.5. Reading higher voltages with a divider

A voltage above Vref will pin the ADC at full-scale and may damage the input if it exceeds the absolute-maximum rating. To read a higher source – a battery, a sensor output that ranges beyond Vref – scale it down with a fixed voltage divider before it reaches the pin:

A voltage divider scaling a high V_in down to an ADC pin. R1 runs from V_in down to a junction, which is tapped off horizontally to the ADC pin; R2 continues from the junction down to ground.

Scaling a high-voltage source to fit the ADC: R1 and R2 form a fixed voltage divider whose tap feeds the ADC pin.

Pick R1 and R2 so the divided voltage stays inside the ADC’s range at the highest input voltage you expect:

V_adc = V_in × R2 / (R1 + R2)

For a maximum V_in = 12 V and a 3.3 V reference, the ratio R2 / (R1 + R2) must be at most 3.3 / 12 0.275. A common pick with a little headroom is R1 = 33 , R2 = 10 . The ratio is 10 / 43 0.233, so V_adc tops out at about 12 × 0.233 2.79 V – safely below Vref.

To recover the original V_in from an ADC reading, invert the divider formula:

V_in = V_adc × (R1 + R2) / R2

In code:

from machine import ADC

R1 = 33_000
R2 = 10_000
VREF = 3.3

adc = ADC("P6")

counts = adc.read_u16()
v_adc = counts * VREF / 65535
v_in = v_adc * (R1 + R2) / R2
print(v_in, "V")

A few practical notes:

  • The divider draws V_in / (R1 + R2) continuously. With R1 + R2 = 43 and V_in = 12 V, that is about 280 µA – usually negligible, but if the source is battery-powered consider larger resistors (100 kΩ to 1 MΩ) to cut idle drain.

  • Resistor tolerance (typically ±1 % or ±5 %) feeds directly into measurement accuracy. Two ±5 % resistors can give the recovered V_in a worst-case error of roughly ±10 %.

  • The divider’s source impedance combines with any stray capacitance to low-pass-filter the input. For fast-changing signals that matters; for a battery-voltage check it does not.