The goal of this mod is to free some analog inputs to read the voltages of various sensors, such as MAF, MAP, Battery, Air/Water/Oil Temp, etc
The original design uses one analog pin for each of the 3 buttons, plus another for VSS, leaving only 2 inputs for extra data collection. By placing resistors between each button, we can give each button a unique value, so that when the designated input is read, we can determine which button (or combination of buttons) is pressed.
This mod requires changes to the hardware, as well as the code.
First, the hardware: The original design has each button tied to Ground, with an internal pullup resistor activated on its assigned input pin. This mod uses external resistors, as shown below. Note that a pure R-2R resistor ladder calls for values of 1x and 2x, but since I didn't have any 20k, I substituted with 15k. I also added a 4th button, since it doesn't cost an extra input. I'm not sure how useful it is, really. 3 buttons is probably enough, with the added "Long Keypress" feature to give an extra "bit" of functionality to them. (Note -- i have not implemented the Long Press thing in my code, but left reference to it for future expansion, maybe). The original 3 buttons are Left, Middle, Right. The 4 in this mod are Left, Down, Up, Right.
The software may take some massaging, to make it work. A long time ago, I went "off the reservation", and rewrote almost all the code, to pack more features into my little 168 chip. As such, the code I post here will not plug-and-play into the 0.86 C++ that everyone is probably using (my version has no ++ anymore, just plain old 'C' -- not as big a change as it sounds). A closer fit will probably be t.vago's 1.86tav, since it is being currently maintained. Hopefully, someone can make use of this.
An overview: The arduino library has an analogRead() function to read a specified pin's voltage. It would be simplest, to just use that function every time we scan the Keys pin. But since we're not using "digital" button inputs (High/Low), we cannot use the PCINT1 interrupt as a trigger. We have to continually scan the pin, to see what the current buttonState is at all times. Since we already have the Timer2 active, which triggers an interrupt every 1ms or so, we will include our button scanning code in there. Button-reading code is in the PCINT1 ISR in 0.86, and is split between timer2 & PCINT1 in 1.86tav. This mod puts it all in timer2. (Edit: the mod also covers the old-style buttons too. i.e., all button code is completely moved into timer2, period)
The problem with analogRead() is that it takes about 120us (according to SomeGuy on the internet) to complete. We don't want to have a delay like that inside an interrupt that fires every 1000us. Instead, we'll use the ADC interrupt to handle the scan. What we do is initiate the read request by calling
queueAdc(), which initiates the process. When the scan is complete, the ADC interrupt is triggered, and that is where we will collect the value, and store it away. During every timer2 int, we will grab the Keys value, analyze it, and order another value to be scanned and ready for our next cycle 1ms later.
Code:
// Global defs
#define SINGLE_PIN_KEYS // combine 4 keys on one input, using resistor ladder.
typedef union { uint ui;
struct {byte b0; byte b1;}; } union16;
#ifdef SINGLE_PIN_KEYS
#define keyDebounceCount 29 //ms
#define lbuttonBit 8 // Left
#define mbuttonBit 16 // Up
#define rbuttonBit 32 // Right
#define dbuttonBit 64 // Down
#define buttonLong 128 // long button press //not used yet
#define buttonsUp (lbuttonBit | mbuttonBit | rbuttonBit | dbuttonBit)
#define allButtonBits (lbuttonBit | mbuttonBit | rbuttonBit | dbuttonBit)
#define LeftButton (buttonState == (buttonsUp ^ lbuttonBit))
#define DownButton (buttonState == (buttonsUp ^ dbuttonBit))
#define MiddleButton (buttonState == (buttonsUp ^ mbuttonBit))
#define RightButton (buttonState == (buttonsUp ^ rbuttonBit))
#define LeftRightButton (buttonState == (buttonsUp ^ (lbuttonBit | rbuttonBit)))
#define LeftMiddleButton (buttonState == (buttonsUp ^ (lbuttonBit | dbuttonBit)))
#define RightMiddleButton (buttonState == (buttonsUp ^ (rbuttonBit | mbuttonBit)))
#else //regular-style, one button per input pin
#define keyDebounceCount 19 //ms
#define lbuttonBit 8 // pin17 is a bitmask 8 on port C
#define mbuttonBit 16 // pin18 is a bitmask 16 on port C
#define rbuttonBit 32 // pin19 is a bitmask 32 on port C
#define buttonLong 128 // long button press
#define buttonsUp (lbuttonBit | mbuttonBit | rbuttonBit) // start with the buttons in the right state
#define LeftButton (buttonState == (buttonsUp ^ lbuttonBit))
#define MiddleButton (buttonState == (buttonsUp ^ mbuttonBit))
#define RightButton (buttonState == (buttonsUp ^ rbuttonBit))
#define LeftRightButton (buttonState == (buttonsUp ^ (lbuttonBit | rbuttonBit)))
#define LeftMiddleButton (buttonState == (buttonsUp ^ (lbuttonBit | mbuttonBit)))
#define DownButton (buttonState == (buttonsUp ^ (lbuttonBit | mbuttonBit)))
#define RightMiddleButton (buttonState == (buttonsUp ^ (rbuttonBit | mbuttonBit)))
#endif
#ifdef SINGLE_PIN_KEYS
// Each element contains button ID in the High bits, ADC value in lower 9 bits
static const uint adcKeyMap[] PROGMEM = {
350 | buttonsUp<<8, //out of our range
435 | (buttonsUp ^ rbuttonBit)<<8,
590 | (buttonsUp ^ mbuttonBit)<<8,
690 | (buttonsUp ^ dbuttonBit)<<8,
730 | (buttonsUp ^ (rbuttonBit | mbuttonBit))<<8,
790 | (buttonsUp ^ lbuttonBit)<<8,
830 | (buttonsUp ^ (lbuttonBit | rbuttonBit))<<8,
850 | (buttonsUp ^ (lbuttonBit | rbuttonBit))<<8,
890 | (buttonsUp ^ (lbuttonBit | dbuttonBit))<<8,
1024| buttonsUp<<8 //out of our range
};
#endif
int keyCounts; //for debouncing
#define keyLongCount 900 //ms
#define ADC_BATTERY 1 //pin #1 reads Battery voltage
#define ADC_BUTTONS 2 //pin #2 reads the button presses
#define ADC_AUX1 3
#define ADC_AUX2 4
#define MAX_ADC 4
volatile char ADCfirst=0, ADClast=0;
volatile int ADCresults[MAX_ADC]; // # of channels we'll use
volatile byte ADCqueue[MAX_ADC];
#define ADMUX_base (0<<REFS1 | 1<<REFS0 | 0<<ADLAR) // set ADC voltage ref to AVCC; right-adjust the reading
ISR (TIMER2_OVF_vect)
{
if (VSScounts > 0) // if there is a VSS debounce countdown in progress
{
if (--VSScounts == 0) // if count has reached zero,
vssFlop ^= 1; //enableVSS();
}
#ifdef SINGLE_PIN_KEYS
int adc=ADCresults[ADC_BUTTONS];
for (byte i=0; i < sizeof(adcKeyMap); i++)
{ // Scan the key map from bottom-up.
union16 x;
x.ui = pgm_read_word(&adcKeyMap[i]);
if (adc < (x.ui & 0x3FF)) // compare the ADC portion of the map
{
buttonStateLatest = x.b1 & buttonsUp; //strip off ADC value, leaving only button bits
break;
}
}
queueAdc(ADC_BUTTONS); //schedule next key read
#else // one-pin per button
buttonStateLatest = PINC & buttonsUp;
#endif
if (buttonStateLatest == buttonsUp) //key released
{
if (buttonStatePending != buttonsUp)
{ buttonState = buttonStateGood; // pass the "good" keypress to the main program
buttonStatePending = buttonsUp; } // clear key state
keyCounts = 0;
}
else //key(s) are pressed
if (buttonStatePending != buttonStateLatest) // && different from before
{ keyCounts = keyDebounceCount;
buttonStatePending = buttonStateLatest; }
if (keyCounts) // if there is a button press debounce countdown in progress
{
if (--keyCounts == 0) //key has been debounced. call it 'good'
buttonStateGood = buttonStatePending;
}
} //ISR timer2
ISR (ADC_vect)
{
// ADC fetch is initiated elsewhere; this routine is triggered
// when fetch has been completed. ADCqueue contains a FIFO queue of pins to read.
// ADCqueue[ADCfirst] is the first pin# to read.
if (ADCfirst == ADClast) // queue is empty
return;
//ADCresults[ADCqueue[ADCfirst]] = ADCL | (ADCH << 8); //34 bytes more
union16 *u = (union16 *) &ADCresults[ ADCqueue[ADCfirst] ];
u->b0 = ADCL; // Must read low first
u->b1 = ADCH;
ADCfirst = (ADCfirst + 1) % MAX_ADC;
if (ADCfirst != ADClast)
{
ADMUX = ADMUX_base | ADCqueue[ADCfirst];
ADCSRA |= (1<<ADSC); // ready to process next ADC
}
} // ISR(ADC)
void queueAdc (char pin) // 38 bytes ////////////////////////////////////////////
{ // Add an item to the ADC scheduler. When the reading is complete,
// the ISR will store the value in ADCresults[pin];
ADClast = (ADClast + 1) % MAX_ADC;
ADCqueue[ADClast] = pin;
ADMUX = ADMUX_base | pin;
ADCSRA |= (1<<ADSC);
}
//// For any other ADC values you want to read, add these lines to your main loop
//// before the final "wait for this loop to end" code
//// queueAdc(ADC_BATTERY);
//// queueAdc(ADC_MAF);
//// etc...
//// while (elapsedMicroseconds(loopStart) < (looptime))
/// These changes go in your timer/port init section
ADCSRA = 1<<ADPS2 | 1<<ADPS1 | 1<<ADPS0 // a2d prescale factor 128 --> 16mhz / 128 = 125 Khz
| 1<<ADEN // enable a2d conversions
//| 1<<ADSC // start conversion)
| 1<<ADIE // enable ADC interrupt
//| 1<<ADATE // enable auto-trigger
| 1<<ADIF; // clear pending interrupt
ADCSRB = 0; // disable analog comparator multiplexer, and set ADC auto trigger source to free-running mode
ADMUX = (ADMUX_base);
//| 1<<MUX1; // set input to ADC2
PCICR |= (1 << PCIE1); // enable interrupts on PCINT8..14
#ifdef SINGLE_PIN_KEYS
PCMSK1 |= VSSPin; //(1<<PCINT8)
#else
PORTC |= lbuttonBit | mbuttonBit | rbuttonBit; //button pullup resistors
PCMSK1 |= VSSPin | lbuttonPin | mbuttonPin | rbuttonPin; //(1<<PCINT8) | (1<<PCINT11,12,13)
#endif
Some notes:
The adcKeyMap array contains a "max" value for each button, as well as the actual button bits. E.g. when Right is pressed, the analog value varies from 420-430; Left varies from 760-770, and so forth. When decoding the Key value to Button bits, we step through the keymap, from lowest to highest. The first entry is a garbage-catcher. Since our lowest valid reading is about 420, we will discard anything below 350. Then we check the next entry. If it's less than 435, then we know it's the RightButton. If not, keep checking. If we get past the highest valid value, 890 for Left+Down, then we have a final garbage-catcher: 1024. 1023 is the highest value returned by the ADC, so anything between 891 and 1024 is junk.
The button bits in the array are also defined in a manner that doesn't require any changes to the rest of the code.
Please don't take my 10k/15k resistor layout as gospel. Those are simply the parts I had lying around. There are probably better values to use. Whatever you use, you will have to test the input values for each keypress combo, and enter them into the adcKeyMap array -- and they must be arranged from smallest to highest.
You may find the following link helpful, as I did:
Tutorial: Analog input for multiple buttons - Part Two
I think his "Part One" article has tips and code for testing the analog values. What I did was temporarily replace one of the fields on my doDisplaySystemInfo, rather than use a separate arduino to read the resistors.
Edit: Warning: Before testing your new buttons, be sure to save a copy of your EEprom, in case a keypress doesn't work as planned, and accidentally clears something. Yes, it happened to me, and yes, i backed it up first.
Can't think of anything else to say right now. Hope it helps someone, and I hope it merges seamlessly into whatever code tree you're using..