Arduino Snippets: Analog Button Input

Reading more than a few pushbuttons requires multiplexing, with a parallel-in shift register similar to the old 74LS166 being popular (and supported by the shiftIn() function). You can also use an Arduino analog input to multiplex the buttons, at the cost of a resistor string that probably draws more current and costs more than a logic IC:

Knob and Buttons

Knob and Buttons

The switches produce voltages at the analog input which are not the evenly spaced 1 V increments you might expect: the 10 kΩ pullup appears in parallel with the sum of all the resistors above the closed switch, so the voltages come out a bit higher. The notation to the right of each switch indicates the voltage and equivalent ADC value, assuming a 5.0 V AVREF that won’t be quite right for your circuit. The analog input spec recommends less than 10 kΩ source resistance, but you could probably go much higher without any problem; the ADC output value need not be particularly accurate.

If you happen to have a SIP resistor pack containing five separate resistors (not the usual nine resistors in a 10 lead SIP), then the circuitry doesn’t amount to much:

Knob and Buttons - breadboard

Knob and Buttons – breadboard

It’s sitting in front of the ZNVL110A MOSFETs driving the RGB LED strip light. Those flat blue surplus buttons came in pairs pre-configured with wire leads and just begged to get out of the heap for this occasion. The encoder knob remains as before, with its shaft push-on momentary switch still going directly to analog input A5. The new button circuitry connects to that switch lead, ungainly though it may appear, with the gray wire bringing VCC from the cluster of sensor inputs.

To simplify reading the buttons, build an array of threshold voltages about halfway between the calculated switch voltages:

enum BUTTONS {SW_KNOB, B_1, B_2, B_3, B_4, N_BUTTONS};
word ButtonThreshold[] = {265/2, (475+265)/2, (658+475)/2, (834+658)/2, (1023+834)/2, 1024};

You could do the circuit calculation and VCC calibration in there, too, but those widely spaced increments don’t pose much of a problem. The table must include an end marker of 1024, greater than any possible analog input.

Then you read the button input voltage and walk upward through the table until the value falls below a threshold, a process I find much cleaner and easier than a pile of conditionals sprinkled with fiddly constants.

byte ReadButtons(byte PinNumber) {

word RawButton;
byte ButtonNum;

	RawButton = analogRead(PinNumber);

	for (ButtonNum = 0; ButtonNum <= N_BUTTONS; ButtonNum++){
		if (RawButton < ButtonThreshold[ButtonNum])
			break;
	}

	return ButtonNum;
}

As long as the button stays down, that function returns its ID number. You can detect both edges of a button press:

	Button = ReadButtons(PIN_BUTTONS);
	if (PrevButton != Button) {
		if (Button == N_BUTTONS) {
			printf("Button %d released\n",PrevButton);
		}
		else
			printf("Button %d pressed\n",Button);
		PrevButton = Button;
	}

The demo code produces results like this:

Ed Nisley - KE4ZNU - December 2012
Knob encoder and buttons
Ed Nisley - KE4ZNU - December 2012
Knob count: 2
Knob count: 3
Knob count: 4
Knob count: 3
Knob count: 2
Knob count: 1
Knob count: 0
Knob count: 2
Knob count: 4
Knob count: 5
Knob count: 6
Knob count: 7
Knob count: 8
Knob count: 11
Knob count: 15
Knob count: 16
Knob count: 17
Button 0 pressed
Button 0 released
Button 1 pressed
Button 1 released
Button 2 pressed
Button 2 released
Button 3 pressed
Button 3 released
Button 4 pressed
Button 4 released
Button 2 pressed
Button 2 released

This scheme works for a single button pressed at a time, which is generally how you use discrete buttons. It’s not appropriate for keyboards or multi-axis joystick button arrays, which you could multiplex using resistors that produce accurate binary steps, but that’s fraught with peril and error.

As with all non-interrupt-driven buttons, you must poll the button input at a reasonable rate to have a responsive UI. Non-blocking loop() code will be your friend.

It made sense to exercise the new buttons in the encoder knob demo code, so this will look familiar…

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_BUTTONS = 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

enum KNOB_STATES {KNOB_CLICK_0,KNOB_CLICK_1};

enum BUTTONS {SW_KNOB, B_1, B_2, B_3, B_4, N_BUTTONS};

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

volatile char KnobCounter = 0;
volatile char KnobState;

char PrevKnobCounter = 0;

byte Button, PrevButton;

// ButtonThreshold must have N_BUTTONS elements, last = 1024

word ButtonThreshold[] = {265/2, (475+265)/2, (658+475)/2, (834+658)/2, (1023+834)/2, 1024};

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 and decipher analog switch inputs
//		returns N_BUTTONS if no buttons pressed

byte ReadButtons(int PinNumber) {

word RawButton;
byte ButtonNum;

	RawButton = analogRead(PinNumber);

//	printf("RawButton: %d ",RawButton);

	for (ButtonNum = 0; ButtonNum <= N_BUTTONS; ButtonNum++){
//		printf(" (%d:%d)",ButtonNum,ButtonThreshold[ButtonNum]);
		if (RawButton < ButtonThreshold[ButtonNum])
			break;
	}

//	printf(" ButtonNum %d\n",ButtonNum);

	return ButtonNum;

}

//------------------
// 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);

	KnobState = digitalRead(PIN_KNOB_A);
	Button = PrevButton = ReadButtons(PIN_BUTTONS);

	attachInterrupt((PIN_KNOB_A - 2),KnobHandler,CHANGE);

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

	printf("Knob encoder and buttons\r\nEd Nisley - KE4ZNU - December 2012\r\n");

	MillisThen = millis();

}

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

void loop() {

	MillisNow = millis();

	if ((MillisNow - MillisThen) < UPDATEMS) {

		digitalWrite(PIN_SYNC,HIGH);

		Button = ReadButtons(PIN_BUTTONS);
		if (PrevButton != Button) {
			if (Button == N_BUTTONS) {
				printf("Button %d released\n",PrevButton);
			}
			else
				printf("Button %d pressed\n",Button);
			PrevButton = Button;
		}

		if (PrevKnobCounter != KnobCounter) {
			printf("Knob count: %d\n",KnobCounter);
			PrevKnobCounter = KnobCounter;
		}

		digitalWrite(PIN_SYNC,LOW);

		MillisThen = MillisNow;
	}

}
About these ads