I've incorporated a rotary encoder into a project using Timer-based rotary encoder logic by Peter Dannegger (the ClickEncoder library). This works very well however since the unit could sit doing its job for months once set up by the user I'd like to put the LCD to sleep and wake it up again based on the encoder movement.
The ClickEncoder library uses the avr interrupt handler and I'm wondering if I can call that, via ClickEncoder, to go to sleep and wake up as required.
Anyone familiar with ClickEncoder and/or the interrupt handler that can toss an idea my way?
Looking at the library, it appears to poll the encoder pins every 1ms looking for changes.
Maybe one of these to control power to the LCD ("M" is the LCD power):
With some sort of State Change Detection based on one of the encoder variables:
I mean that you should do change detection on the encoder variables, and when they change, reset a timer and make sure the power is on. When the timer gets old, turn off the power to the LCD.
If you are talking about powering down the Arduino too, that would be more involved.
Its a Nokia 5110 which accepts a power down command like this
display.command(PCD8544_POWERDOWN);
That works fine and it will wake up with simply
display.begin();
Like Dave X said, the library polls encoder pins looking for movement which I was thinking of utilizing. I was hoping someone had already done this so I'm not reinventing the wheel
It's not an Arduino its an embedded Mega328P (32 pin TQFP) in a custom circuit and board I designed. The system works well however the Nokia LCD will, given enough time sitting idle, get slightly scrambled. I believe its losing the clock edge or something and getting out of sync. It takes a while but the system sits and looks for PIR signals for months and only needs the screen for system setup.
You tweaked to the same thing I've been looking at, the polling.
// call this every 1 millisecond via timer ISR
//
void ClickEncoder::service(void)
{
bool moved = false;
unsigned long now = millis();
if (accelerationEnabled) { // decelerate every tick
acceleration -= ENC_ACCEL_DEC;
if (acceleration & 0x8000) { // handle overflow of MSB is set
acceleration = 0;
}
}
I'm thinking of the bool moved = false. Might be able to do something with that.
You'd insert a bit of code in it to wake up your LCD if it was off, and also add in something to record the timestamp for when the encoder was tweaked. For example:
Then in a separate block, use the timestamp to check if it's been a while since the encoder was tweaked, and if so, turn off the LCD. Somehting like this completely untested snippet:
No I wouldn't mess with the library but I was thinking of using the library to give me what I need and yes it would need a time related check.
My application is a variation on the ClickEncoder example since its scrolling through a list of menu and sub menu items. What's shown here comprises most of loop and the rest of loop is only 23 lines. (The sketch has nearly 800 lines of code, primarily functions). I've included the sections that apply here and the function called in readRotaryEncoder().
Forgive the formatting. It was done after beta testing was completed to allow less scrolling while working later with the sketch.
readRotaryEncoder();
/*** BUTTON ACTIONS and MENU NAVIGATION ***/
/*
Lots going on here however its simply scrolling through the main and sub menus. The cursor (in reverse video) moves up or down the
screen with the next menu line appearing as the page changes.
*/
ClickEncoder::Button b = encoder->getButton();
if (b != ClickEncoder::Open) {
switch (b) {
case ClickEncoder::DoubleClicked: //look for a double click
if (backLightState) switchBacklight(false); // if backlight is off turn it on and vice versa
else switchBacklight(true);
break;
case ClickEncoder::Clicked: //single click
middle = true;
break;
}
} //endif
//remainder of the loop has been formatted for spacing
if (up && page == 1) {
up = false;
if (menuitem == 2 && frame == 2) frame--;
if (menuitem == 4 && frame == 4) frame--;
if (menuitem == 3 && frame == 3) frame--;
lastMenuItem = menuitem;
menuitem--;
if (menuitem == 0) menuitem = 1;
} else if (up && page == 2 && menuitem == 1) {
up = false;
limitArea1--;
if (limitArea1 < 5) limitArea1++; //see notes above for more info on area timing
} else if (up && page == 2 && menuitem == 2) {
up = false;
limitArea2--;
if (limitArea2 < 5) limitArea2++;
} else if (up && page == 2 && menuitem == 3) {
up = false;
selectedZone--;
if (selectedZone == -1) selectedZone = 3;
} else if (up && page == 2 && menuitem == 4) {
up = false;
selectedAlarm--;
if (selectedAlarm == -1) selectedAlarm = 1;
}
if (down && page == 1) {
down = false;
if (menuitem == 3 && lastMenuItem == 2) frame++;
else if (menuitem == 4 && lastMenuItem == 3) frame++;
else if (menuitem == 5 && lastMenuItem == 4 && frame != 4) frame++;
lastMenuItem = menuitem;
menuitem++;
if (menuitem == 7) menuitem--;
} //endif
else if (down && page == 2 && menuitem == 1) {
down = false;
limitArea1++;
if (limitArea1 > 999) limitArea1--; //only have space for 3 digits
} else if (down && page == 2 && menuitem == 2) {
down = false;
limitArea2++;
if (limitArea2 > 999) limitArea2--;
} else if (down && page == 2 && menuitem == 3) {
down = false;
selectedZone++;
if (selectedZone == 4) selectedZone = 0;
}
/*** MIDDLE BUTTON PRESS ***/
//the action taken on a button press depends on which page and menu item are in focus (reverse video)
if (middle) { //button pressed
middle = false; //reset for the next press
if (page == 2 && menuitem == 1) { //Area1 Timer highlighted on screen
if (limitArea1 != sysArray[0]) { //test for a value change made by the user
sysArray[0] = limitArea1; //update the value
eeprmJob('W', 1); //store the value
sendMessage('S'); //tell the user
}
}
if (page == 2 && menuitem == 2) { //Area2 Timer
if (limitArea2 != sysArray[1]) { //test for a value change
sysArray[1] = limitArea2; //update the value
eeprmJob('W', 2); //store the value
sendMessage('S'); //tell the user
}
}
if (page == 2 && menuitem == 3) triggerZone(selectedZone); //manual triggering with an encoder button press
if (page == 1 && menuitem == 5) { // backlight control
if (backLightState) switchBacklight(false); // if its off turn it on and vice versa
else switchBacklight(true);
}
if (page == 1 && menuitem == 6) resetDefaults(); // reset area times to default values, also turns off the backlight
else if (page == 1 && menuitem <= 4) page = 2;
else if (page == 2) page = 1;
} //end if
The function
void readRotaryEncoder() {
value += encoder->getValue(); //** THIS LINE IS OF INTEREST, SEE BELOW
//up and down were reversed here to detect proper direction of rotation
if (value / 2 > last) {
last = value / 2;
up = true;
delay(50);
} else if (value / 2 < last) {
last = value / 2;
down = true;
delay(50);
}
} //end readRotaryEncoder
I was thinking of simply using the value 'r' returned from the library function getValue() in a timer.
int16_t ClickEncoder::getValue(void)
{
int16_t val;
cli();
val = delta;
if (steps == 2) delta = val & 1;
else if (steps == 4) delta = val & 3;
else delta = 0; // default to 1 step per notch
sei();
if (steps == 4) val >>= 2;
if (steps == 2) val >>= 1;
int16_t r = 0;
int16_t accel = ((accelerationEnabled) ? (acceleration >> 8) : 0);
if (val < 0) {
r -= 1 + accel;
}
else if (val > 0) {
r += 1 + accel;
}
return r;
}
I decided to use the data passed back to getValue() to do the job.
Added this before setup()
//*NEW statements
boolean lcdState; //current LCD power state
unsigned long lastEncoderMove; //last time the encoder was used
#define ENCODER_IDLE 180000 //time since the encoder was used
modified the LCD initialization in setup()
//initialize the LCD and show the splash screen
display.begin();
lcdState = true; //*NEW the LCD is powered up
resetDisplay(1);
display.setCursor(0, 5);
display.print(__SPLASH__);
display.display();
delay(4000); //allow for read
display.clearDisplay();
setContrast();
Timer1.initialize(500); //used by the encoder
Timer1.attachInterrupt(timerIsr); //used by the encoder
last = encoder->getValue();
} //end setup
Added an if statement to the drawMenu() function in loop()
//*NEW if statement
if (lcdState == true) drawMenu(); //draw the LCD if not powered down
and then did everything in readRotaryEncoder()
void readRotaryEncoder() {
value += encoder->getValue();
//up and down were reversed here to detect proper direction of rotation
if (value / 2 > last) {
last = value / 2;
up = true;
delay(50);
lastEncoderMove = ms; //*NEW record the movment
lcdState = true; //*NEW update the LCD power state
} else if (value / 2 < last) {
last = value / 2;
down = true;
delay(50);
lastEncoderMove = ms; //*NEW record the movment
lcdState = true; //*NEW update
}//*NEW - the actual power down is done here
if (ms - lastEncoderMove > ENCODER_IDLE && lcdState == true) { //its powered up and been idle for the designated time
display.command(PCD8544_POWERDOWN); //put the screen to sleep
lcdState = !lcdState; //update
}
} //end readRotaryEncoder
The power down and prep for power up is done in the one function by monitoring the encoder acceleration. The actual power up is done on the first redraw after lcdState is set back to true. Something could probably be done using the interrupt attached to Timer1 prior to setup but it wasn't worth the trouble and I would still need the new global definitions so no space savings.
Thanks for the input. Always helps to bounce an idea off someone.