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:
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:
By the same token we define some structures for the buttons:
Next we write the handler for the LEDs:
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[].
Next we write the button handler:
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.
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:
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.
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.
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:
- 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.
- 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).
- I wanted the routines to be general enough to scale to several LEDs and buttons concurrently and independently of one another.
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 tickThe 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.