Close

Front Panel

A project log for Polling a Push Button Rotary Encoder

A push button rotary encoder is a neat input device for an Arduino. Unfortunately interfacing to it is not!

agp.cooperagp.cooper 02/01/2018 at 12:280 Comments

Front Panel

The front panel did not go very smoothly despite the effort in setting it up. To ensure I did not waste my supply of Birch plywood I just made a prototype of the front panel with some fiberboard.

I forgot the LCD window on the first front panel!, even so it demonstrated that the Keyes rotary encoder was a bad choice. The encoder shaft collar has no thread and the two bolt holes on the PCB are on one side of the shaft. The encoder moves to the side when pressed. Other than gluing the encoder to the back of the front panel it is not going to work.

Made a second front panel (with an LCD window) for a bare rotary encoder. Made up a small strip board with legs for the rotary encoder to fit level with the LCD display :

The Nano sockets were removed and the Nano was soldered directly to the stripboard as it was no going to fit (in the image above, the red led thing under the rotay switch).

The RTC sockets will need to go as well.

Testing

At first the rotation increment/decrement was the wrong way. No problem I have swapped the A and B pins on the rotatry encoder. I fixed it in software for now and will rewire it later.

The main problem was that the menu and setting increments and decrements in steps of two? This puzzled me for a while until I realised it could only be the rotary encoder. Sure enough if you rotate the encode 1/2 a detent if increments once and then again at the detent. It reliably does this so the double increment is by design! How stupid! To fix I added a flag to the polling routine to only update the second increment (at detent).

It now work fine, upon boot up:

The menu (item) after one press (for about a second):

This is the Menu Item on the first line and the current setting on the second line (n.b. "X" means abort all changes while "Y" means accept and "N" means cancel this Menu Item.

Second press to change or set the menu item:

This Set Menu allows you to change a setting.

And a third press to accept the setting which in this case "X" will return you to the UT Clock (with no changes to the clock settings).

The location of the rotary encoder ideal for a right hander as it keeps your fingers from blocking your view of the LCD while rotating the knob.

Now the only problem I noticed was that the day number is wrong! Which means the timeSpan() does not like negative numbers. Fixed, don't use timeSpan()!

Updated Code

Here is the updated code. Note this code is configured for my stupid rotary encoder!

// Include RTC libraries and initialise
#include <Wire.h>
#include "RTClib.h"
RTC_DS1307 rtc;

// Include LCD library and initialise
#include <LiquidCrystal.h>
// Define Adruino pin numbers for LCD
#define en   7
#define rs   8
#define d4   9
#define d5  10
#define d6  11
#define d7  12
#define led 13
// Set LCD
LiquidCrystal lcd(rs,en,d4,d5,d6,d7);

/* ROTARY ENCODER AND PUSH BUTTON POLLING CODE */
#define Clk 3
#define DT  2
#define SW  4
// Poll and debounces a Keyes rotary and push button switch
// Inputs:
//   Clk (Pin A without a pullup resistor)
//   DT  (Pin B without a pullup resistor)
//   SW  (Switch without a pullup resistor)
// Exports:
//   UpdateSwitch (bool)
//   Switch [LOW|HIGH]
//   EncoderPos (int)
volatile bool UpdateSwitch=false;
volatile byte Switch=HIGH;
volatile int EncoderPos=0;
volatile bool EncoderFlag=true;
ISR(TIMER0_COMPB_vect) {
  static byte testClk=(PIND>>Clk)&1;
  static byte testDT=(PIND>>DT)&1;
  static byte lastClk=LOW;
  static byte lastDT=LOW;
  static bool flagClk=false;
  static bool flagDT=false;
  static int encoderDir=0;
  static byte testSW=HIGH;
  static byte statusSW=HIGH;
  static byte cntSW=0;  
  
  
  // Update Encoder Position
  lastClk=testClk;             // Save Clk
  lastDT=testDT;               // Save DT
  testClk=(PIND>>Clk)&1;       // Get Clk
  testDT=(PIND>>DT)&1;         // Get DT
  if (testClk!=lastClk) {      // Change in Clk?
    flagClk=true;              // Flag Clk has changed
    encoderDir=-1;             // Assume it is the last flag to change
  }
  if (testDT!=lastDT) {        // Change in DT?
    flagDT=true;               // Flag DT has changed
    encoderDir=1;              // Assume it is the last flag to change
  }
  if (flagClk&&flagDT) {       // Both flags have changed
    // This rotary encoder updates twice per detent
    if (EncoderFlag) {         // First transition
      EncoderPos+=encoderDir;
    }
    EncoderFlag=(!EncoderFlag);    
    flagClk=false;             // Reset Clk flag
    flagDT=false;              // Reset DT flag
  }
  
  // Update switch with 10ms debounce
  testSW=(PIND>>SW)&1;
  if (testSW!=statusSW) {
    statusSW=testSW;
    cntSW=10;
  }
  if (cntSW>0) {
    cntSW--;
    if (cntSW==0) {
      Switch=statusSW;
      UpdateSwitch=true;
    }
  }
}


/* MENU SET UP */
char* menuName[]={"Done ","Year ","Month ","Day ","Hour ","Minute ","Second "};
int menuSize=sizeof(menuName)/sizeof(char*);
int menuValue[]=   {  1,2000,  1,  1,  0,  0,  0};
char menuNumeric[]={'N', 'Y','Y','Y','Y','Y','Y'};
int menuValueMin[]={  0,2000,  1,  1,  0,  0,  0};  // Non-numeric values 'N','X' and 'Y'
int menuValueMax[]={  2,2999, 12, 31, 23, 59, 59};
int menuLevel=0;

void setup() {
  // Initialise rotary encoder and push button
  pinMode(Clk,INPUT_PULLUP); // Rotary Clk
  pinMode(DT,INPUT_PULLUP);  // Rotary DT
  pinMode(SW,INPUT_PULLUP);  // Rotary SW

  // Turn on polling ISR
  OCR0B=0xA0;
  TIMSK0|=1<<OCIE0B;

  // Initialise LCD
  pinMode(led,OUTPUT);
  digitalWrite(led,HIGH);
  lcd.begin(16,2);

  // Print a message to the LCD.
  lcd.setCursor(0,0);lcd.print("Universal Time");
  // Check RTC
  if (!rtc.begin()) {
    lcd.setCursor(0,1);lcd.print("RTC not found");
    while (true);
  } else if (!rtc.isrunning()) {
    lcd.setCursor(0,1);lcd.print("RTC not running");
  } else {
    lcd.setCursor(0,1);lcd.print("RTC found");
  }
  
  // Set Universal Time based on Compile Time
  int localTimeHour=-8; // Perth Western Australia
  int localTimeMin=0;   // Perth Western Australia
  int uploadTimeSec=7;  // For my computer

  rtc.adjust(DateTime(F(__DATE__),F(__TIME__)));
  DateTime now=rtc.now();
  rtc.adjust(DateTime(now+TimeSpan(0,localTimeHour,localTimeMin,uploadTimeSec)));
  
  // Set menu to current time
  now=rtc.now();
  menuLevel=-1;              // Don't enter menu on start up
  menuValue[0]='X';          // Set "done" status to abort 
  menuValue[1]=now.year();
  menuValue[2]=now.month();
  menuValue[3]=now.day();
  menuValue[4]=now.hour();
  menuValue[5]=now.minute();
  menuValue[6]=now.second();
  
  delay(1000);
  lcd.clear();
  
}

bool processMenu(void) {
  static int lastPos=0;
  static int lastMenuLevel=-1;

  // Pre-empt menu level display 
  if (menuLevel!=lastMenuLevel) {
    lastMenuLevel=menuLevel;
    lcd.clear();
    lcd.setCursor(0,0);
    if (menuLevel==0) {
      lcd.print("Menu:");
      lcd.setCursor(0,1);
      lcd.print(menuName[EncoderPos]);
      if (menuNumeric[EncoderPos]=='Y') {
        lcd.print(menuValue[EncoderPos]);
      } else {       
        if (menuValue[EncoderPos]==0) {
          lcd.print("N");
        } else if (menuValue[EncoderPos]==1) {
          lcd.print("X");      
        } else {
          lcd.print("Y");      
        }
      }          
    } else if (menuLevel==1) {
      lcd.print("Set:");
      lcd.setCursor(0,1);
      lcd.print(menuName[lastPos]);
      if (menuNumeric[lastPos]=='Y') {
        lcd.print(menuValue[lastPos]);
      } else {       
        if (menuValue[lastPos]==0) {
          lcd.print("N");
        } else if (menuValue[lastPos]==2) {
          lcd.print("Y");      
        } else {
          lcd.print("X");      
        }
      }      
    } else {
      lcd.print("Rotary Encoder?");      
    }
  }
  
  // If push button pushed toggle menuLevel
  if (UpdateSwitch) {
    UpdateSwitch=false;
    if (Switch==LOW) {
      // Re-enter menu if button pushed (for long enough)
      if (menuLevel==-1) {
        menuLevel=0;       // Item menu
        lastPos=1;         // Trigger menu update 
        EncoderPos=0;      // Done
        EncoderFlag=false; // Detent
        menuValue[0]=1;    // 'X'
      } else {
        // Toggle menu level
        menuLevel=1-menuLevel;
        if (menuLevel==0) {
          // Save item menu position
          EncoderPos=lastPos;
          // Exit menu if done!
          if ((EncoderPos==0)&&(menuValue[EncoderPos]!=0)) menuLevel=-1;
          // Any final work to be done? Set the time!
          if (menuValue[EncoderPos]==2) {
            rtc.adjust(DateTime(menuValue[1],menuValue[2],menuValue[3],menuValue[4],menuValue[5],menuValue[6]));
          }
        } else {
          // Set value for edit menu
          EncoderPos=menuValue[lastPos];
        }
      }
    }
  }
  
  // If encoder turned
  if (menuLevel==0) { // Select menu item
    if (lastPos!=EncoderPos) {
      if (EncoderPos>=menuSize) EncoderPos=0;
      if (EncoderPos<0) EncoderPos=menuSize-1;
      lastPos=EncoderPos;
      
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print("Menu:");
      lcd.setCursor(0,1);
      lcd.print(menuName[EncoderPos]);
      if (menuNumeric[EncoderPos]=='Y') {
        lcd.print(menuValue[EncoderPos]);
      } else {       
        if (menuValue[EncoderPos]==0) {
          lcd.print("N");
        } else if (menuValue[EncoderPos]==2) {
          lcd.print("Y");      
        } else {
          lcd.print("X");      
        }
      }

    }
  } else if (menuLevel==1) { // Edit menu item value
    if (menuValue[lastPos]!=EncoderPos) {
      if (EncoderPos>menuValueMax[lastPos]) EncoderPos=menuValueMin[lastPos];
      if (EncoderPos<menuValueMin[lastPos]) EncoderPos=menuValueMax[lastPos];
      menuValue[lastPos]=EncoderPos;
      
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print("Set:");
      lcd.setCursor(0,1);
      lcd.print(menuName[lastPos]);
      if (menuNumeric[lastPos]=='Y') {
        lcd.print(menuValue[lastPos]);
      } else {       
        if (menuValue[lastPos]==0) {
          lcd.print("N");
        } else if (menuValue[lastPos]==2) {
          lcd.print("Y");      
        } else {
          lcd.print("X");      
        }
      }
      
    }
  }

  return (menuLevel!=-1); // Return true if menu active
}

void loop() {

  if (!processMenu()) { // Menu not active
    // Present the results

    DateTime now=rtc.now();
    // Print date on first line of LCD
    lcd.clear();
    lcd.setCursor(0,0);
      lcd.print(now.year());
      lcd.print('/');
      if (now.month()<10) lcd.print(0);
      lcd.print(now.month());
      lcd.print('/');
      if (now.day()<10) lcd.print(0);
      lcd.print(now.day());
    
    // Print time on second line of LCD
    lcd.setCursor(0,1);lcd.print("UT: ");
      if (now.hour()<10) lcd.print(0);
      lcd.print(now.hour());
      lcd.print(':');
      if (now.minute()<10) lcd.print(0);
      lcd.print(now.minute());
      lcd.print(':');
      if (now.second()<10) lcd.print(0);
      lcd.print(now.second());
    
    delay(1000);
  }

}

Magic

Discussions