Tuesday, May 8, 2018

Library for reading multiple buttons with 1 ADC pin | AVR microcontrollers

There are many ways of reading switches, and one of them is using ADC pins. The advantages of this method is that it uses very low pin count compared to other methods, and all is needed are switches, resistors and an ADC (analog to digital converter). With a 10bit ADC, at least 20 switches can be used per pin.

To read the buttons the conventional way where every button is connected directly to a GPIO pin, check out this other library https://www.programming-electronics-diy.xyz/2021/01/button-debouncing-library-for-avr.html.

Library Features

  • Supports multiple groups of buttons on different ADC pins
  • Ability to read combination of multiple button press on different ADC pins
  • Can have different number of buttons on each pin
  • Check if a button was pressed for a certain amount of time
  • Timer 2 and ADC is setup by default 

Reading buttons using ADC (configuration #1)

Reading buttons using ADC - Configuration 1
Fig. 1 Reading buttons using ADC (configuration #1)


Multiple configurations of switches and resistors can be used. Figure 1 depicts a configuration in which resistors are in series and in idle mode (no button is pressed) the ADC pin is at Vcc level.
R1 limits the current when S1 is pressed and also is the R1 in resistor divider formula with the rest of resistors (R2 to R7) forming the R2. The ADC pin is connected to Vout.

Resistor divider


Resistor divider formula

When a button is pressed a voltage drop will be produced and using the ADC, the microcontroller will decide which button is pressed. Let's say the switch S4 is pressed. The current will flow from Vcc through R1, R2, R3, R4 and S4 to ground, and since the ADC is connected after R1, the voltage will be divided between R1 and the sum of R2, R3 and R4 which will form the R2 in the above formula.
R1 is 10k, the sum of R2, R3 and R4 is 5.7k. Using the voltage divider formula, the voltage at ADC will be 1.81V. So when S4 is pressed the voltage will be 1.81V, when S1 is pressed the voltage will be 0 because R2 is 0 (ground) in this case.



Reading buttons using ADC (configuration #2)

Reading buttons using ADC - Configuration 2
Fig. 2 Reading buttons using ADC (configuration #2)

Another way of wiring is shown in Figure 2. In this case the voltage at ADC in idle mode will be 0 and R1 is connected to GND instead of Vcc and is R2 in the above formula with R1 being formed by any other resistor. Notice that while in the first configuration the resistors add up, in this case each resistor is in parallel with the others.

Choosing the resistors in ADC switch reading

There are a few things to consider while choosing the resistor values. First thing is the current - when a button is pressed you don't want to create a short or blow up the resistors with to much current. A small SMD resistor can take up to 63mW of power and this mean 12mA on 5V power supply. So R1 should be at least 470 ohms in this case but not more than 10k because in the datasheet of an AVR microcontroller is not recommended more than 10k impedance on an ADC pin.

The ADC is optimized for analog signals with an output impedance of approximately 10 k or less. If such a source is used, the sampling time will be negligible. If a source with higher impedance is used, the sampling time will depend on how long time the source needs to charge the S/H capacitor, with can vary widely. The user is recommended to only use low impedance sources with slowly varying signals, since this minimizes the required charge transfer to the S/H capacitor.
The resistor values should be chosen so that the voltages produced when a button is pressed, have at least 20 ADC values between them. This will prevent the software to mix up the buttons.
Going back to example in Figure 1, on 8 bit ADC, each button when pressed will produce an ADC value as shown in the following chart. With each button the ADC value increases with 28 on average making easy for the software to differentiate them.

ADC distribution when reading buttons (config 1)
ADC distribution when reading buttons (config 1)

Spreadsheet for calculating resistors in ADC switch reading

Linked in the download section bellow is a spreadsheet that helps in resistor values calculation.
In Sheet1 are the examples from figures 1 and 2. To add more buttons, simply select the last row and drag down from the corner with the plus cursor. Sheet 1 is just  for manually selecting resistor values and see the generated results. To generate resistor values automatically, check out the next two sheets - Config 1 and Config 2 - depending on what configuration is used. To update the chart with the added resistors, select all the cells starting from F3 to F[last row] and drag the selection anywhere on the chart. In the popup dialog, leave the two check boxes unchecked. In the setup section select supply voltage, ADC reference voltage and ADC bit resolution used.

Spreadsheet for calculating resistors in ADC switch reading

In the sheet Config 1 is the same as above except that here the resistor values are calculated automatically. All you have to do is to add or delete rows according with the number of buttons needed. Is important not to forget to modify the number of buttons in A3 ass well.
When choosing the resistor values, it is important that the values in the ADC Step Difference column are minimum 20.

Spreadsheet for calculating resistors in ADC switch reading 2

Bellow is a copy of the above rows, except here the resistor values can be tweaked with standard values or other values (the green column).

Spreadsheet for calculating resistors in ADC switch reading 3

Practicality and implementation

Can this method be used in real applications? Yes. I wrote this article and the code after finding an audio system Samsung MM-N6, MM-N7 (in the dumpster), that uses 2 ADC pins to read 13 buttons on a panel that slides and is connected with a few cm of flat flex cable, so it's exposed more to electrical noise. Some components were literally rusted and the buttons very dodgy from extended use. After writing the code and tweaking the debouncing method, I've start pressing buttons for a few minutes and I didn't see false triggering. Also the response speed was pretty good.

However I don't recommend this method in automotive, aeronautics, medical applications or in rocket launcher where safety is an issue, because this method is still less reliable than other methods of reading buttons.



Library code for reading switches using analog pins

How the code works

Timer 2 is set up to trigger an interrupt every 1 ms, and in the ISR function, a variable is incremented every 1 ms. This variable is used for debouncing and to set the interval at which the switches are red. The function for reading buttons can be executed in the main loop without delay but the reading of buttons will be made at a preset interval - 10ms by default. At every 10ms the ADC will start and read every analog pin that has buttons connected to them set by the user. The pressed button will be returned only after a certain number of readings depending of the number of debounces the user chose.

How to use the library

First, this library uses adc.h, a library for ADC that I've made, so be sure to download it also. Also, the ISR in the buttonsADC.h file, can be moved to your main.c file in case the Timer 2 is needed for something else.

In the User Setup section, a few things needs to be modified.

#define ADC_RESOLUTION         8 // bits [default 8]
8 bit resolution is preferred because the variables take less space and the conversion is a bit faster, but for more than 10 buttons per pin, 10 bit resolution is better.

#define BUTTONS_READ_INTERVAL  20
Interval at which the buttons are scanned. 20 ms is a good value. [default 20]

#define BUTTONS_DEBOUNCE_NR  5
How many times to read the buttons until it is considered pressed [default 5].
With the above values, the debouncing will be finished after 5 X 20 = 100 milliseconds. Lowering the debounce number will increase the response time when a button is pressed but also the false triggering - the software will mix up the buttons. This depends of many variables such as switch quality and age.

#define NUMBER_OF_BUTTON_SETS  2
You can have groups of buttons on multiple ADC pins. Here is defined how many ADC pins have buttons on them. [default 1]

#define NUMBER_OF_BUTTONS_PER_SET 7
How many buttons are on a single ADC pin. If you have 2 analog pins and have 6 buttons on one and 7 on the other, put here the greater number.

#define ADC_KEY_OFFSET_ERROR  10
#define ADC_KEY_OFFSET_ERROR_NEGATIVE -10
As I show bellow, each button has an ADC value when pressed that represents the voltage drop across the resistor divider. However the button contacts can oxidize and is better to have an error margin like +-10 ADC values around the hard coded button value. This is why there must be at least 20 ADC values between each button to help the software distinguish between them. [default 10 and -10]

#define MULTIPLE_PRESSED_BUTTONS 0
0 or 1. If 1, multiple pressed buttons can be detected, if each button that is pressed is located on
a different set of an ADC pin. The function will update an array variable with the pressed buttons.
If looping an array is not desired and multiple button press detection is not needed, make this to 0 and so the function will return the number of the pressed button. [default 0]

#define CHECK_IF_BUTTON_PRESSED_FOR_x_TIME 0 
Check if a button is hold down for certain amount of time. If not used, write 0 to save space. [default 0]

#define DEBUG_BUTTONS    0
If 1, will enable the function 'displayButtonADC' that returns the ADC reading of the pressed button.
If not used, write 0 to save space. [default 0]

Scrolling down you will find
/*************************************************************
 GLOBAL VARIABLES
**************************************************************/
/*-------------------------------------------------------------
 USER SETUP
--------------------------------------------------------------*/ 
// ADC channel numbers
uint8_t KEY_ADC_PINS[NUMBER_OF_BUTTON_SETS] = {6, 5}; // ADC6, ADC5
Here replace 6, 5 with the number(s) of the ADC channel(s) used.

Bellow this is another array that holds the ADC values of each button when is pressed. If you have only one ADC pin, remove Set 2 or add more if needed as shown in the example. Remember not to put 0 as a value because won't work; use 1 instead. Also if you have a different number of buttons on each analog pin, then use the null character '\0' instead of a value. The values can be calculated using the spreadsheet or by using the function displayButtonADC(). The function will return the ADC value that can be displayed on an LCD and noted down after every button press.


// ADC values for each button when the button is pressed. Doesn't work with 0 as a value
#if ADC_RESOLUTION > 8
uint16_t BUTTONS_ADC_VALUES[NUMBER_OF_BUTTON_SETS][NUMBER_OF_BUTTONS_PER_SET] = {
#else
uint8_t BUTTONS_ADC_VALUES[NUMBER_OF_BUTTON_SETS][NUMBER_OF_BUTTONS_PER_SET] = {
#endif
 {1, 34, 60, 94, 150, 170, '\0'}, // Set 1 (set 1 has only 6 buttons, so the last one is null
 {3, 34, 60, 94, 126, 150, 174} // Set 2
};

Functions


readButtonsADC()

If  MULTIPLE_PRESSED_BUTTONS is set to 0, the function returns the number of the button pressed from 1 to 255, depending on the number of buttons. Looking at the above array BUTTONS_ADC_VALUES as an example, the first element is button number 1, button 6 has the value 170 and button 7 is the one with the value 3 because the null character was skipped.
If no button is pressed the return value will be 0.

If  MULTIPLE_PRESSED_BUTTONS is set to 1 in the setup section, the function returns 1 if a button was pressed or 0 if not and update the PRESSED_BUTTONS array with the button number from each group. This way you can detect a combination of button press.
Say you have 2 ADC pins with 7 buttons on each one and the user presses two buttons at once and each button is located on a different ADC pin. The array will look like this for example:  
PRESSED_BUTTONS[0] = 3  // Button number 3 located in group 0 was pressed
PRESSED_BUTTONS[1] = 5  // Button number 5 located in group 1 was pressed

isButtonPressedFor(param1, param 2)

Check if a button is pressed down for a certain amount of time. Returns 1 if the button was held down for a certain amount of time or 0 if not.
param1 is the button number to be checked (from 1 to 255)
param2 is the time in milliseconds to check against (maximum 65535)
This function works only in conjunction with readButtonsADC. The button number must not be hard coded but the returned value from readButtonsADC. See the example bellow on how to use it.

Variables


ALLOW_BUTTON_ACTION

Boolean value, initially set to true. Is helpful when, after a button is pressed you don't want the code to be executed again and again. In example 1 bellow, after the button is pressed the ALLOW_BUTTON_ACTION is set to false, and even if the button remains pressed, the setTemperature variable won't be incremented or decremented. After the button is released it will be set back to true automatically.

Example 1 - Reading single button press

/*************************************************************
 INCLUDES
**************************************************************/
#include <avr/io.h>
#include <util/delay.h>
#include "LC75824.h" // REPLACE WITH AN LCD LIBRARY OF CHOICE
#include "buttonsADC.h"

 
/*************************************************************
 MAIN FUNCTION
**************************************************************/
int main(void){
 uint8_t pressedButton = 0;
 uint8_t setTemperature = 0;  
 
 turnOnDisplay();
 LCD_Clear(0);  

 while(1){
  pressedButton = readButtonsADC();
    
  // If not 0
  if(pressedButton && ALLOW_BUTTON_ACTION){
   ALLOW_BUTTON_ACTION = 0; 
   LCD_Clear(0);
   
   switch(pressedButton){
    case 1:
     // Display button number
     LCD_WriteString("1", 1);
     // Increase temperature
     setTemperature++;
     // Display temperature
     LCD_WriteInt(setTemperature, 3, 4); 
    break;
    case 2:
     // Display button number
     LCD_WriteString("2", 1);
     // Decrease temperature
     setTemperature--;
     // Display temperature
     LCD_WriteInt(setTemperature, 3, 4); 
    break;
    case 3:
     LCD_WriteString("3", 1);
    break;
    case 4:
     LCD_WriteString("4", 1);
    break;
    case 5:
     LCD_WriteString("5", 1);
    break;
    case 6:
     LCD_WriteString("6", 1);
    break;
    case 7:
     LCD_WriteString("7", 1);
    break;
    case 8:
     LCD_WriteString("8", 1);
    break;
    case 9:
     LCD_WriteString("9", 1);
    break;
    case 10:
     LCD_WriteString("10", 1);
    break;
    case 11:
     LCD_WriteString("11", 1);
    break;
    case 12:
     LCD_WriteString("12", 1);
    break;
    case 13:
     LCD_WriteString("13", 1); 
    break;
   }
  }
 }
}


Example 2 - Reading simultaneously button presses


/*************************************************************
 INCLUDES
**************************************************************/
#include <avr/io.h>
#include <util/delay.h>
#include "LC75824.h" // Replace with a LCD library if needed
#include "buttonsADC.h"

 
/*************************************************************
 MAIN FUNCTION
**************************************************************/
int main(void){
 uint8_t pressedButton = 0;
 uint8_t setTemperature = 0;  
 
 turnOnDisplay(); // LCD library function
 LCD_Clear(0); // LCD library function

 while(1){
  pressedButton = readButtonsADC();
  
  // If not 0
  if(pressedButton){
   // Don't use ALLOW_BUTTON_ACTION in this case because once a button is pressed
   // the other button won't be registered
   //ALLOW_BUTTON_ACTION = 0; 
   LCD_Clear(0);
   
   // PRESSED_BUTTONS[0] is the group of buttons on a single ADC pin
   // PRESSED_BUTTONS[1] is another group of buttons on other ADC pin
   switch(PRESSED_BUTTONS[0]){
    case 1: 
     // Display button number
     LCD_WriteString("1", 1);
          
     if(PRESSED_BUTTONS[1] == 8){
      // Buttons 1 and 8 are pressed. Each one must be located 
      // on a different ADC pin
      LCD_WriteString("M", 3);
     } 
    break;
    case 2:
     // Display button number
     LCD_WriteString("2", 1);
     // Decrease temperature
     setTemperature--;
     // Display temperature
     LCD_WriteInt(setTemperature, 3, 4); 
    break;
    case 3:
     LCD_WriteString("3", 1);
    break;
    case 4:
     LCD_WriteString("4", 1);
    break;
    case 5:
     LCD_WriteString("5", 1);
    break;
    case 6:
     LCD_WriteString("6", 1);
    break;
    case 7:
     LCD_WriteString("7", 1);
    break;
    case 8:
     LCD_WriteString("8", 1);
    break;
    case 9:
     LCD_WriteString("9", 1);
    break;
    case 10:
     LCD_WriteString("10", 1);
    break;
    case 11:
     LCD_WriteString("11", 1);
    break;
    case 12:
     LCD_WriteString("12", 1);
    break;
    case 13:
     LCD_WriteString("13", 1); 
    break;
   }
  }
 }
}


Example 3 - Checking if a button is pressed for certain amount of time


/*************************************************************
 INCLUDES
**************************************************************/
#include <avr/io.h>
#include <util/delay.h>
#include "LC75824.h" // Replace with a LCD library if needed
#include "buttonsADC.h"
 
/*************************************************************
 MAIN FUNCTION
**************************************************************/
int main(void){
 uint8_t pressedButton = 0;
 uint8_t setTemperature = 0;
 uint8_t isbtnHoldDown = 0;  
 
 turnOnDisplay(); // LCD library function
 LCD_Clear(0); // LCD library function

 while(1){
  // Read ADC buttons
  pressedButton = readButtonsADC();
  // Check if a button was pressed for 5 seconds (5000 milliseconds)
  isbtnHoldDown = isButtonPressedFor(pressedButton, 5000);
  
  if(isbtnHoldDown && pressedButton == 2){
   // Button number 2 was pressed for at least 5 seconds
  }
 }
}


Q&A


Q: Can I read multiple buttons pressed at the same time on the same pin?
A: theoretically yes but the code would be a nightmare and i wouldn't touch it with a 10 feet (3.048 meters) pole, so practically no. However you can do it if they are located on different pins.

Q: How many buttons can I have on a single ADC pin? Can I put 100?
A: Didn't try it but if you do let me know if it works. Good luck soldering so many resistors.

Q: I will make a keyboard with just one ADC pin and an ATtiny13 MCU
A: That's a great idea!

Download

v1.0
buttonsADC.h 

Library dependencies

adc.h

Other Resources

v1.0
Resistor calculator spreadsheet.ods

2 comments:

  1. ADC.H link is missing !! Excelent work !

    ReplyDelete
    Replies
    1. Thanks for reporting the issue. I have updated the link. Glad you like it.

      Delete