Bits and Droids logo
Mail icon

Back to basics: Potentiometers

3/30/2024 | by Bits and Droids

Perhaps one of the most basic components in our toolbox never got the attention it deserved. It's a cheap, versatile, and, most of all, easy-to-use component. In this article, we will focus only on using a potentiometer as a voltage divider.

The potentiometer has a finite start and endpoint. In contrast, the rotary encoder we've covered in the past had an infinite start and endpoint. If you wanted to, you could turn the rotary encoder all night until the hardware gives up. But not the potentiometer; it can range from open to fully closed and anything in between. I included this part because I can remember my disappointment when I ordered a potentiometer in the hopes of using it for my radio stack. I was oblivious and a complete rookie, but I hope I can save at least one like-minded rookie from the same pain. Don't worry. I got your back!

The potentiometer is a component that provides a gradual scale of resistance to your circuits. We could use it to limit the voltage that flows from point A to point B (i.e., from your power source to an LED to control the brightness). Perhaps more interesting would be measuring that voltage to control game states. The amount of resistance can differ per potentiometer. It is usually listed on the product page or at the top of the potentiometer (the resistance is marked with an Ω sign).

There usually are 3 pins on a potentiometer (sometimes 6 to send the same signal towards 2 outputs. Both sets will have the same function in these instances). The first (5v) and third (gnd) pins are interchangeable. The current will enter the potentiometer through either pin and try to find its way out of the other. The middle pin is the analog output. It's connected to a wiper internally that moves towards either side when you rotate the physical knob. This may seem logical, but let's open up the potentiometer to get a clear view of these mechanics.

In this first example, we can see a global representation of a potentiometer. The current comes in from the red dot, and it tries to take the path of the least resistance towards either the wiper in the middle or the ground.

potentiometer wiper concept

The moment we add some power, we can see what happens to the resistance. Remember, when it's fully closed, the resistance will be 10.000 Ohms, and when it's fully open, it will be 0 Ohms. There might be some slight deviation, but in theory, the middle will roughly be the halfway point of the resistance. In our case, that would be 5K (5000) Ohms. The voltage will travel from the entry point down the wiper (if it's hooked up to something) towards the next component.

potentiometer wiper variable position

The further we move the wiper to either side, the higher/lower the voltage will become at the output.

Potentiometer wiper 3/4 position

Where to use it?

As I mentioned earlier, it has a clear start and endpoint. We can also clearly measure where the wiper is positioned on a scale by measuring the voltage at the wiper. We only need to wire it to an analog port on our Arduino if we want to do so. This makes it an ideal candidate for use cases with a gradual scale—think about throttles, flaps, axes, etc.

Analog read

We will use the Arduino function called analogRead(//parameter byte potentiometer pin) to measure the position. The analogRead function converts the analog signal to a digital one. What we perceive in the IDE is a digital signal and not an analog signal. What's up with that? Our Arduino uses an ADC (analog-to-digital converter). This makes it easy for us to interpret and work with the signal on our PC. The Arduino family uses a 10-bit ADC. This will map the signal on a scale of 0 to 1023. Other microprocessors can sometimes support an even higher resolution, but it often comes at the cost of extra noise. A 10-bit resolution will be more than enough for most use cases for our needs.

An ADC is an analog to digital converter. The other way around would be a DAC. Most people are more familiar with DAC's since they are widely used in audio setups. An example would be a DAC to drive your stereo where it converts digital music to analog signals.

When reading the analog value, we use the Arduino function called analogRead(*Pin of the potentiometer*). It's important to wire up your rotary encoder to an analog port on your board (usually prefixed with an A).

cpp
// in this example the potentiometer is hooked up to A0 byte potPin = A0; void setup(){ Serial.begin(115200); // this sets the pin (A0) to act like an input pinMode(potPin, INPUT); } void loop(){ Serial.println(analogRead(potPin); // if you don't add a delay you will be bombarded by values in the monitor delay(100); }

Smoothing the output

Now if we upload this sketch and open the serial monitor we will be greeted by something like this.

Serial monitor with potentiometer

Now, these values will change depending on the direction you twist the knob. But perhaps you already noticed that the first 7 values are similar. These little jumps will occur even when we don't touch the pot. This can be caused by slight fluctuations in the voltage, dust in the potentiometer, or a range of other factors. Ideally, we might want to smooth the results slightly to avoid this jitter. There isn't always a "best" approach in coding; perhaps you can even develop a better solution to the same problem. But today, we're going to check 2 commonly used solutions.

Averaging (big jumps)

If you encounter large jumps in your values, averaging your output might be a good solution. Each value will be stored individually in an array of a defined size—the bigger the array, the smoother the result. But the time it takes to calculate 1 value increases as well. Finding that sweet spot is a matter of trial and error to see what suits your needs.

cpp
int BitsAndDroidsFlightConnector::smoothPot(byte potPin){ int readings[10]={}; total = 0; for (byte i = 0; i < samples; i++){ total = total - readings[i]; readings[i] = analogRead(potPin); total = total + readings[i]; delay(1); } average = total / samples; return average; }

The example above is the one that is present in the library I made. It takes 10 samples and runs through a loop 10 times. Storing the value in an array, adding that value to the total, and dividing it by 10 once the loop is finished will give us an average of 10 readings, smoothing out any big jumps.

EXAMPLE: (10 10 11 11 10 15 10 11 12 10) / 10 = 11. We can clearly see that even with a jump towards 15, the average still came out to only 11.

Filtering (small jumps)

Sometimes, the jumps are tiny. Let us take these values as an example: 80, 81, 81, 81, 80. Even though the potentiometer isn't touched, it can still flicker slightly. The easiest solution would be to check the current value versus an old value and compare the result to a cut-off value. The bigger the value we check against, the less noise will be present. The bigger the cut-off value, the more fidelity will drop. When we use it for throttle or as a flaps lever, a slight loss of fidelity (in my opinion) isn't noticeable.

cpp
int value; int oldValue; void setup(){ } void loop(){ value = analogRead(potPin); //The abs function will return a positive result even when the result will be negative. This makes it easier to use for our statement if (value != oldValue && abs(oldValue - value) > 1) { oldValue = value; } }

The code example above will filter out the results if the difference isn't bigger than 1 (2 and onwards). When the result passes our filter, you must set the old value to the new value.

When you apply these smoothing tricks, your Serial monitor will be as calm as an ocean.

Out of the box

You don't have to use a potentiometer to represent a curve/axis or scale. We could also develop solutions that bind certain actions to certain values. In the example below, we read the value. If it's bigger than 600, we will decrease the Com 1 kHz. If the value is smaller than 400, we increase the Khz value of Com 1. To smoothen the experience, we added a slight delay to ensure our frequency doesn't skyrocket.

Notice a gap (<400 | 400-600 |>600)? There won't be any commands sent in the range of 400 to 600. In theory, we created a digital 3-way switch. We switch between an upstate, downstate, and idle state depending on the value. This way, we can use our potentiometer to control our radio (perhaps in a rather complicated way).

cpp
#include <BitsAndDroidsFlightConnector.h> BitsAndDroidsFlightConnector connector = BitsAndDroidsFlightConnector(true); void setup() { // put your setup code here, to run once: Serial.begin(115200); pinMode(A0, INPUT); } void loop() { // put your main code here, to run repeatedly: int value = analogRead(A0); if(value < 400){ Serial.println(connector.sendCom1FractInc()); delay(200); } if (value > 600){ Serial.println(connector.sendCom1FractDecr()); delay(200); } }