Wednesday, July 5, 2017

Arduino LED driver and button press handler / debouncer: another "Hello, World"

Long story short, I've bought an Arduino for some small home automation (specifically, imitating remote control commands for a portable air conditioner in order to operate it on schedule).

Having run a few simple examples, I realized that whatever my device might be, I'd need some sort of a rudimentary GUI with some buttons to control the device and some LEDs to diagnose the device state. After some testing I realized that I need to solve several problems:
  1. The function digitalRead() only polls instantaneous button state ("is the button pressed now?"); some memory-based logic is needed for the detection of the event "the button was pressed and then released", which is a relevant GUI input. This logic should also filter out contact bounce , otherwise (as I quickly found out) a button press (and/or release) is very prone to being interpreted as several presses.
  2. I wanted to led LEDs blink (so that my device would signal its status without doubling up as a night light) concurrently with doing other tasks (such as the device's main function). 
  3. I wanted the routines to be general enough to scale to several LEDs and buttons concurrently and independently of one another.
Essentially I wanted to implement the two basic tutorials (Debounce and BlinkWithoutDelay) in a robust, encapsulated, scalable manner.

So, here's what I came up with, and decided to share it here in case it is useful for someone else.

Let's first define some global variables and structures:

// button names
#define B_UP 0
#define B_DN 1
#define B_SEL 2
#define B_MOD 3

//global constants
const byte nL=5;
const byte nB=4;
const byte pinL[nL]={11,10,9,6,5}; // should be PWM-enabled pins
const byte pinB[nB]={7,8,2,3};

const byte NOISE = 15; // ms to hit timed event for debouncing
const byte REP_DELAY = 400; // ms to initiate autorepeat
const byte REP_RATE = 100; // ms to regenerate autorepeat

//global variables, initialized
byte thisLED=0;
byte thisMODE=4;
For our array of LEDs we define some working data structures:
//data structures - LEDs
word quant[nL] = {125,125,125,125,125}; // time between ticks
word flash[nL] = {75,75,75,75,75}; // overrides if less than quant;
byte level[nL] = {120,140,100,80,40}; //brightness level 
byte mask[nL] = {B00000001,B00010001,B01010101,B00000011,B11001100};//bitwise mask

word next[nL]; // time of next LED handle event
word nextOff[nL]; // time of next turn-off event
byte cycle[nL] = {1,1,1,1,1}; //current bit in the mask, starting with 00000001, rotated left on every tick
The blinking pattern is defined as follows: every quant[i] milliseconds we define a "tick" for the LED number i; at each tick, we cycle the "current bit number" between 1 and 8 (using the internal variable cycle[i]), and turn the i-th LED either on or off, depending on the current bit in mask[i]. Additionally the LED is turned off on a "quench", when flash[i] milliseconds have elapsed since it was last turned on. Finally, level[i] simply defines the LED's brightness level. This can be illustrated like this:

This approach allows us to create a considerable variety of patterns ranging from simply "pulse N times per second" or "do N short blinks once in a while" to more complicated dash-dot patterns. It has its limitations (mostly related to the number of bits in the mask) but it is more than enough for the kinds of diagnostic output we aim for. Note that level and pattern are independently set for each LED.

By the same token we define some structures for the buttons:
//data structures - buttons
byte raw[nB] = {0,0,0,0}; //raw button state 
byte b_ready[nB] = {1,1,1,1}; //whether button is ready 
byte cooked[nB] = {0,0,0,0}; // filtered button state
byte autorepeat[nB] = {1,1,0,0}; //whether to autorepeat on button xx
byte event[nB] = {0,0,0,0}; //button press event, must be consumed on handle and re-generated on autorepeat

word last[nB] = {0,0,0,0}; // time last pressed

Here the only "parameter" is autorepeat[i], defining whether the button number i should auto-repeat. The rest are internal variables needed for debouncing and handling button press events.

Next we write the handler for the LEDs:

void pollLEDs()
{
  word now = (word)millis();
  for (byte i=0;i<nL;++i)
  {
    if ((int)(now-next[i])>=0) // next tick reached
    {
      nextOff[i] = next[i]+flash[i];
      next[i]+=quant[i]; // move next (and nextoff) forward
      boolean state = ((cycle[i] & mask[i])!=0); 
         // determine current state from mask
      analogWrite(pinL[i], (state)?level[i]:0); \
         // light up or quench according to state
      cycle[i]=cycle[i]<<1; if(cycle[i]==0) cycle[i]=1; 
         // cycle current bit in mask 
    }
    if ((int)(now-nextOff[i])>=0) 
    { // flash time exceeded before next tick
      nextOff[i]+=quant[i]; // just move forward
      analogWrite(pinL[i], 0); // quench
    }
  }
}

We see that it just implements the logic in the diagram above, using next[] and nextOff[] to store the timer values for the next tick and quench respectively. The only trick here is the use of bitwise arithmetic for cycle[] to enable quick comparison against mask[].

You can see that it is easy to make the time span between patterns longer than 8*quant[] without increasing the size of the mask by simply defining a separate period[nL] and then replace next[i]+=quant[i] with next[i]+=(cycle[i]!=1)?quant[i]:period[i]


Next we write the button handler:

void pollButtons()
{
 word now = (word)millis();
 for (byte i=0;i<nB;i++)
 {
   raw[i] = (digitalRead(pinB[i])==LOW)?1:0; // store to avoid repeated calls to digitalRead()
   // if button is ready and LOW recorded, record last pressed time and clear ready
   if (b_ready[i]==1 && cooked[i]==0 && raw[i]==1) {b_ready[i]=0;last[i]=now;}
   // if not ready and still LOW after tolerance, set filtered state, button press detected
   if (b_ready[i]==0 && cooked[i]==0 && raw[i]==1 && (word)(now-last[i]) > NOISE) cooked[i]=1;
   // if state is pressed and button released : clear filtered, restore b_ready, and generate event unless auto repeating
   if (b_ready[i]==0 && cooked[i]==1 && raw[i]==0 && (word)(now-last[i]) > NOISE) 
   {
     cooked[i]=0;
     b_ready[i]=1;
     if (autorepeat[i]<2) event[i]=1; else autorepeat[i]=1;
   }
   
   // handle auto repeat
   if (autorepeat[i]==1 && cooked[i]==1 && raw[i]==1 && (word)(now-last[i]) > REP_DELAY)
   { // initiate auto repeat
      autorepeat[i]=2;
      last[i] = now;
      event[i]=1;
   }
   if (autorepeat[i]==2 && cooked[i]==1 && raw[i]==1 && (word)(now-last[i]) > REP_RATE)
   { // if auto repeating, re-generate event periodically
      last[i] = now;
      event[i]=1;
   }
   
   //handle event once generated
   if (event[i]==1)
   {
     event[i]=0; //consume event
     control(i); //handle event
   }
 }
}

Note that it uses raw[i] to store the instantaneous state of the button number i, whereas ready[i] is a flag indicating whether that button is expected to receive input; it is set initially and cleared the instant the button press is detected (at which time the button timer is set via last[i]). The logic to determine whether the button has actually been pressed (so its filtered state, cooked[i], can be set) is "the button is still pressed some predetermined time after it was pressed initially"; I've used 15 ms as a ballpark and found it to work fine with my buttons. Once cooked[i] is set, the handler listens for the button release; once this happens, it generates an event (via setting event[i]) and return the button to its initial ready state.

For auto repeat support the amount of extension is minimal. We use the same timer to determine whether a certain time has elapsed since the button was pressed, and start generating events repeatedly at regular intervals until the button is released.

We can see from the code and description that this approach filters out the contact bounce both upon press and upon release. I am leaving it as an exercise for you to see why.


To test the implementation I made a "blink machine" quick breadboard circuit with five LEDs and four buttons, which would be used to control the way LEDs blink, like so:

 

The buttons functions are as follows:

  • [SELECT]: Cycle through LEDs to control.
  • [MODE]: Cycle through parameters to adjust:
    brightness, blink frequency, blink duration, blink pattern
  • [UP]: Increase the current parameter for the current LED.
  • [DOWN]: Decrease the current parameter for the current LED.
This corresponds to the following implementation of the event handler control()


void control(byte code)
{
 switch(code)
 {
   case B_SEL:
    thisLED++; if (thisLED>=nL) thisLED=0;
    notify();
   return;
   
   case B_MOD:
     thisMODE++; if (thisMODE>5) thisMODE=1;
     notify();
   return;

   case B_UP: case B_DN:
   {
    switch(thisMODE)
    {
  switch(thisMODE)
    {
      case 1: //level
       if (code==B_UP && level[thisLED]<255) level[thisLED]++ ;
       if (code==B_DN && level[thisLED]>0) level[thisLED]-- ;
       break;
      case 2: //period
       if (code==B_UP && quant[thisLED]<1000) quant[thisLED]+=20 ;
       if (code==B_DN && quant[thisLED]>40) quant[thisLED]-=20 ;
       break;
     case 3: //flash
       if (code==B_UP && flash[thisLED]<quant[thisLED]) flash[thisLED]+=10 ;
       if (code==B_DN && flash[thisLED]>20) flash[thisLED]-=10 ;
       break;
     case 4: //pattern up/down
       if (code==B_UP) mask[thisLED]++ ;
       if (code==B_DN) mask[thisLED]-- ;
       break;
     case 5: //pattern scramble/reset
       if (code==B_UP) mask[thisLED]=(byte)millis() ;
       if (code==B_DN) mask[thisLED]=1 ;
       break;
    }         
   }
   return;
 }
}

void notify() // show currently selected LED and mode
{
    analogWrite(pinL[thisLED],0);delay(100);
    for (byte i=1;i<=thisMODE;++i)
    {
      analogWrite(pinL[thisLED],255);delay(50);
      analogWrite(pinL[thisLED],0);delay(50);
    }
}

Note the fifth mode to quickly scramble and reset the bitwise mask. The auxiliary function notify() is used to visually indicate the currently selected LED and mode. Here, for simplicity and because the interface should be synchronous with user input, I do use delay() (pausing all other operations in the process). Exercise: During the operation of notify(), some of the LEDs may miss a tick. What will happen to their blinking pattern?

Finally to complete the sketch, here are its two main functions, setup() and loop(). Note that these are really short to isolate the button/LED workings from the rest of your code.

void setup() 
{
// initialize pins
  for (byte i=0;i<nL;++i) pinMode(pinL[i],OUTPUT);
  for (byte i=0;i<nB;++i) pinMode(pinB[i],INPUT_PULLUP);
  for (int j=0;j<nL;++j) {analogWrite(pinL[j],255);delay(75);analogWrite(pinL[j],0);delay(150);} // "splash screen"
  Serial.begin(9600); // optional, for debugging
// initialize LED timers
  word now = (word)millis();
  for (byte i=0;i<nL;++i)
  {
    next[i] = now+quant[i];
    nextOff[i] = next[i]+flash[i];
  }
}

void loop() 
{
    // just call the two handlers
    pollLEDs();
    pollButtons();
}

After some debugging the implementation tested fine, but the real use is that pollLEDs() and pollButtons() can be used within any sketch; of course, control() will need to be re-written. The pattern for the LEDs can be altered at runtime by changing the corresponding variables.
Some concluding thoughts:
  • I designed the LED pins to be PWM pins but non-PWM pins may be used just as well; just replace analogWrite with digitalWrite, disregarding level[].
  • LEDs can be any driving circuits that need timed controls. Similarly, buttons can be any form of digital input. If you use analog input, keep in mind that analogRead takes time and a much slower dead-time processing will be needed instead of debouncing.
  • This is really basic, but -- don't use pins 0 and 1 if you use serial communication for debugging. I've spent a few hours debugging until I figured out my debug messages were mimicking button presses.

No comments:

Post a Comment