Reading a Quadrature Encoded Knob in Double-Quick Time

The simple technique of reading a quadrature knob I described there works fine, except for the knob I picked for a recent project. That’s what I get for using surplus knobs, right?

I picked this knob because it has a momentary push-on switch that I’ll be using for power; the gizmo should operate only when the knob is pressed. The rotary encoder part of the knob has 30 detents, but successive “clicks” correspond to rising and falling clock edges: the encoder has only 15 pulses in a full turn.

So, while advancing the knob counter on, say, the falling edges of the A input worked, it meant that the count advanced only one step for every other click: half the clicks did nothing at all. Disconcerting, indeed, when you’re controlling a voltage in teeny little steps.

Worse, the encoder contacts are painfully glitchy; the A input (and the B, for that matter) occasionally generated several pulses that turned into multiple counts for a single click.

Fortunately, the fix for both those problems is a simple matter of software…

The Arduino interrupt setup function can take advantage of the ATmega168’s ability to generate an interrupt on a pin change, at least for the two external interrupts that the Arduino runtime code supports. So it’s an easy matter to get control on both rising & falling edges of the A input, then make something happen on every click of the knob as you’d expect.

The hardware is straightforward: connect the knob’s A output to INT0, the B  output to D7, and the common contact to circuit ground. Although you can use the internal pullups, they’re pretty high-value, so I added a 4.7 kΩ resistor to Vcc on each input. The code defining that setup:

#define PIN_KNOB_A	2			// LSB - digital input for knob clock (must be 2 or 3!))
#define IRQ_KNOB_A	(PIN_KNOB_A - 2)	//  set IRQ from pin
#define PIN_KNOB_B	7			// MSB - digital input for knob quadrature

Because we’ll get an interrupt for each click in either direction, we can’t simply look at the B input to tell which way the knob is turning. The classy way to do this is to remember where we were, then look at the new inputs and figure out where we are. This buys two things:

  • Action on each edge of the A input, thus each detent
  • Automatic deglitching of crappy input transitions

So we need a state machine. Two states corresponding to the value of the A input will suffice:

enum KNOB_STATES {KNOB_CLICK_0,KNOB_CLICK_1};

A sketch (from one of these scratch pads) shows the states in relation to the knob inputs. Think of the knob as being between the detents for each state; the “click” happens when the state changes.

Knob encoder states and inputs

Knob encoder states and inputs

In order to mechanize that, put it in table format. The knob state on the left shows where the knob was and the inputs along the top determine what we do.

Knob state table

Knob state table

So, for example, if the knob was resting with input A = 0 (state KNOB_CLICK_0), then one detent clockwise means the inputs are 01. The second entry in the top row has a right-pointing arrow (→) showing that the knob turned clockwise and the next state is KNOB_CLICK_1. In that condition, the code can increment the knob’s position variable.

The entries marked with X show glitches: an interrupt happened, but the inputs didn’t change out of that state. It could be due to noise or a glitchy transition, but we don’t care: if the inputs don’t change, the state doesn’t change, and the code won’t produce an output. Eventually the glitch will either vanish or turn into a stable input in one direction or the other, at which time it’s appropriate to generate an output.

Two variables hold all the information we need:

volatile char KnobCounter = 0;
volatile char KnobState;

KnobCounter holds the number of clicks the knob has made since the last time the mainline code read the value.

KnobState holds the current (soon to be previous) state of the A input.

Now we can start up the knob hardware interface:

pinMode(PIN_KNOB_B,INPUT);
digitalWrite(PIN_KNOB_B,HIGH);
pinMode(PIN_KNOB_A,INPUT);
digitalWrite(PIN_KNOB_A,HIGH);
KnobState = digitalRead(PIN_KNOB_A);
attachInterrupt(IRQ_KNOB_A,KnobHandler,CHANGE);

An easy way to handle all the logic in the state table, at least for small values of state table, is to combine the state and input bits into a single value for a switch statement. With only eight possible combinations, here’s what it the interrupt handler looks like:

void KnobHandler(void)
{
byte Inputs;
	Inputs = digitalRead(PIN_KNOB_B) << 1 | digitalRead(PIN_KNOB_A);	// align raw inputs
	Inputs ^= 0x02;								// fix direction

	switch (KnobState << 2 | Inputs)
	{
	case 0x00 : // 0 00 - glitch
		break;
	case 0x01 : // 0 01 - UP to 1
		KnobCounter++;
		KnobState = KNOB_CLICK_1;
		break;
	case 0x03 :	// 0 11 - DOWN to 1
		KnobCounter--;
		KnobState = KNOB_CLICK_1;
		break;
	case 0x02 : // 0 10 - glitch
		break;
	case 0x04 : // 1 00 - DOWN to 0
		KnobCounter--;
		KnobState = KNOB_CLICK_0;
		break;
	case 0x05 : // 1 01 - glitch
		break;
	case 0x07 : // 1 11 - glitch
		break;
	case 0x06 :	// 1 10 - UP to 0
		KnobCounter++;
		KnobState = KNOB_CLICK_0;
		break;
	default :	// something is broken!
		KnobCounter = 0;
		KnobState = KNOB_CLICK_0;
	}
}

Reading the knob counter in the main loop is the same as before:

noInterrupts();
KnobCountIs = KnobCounter;	// fetch the knob value
KnobCounter = 0;		//  and indicate that we have it
interrupts();

And that’s all there is to it!