LCD Shield Button Reliability

Hi Folks,

I’m using a common LCD shield from Hobby King, the one with a 16x2 LCD and the Select, Left, Right, Up, Down, and Reset buttons.

I’m finding the buttons may occasionally read incorrectly and select the wrong button, so I’m looking for solutions in how to fix the problem. I tried tightening up the conditions, but it does not help, actually the below code does not read the buttons correctly.

I can print the read value and it is fairly stable on a firm press, but will vary quite a bit when varying pressure on the button.

int read_LCD_buttons()
{
  adc_key_in = analogRead(0);      // read the value from the sensor 
  // default buttons when read are centered at these valies: 0, 144, 329, 504, 741
  // my buttons when read are centered at these valies: 5, 131, 307, 481, 722
  // we add approx 50 to those values and check to see if we are close
  if (adc_key_in > 1000) return btnNONE; // We make this the 1st option for speed reasons since it will be the most likely result
  if (adc_key_in < 10 || adc_key_in > 0)   return btnRIGHT;  
  if (adc_key_in < 136 || adc_key_in > 126)  return btnUP; 
  if (adc_key_in < 312 || adc_key_in > 302)  return btnDOWN; 
  if (adc_key_in < 486 || adc_key_in > 476)  return btnLEFT; 
  if (adc_key_in < 727 || adc_key_in > 717)  return btnSELECT;   
  return btnNONE;  // when all others fail, return this...
}

Any ideas?

OK, a realization about the simple miss-logic in my code snip-it.

The following does work, but I need a very reliable button as they are for machine control and a miss-step will cause issues.

int read_LCD_buttons()
{
  adc_key_in = analogRead(0);      // read the value from the sensor 
  // default buttons when read are centered at these valies: 0, 144, 329, 504, 741
  // my buttons when read are centered at these valies: 5, 131, 307, 481, 722
  // we add approx 50 to those values and check to see if we are close
  if (adc_key_in > 1000) return btnNONE; // We make this the 1st option for speed reasons since it will be the most likely result
  if (adc_key_in < 10 && adc_key_in > 0)   return btnRIGHT;  
  if (adc_key_in < 136 && adc_key_in > 126)  return btnUP; 
  if (adc_key_in < 312 && adc_key_in > 302)  return btnDOWN; 
  if (adc_key_in < 486 && adc_key_in > 476)  return btnLEFT; 
  if (adc_key_in < 727 && adc_key_in > 717)  return btnSELECT;   
  return btnNONE;  // when all others fail, return this...
}

I can see you did something to register a range.
But the sum of the ranges leave a lot of space for values that might be registered but will return a ‘btnNONE’.
I took an existing example sketch for my LCD keypad shield that was testing for exact values.
I found that this wasn’t always working, because sometimes the registered values were not the ones that was tested for.
So i changed the test (just two characters) to test for a range.
You did so too.
But i test for low to high values.
If the keypress returns a value that is not lower than the lowest threshold, then the first key wasn’t pressed.
If it isn’t lower than the second threshold, the second key wasn’t pressed either.
If it was lower than the third threshold, then that third key must have been pressed.
So i’m just testing for a value lower than some threshold.
I’m not testing for a higher threshold, limiting the measuring range (you do).
This way i’m trying to eliminate a power supply that is dropping (maybe because of some load), or other causes that will have impact on the returned values.
I’ve had it working for over 6 months now and can’t remember it failing to return keypresses (so no incorrect keys or missed keys at all).
This is a snippet that shows the declarations, and the testing and assigning of key numbers to the keypresses.

int adc_key_val[5] ={50, 200, 400, 600, 800 };          // Original, works after actually reading the analog0
                                                                                  // You might want to change these values to fit your resistor array
int NUM_KEYS = 5;
int adc_key_in;
int key=-1;                                                                   // Presets the pressed key; -1 means no key pressed yet

void loop()
{ 
    adc_key_in = analogRead(0);    // Read the value from the sensor 
    key = get_key(adc_key_in);        // Convert into key press
}

int get_key(unsigned int input)
{
    int k;
   
    for (k = 0; k < NUM_KEYS; k++)    // Start counting
    {
      //if (input == adc_key_val[k])        // Works, but doesn't allow for deviations
      if (input < adc_key_val[k])            // should be more reliable as it looks for a range instead of a fixed value.
 {
            return k;                                  // Breaks the for loop
        }
   }
   
    if (k >= NUM_KEYS)k = -1;            // No valid key pressed
    return k;                                          // So break the loop and look for another keypress
}

This only shows the part that reads and “decodes” the keypresses.
It will return a number, and -1 means no or invalid key.
This approach makes it relatively easy to extend the number of keys in use in the sketch.

And there you have it.

This is not a method of key multiplexing for use in industrial applications; anywhere reliability is of any importance whatsoever.

In my - albeit limited - experience of devices using this method, video monitor control panels and MP3 players, this is the more common cause for the demise of the appliance, as the buttons develop leakage due to moisture in the lubricant and cause "ciphers" which are often unrepairable (for the usual reasons regarding disassembly).

The only really practical "solution" is to calibrate each individual module by the (most stable) value for each key using a display sketch (indicating of course, on the LCD), and set boundaries halfway between each of those values. Note also the heed to debounce the keys just as with any other input system, presumably by repetitive reads and comparing the values to stay within the same range for a number of reads.

The sketch have pasted the snippet from, does do some debouncing and checks for key changes too.
That calibration Paul__B is mentioning, using a LCD (thanks to th LCD Key shield this was available at the time) is how i found out about variations in the analog reads upon keypresses.
This was also how i learned the power of having a LCD available for debugging.
And so i came up with this solution.
Paul__B’s boundaries seem to be present in C2 (PO ?)'s sketch, he also checks for a range and not a fixed value.
The ranges he checks for are not very large however (10 ADC values).

Well, your code (MAS3) makes far more sense - you don't look for a particular value or range, you look to see if the value is on either side of set points halfway between the expected values.

I can't remember exactly how I reset the way points for my shield when I set up the test sketch, but the original values certainly were not correct. It has been just sitting here next to me for some months now to see how many seconds it could count to between power failures. :D

I would certainly not recommend incorporating this sort of design into an appliance.

And if you have one of these shields, you must read and understand the warning thread for this topic! :astonished: It is unfortunately a bit long, but absolutely critical.

Thanks Folks.

MAS3 code works very similar to the original code I found for the shield, only using an array for the values and loop is much more elegant.

What I suspect is that when a button is pressed, there is a certain voltage fall time before settling in to a steady state voltage level. Since the processor is sampling the adc_key so often, there is enough of a chance that it will sample during this fall time and return an erroneous result. This is why I changed from looking for values under certain thresholds and went to a narrower range, to decrease the likelihood of finding a valid result somewhere between button states.

Unfortunately, my most used button is "Right", which takes the reading from 1023 to 0 and presents the highest likelihood that any other button could be erroneously returned during this transition, and I observe that a large whole percentage of tries.

I had hoped to use the shield at least in prototype to run the machinery, but it looks like I'll have to build the control panel first, where I will use individual analog pins for each button, or find a better way to implement 5 button control.

I'll be sure to check the backlight issue and have the test code, the thread I found while searching for solutions to this button issue. Thanks!

C2:
What I suspect is that when a button is pressed, there is a certain voltage fall time before settling in to a steady state voltage level. Since the processor is sampling the adc_key so often, there is enough of a chance that it will sample during this fall time and return an erroneous result. This is why I changed from looking for values under certain thresholds and went to a narrower range, to decrease the likelihood of finding a valid result somewhere between button states.

But - that is the wrong approach. If it is a debounce issue as you suggest, the solution is to repeat the whole key-determining process using the basic set of thresholds, a number of times until that returned condition is returned consistently (such as at least ten times a millisecond apart).

C2:
Unfortunately, my most used button is “Right”, which takes the reading from 1023 to 0 and presents the highest likelihood that any other button could be erroneously returned during this transition, and I observe that a large whole percentage of tries.

If you were going to use this shield for a serious application, you also need to remove the “Reset” button to avoid pressing it by accident instead of one of the cursor buttons.

C2:
I had hoped to use the shield at least in prototype to run the machinery, but it looks like I’ll have to build the control panel first, where I will use individual analog pins for each button, or find a better way to implement 5 button control.

You mean digital pins, but most of the analog pins are in fact, digital pins and have internal pull-ups. Of course, all buttons should switch to ground, even if you use external pull-ups.

Turns out that you cannot matrix four buttons with less than four wires, but you can “Charlieplex” up to twelve buttons with four wires and as many diodes as buttons, and that is reliable. And you always need debounce for any configuration.

I've had a hard time completely grasping debouncing solutions, but came close at one time... I was not suggesting a debounce issue alone, although that would be an extreme exacerbation of the issue. I suppose this could be determined if the adc_key state was output or recorded for each read. The timing to do so seems to overcome that particular issue, so external means become obvious, yet not available here. This is also a reason why hardware debouncing seems desirable.

This and LCD update speed are two areas I need to overcome.

Edit: actually, I do not need to update the LCD that quickly, but just need to offload the data and allow for the button control to take over ASAP. Human visual perception is all that needs to be overcome there, but machine control is not so easily disguised.

Thanks for the insights.

C2: I've had a hard time completely grasping debouncing solutions, but came close at one time... I was not suggesting a debounce issue alone, although that would be an extreme exacerbation of the issue. I suppose this could be determined if the adc_key state was output or recorded for each read. The timing to do so seems to overcome that particular issue, so external means become obvious, yet not available here. This is also a reason why hardware debouncing seems desirable.

Yes, in fact debouncing is the issue, and is the whole of the issue. Switch "bounce" in a digital domain causes the state to rapidly alternate between the levels corresponding to "on" and "off". In an analog domain, this means an unstable analog value in the action of pressing or release, but also as you have noted, the possibility of an unstable held value when we are dealing with pressure-sensitive membrane switches.

To say "hardware debouncing seems desirable" suggests that you have not come to grips with the problem. The whole point of key multiplexing - of any sort - is to avoid unnecessary hardware. And that is what is so elegant about having a microprocessor, you use it to "solve" problems such as contact bounce, and it is drastically effective in doing so, to the extent that not doing the debounce in software is simply wasting processing power since in the context of a "HID", the machine is already ipso facto, "twiddling its thumbs" waiting for human input.

Debouncing is by no means a difficult matter. You simply require that any noisy input, particularly mechanical contacts, deliver the same value (in this case, the result of determining in which range of values the analog measurement lies) on say, ten successive "polls", each a millisecond apart, before being recognised as a valid new state (including the state of no button pressed). The code to do so is quite easy and whilst implicit in this situation (where you are in any case unable to distinguish simultaneous multiple key-presses), it is generally practical to execute the algorithm so that all keys are simultaneously polled and require that all are stable before a "new" condition is recognised.

I understand that, but am unsure how to implement such a solution. I am using the L/R buttons to control a stepper motor, which requires continuous pulses. If I am waiting milliseconds to determine button state, then the pulses will be delayed.

This is a (crude) solution I've implemented before on a one-button ISR, and I think the debounce time was minimum around 4ms. I think this would not allow the button to remain pressed for continuous signaling.

void debounce() {
  if((micros() - last_micros) >= debounce_time) {
    last_micros = micros();
    mode = !mode;
  }
}

The code snip-it below will need significant refinement as it is because the LCD update is slowing the motor down already. The fastest I would like to run the motor is 10 kHz steps, or more practically in this example around 4 kHz (at least cycle through the while loop and execute the btnRIGHT case every 250 microseconds while the button is pressed).

  while (menu == 6) {
    lcd_key = read_LCD_buttons();  // read the buttons 
    switch (lcd_key) {              // depending on which button was pushed, we perform an action
    case btnRIGHT: 
      {
        digitalWrite(dir, HIGH);
        digitalWrite(stp, HIGH);   
        delayMicroseconds(rate);               
        digitalWrite(stp, LOW);  
        delayMicroseconds(rate);
        pos++;
        if (pos == 36000) pos = 0;
        //runLCDupdate(pos, rate);
        lcd.setCursor(1,1);
        lcd.print((float(pos)/100));
        break;
      }
  }

I managed to come back to this briefly, so I now compare multiple reads and only accept a value if it is repeated a number of times, or optionally within a range:

int read_LCD_buttons() {
  int test = 10;
  int range = 5;
  adc_key_in = analogRead(0);      // read the value from the sensor 
  for (int i = test; i > 0; i--) { // my loop to test button state
//    if ((adc_key_in < analogRead(0) + range) || (adc_key_in > analogRead(0) - range)) test--; //test for button range
    if (adc_key_in == analogRead(0)) test--; //test for exact button value
  }
  if (test == 0) {
      if (adc_key_in > 1000) return btnNONE;
      if (adc_key_in < 10)   return btnRIGHT;
      if (adc_key_in < 136)  return btnUP;
      if (adc_key_in < 312)  return btnDOWN;
      if (adc_key_in < 486)  return btnLEFT;
      if (adc_key_in < 727)  return btnSELECT;
  }
      return btnNONE;  // when all others fail, return this...
}

This slowed the button read code only slightly and seems much more reliable. It may fail to read a button, but that is more of a nuisance than reading the incorrect button and causing the machine to take an incorrect action. I’ll still implement a better solution in the final design, but feel I can test drive the machine with this.

Thanks for the mention of charlieplex, something I had not heard before and watched a quick tutorial.

C2: I am using the L/R buttons to control a stepper motor, which requires continuous pulses. If I am waiting milliseconds to determine button state, then the pulses will be delayed.

I didn't think analogRead takes milliseconds to execute, so I don't follow why it should interfere with stepping pulses.,

C2: This is a (crude) solution I've implemented before on a one-button ISR, and I think the debounce time was minimum around 4ms. I think this would not allow the button to remain pressed for continuous signalling.

You should not be using interrupts to read buttons.

C2: I managed to come back to this briefly, so I now compare multiple reads and only accept a value if it is repeated a number of times, or optionally within a range:

Bad code. You should not expect the button to repeatedly read the same; you simply want it to be repeatedly within the acceptable range. And - particularly if you wish it to be compatible with your simultaneous stepper code, you should not be "busy waiting" for several milliseconds whilst you perform the critical task of reading buttons - the debounce count should occur as part of your main loop which performs all the time-related operations.

You need to get thoroughly acquainted with this discipline of the "main loop" of code which runs without interruption or hesitation - it waits for nothing and internally repeats or loops nothing. Anything which needs to be repeated, checked again or a time compared to the next desired "event", is only ever done once within the loop, checked again on the next pass through.