Arduino Snippets: Quadrature Knob

Any gadget goes better with a knob and what better knob than one with detents all the way around? This one even has a push-on momentary switch on the shaft, so you can switch gain / change modes / control power while turning:

Quadrature Knob with Push Switch

Quadrature Knob with Push Switch

Using the Arduino’s internal pullups means you just plug the wires into the header sockets and it works:

Quadrature Knob and Switch - breadboard

Quadrature Knob and Switch – breadboard

The code uses the same interrupt-driven state machine I described there to avoid glitches and twitches. The test code dumps the accumulated knob rotation count and switch state out the serial port:

Knob encoder
Ed Nisley - KE4ZNU - November 2012
Knob count: 0 Switch: 0
Knob count: 0 Switch: 1
Knob count: 1 Switch: 1
Knob count: 1 Switch: 0
Knob count: 2 Switch: 0
Knob count: 3 Switch: 0
Knob count: 4 Switch: 0
Knob count: 4 Switch: 1
Knob count: 3 Switch: 1
Knob count: 2 Switch: 1
Knob count: 1 Switch: 1
Knob count: 0 Switch: 1
Knob count: -1 Switch: 1
Knob count: -2 Switch: 1
Knob count: -3 Switch: 1
Knob count: -4 Switch: 1
Knob count: -5 Switch: 1
Knob count: -5 Switch: 0
Knob count: -4 Switch: 0

Yes, you (well, I) can spin the knob fast enough to accumulate a few counts between the 10 ms updates and it will return to zero at the same physical location, so it’s not losing any counts in the process.

Knob encoder
Ed Nisley - KE4ZNU - November 2012
Knob count: 0 Switch: 0
Knob count: -4 Switch: 0
Knob count: -6 Switch: 0
Knob count: -7 Switch: 0
Knob count: -6 Switch: 0
Knob count: -4 Switch: 0
Knob count: -3 Switch: 0
Knob count: -6 Switch: 0
Knob count: -8 Switch: 0
Knob count: -9 Switch: 0
Knob count: -10 Switch: 0
Knob count: -12 Switch: 0
Knob count: -18 Switch: 0
Knob count: -19 Switch: 0
Knob count: -20 Switch: 0
Knob count: -19 Switch: 0
Knob count: -18 Switch: 0
Knob count: -15 Switch: 0
Knob count: -9 Switch: 0
Knob count: -7 Switch: 0
Knob count: -6 Switch: 0
Knob count: -4 Switch: 0
Knob count: -3 Switch: 0
Knob count: -2 Switch: 0
Knob count: -1 Switch: 0
Knob count: 0 Switch: 0

Not very exciting as it stands, but it’s got plenty of upside potential.

The Arduino source code:

// Quadrature knob with switch
// Ed Nisley - KE4ANU - November 2012
// Based on:
// http://softsolder.com/2009/03/03/reading-a-quadrature-encoded-knob-in-double-quick-time/

//----------
// Pin assignments

const byte PIN_KNOB_A = 2;			// knob A switch - must be on ext interrupt 2
const byte PIN_KNOB_B = 4;			//  .. B switch
const byte PIN_KNOB_SW  = A5;		//  .. push-close momentary switch

const byte PIN_SYNC = 13;			// scope sync

//----------
// Constants

const int UPDATEMS = 10;				// update LEDs only this many ms apart

#define TCCRxB 0x02						// Timer prescaler

//----------
// Globals

enum KNOB_STATES {KNOB_CLICK_0,KNOB_CLICK_1};

volatile char KnobCounter = 0;
volatile char KnobState;

char PrevKnobCounter = 0;
char KnobSwitch, PrevSwitch = 0;

unsigned long MillisNow;
unsigned long MillisThen;

//-- Helper routine for printf()

int s_putc(char c, FILE *t) {
  Serial.write(c);
}

//-- Knob interrupt handler

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;
	}
}

//-- Read switch, flip sense

char ReadSwitch(int PinNumber) {
	return !digitalRead(PinNumber);
}

//------------------
// Set things up

void setup() {
	pinMode(PIN_SYNC,OUTPUT);
	digitalWrite(PIN_SYNC,LOW);	// show we arrived

//	TCCR1B = TCCRxB;					// set frequency for PWM 9 & 10
//	TCCR2B = TCCRxB;					// set frequency for PWM 3 & 11

	pinMode(PIN_KNOB_B,INPUT_PULLUP);
	pinMode(PIN_KNOB_A,INPUT_PULLUP);
	pinMode(PIN_KNOB_SW,INPUT_PULLUP);

	KnobState = digitalRead(PIN_KNOB_A);
	PrevSwitch = digitalRead(PIN_KNOB_SW);
	attachInterrupt((PIN_KNOB_A - 2),KnobHandler,CHANGE);

	Serial.begin(9600);
	fdevopen(&s_putc,0);				// set up serial output for printf()

	printf("Knob encoder\r\nEd Nisley - KE4ZNU - November 2012\r\n");

	MillisThen = millis();

}

//------------------
// Run the test loop

void loop() {

	MillisNow = millis();

	if ((MillisNow - MillisThen) > UPDATEMS) {

		digitalWrite(PIN_SYNC,HIGH);

		KnobSwitch = ReadSwitch(PIN_KNOB_SW);
		if ((PrevKnobCounter != KnobCounter) || (PrevSwitch != KnobSwitch)) {
			printf("Knob count: %d Switch: %d\n",KnobCounter,KnobSwitch);
			PrevKnobCounter = KnobCounter;
			PrevSwitch = KnobSwitch;
		}

		digitalWrite(PIN_SYNC,LOW);

		MillisThen = MillisNow;
	}

}

  1. #1 by Bill Rutiser on 2012-12-11 - 09:50

    _The OpenSCAD source code:_

    So what happens when this gets to the 3D printer ??

    • #2 by Ed on 2012-12-11 - 11:10

      Why, it just prints the equivalent FPGA pattern, of course…

      Thanks for the catch!

  2. #3 by Steve on 2012-12-11 - 11:30

    How did you know I was looking for this solution last night? Quick googling showed that Quadrature decoding isn’t supported in hardware on AVRMegas, so I pulled out an old XMega prototype board and started trying to implement app note AVR1600. This morning I saw you had a complete solution, so I spent 5 minutes and gave it a go… it appears to be good enough that I can do what I want with an Arduino. For what it’s worth, I’ve got a motor with a shaft encoder that reports 1024 counts per turn, and adjusting the voltage on the motor till the index pulse shows about 30 Hz on my scope, and letting it run for a while, your code doesn’t loose position. Somewhere between 30 Hz and 35 Hz it starts to miss counts. 30 rev/sec 1024 interrupts/rev is about 30,000 interrupts a second. I’m pretty sure you won’t be able to spin your encoder by hand too fast :-)

    – Steven Ciciora

    • #4 by Ed on 2012-12-11 - 12:02

      How did you know I was looking for this solution last night?

      If the truth be known, I realized you needed this info a week ago! [grin]

      about 30,000 interrupts a second

      Which works out to a bit over 30 microseconds/count. At 16 MHz, that’s about 500 machine ops/count and, using my “It takes 10 machine ops per line of C” rule-of-thumb, the poor thing must handle everything in a mere 50 lines of code.

      Yeah, I can see why it’d run out of steam above that speed…

      Glad it works for you!

  1. Arduino Snippets: Analog Button Input « The Smell of Molten Projects in the Morning
  2. Arduino Snippets: LED Stroboscopic Tachometer « The Smell of Molten Projects in the Morning
  3. Strobe Photography: Circuit Doodles | The Smell of Molten Projects in the Morning
  4. Strobe Photography: Control Program | The Smell of Molten Projects in the Morning