View Single Post
Old 09-11-2013, 05:01 PM   #1 (permalink)
nickdigger
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..

Attached Thumbnails
Click image for larger version

Name:	IMAG0042a.jpg
Views:	146
Size:	109.8 KB
ID:	13758  

Last edited by nickdigger; 09-11-2013 at 07:20 PM..
  Reply With Quote