A resistor ladder is a brilliant but super simple way to attach multiple buttons to a single analog to digital converter (ADC) pin on an Arduino or other microcontroller. By expanding on the concept of the voltage divider, the resistor ladder offers a way to create various reference voltages by tapping into each ‘rung’ of the ladder. Consider the following schematic:
By attaching our analog pin between the first and subsequent series resistors and a button to earth at each ‘rung’, we’re effectively creating a variable voltage divider. If we measure or calculate the values read by our analog pin, we can interpret each reading as a different button being pressed with a few lines of code:
Rather than telling our code to look for the exact values we expect the analog pin to read, we set a range of values that can be interpreted as belonging to a specific button. We do this by calculating the points halfway between each expected ADC reading and set these as as the boundaries of our ranges. Just like that, we’ve got 5 buttons on the one pin! We’re not done just yet though, because this is just about the worst possible way we could implement this.
Assuming perfect resistors, the voltage readings for each button should be around 0.0V, 2.5V, 3.33V, 3.75V and 4.0V, with corresponding ADC readings of around 0, 512, 683, 768 and 819. You might have already noticed a trend by this point…
If we add another 5 switches each with a 1kΩ resistor, the subsequent voltages will be 4.17V, 4.29V, 4.38V, 4.44V and 4.5V, and will continue to grow logarithmically with each 1kΩ resistor we add:
This might work for a project with a small number of buttons if you measure each value, and calculate and hard-code your ADC boundaries for each button – but magic numbers are bad, and you should feel bad.
If we change the voltage scaling from logarithmic to linear, each adjacent ADC value will become roughly equidistant. This means that we can dynamically calculate the corresponding button for any given reading, rather than hard-coding fixed values. This allows us to use the same piece of code for any number of buttons and as a bonus, uses the full range of our ADC – in the above example, we jump straight from 0 to 512 between the first two buttons – that’s half our resolution!
So, how do we know what resistors we should use?
For the following example, I’ve used a 47k Ω resistor for R1. I’ve chosen this value as it allows us to operate the circuit at a super low current (by Ohm’s law, 5V / 47kΩ = 0.11 mA) and still use a wide-range of commonly available resistor values. We’ll use 5 buttons total, and calculate the value for button 1 (which is actually the second button, like a zero-indexed array).
Since we have a fixed value of R1 we can work out the value for R2 using the following formula:
Rbutton = R1 / (1 – (Bindex / Btotal)) – Rtotal
Where Bindex is the index of the button in question, Btotal is the total number of buttons and Rtotal is the combined value of all resistors in the circuit prior to the current (in this case, it’s just the 47kΩ for button 0). So from the above, we have 47000 / (1 – (1 / 5)) – 47000 = 11750Ω. We can use a lookup table to find our closest equivalent value, which in this instance is 12kΩ.
If we do this for the remaining buttons, we get values of 20k, 29k and 120k, which should give us corresponding ADC readings of around 0, 208, 415, 616 and 822.
That’s still a bit of a process to work out – especially if you’ve got more than a handful of buttons, which is why I made this Google Sheets spreadsheet to do all the heavy lifting! Please note you’ll need to make a copy of this if you want to edit it and use your own values. Just enter the number of buttons you want to use and the tolerance of your resistors (which is only a consideration with a very large number of buttons – otherwise you can leave this at the default 2%.)
The column you’re most interested in is “Closest R” – this performs a lookup of the closest common resistor value to the calculated required value. The “Req’d” columns indicate the exact values we’re looking for, while the “Actual” columns indicate the voltages for the calculated closest available resistor. “Variance” simply tells us the difference between the current and previous ADC reading – if the result here is greater than the specified resistor tolerance, this will be indicated by the “In Tolerance” column.
Great – so we’ve got our resistors all worked out – all that’s left now is the code!
We start by determining the average difference between adjacent ADC readings, enabling us to dynamically calculate each button’s expected ADC reading. To test for an open circuit – the most likely result – we check if the measured value is less than halfway between 0 and our dynamically-calculated reading for the first button. If this evaluates to false, we begin by testing for each button in turn, comparing our ADC reading against the range boundaries that lie between adjacent readings, until we find a match.
Note that because “no button pressed” is a valid result in addition to each individual button press, we treat a zero value as an open circuit, and commence indexing our buttons from 1, as opposed to the zero index used during calculation.
With the above code, we can scale up and down by simply changing the value defined by the macro #define BUTTON 5 , with the number of buttons only limited by the tolerance of your resistors and resolution of your microcontroller’s ADC. We’ll see just how far we can push this in a future post!