Simple rotary encoder controlled PWM dimmer

This post is basically a proof of concept for how to use a rotary (quadrature) encoder with an Arduino. Rotary encoders look a bit like classic potentiometers, but instead of changing resistance between its pins, it has two outputs with digital signals 90 degrees shifted with respect to each other. Because of this 90° phase difference between its outputs, it is relatively simple to determine in which direction it was turned as at any given time only one of its outputs will change. An example of the output sequence is given in the table below.

       Left   Right
Step   B A    B A
 a     1 1    1 1
 b     1 0    0 1 
 c     0 0    0 0
 d     0 1    1 0
 a     1 1    1 1
 ...

There are two ways of implementing a rotary encoder in an Arduino sketch. The first one simply polls the state of the input pins and checks the current state with the previous state. The other option is to use one encoder output as interrupt trigger and the other output to indicate direction.

Using pin state polling

In this example Arduino’s inputs are continuously polled and stored in currentState. When currentState is different from the previousState polled, then the rotary encoder has changed its position and the PWM value has to be updated. The if-then construct (lines 37-41 and 45-49) is used to decide whether the change implies an increase or a decrease. Then a second if-then (lines 42 and 50) prevents PWM from changing from 100% to 0% (and the other way around) by passing end of scale.

The code:

/*
This sketch implements a simple dimmer using a rotary encoder. The
encoder is connected to pins 22 and 23.
The standard led on pin 13 will be dimmed using pwm.
More on: https://blog.linformatronics.nl/58/electronics/simple-rotary-encoder-controlled-pwm-dimmer
*/

// Arduino Mega1280

const uint8_t pwmPin = 13;
const uint8_t encoderPinA = 22; // encoder input channel A
const uint8_t encoderPinB = 23; // encoder input channel B
uint8_t previousState = 0;
uint8_t analogValue = 128; // initialize at 50% PWM
const uint8_t stepSize = 2; // step size to increase PWM setting

void setup(){
analogWrite( pwmPin , analogValue );
pinMode( pwmPin , OUTPUT );
pinMode( encoderPinA , INPUT_PULLUP );
pinMode( encoderPinB , INPUT_PULLUP );
}

void loop() {
uint8_t currentState = ( digitalRead( encoderPinA ) << 0 ) | ( digitalRead( encoderPinB ) << 1 );

/*
State
B A
red yellow Digital
1 1 3
1 0 2
0 0 0
0 1 1
*/

if (
( ( previousState == 3 ) & ( currentState == 2 ) ) |
( ( previousState == 2 ) & ( currentState == 0 ) ) |
( ( previousState == 0 ) & ( currentState == 1 ) ) |
( ( previousState == 1 ) & ( currentState == 3 ) ) ) {
if ( analogValue <= ( 255 - stepSize ) ) {
analogValue += stepSize;
}
} else if (
( ( previousState == 1 ) & ( currentState == 0 ) ) |
( ( previousState == 0 ) & ( currentState == 2 ) ) |
( ( previousState == 2 ) & ( currentState == 3 ) ) |
( ( previousState == 3 ) & ( currentState == 1 ) ) ) {
if ( analogValue >= stepSize ) {
analogValue -= stepSize;
}
}

if ( previousState != currentState ) {
analogWrite( pwmPin , analogValue );
previousState = currentState;
}
}

It is fairly easy to adapt this code for any other Arduino board types. It doesn’t rely on any specific hardware, just requires two digital input pins and one pin that can drive PWM.  Just change encoderPinA and encoderPinB. Most Arduino boards have an on board LED on pin 13 which is used for PWM here.

Using interrupts

The second option to implement a rotary encoder is using interrupts. An interrupt can be used to temporarily stop execution of the main program to service an event (interrupt), after which the main program is being resumed. The trick used here is to use one of the encoder outputs as a clock event (both rising and falling edges) attached to the interrupt input and the other input as direction indicator.

All the main loop does in this example is setting PWM value from analogValue, where the interrupt routine increases/decreases this value. This only works because of the fact that both edges (rising and falling) can be used to trigger the routine. Whenever the interrupt routine is entered, both encoder inputs are read. When both inputs are different (LOW, HIGH) the analogValue is increased, if both inputs are identical the analogValue is decreased.

The interrupt driven version for an Arduino Mega:

/*
This sketch implements a simple dimmer using a rotary encoder. The
encoder is connected to pins 21 and 22.
The standard led on pin 13 will be dimmed using pwm.
More on: https://blog.linformatronics.nl/58/electronics/simple-rotary-encoder-controlled-pwm-dimmer
*/

// Arduino Mega1280

/*
Encoder state
B A
red yellow Numeric
1 1 3
1 0 2
0 0 0
0 1 1
*/

const uint8_t pwmPin = 13;
const uint8_t encoderPinA = 21; // encoder input channel A
const uint8_t encoderPinB = 22; // encoder input channel B
const uint8_t interruptChannel = 2; // pin 21, encoder input channel A
const uint8_t stepSize = 1; // step size to increase PWM setting

volatile uint8_t analogValue = 128; // initialize at 50% PWM

static void interruptHandler(){
delay( 10 ); // debounce
if ( digitalRead( encoderPinA ) != digitalRead( encoderPinB ) ) {
if ( analogValue <= ( 255 - stepSize ) ) {
analogValue += stepSize;
}
} else {
if ( analogValue >= stepSize ) {
analogValue -= stepSize;
}
}
}

void setup(){
analogWrite( pwmPin , analogValue );
pinMode( pwmPin , OUTPUT );
pinMode( encoderPinA , INPUT_PULLUP );
pinMode( encoderPinB , INPUT_PULLUP );

attachInterrupt( interruptChannel , interruptHandler , CHANGE );
}

void loop() {
analogWrite( pwmPin , analogValue );
delay( 100 );
}

Notice that the interrupt pins vary with Arduino board type, check its product page which exact pins you have to use and don’t forget to update the settings for interruptChannel and encoderPinA.

Troubleshooting cronjobs using an interactive shell

One of the most common little annoyances on Linux machines is troubleshooting cronjobs. You write a little script that you want to be run from cron and you test it extensively on you command line. Once the script runs flawlessly you schedule it to run somewhere during the night, only to find out the next morning that nothing happened.

Puzzled by why nothing happened you start running the script from the command line where, of course, all works fine. Searching for the problem you realize the environment when run from cron must be different from your normal shell. You start running the job from cron, adding debugging messages, redirecting the errors to a temporary file, and reconfiguring the crontab to run the job in one or two minutes … and again in one or two minutes … and again in one or two minutes … . Step by step every little (usually PATH-related) problem is solved by manually rescheduling the job. Struggling to find more details, waiting for cron to re-run the job, it can take a long time to solve all issues with the script.

If only you were able to run an interactive shell with an environment set identical as if it was run from cron! The script below does exactly that. It sets an excellent environment to debug scripts that are required to run from cron. It allows you to execute individual commands and inspect its result one by one, just like you would troubleshoot a regular shell script. No more waiting for cron to fire the job only find out you still overlooked another little issue.

The ‘cronShell.sh’ script below is an optimized version that can be directly used on most Linux machines. It automatically adapts for the current user.

#!/bin/bash

# This script starts a shell with an environment that has
# identical environment as a shell script started from cron.

# More info: https://blog.linformatronics.nl/16/linux/troubleshooting−cronjobs

user=$( id -nu )

cd && env -i sh -c "
### Ubuntu Linux 12.10
export HOME='${HOME}'
export LANG='en_US.UTF−8'
export LANGUAGE='en_US:en'
export LOGNAME='${user}'
export PATH='/usr/bin:/bin'
export PWD='${HOME}'
export SHELL='/bin/sh'
###
exec sh"

And this is what it looks like when you run the script:

jhendrix@diablo:~$ cronShell.sh
$ set
HOME='/home/jhendrix'
IFS='
'
LANG='en_US.UTF−8'
LANGUAGE='en_US:en'
LOGNAME='jhendrix'
OPTIND='1'
PATH='/usr/bin:/bin'
PPID='32184'
PS1='$ '
PS2='> '
PS4='+ '
PWD='/home/jhendrix'
SHELL='/bin/sh'
$ # Type your commands here ...

Credit where credit is due: This post is heavily inspired on an excellent answer by Stephane Chazelas on Unix.StackExchange.com.