Go Back   EcoModder Forum > EcoModding > Instrumentation > OpenGauge / MPGuino FE computer
Register Now
 Register Now
 

Reply  Post New Thread
 
Submit Tools LinkBack Thread Tools
Old 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..

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
Alt Today
Popular topics

Other popular topics in this forum...

   
Old 09-11-2013, 05:04 PM   #2 (permalink)
Administrator
 
Daox's Avatar
 
Join Date: Dec 2007
Location: Germantown, WI
Posts: 11,203

CM400E - '81 Honda CM400E
90 day: 51.49 mpg (US)

Daox's Grey Prius - '04 Toyota Prius
Team Toyota
90 day: 49.53 mpg (US)

Daox's Insight - '00 Honda Insight
90 day: 64.33 mpg (US)

Swarthy - '14 Mitsubishi Mirage DE
Mitsubishi
90 day: 56.69 mpg (US)

Daox's Volt - '13 Chevrolet Volt
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...
__________________
Current project: A better alternator delete
  Reply With Quote
Old 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.
  Reply With Quote
Old 09-11-2013, 05:52 PM   #4 (permalink)
MPGuino Supporter
 
t vago's Avatar
 
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.
  Reply With Quote
Old 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.
  Reply With Quote
Old 09-11-2013, 06:20 PM   #6 (permalink)
MPGuino Supporter
 
t vago's Avatar
 
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 View Post
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.
  Reply With Quote
Old 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)
  Reply With Quote
Old 09-11-2013, 06:42 PM   #8 (permalink)
MPGuino Supporter
 
t vago's Avatar
 
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 View Post
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...
  Reply With Quote
Old 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 View Post
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.

  Reply With Quote
Old 09-13-2013, 09:19 AM   #10 (permalink)
Scandinavian creature
 
Join Date: Jun 2012
Location: Finland
Posts: 146

Golf ball - '94 Volkswagen Golf III
90 day: 28.46 mpg (US)
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.

__________________

Brrrmm
Now selling preassembled MPGuino's!
http://www.mthtek.net/mpguino/
  Reply With Quote
Reply  Post New Thread






Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.
Content Relevant URLs by vBSEO 3.5.2
All content copyright EcoModder.com