09-11-2013, 05:01 PM
|
#1 (permalink)
|
EcoModding Apprentice
Join Date: Aug 2009
Location: terra firma
Posts: 138
Thanks: 4
Thanked 24 Times in 22 Posts
|
Multiple Buttons on a Single Analog Input
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..
Last edited by nickdigger; 09-11-2013 at 07:20 PM..
|
|
|
Today
|
|
|
Other popular topics in this forum...
|
|
|
09-11-2013, 05:04 PM
|
#2 (permalink)
|
Administrator
Join Date: Dec 2007
Location: Germantown, WI
Posts: 11,203
Thanks: 2,501
Thanked 2,587 Times in 1,554 Posts
|
Sorry, not real familiar with the project so excuse the ignorance, but why are you using analog pins for buttons? Why not use digital pins?
Also, I thought you could still do a digitalread on analog pins...
|
|
|
09-11-2013, 05:43 PM
|
#3 (permalink)
|
EcoModding Apprentice
Join Date: Aug 2009
Location: terra firma
Posts: 138
Thanks: 4
Thanked 24 Times in 22 Posts
|
I think it's because the LCD took most of the digital pins, leaving 2 for RX/TX and 2 others free. The original design is basically reading the 3 analog pins as digital.
|
|
|
09-11-2013, 05:52 PM
|
#4 (permalink)
|
MPGuino Supporter
Join Date: Oct 2010
Location: Hungary
Posts: 1,807
iNXS - '10 Opel Zafira 111 Anniversary Suzi - '02 Suzuki Swift GL
Thanks: 830
Thanked 708 Times in 456 Posts
|
Schweet!
This is a big deal, since it is now possible to free up two other pins to be able to read more analog values.
For instance, the JellyBeanDriver MPGuino hardware has provisions for being able to read two user-provided analog signals. Some people here read battery voltage, while others are working to read other things (like flowmeters, and such). I am reading ambient and manifold pressure.
nickdigger's code change will enable the hardware to read 4 different user-provided analog signals. If you don't mind, I'd like to incorporate your code into v1.86.
|
|
|
09-11-2013, 06:16 PM
|
#5 (permalink)
|
EcoModding Apprentice
Join Date: Aug 2009
Location: terra firma
Posts: 138
Thanks: 4
Thanked 24 Times in 22 Posts
|
That's why i posted it
The biggest problem, IMO, is the lack of distinguishable ranges given for each button combo. During my trial run, i tried to make a spreadsheet that gave predicted resistance readings. I was hoping that, once finding R1, R2, R3, R4, it would be trivial to calc R1+R2, R1+R3, etc for all combinations. No dice. If one of the pre-assembled vendors here wants to change their design to allow more ADC, I would expect them to do their own R+D for their optimal resistor values.
I kinda think in retrospect, that it would be cleaner to do a simple 1-2-3-4, single resistor per button, and use the LongPress flag to assign special functions to each of the 4: such as: LongLeft= Save Tank/Trip to EEprom, LongRight = Clear Tank/Trip, LongUp = Setup Menu. Either way, the basic code foundation is there, for single-resistor or ladder-resistors.
Also, we could get even one more ADC, if we changed the VSS over to a digital pin. I believe that analog pins 4+5 also can be used as a two-wire serial interface (TWI), so that opens up other expansion options as well.
|
|
|
09-11-2013, 06:20 PM
|
#6 (permalink)
|
MPGuino Supporter
Join Date: Oct 2010
Location: Hungary
Posts: 1,807
iNXS - '10 Opel Zafira 111 Anniversary Suzi - '02 Suzuki Swift GL
Thanks: 830
Thanked 708 Times in 456 Posts
|
Quote:
Originally Posted by nickdigger
That's why i posted it
The biggest problem, IMO, is the lack of distinguishable ranges given for each button combo. During my trial run, i tried to make a spreadsheet that gave predicted resistance readings. I was hoping that, once finding R1, R2, R3, R4, it would be trivial to calc R1+R2, R1+R3, etc for all combinations. No dice. If one of the pre-assembled vendors here wants to change their design to allow more ADC, I would expect them to do their own R+D for their optimal resistor values.
I kinda think in retrospect, that it would be cleaner to do a simple 1-2-3-4, single resistor per button, and use the LongPress flag to assign special functions to each of the 4: such as: LongLeft= Save Tank/Trip to EEprom, LongRight = Clear Tank/Trip, LongUp = Setup Menu. Either way, the basic code foundation is there, for single-resistor or ladder-resistors.
Also, we could get even one more ADC, if we changed the VSS over to a digital pin. I believe that analog pins 4+5 also can be used as a two-wire serial interface (TWI), so that opens up other expansion options as well.
|
Heck, shoot for the moon!
No, seriously. I bought a 2.7" TFT touchscreen, made by Seeeeeed Studios. It was on clearance, so I got it for a song. I've been wanting to make an MPGuino out of it (and an Arduino Uno). The touchscreen uses two analog channels, in a sort of an x/y configuration, to determine where the user has touched the screen.
|
|
|
09-11-2013, 06:41 PM
|
#7 (permalink)
|
EcoModding Apprentice
Join Date: Aug 2009
Location: terra firma
Posts: 138
Thanks: 4
Thanked 24 Times in 22 Posts
|
Don't those screens also use a crapload of digital i/o? I always assumed i'd need a 1280 or 2560 to run a TFT (not the smaller SPI models)
|
|
|
09-11-2013, 06:42 PM
|
#8 (permalink)
|
MPGuino Supporter
Join Date: Oct 2010
Location: Hungary
Posts: 1,807
iNXS - '10 Opel Zafira 111 Anniversary Suzi - '02 Suzuki Swift GL
Thanks: 830
Thanked 708 Times in 456 Posts
|
Quote:
Originally Posted by nickdigger
Don't those screens also use a crapload of digital i/o? I always assumed i'd need a 1280 or 2560 to run a TFT (not the smaller SPI models)
|
Yah, The TFT uses 8 digital pins for data transfer, and a separate 4 pin control interface. It's 5 more pins than what the MPGuino uses. Hm...
|
|
|
09-12-2013, 06:12 PM
|
#9 (permalink)
|
EcoModding Apprentice
Join Date: Dec 2012
Location: Portugal
Posts: 197
Thanks: 93
Thanked 70 Times in 64 Posts
|
Good job, had not tested that way, with two resistances by button only used one of each button, but this paresse be a good idea.
Quote:
Originally Posted by nickdigger
I think it's because the LCD took most of the digital pins, leaving 2 for RX/TX and 2 others free. The original design is basically reading the 3 analog pins as digital.
|
Also for reducing the digital pins that are used by LCD, this scheme can be used or similar with the library LiquidCrystal595.h, thereby reducing 3 pins.
|
|
|
09-13-2013, 09:19 AM
|
#10 (permalink)
|
Scandinavian creature
Join Date: Jun 2012
Location: Finland
Posts: 146
Thanks: 4
Thanked 27 Times in 22 Posts
|
Also, i dont know if you are aware, you get two additional analog pins if you switch to TQFP package instead of DIL or SSOP, on ATMEGA328.
|
|
|
|