Skip to main content
Potentiometers and Encoders For DIY Arduino Projects

Potentiometers and Encoders For DIY Arduino Projects

Brendan...About 11 minTechnologyArduinoPotentiometerRotary EncoderEncoderDIY

Every twist and turn matters in the world of electronics. Imagine the last time you adjusted the volume on your stereo or set the temperature on your oven. Did you know the secret behind these satisfying twists are often rotary encoders and potentiometers? In this post we'll dive deep into the mechanics of rotary encoders and potentiometers, unraveling their roles, differences, and how they can elevate your projects.

While both devices are essential for measuring rotational movement, they have distinct ways of operating. Rotary encoders excel in generating digital signals, capturing both the degree of rotation and its direction – perfect for projects where precision is key. On the flip side, potentiometers act as variable resistors, offering an analog output that's invaluable for it's simplicity in many applications.

Potentiometers

image of various types of potentiometers
image of various types of potentiometers

Potentiometers, often abbreviated as 'pots,' can be likened to film cameras because they operate through an analog process. More straightforward in design compared to encoders, pots are well-suited for applications where a high level of precision is not critical. They fall under the category of variable resistors and are characterized by having three terminals:

In order for the image below

  • Low: The lower potential pin should be connected to a constant source near the ground potential
  • Wiper: This pin is the variable part which will vary in potential (somewhere between the HIGH and LOW pin depending on where the knob is turned to)
  • High: The Higher potential pin which should connect to a constant source above ground potential
image of potentiometer with leads marked
image of potentiometer with leads marked

Taking Apart a Potentiometer

Let's take apart a potentiometer so you can see what's inside:

In this image, you can see the resistive ring (the blackish ring between the two outer pins) made out of a semi-conductive material (in this case carbon). The wiper (middle pin) connects to a lead that drags along this ring, creating a variable resistance between the wiper pin and the external pins.

image of a potentiometer ring the resistor part of the potentiometer
image of a potentiometer ring the resistor part of the potentiometer

This is a side view of a partially disassembled pot. You can see at the top (where I circled it) that the wiper (gold lead, shown in last photo) touches the resistive ring. The other lead, I circled in the lower half, is where the wiper touches the wiper pin (the metallic ring shown in the first photo).

image of a potentiometer how it functions the wiper touching the resistive ring
image of a potentiometer how it functions the wiper touching the resistive ring

This photo shows the wiper, a highly conductive brass mechanism with springs pressing against the two rings (resistive ring and wiper pin) which connects them all together at different points on resistive ring creating different resistances at various positions.

another image of potentiometer wiper
another image of potentiometer wiper

The code (Arduino implementations)

Here is the simplest way to use a potentiometer; however, it will not work well!

// Low pin should connect to the ground
// Wiper will connect to A0
// High pin has a few options. It needs to be connected to a higher potential than Low pin. I connected it to 3.3v

void setup() {
  Serial.begin(9600);
}

void loop() {
  // Read the input on analog pin 0 (where the potentiometer is connected):
  int sensorValue = analogRead(A0);

  Serial.println(sensorValue);

  // Convert the sensor reading from a range of 0-1023 to 0-100:
  int scaledValue = map(sensorValue, 0, 1023, 0, 100);


  // Print out the scaled value to the Serial Monitor:
  Serial.println(scaledValue);


  // Wait for a bit to see the changes more clearly:
  delay(100);
}

You will understand why this doesn't perform well if you try it yourself. Spoiler: it jitters around quite a bit, even when the dial isn't being touched.

Fixing the Jitter

The Jitter occurs for a few reasons. Firstly, the quality of the hardware can cause this. Aside from selecting higher end hardware, you can solve this in a few other ways:

  • Software Debouncing: Introducing delays between readings to avoid any jittering.
  • Hardware filtering: Introducing hardware such as resistors or capacitors to filter readings
  • Digital Filtering: Low pass filters or moving averages.

Software debouncing

Unbeknownst to you, subtle vibrations occur when the wiper is moved and then halted, leading to fluctuations in resistance. This issue can be effectively addressed by introducing delays in the system:

const int potPin = A0; // Potentiometer connected to analog pin A0
int lastPotValue = 0;  // Variable to store the last potentiometer value
int potValue = 0;      // Variable to store the current potentiometer value
int debounceDelay = 50; // Debounce time in milliseconds

void setup() {
  Serial.begin(9600);
}

void loop() {
  int reading = analogRead(potPin);

  // Check if the potentiometer reading is different from the last reading
  if (abs(reading - lastPotValue) > 5) {
    // If the reading is different, reset the debouncing timer
    lastPotValue = reading;
    delay(debounceDelay);
  }

  // Take a new reading after the debounce period
  potValue = analogRead(potPin);

  // Print the stable value to the Serial Monitor
  Serial.println(potValue);

  // Small delay to prevent flooding the serial output
  delay(100);
}

The overall effect is a more stable output. You can adjust and tune the delay to whatever you deem best fit for your hardware.

Note

If you are more advanced with Arduino and C, you may already appreciate interrupts and their benefits. This debouncing technique is also possible with interrupts by storing the time taken at the reading in a global variable and checking if the delay has elapsed.

Hardware Filtering

This is beyond the scope of this article, but i will mention that in addition to high-quality potentiometers, you can also consider adding:

  • Hardware low pass filter (using resistors and capacitors)
  • Additional shielding and grounding

Digital Filtering

Here, we are going to implement a simple type of Low-pass filter known as a moving average.

SMAt=1ni=tn+1tXi \text{SMA}_t = \frac{1}{n} \sum_{i=t-n+1}^{t} X_i

where:

SMAt\text{SMA}_t: Simple Moving Average at time tt. This represents the average value of the series over a specific number of periods up to time tt.

nn: The number of periods over which the average is calculated. This determines the 'window' size of the moving average and affects how responsive the SMA is to changes in the data.

XiX_i: The value of the series at time ii. In this case a sam[ple from the pot.

const int potPin = A0; // Potentiometer connected to analog pin A0
const int numReadings = 10; // Number of readings to average
int readings[numReadings]; // Array to store the readings
int readIndex = 0; // Index of the current reading
int total = 0; // Running total of the readings
int average = 0; // Average of the readings

void setup() {
  Serial.begin(9600);

  // Initialize all the readings to 0:
  for (int thisReading = 0; thisReading < numReadings; thisReading++) {
    readings[thisReading] = 0;
  }
}

void loop() {
  // Subtract the last reading:
  total = total - readings[readIndex];

  // Read from the sensor:
  readings[readIndex] = analogRead(potPin);

  // Add this reading to the total:
  total = total + readings[readIndex];

  // Advance to the next position in the array:
  readIndex = readIndex + 1;

  // If we're at the end of the array, wrap around to the beginning:
  if (readIndex >= numReadings) {
    readIndex = 0;
  }

  // Calculate the average:
  average = total / numReadings;

  // Send the average to the computer:
  Serial.println(average);

  // Delay a bit to keep the serial output readable:
  delay(100);
}

Warning

You should tune the number of samples, balancing the responsiveness and improvement of Jitter.

This method combined with the debouncing, will significantly improve the most straightforward method.

EMA Filter

Another filter you can implement is an Exponential Moving Average (EMA). This gives more weight to recent data points, making it more responsive to new information.

EMAt=αXt+(1α)EMAt1 \text{EMA}_t = \alpha \cdot X_t + (1 - \alpha) \cdot \text{EMA}_{t-1}

where:

Where: EMAt\text{EMA}_t is the Exponential Moving Average at time tt.

α\alpha is the smoothing factor, calculated as 2/(n+1)2 / (n + 1), where nn is the number of periods.

XtX_t is the value of the data series at time tt.

EMAt1\text{EMA}_{t-1} is the EMA value of the previous period.

Simple Average

Yet another filtering technique is a simple average. In this case rather than sample the pot once each iteration, you would collect multiple samples at each iteration to improve the reading quality. This comes at the price of being much slower (if each read takes 5ms, 5 samples would make each read take > 25ms), but offers improved performance and can even be combined with SMA or EMA.

μ=i=1nXin \mu = \frac{\sum_{i=1}^{n} X_i}{n}

Where:

nn is the total number of data points in the series.

XiX_i is the value of the data series at the ii-th position.

Extremes

Potentiometers have a notable limitation in their response curve, especially noticeable at the beginning and end of their range. This issue arises from their construction and operational principles. The resistive elements in potentiometers, made from carbon, ceramic-metal compounds, and conductive plastics, are similar to those used in resistors. As the wiper slides over this resistive path, it alters the resistance between the wiper and the high and/or low connection points. While this mechanism functions effectively around the middle of the resistive path, minor movements can cause substantial variations in output at the extreme ends. This results in a lack of precision and is often manifested as sudden jumps in value at these extreme positions. Such behavior is disadvantageous in scenarios where uniform and smooth control is required across the entire movement range.

There are a few ways to solve these issues with potentiometers:

  • Mapping and Calibration: Collect data from each potentiometer to produce a mapping. The mapping can then convert the analog reading to a discreet value.
  • Thresholds: Introducing thresholds at the extremes to ignore changes in the potentiometer.

Mapping and calibration

This is beyond the scope of this article. Basically, you would collect samples of the potentiometer readings and then map the samples to a linear (or logarithmic) scale.

You could use a motor to control the precise angular movements of the potentiometer and collect the samples as you go, mapping them to radians and then converting them to scale. This requires very precise and well-tested hardware for the test setup as well as a more complex software suite.

Thresholds

A much simpler approach is to introduce thresholds at the extremes instead.

const int potPin = A0; // Potentiometer connected to A0
const int lowerThreshold = 200; // Lower threshold value
const int upperThreshold = 800; // Upper threshold value
const int minValue = 0; // Minimum output value
const int maxValue = 100; // Maximum output value

void setup() {
  Serial.begin(9600);
}

void loop() {
  int potValue = analogRead(potPin); // Read the potentiometer
  int outputValue;

  if (potValue < lowerThreshold) {
    outputValue = minValue; // Set to minimum if below the lower threshold
  } else if (potValue > upperThreshold) {
    outputValue = maxValue; // Set to maximum if above the upper threshold
  } else {
    // Scale the value between the thresholds
    outputValue = map(potValue, lowerThreshold, upperThreshold, minValue, maxValue);
  }

  Serial.println(outputValue); // Print the output value
  delay(100); // Short delay for readability
}

Since the worst of the Jitter is at the extremes (highest or lowest positions), this method also helps eliminate most issues.

In conclusion, you should be able to use all of this advice and produce a potentiometer that is much more stable than the default simple way of doing it.

Encoders

Picture of various encoders
Picture of various encoders

While potentiometers are preferred for their simplicity, analog signal control encoders excel in precision and digital applications. You can think of an encoder like the digital camera of rotary information. Unlike pots, encoders do not involve a chemical or analog process.

There are two different types of encoders:

  • Incremental Encoders: Provide relative position information by outputting a series of pulses as they rotate. They typically have two output channels (A and B) that produce square wave pulses in a quadrature-encoded pattern, which allows for detecting the direction of rotation.
  • Absolute Encoders: Provide absolute position information for each rotation angle. Each position is uniquely coded, typically using binary or Gray code, which can be read through parallel or serial communication.

There are a few different hardware implementations for encoders:

  • Hall Effect Encoders: These use Hall effect sensors to detect magnetic fields generated by a rotating magnet.
  • Optical Encoders: Use light passing through a coded disk to detect rotation. Absolute types read the unique patterns for each position, which might involve complex decoding algorithms.
  • Magnetic Encoders: Similar to Hall effect encoders, they use magnetic fields but often with different sensor arrangements.
  • Mechanical Encoders: Cheap and simple, often used in DIY projects and marketed to Arduino users.

Taking apart a simple Incremental/ Mechanical Encoder

I bought these cheap encoders from Amazon and accidentally broke one while desoldering it from the breakout board, so let's look at what's inside.

Here is the encoder we are working with

A cheap mechanical encoder
A cheap mechanical encoder

On this side of the encode, you can see a ring with little metal leeds sticking out in a regular pattern. In a moment, we learn that that creates the quadrate-encoded pattern. For now, understand it mechanically.

One side of the encoder taken apart
One side of the encoder taken apart

On this side, you can see a few spring-loaded leads (circled in grey) similar to the wiper on the potentiometer. The flap-looking object in the center is just a regular push-button as this encoder features a push button when pressing into the knob.

other side of the encoder taken apart
other side of the encoder taken apart

Note

N.b. I broke one pin coming off the device when i desoldered it from a breakout board. It would normally have a total of 5 pins.

Incremental encoders

First, we need to understand the quadrature-encoded pattern.

Let's start with some experiential data here's my set up using the diligent Discovery 2open in new window (a digital oscilloscope) and an Arduino unoopen in new window.

Photo of my setup using Analog Discovery 2 and Arduino uno
Photo of my setup using Analog Discovery 2 and Arduino uno

You can see the quadrature-encoded signal after turning the dial clockwise and capturing a few frames.

photo of the oscilloscope output of quadrature-encoded signal
photo of the oscilloscope output of quadrature-encoded signal

Here is a table of the various ways to indicate which way the encoder is rotating

StepSignal ASignal BDirection
100Clockwise
210Clockwise
311Clockwise
401Clockwise
100Counterclockwise
401Counterclockwise
311Counterclockwise
210Counterclockwise

Looking back to the captured data above, I was turning the encoder clockwise, so let's break it down starting at the -200ms point.

The blue line (signal A) is high as is the orange line (signal B). This corresponds to step 3 in our chart, showing both signals as high (1,1). Next, A falls first, leading to step 4 (0,1). Then, B falls, indicating step 1 (0,0). Following this, A rises, moving to step 2 (1,0). As this pattern continues while I turn the knob clockwise, each step is repeated successively.

Now let's look at counter-clockwise:

Oscilloscope output well turning counter clockwise
Oscilloscope output well turning counter clockwise

In this case, we have the exact opposite. We start at the point -100ms, where both Signal A and Signal B are at a high potential, corresponding to step 3 (1,1). This time, B falls first, transitioning to step 2 (1,0). Next, A falls, indicating step 1 (0,0). Then, B rises, moving to step 4 (0,1). The process continues in this manner.

Here is the pseudo-code for determining the direction of the rotary encoder:

Initialize previousA = 0, previousB = 0
Initialize currentA, currentB

Function readEncoder:
    currentA = Read signal A (as binary 0 or 1) // in an actual application would be handled by interrupts
    currentB = Read signal B (as binary 0 or 1)

    If (previousA == 0 and previousB == 0):
        If (currentA == 1):
            Direction = Clockwise
        Else If (currentB == 1):
            Direction = Counterclockwise

    Else If (previousA == 1 and previousB == 0):
        If (currentB == 1):
            Direction = Clockwise
        Else If (currentA == 0):
            Direction = Counterclockwise

    Else If (previousA == 1 and previousB == 1):
        If (currentA == 0):
            Direction = Clockwise
        Else If (currentB == 0):
            Direction = Counterclockwise

    Else If (previousA == 0 and previousB == 1):
        If (currentB == 0):
            Direction = Clockwise
        Else If (currentA == 1):
            Direction = Counterclockwise

    previousA = currentA
    previousB = currentB

    Return Direction

Arduino Code for Mechanical Encoder

Though the pseudo code above works theoretically, it does not work on an Arduino with a mechanical encoder. Instead, this is by far the best method I could find. It was a learning experience for me as the debouncing method was not the most intuitive, but it performs well. My intuition told me that setting up interrupts to listen for changes in each signal and then determining the direction in the loop would work best. However, reacting to the signal every time it changes poses multiple issues and hardware constraints more suited for a faster MCU. Alternatively, this code handles double counting and debouncing nicely. It avoids double counting (mechanical encoders send two pulses per increment) by reacting only when the signal A line goes high and debounces by only checking specific transitions of Signal A, reducing the likelihood of noise or small fluctuations causing false counts.

// Rotary Encoder Inputs
#define PIN_A 2
#define PIN_B 3

volatile int counter = 0; // Counter for the encoder's position
int currentStateA; // Current state of pin A
int lastStateA; // Last state of pin A
bool cw = true; // true indicates CW (Clockwise), false indicates CCW (Counter-Clockwise)

volatile bool updateRequired = false; // Flag to indicate an update is required

void setup() {
  pinMode(PIN_A, INPUT);
  pinMode(PIN_B, INPUT);

  // Setup Serial Monitor
  Serial.begin(9600);

  // Read and store the initial state of PIN_A
  lastStateA = digitalRead(PIN_A);

  attachInterrupt(digitalPinToInterrupt(PIN_A), updateEncoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(PIN_B), updateEncoder, CHANGE);
}

void loop() {

  if (updateRequired) {
    updateRequired = false;
    String direction = (cw) ? "CW" : "CCW";
    Serial.println(direction);
  }
}

void updateEncoder() {

  currentStateA = digitalRead(PIN_A);

  // Check for state change in PIN_A to avoid double count
  if (currentStateA != lastStateA && currentStateA == HIGH) {

    // Determine rotation direction based on states of PIN_A and PIN_B
    cw = (digitalRead(PIN_B) != currentStateA);

    updateRequired = true;
  }

  lastStateA = currentStateA;
}

Conclusion

So far, I have covered mechanical encoders only but will update in the future when more encoders arrive. This blog post helps you with your projects.

Last update:
Contributors: Brendan
Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v3.0.0-alpha.10