Out of the box, the Raspberry Pi lacks an analog input. This puts it at a disadvantage compared to microcontroller-based boards like the Arduino.

But don’t despair: there are plenty of options to consider. Get up and running with Raspberry Pi and an external ADC.

Why Add Inputs?

The real world is full of phenomena that, if you have the right circuitry, can be easily described using a voltage. Get those voltages into digital form, and you can record them, manipulate them, and use them to control other parameters and devices.

You might be looking to monitor the moisture of your soil, the temperature of your greenhouse, or the weight of your hamster. You might be looking to add a volume control to your Pi, build an entire bank of faders, or design a joystick from scratch. The possibilities are, more or less, limitless.

Options for ADCs

So, which ADC is best for beginners?

Among the most popular and straightforward options are the MCP3004 (and MCP3008) chips from Microchip. You’ll get four (or eight) channels of 10 bits each, which can read up to 200 kSPS. On the other hand, there are the ADS111x devices from Texas Instruments, which read 16 bits at 860 SPS. So, there’s a tradeoff between speed and precision (and, naturally, price).

Many microcontrollers come with built-in ADCs. The ATMega you find on the average Arduino will offer several 10-bit channels, on top of everything else. This is what allows the Arduino to provide analog inputs where the Raspberry Pi can’t. If you already have an Arduino involved in your setup, and 10 bits is enough fidelity, then this might actually be the easiest way to go.

Here, we’ll keep it simple, with an ADS1115 from Adafruit.

A photograph of an ADC
Photo by Ben Allen - no attribution required.

What Is a Programmable Gain Amplifier?

This chip comes with a few interesting features, including a Programmable Gain Amplifier (PGA). This will let you set the desired range of values digitally, down to a fraction of a volt. With the number of values that 16 bits can represent, this will allow you to detect differences of just a few microvolts.

The advantage here is that you can change the gain midway through the program. Other chips, like the MCP3004, take a different approach; they come with an extra pin, to which you can supply a reference voltage.

What About Multiplexing?

A multiplexer (or mux) is a switch that lets you read many inputs using a single ADC. If your ADC chip comes with many input pins, then there’s some internal multiplexing going on. The ADS1115’s mux allows for four inputs, which you can select via the internal registers.

Dealing With Registers

The ADS1115 provides these options, and a few more besides. You can deal with the multiplexer, adjust the gain, activate the built-in comparator, change the sample rate, and put the device into low-power sleep mode, all by flipping a few switches.

But where are those switches? They’re inside the package, in the form of very small bits of memory called registers. To activate a given feature, you just need to set the relevant bit to a 1, rather than a 0.

Looking at the ADS111x datasheet, you’ll find that these models come with four registers, including the configuration registers that govern the device’s behavior.

a table from the ADS datasheet
from the ADS1115 datasheet

For example, bits 14 to 12 control the multiplexer. Using these three bits, you can select from eight configurations. The one you’ll want here is “100”, which will give the difference between input zero and ground. Bits 7 to 5, on the other hand, govern the sample rate. If you want the maximum of 860 samples per second, you could set these to “111”.

Once you know which options to set, you’ll have two bytes to send to the ADC. If you later want to set a single bit here or there, then you can deal with them individually using bitwise operators.

Here’s where it might get confusing. In this case, the binary isn’t representing a value, but the values of individual switches. You could express these variables as one big number, in decimal or in hexadecimal. But if you want to avoid headaches, you should stick to the binary version, which is easier to read.

Wiring It Up

You can plug this device straight into the breadboard. The positive voltage input will accept anywhere between 2 and 5.5v, which means that the 3.3v rail on the Raspberry Pi will work nicely.

Wire the SDA and SCL inputs to counterparts on the RPi, and do the same things with the ground and 3.3v. Get a potentiometer between the ground and voltage lines, and put the middle lead into the first input of the ADC. That’s all you need to get going!

Dealing With I2C

Different ADCs work via different protocols. In the case of our ADS1115, we’re going to be using I2C.

The following example will interact with the ADC using Python. But before you do that, you’ll need to set it up. Recent versions of Raspberry Pi OS have made this very simple. Head to Preferences > Raspberry Pi Configuration. Then, from the Interfaces tab, switch I2C on.

A screenshot of the Raspberry Pi Configuration Window
Screenshot by Ben Allen -- no attribution required

To check everything is working, open a terminal and run:

 sudo i2cdetect -y 1 

This command will output a grid. Assuming everything is working, and you’ve wired it up correctly, you’ll see a new value appear in the grid. This is the address of your ADC. Bear in mind here that it’s a hexadecimal value, so you need to prefix it with “0x” when you use it in the code below. Here, it’s 0x48:

a screenshot of the RPi console
Screenshot by Ben Allen - no attribution required

Once you have the address, you can use the SMBus library to send I2C commands. You’ll be dealing with two methods here. The first is write_word_data(), which accepts three arguments: the device address, the register you’re writing to, and the value you want to write.

The second is read_word_data(), which accepts just the device address and the register. The ADC will be continuously reading voltages and storing the result in the conversion register. With this method, you can retrieve the contents of that register.

You can beautify the result a little bit, and then print it. Before you go back to the start of the loop, introduce a short delay. This will ensure you’re not overwhelmed with data.

 from smbus import SMBus
import time
addr = 0x48
bus = SMBus(1)

# set the registers for reading
CONFIGREG = 1
CONVERSIONREG = 0

# set the address register to point to the config register
# write to the config registers
bus.write_word_data(addr, CONFIGREG, (0b00000100 << 8 | 0b10000010))

# define the top of the range
TOP = 26300

while True:
    # read the register
    b = bus.read_word_data(addr, CONVERSIONREG)

    # swap the two bytes
    b = ((b & 0xFF) << 8) | ((b >> 8) & 0xFF)
    
    # subtract half the range to set ground to zero
    b -= 0x8000

    # divide the result by the range to give us a value between zero and one
    b /= TOP

    # cap at one
    b = min(b, 1)

    # bottom is zero
    b = max(b, 0)

    # two decimal places
    b = round(b, 2)
    print(b)
    time.sleep(.01)

You’re just about done. Map the range of values you’re getting to the one you prefer, and then truncate to the desired number of decimal places. You can tailor the print function so that you only print a new value when it’s different from the last value. If you're unsure about max, min, and round, you can check out our list of the 20 most important Python functions!

Dealing With Noise

Now, unless your setup is super, super neat and tidy, you’ll notice some noise. This is the inherent downside of using 16 bits rather than just ten: that little bit of noise will be more perceptible.

By tying the adjacent input (input 1) to ground, and switching the mode so that you’re comparing inputs one and two, you can get much more stable results. You could also swap out those long, noise-collecting jumper cables for small ones, and add a few capacitors while you’re at it. The value of your potentiometer can make a difference, too.

There are also software options. You might create a rolling average, or simply disregard small changes. The downside there is that extra code will impose a computational cost. If you’re writing conditional statements in a high-level language like Python, and taking thousands of samples every second, these costs will compound rapidly.

Go Further With Many Possible Next Steps

Taking readings via I2C is pretty straightforward and the same is largely true of other methods, like SPI. While it might seem that there are big differences between the available ADC options, the truth is that once you’ve got one of them working, it’s easy to apply the knowledge to the others.

So, why not take things further? Tie multiple potentiometers together, or try reading light, sound, or temperature. Expand the controller you’ve just made, and create a Raspberry Pi setup that’s truly hands-on!