Close

Still Testing II

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/03/2018 at 06:180 Comments

Still Testing II

A bit of code clean up!

Used enum types rather and constants as this makes changing the menu easier:

enum NoYes {N,Y};
enum MenuLevel {Top,Menu,Set};
enum MenuItem      { Exit_Menu  , Year  , Month  , Day  , Hour  , Minute  , Second  , Use_Time ,  Local_Hour  , Local_Min  , Use_UT  , Sample_Min  , Upload_Data  };
char* menuName[]  ={"Exit Menu ","Year ","Month ","Day ","Hour ","Minute ","Second ","Use Time ","Local Hour ","Local Min ","Use UT ","Sample Min ","Upload Data "};
int menuValue[]   ={          Y ,  2000 ,      1 ,    1 ,     0 ,       0 ,       0 ,         Y ,           8 ,          0 ,       Y ,           1 ,            Y };
char menuNumeric[]={          N ,     Y ,      Y ,    Y ,     Y ,       Y ,       Y ,         N ,           Y ,          Y ,       N ,           Y ,            N };
int menuValueMin[]={          N ,  1970 ,      1 ,    1 ,     0 ,       0 ,       0 ,         N ,         -12 ,        -59 ,       N ,           1 ,            N };
int menuValueMax[]={          Y ,  2100 ,     12 ,   31 ,    23 ,      59 ,      59 ,         Y ,          12 ,         59 ,       Y ,        1440 ,            Y };
int menuSize=sizeof(menuName)/sizeof(char*);
int menuLevel=Menu;

The menu set up is very clear to read and easy to modify. Just stay linear in your menu layout!

Reworked the polling ISR to take the guess work out of detent status of the rotary encoder:

  // Update Encoder Position
  lastPinA=testPinA;             // Save PinA
  lastPinB=testPinB;             // Save PinB
  testPinA=(PIND>>PinA)&1;       // Get PinA
  testPinB=(PIND>>PinB)&1;       // Get PinB
  
  // This rotary encoder updates twice per detent!
  if ((testPinA==HIGH)&&(testPinB==HIGH)) encoderFlag=true; // Encoder is in detent
  if ((testPinA==LOW)&&(testPinB==LOW)) encoderFlag=false;  // Encoder is between detents
  if (encoderFlag) {                                        // First transition (leaving detent) only
    if (testPinA!=lastPinA) {      // Change in PinA?
      flagPinA=true;               // Flag PinA has changed
      encoderDir=-1;               // Assume it is the last flag to change
    }
    if (testPinB!=lastPinB) {      // Change in PinB?
      flagPinB=true;               // Flag PinB has changed
      encoderDir=1;                // Assume it is the last flag to change
    }
    if (flagPinA&&flagPinB) {      // Both flags have changed
      EncoderPos+=encoderDir;
      flagPinA=false;              // Reset PinA flag
      flagPinB=false;              // Reset PinB flag
    }
  }

NOTE: YOUR ROTARY ENCODER MAY BE DIFFERENT! You can comment out these two lines:

  // This rotary encoder updates twice per detent!
  if ((testPinA==HIGH)&&(testPinB==HIGH)) encoderFlag=true; // Encoder is in detent
  if ((testPinA==LOW)&&(testPinB==LOW)) encoderFlag=false;  // Encoder is between detents

Disabled polling during menu processing.

Reworked the start up for a local time at start up with a menu option for Universal Time (UT).

The code is long but not much I can do to reduce this (other than hide it in an include file):

/* 
  Universal Time Clock with Periodic Sampler
  ==========================================
  Written by Alan Cooper (agp.cooper@gmail.com)
  This work is licensed under the 
  Creative Commons Attribution - Non Commercial 2.5 License.
  This means you are free to copy and share the code (but not to sell it).
  Also it is good karma to attribute the source of the code.
*/

// Compile and upload time adjustment (secs)
#define UploadTime 11

// Analog read on A7
#define Sensor A7
 
// Need the Wire library for RTC and AT24C32
#include <Wire.h>

// Use RTC library
#include "RTClib.h"
RTC_DS1307 rtc;

// Use AT24Cxx library
#include <AT24Cxx.h>
AT24Cxx AT24C32(0x50);

// Useful union/structures to read/write A  if (LEDTimer==-1) LEDTimer=menuValue[LED_Timeout];T24C32 data
union {
  struct{
    char Year;
    char Month;
    char Day;
    char Hour;
    char Minute;
    char Second;
    int Value;
  };
  char data[8]; 
} AT24C32_Slot;
union {
  struct{
    int Value;
  };
  char data[2]; 
} AT24C32_Int;
  
// Use LCD library
#include <LiquidCrystal.h>
#define en   7
#define rs   8
#define d4   9
#define d5  10
#define d6  11
#define d7  12
#define led 13
LiquidCrystal lcd(rs,en,d4,d5,d6,d7);

/* ROTARY ENCODER AND PUSH BUTTON POLLING CODE */
#define PinA 2
#define PinB 3
#define SW   4
volatile bool UpdateSwitch=false;
volatile byte Switch=HIGH;
volatile int EncoderPos=0;
ISR(TIMER0_COMPB_vect) {
  static byte testPinA=(PIND>>PinA)&1;
  static byte testPinB=(PIND>>PinB)&1;
  static byte lastPinA=LOW;
  static byte lastPinB=LOW;
  static bool flagPinA=false;
  static bool flagPinB=false;
  static bool encoderFlag=true;
  static int encoderDir=0;
  static byte testSW=HIGH;
  static byte statusSW=HIGH;
  static byte cntSW=0;  
  
  // Update Encoder Position
  lastPinA=testPinA;             // Save PinA
  lastPinB=testPinB;             // Save PinB
  testPinA=(PIND>>PinA)&1;       // Get PinA
  testPinB=(PIND>>PinB)&1;       // Get PinB
  
  // This rotary encoder updates twice per detent!
  if ((testPinA==HIGH)&&(testPinB==HIGH)) encoderFlag=true; // Encoder is in detent
  if ((testPinA==LOW)&&(testPinB==LOW)) encoderFlag=false;  // Encoder is between detents
  if (encoderFlag) {                                        // First transition (leaving detent) only
    if (testPinA!=lastPinA) {      // Change in PinA?
      flagPinA=true;               // Flag PinA has changed
      encoderDir=-1;               // Assume it is the last flag to change
    }
    if (testPinB!=lastPinB) {      // Change in PinB?
      flagPinB=true;               // Flag PinB has changed
      encoderDir=1;                // Assume it is the last flag to change
    }
    if (flagPinA&&flagPinB) {      // Both flags have changed
      EncoderPos+=encoderDir;
      flagPinA=false;              // Reset PinA flag
      flagPinB=false;              // Reset PinB flag
    }
  }
  
  // Update switch with 20 ms debounce
  testSW=(PIND>>SW)&1;
  if (testSW!=statusSW) {
    encoderFlag=true;            // Reset encoder flag (precaution)
    statusSW=testSW;
    cntSW=20;
  }
  if (cntSW>0) {
    cntSW--;
    if (cntSW==0) {
      Switch=statusSW;
      UpdateSwitch=true;
    }
  }
}


/* MENU SET UP */
enum NoYes {N,Y};
enum MenuLevel {Top,Menu,Set};
enum MenuItem      { Exit_Menu  , Year  , Month  , Day  , Hour  , Minute  , Second  , Use_Time  , Local_Hour  , Local_Min  , Use_UT  , Sample_Min  , Download_Data  , Reset_Data  , LED_Timeout  , Compile_Time  };
char* menuName[]  ={"Exit Menu ","Year ","Month ","Day ","Hour ","Minute ","Second ","Use Time ","Local Hour ","Local Min ","Use UT ","Sample Min ","Download Data ","Reset Data ","LED Timeout ","Compile Time "};
char menuNumeric[]={         N  ,    Y  ,     Y  ,   Y  ,    Y  ,      Y  ,      Y  ,        N  ,          Y  ,         Y  ,      N  ,          Y  ,             N  ,          N  ,           Y  ,            N  };
int menuValue[]   ={         Y  , 2000  ,     1  ,   1  ,    0  ,      0  ,      0  ,        Y  ,          8  ,         0  ,      Y  ,          1  ,             N  ,          N  ,          -1  ,            N  };
int menuValueMin[]={         N  , 1970  ,     1  ,   1  ,    0  ,      0  ,      0  ,        N  ,        -12  ,       -59  ,      N  ,          1  ,             N  ,          N  ,          -1  ,            N  };
int menuValueMax[]={         Y  , 2100  ,    12  ,  31  ,   23  ,     59  ,     59  ,        Y  ,         12  ,        59  ,      Y  ,       1440  ,             Y  ,          Y  ,        3600  ,            Y  };
int menuSize=sizeof(menuName)/sizeof(char*);
int menuLevel=Menu;

// AT24C32 usage
char settings[8];          // Start address 32752

bool processMenu(void) {
  static int lastPos=Exit_Menu;
  static int lastMenuLevel=Top;
  
  // Disable polling
  TIMSK0&=~(1<<OCIE0B);
  
  // Pre-empt menu level display 
  if (menuLevel!=lastMenuLevel) {
    lastMenuLevel=menuLevel;
    lcd.clear();
    lcd.setCursor(0,0);
    if (menuLevel==Menu) {
      lcd.print("Menu:");
      lcd.setCursor(0,1);
      lcd.print(menuName[EncoderPos]);
      if (menuNumeric[EncoderPos]==Y) {
        lcd.print(menuValue[EncoderPos]);
      } else {       
        if (menuValue[EncoderPos]==N) {
          lcd.print("N");      
        } else {
          lcd.print("Y");      
        }
      }          
    } else if (menuLevel==Set) {
      lcd.print("Set:");
      lcd.setCursor(0,1);
      lcd.print(menuName[lastPos]);
      if (menuNumeric[lastPos]==Y) {
        lcd.print(menuValue[lastPos]);
      } else {       
        if (menuValue[lastPos]==N) {
          lcd.print("N");      
        } else {
          lcd.print("Y");      
        }
      }      
    } else {
      lcd.print("Rotary Encoder?");      
    }
  }
  
  // If push button pushed toggle menu level
  if (UpdateSwitch) {
    UpdateSwitch=false;
    if (Switch==LOW) {
      // Re-enter menu if button pushed (for long enough)
      if (menuLevel==Top) {
        menuLevel=Menu;
        lastMenuLevel=Top;
        lastPos=Exit_Menu;                               
        EncoderPos=Exit_Menu;
        menuValue[Exit_Menu]=Y;
      } else {
        // Toggle menu level
        if (menuLevel==Menu) {
          menuLevel=Set;
        } else {
          menuLevel=Menu;
        }
        if (menuLevel==Menu) {
          // Restore item menu position
          EncoderPos=lastPos;
          // Exit menu if done!
          if ((EncoderPos==Exit_Menu)&&(menuValue[Exit_Menu]==Y)) {
            menuLevel=Top;
            // Save Local Time and Sample settings upon exit menu
            strncpy(settings,"UTC",3);
            settings[3]=menuValue[Local_Hour];
            settings[4]=menuValue[Local_Min];
            settings[5]=menuValue[Sample_Min];
            settings[6]=menuValue[LED_Timeout];
            settings[7]=menuValue[Compile_Time];
            AT24C32.WriteMem(8*4094,settings,8);
          }
          // Check for use new time
          if ((EncoderPos==Use_Time)&&(menuValue[Use_Time]==Y)) {
            rtc.adjust(DateTime(menuValue[Year],menuValue[Month],menuValue[Day],menuValue[Hour],menuValue[Minute],menuValue[Second]));
          }
          // Check for use UT
          if ((EncoderPos==Use_UT)&&(menuValue[Use_UT]==Y)) {
            DateTime now=rtc.now();
            rtc.adjust(DateTime(now+(-menuValue[Local_Hour]*3600-menuValue[Local_Min]*60)));
          }
          // Check for data download
          if ((EncoderPos==Download_Data)&&(menuValue[Download_Data]==Y)) {
            AT24C32.ReadMem(8*4095,AT24C32_Int.data,2); // Get sample slots used
            lcd.clear();
            lcd.setCursor(0,0);
            lcd.print("Download Data: ");
            lcd.setCursor(0,1);
            lcd.print("Slots used ");
            lcd.print(AT24C32_Int.Value);
            delay(1000);
            Serial.begin(9600);
            while (!Serial);
            for (int i=0;i<AT24C32_Int.Value;i++) {
              AT24C32.ReadMem(i*8,AT24C32_Slot.data,8);
              Serial.print(AT24C32_Slot.Year+2000,DEC);
              Serial.print("/");
              if (AT24C32_Slot.Month<10) Serial.print(0);       
              Serial.print(AT24C32_Slot.Month,DEC);     
              Serial.print("/");
              if (AT24C32_Slot.Day<10) Serial.print(0);         
              Serial.print(AT24C32_Slot.Day,DEC);     
              Serial.print(" "); 
              if (AT24C32_Slot.Hour<10) Serial.print(0);                        
              Serial.print(AT24C32_Slot.Hour,DEC);     
              Serial.print(":"); 
              if (AT24C32_Slot.Minute<10) Serial.print(0);                        
              Serial.print(AT24C32_Slot.Minute,DEC);     
              Serial.print(":");    
              if (AT24C32_Slot.Second<10) Serial.print(0);     
              Serial.print(AT24C32_Slot.Second,DEC);
              Serial.print(" = ");        
              Serial.println(AT24C32_Slot.Value);  
            }
            Serial.end();
            lcd.clear();
            lcd.setCursor(0,0);
            lcd.print("Download Data: ");
            lcd.setCursor(0,1);
            lcd.print("Done ");
            delay(1000);
          }
          // Check for data reset
          if ((EncoderPos==Reset_Data)&&(menuValue[Reset_Data]==Y)) {
            menuValue[Reset_Data]=N;
            AT24C32_Int.Value=0;
            AT24C32.ReadMem(8*4095,AT24C32_Int.data,2); // Reset sample slots used
          }
        } else {
          // Set value for edit menu
          EncoderPos=menuValue[lastPos];
        }
      }
    }
  }
  
  // If encoder turned
  if (menuLevel==Menu) { // 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[lastPos]);
      if (menuNumeric[lastPos]==Y) {
        lcd.print(menuValue[lastPos]);
      } else {       
        if (menuValue[lastPos]==N) {
          lcd.print("N");   
        } else {
          lcd.print("Y");      
        }
      }

    }
  } else if (menuLevel==Set) { // Set/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]==N) {
          lcd.print("N");   
        } else {
          lcd.print("Y");      
        }
      }
      
    }
  }
  // Enable polling
  TIMSK0|=(1<<OCIE0B);
  
  return (menuLevel!=Top); // Return true if menu active
}

void setup() {
  // Initialise rotary encoder and push button
  pinMode(PinA,INPUT_PULLUP); // Rotary PinA
  pinMode(PinB,INPUT_PULLUP); // Rotary PinB
  pinMode(SW,INPUT_PULLUP);   // Rotary SW
  pinMode(Sensor,INPUT);      // LDR sensor (150k pullup)
  
  // Turn on polling ISR
  OCR0B=0xA0;
  TIMSK0|=(1<<OCIE0B);

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

  // Print welcome message to the LCD
  lcd.setCursor(0,0);lcd.print("Clock & Sampler");
  // 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");
  }
  // Check if AT24C32 is present
  if (!AT24C32.isPresent()) {
    lcd.setCursor(0,1);lcd.print("AT24C32 not installed"); 
    while (true);
  }  

  // Set Universal Time based on saved local time
  int localTimeHour=8;   // 8 hours for Perth Western Australia
  int localTimeMin=0;    // 0 minutes for Perth Western Australia
  int samplePeriodMin=1; // 1 minutes between samples
  int LEDTimeOut=-1;     // Do not turn off LED
  int useCompileTime=Y;  // Use compile time to set clock

  // Read saved settings
  AT24C32.ReadMem(8*4094,settings,8);
  if (strncmp(settings,"UTC",3)==0) {
    localTimeHour=settings[3];            
    localTimeMin=settings[4];     
    samplePeriodMin=settings[5];
    LEDTimeOut=settings[6];
    useCompileTime=settings[7];
  }

  // Set time
  DateTime now;
  // Set compile time
  if (useCompileTime==Y) {
    useCompileTime=N;
    // Set compile time
    rtc.adjust(DateTime(F(__DATE__),F(__TIME__)));
    // Adjust for upload time
    now=rtc.now();
    rtc.adjust(DateTime(now+UploadTime));
  }
  
  // Set menu to current time
  now=rtc.now();
  menuLevel=Top;                          // Start in Top (else start in Menu)
  menuValue[Exit_Menu]=Y;                 // Set "Exit Menu" status
  menuValue[Year]=now.year();
  menuValue[Month]=now.month();
  menuValue[Day]=now.day();
  menuValue[Hour]=now.hour();
  menuValue[Minute]=now.minute();
  menuValue[Second]=now.second();
  menuValue[Use_Time]=Y;                  // Set "Use UT" status 
  menuValue[Local_Hour]=localTimeHour;
  menuValue[Local_Min]=localTimeMin;
  menuValue[Sample_Min]=samplePeriodMin;
  menuValue[Download_Data]=Y;
  menuValue[LED_Timeout]=LEDTimeOut;
  menuValue[Compile_Time]=useCompileTime;
  
  delay(1000);
  lcd.clear();
  
}

void loop() {
  DateTime now;
  static int LEDTimer=-1;
  if (LEDTimer==-1) LEDTimer=menuValue[LED_Timeout]; 
  
  if (!processMenu()) { // Menu not active
    // Present the results

    now=rtc.now();
    // Print date on first line of LCD
    lcd.clear();
    lcd.setCursor(0,0);lcd.print("Date ");
      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("Time   ");
      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());

    // Save data to RTC ATC32 (4094 slots) every sample period (minute)
    if ((now.second()==0)&&(now.minute()%menuValue[Sample_Min]==0)) {
      AT24C32_Slot.Year=now.year()-2000;
      AT24C32_Slot.Month=now.month();
      AT24C32_Slot.Day=now.day();
      AT24C32_Slot.Hour=now.hour();
      AT24C32_Slot.Minute=now.minute();
      AT24C32_Slot.Second=0;
      AT24C32_Slot.Value=analogRead(Sensor);
      AT24C32.ReadMem(8*4095,AT24C32_Int.data,2);
      AT24C32.WriteMem(AT24C32_Int.Value*8,AT24C32_Slot.data,8);      
      // Update next address
      AT24C32_Int.Value++;
      if (AT24C32_Int.Value>4093) AT24C32_Int.Value=0;
      AT24C32.WriteMem(8*4095,AT24C32_Int.data,2);
      lcd.clear();
      lcd.setCursor(0,0);
        lcd.print("Saved data ");
        lcd.print(AT24C32_Slot.Value);
      lcd.setCursor(0,1);
        lcd.print("Slots used ");
        lcd.print(AT24C32_Int.Value);
      delay(2000);

    }
    
    delay(1000);
    if (LEDTimer>0) {
      LEDTimer--;
      if (LEDTimer==0) digitalWrite(led,LOW);
    }
  } else {
    digitalWrite(led,HIGH);
    LEDTimer=menuValue[LED_Timeout];
  }
}

 
 I think the code is working properly now (famous last words!).

Pulled the board and rewired PinA (now D2) and PinB (now D3) and repositioned the RTC. I also removed the Power and LED (D13), LEDs from the Nano. D13 powers the LCD LED and the Power LED is pretty useless. It saves some power. I think the LCD uses about a 1 mA with the LCD LED off, so a power off after 15 seconds sounds like a good idea.

Magic

Discussions