Close

Sub-Systems

A project log for Daylight Geolocator Remix

This is a remix of https://hackaday.io/project/28550-light-level-geolocator

agpcooperagp.cooper 02/06/2018 at 02:370 Comments

Sub-Systems! What is that?

This project is the first project for me that has lots of necessary supporting sub-systems. They include:

So it is a big deal to coordinate all these systems.

LCD display

The LCD library is very easy to use, the biggest difficulty is selecting the pins to use. Some pins have special functions such as interrupt, PWM, SPI etc. Some other types of display (that use SPI) have fixed pin requirements. The final selection for the pins was designed to be useful for both an LCD display with the typical 4 bit parallel interface, and an SPI type display such as the WaveShare E-Ink display.

For the LCD display:

For the SPI display:

As the display is using the D13, I removed both the power and the built in LEDs from the Nano board.

Rotary encoder and push button input

There are different types of rotary encoders and they are not compatible! One type has one transition per detent (the stop position) and the other has two transitions per detent. The first type is the best to use. I am unfortunately using the other type.

Understanding the Rotary Encoder

  1. When you rotate the encoder clockwise then between detents, Pin A will change state before Pin B.
  2. When you rotate the encoder anti-clockwise then Pin B will change state before Pin A.
  3. When both pins have changed state then you have moved from one detent to the next detent (assuming the rotary encoder is of the first type).
  4. Now all you need to do is flag when both pins have changed state and use the the direction (flag) of the last pin to change state.

Got that? Okay here is the code:

  // Update Encoder Position
  lastPinA=testPinA;             // Save PinA
  lastPinB=testPinB;             // Save PinB
  testPinA=(PIND>>PinA)&1;       // Get PinA
  testPinB=(PIND>>PinB)&1;       // Get PinB
  
    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
    }

(Don't you love the random colours the editor gives to your code!)

Polling or an Interrupt Service Routine (ISR)

There are many option to "connect" the rotary encoder to your code (as above). In my case I used one of the two "unused" interrupt vectors associated with the Timer0 for polling. Basically rather than use an interrupt monitoring a change on the pins, I "poll" the pins using the millis() timer. Timer0 is used for millis(), delay(), micros(), delayMicroseconds(), and PWM output on pins 5 and 6. So providing you don't need to use PWM of pin 5 or Pin 6, you can use the interrupt vectors ISR(TIMER0_COMPB_vect) or ISR(TIMER0_COMPA_vect).

The main reason for polling was that I would have needed three level change interrupts. One for Pin A, one for Pin B and one for the (push button) Switch, and I would still need a timer for debounce of the switch. In the case of polling, these can all be combined into one ISR. Here is the rest of the code:

/* 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) {
    statusSW=testSW;
    cntSW=20;
  }
  if (cntSW>0) {
    cntSW--;
    if (cntSW==0) {
      Switch=statusSW;
      UpdateSwitch=true;
    }
  }
}

Dealing with two transitions per detent

The code below deals with two transitions per detent by only accepting the one transition per pin. Just comment out these two line if you have a rotary encoder of the first type:

  // 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

The push button switch

The push button code looks for a change in state and starts a countdown timer. Upon timeout it sets the switch flags:

  // Update switch with 20 ms debounce
  testSW=(PIND>>SW)&1;
  if (testSW!=statusSW) {
    statusSW=testSW;
    cntSW=20;
  }
  if (cntSW>0) {
    cntSW--;
    if (cntSW==0) {
      Switch=statusSW;
      UpdateSwitch=true;
    }
  }

What is inside the box?

I replaced the LDR load resistor so while the box was open, here is a snap shot:

The Menu System

The menu system has been designed to be very simple to update or expand. I have put together a demostration using the rotary encoder to control the blink rate of the builtin LED:

/* 
  Rotary Encoder Blink
  ====================
  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.
*/

/*
  ROTARY ENCODER AND PUSH BUTTON POLLING CODE
    Uses Timer0 without upsetting millis(), delay() etc.
    You lose PWM on Arduino/Nano pin 5 (D5).
    Don't turn the encoder too fast as it will not work!
*/
#define PinA 5
#define PinB 4
#define SW   3
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
  
  /* If your encoder jumps in steps of two, uncomment this code */
  // 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 10 ms debounce
  testSW=(PIND>>SW)&1;
  if (testSW!=statusSW) {
    encoderFlag=true;            // Reset encoder flag (precaution)
    statusSW=testSW;
    cntSW=10;
  }
  if (cntSW>0) {
    cntSW--;
    if (cntSW==0) {
      Switch=statusSW;
      UpdateSwitch=true;
    }
  }
}


/* MENU SET UP */
enum NoYes {N,Y,A};
enum MenuLevel {Top,Menu,Set};
enum MenuItem       { Exit_Menu  , Delay_MS  };
char* menuName[]   ={"Exit Menu ","Delay ms "};
char menuNumeric[] ={         N  ,        Y  };
int menuValue[]    ={         Y  ,      500  };
int menuValueMin[] ={         N  ,       10  };
int menuValueMax[] ={         Y  ,    32750  };
int menuValueStep[]={         Y  ,       10  };
int menuSize=sizeof(menuName)/sizeof(char*);
int menuLevel=Menu;

// Our variable to set the delay period
unsigned long intervalMillis=1000;

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;
    if (menuLevel==Menu) {
      Serial.print("Menu: ");
      Serial.print(menuName[EncoderPos]);
      if (menuNumeric[EncoderPos]==Y) {
        Serial.println(menuValue[EncoderPos]);
      } else {       
        if (menuValue[EncoderPos]==N) {
          Serial.println("N");      
        } else {
          Serial.println("Y");      
        }
      }          
    } else if (menuLevel==Set) {
      Serial.print("Set:  ");
      Serial.print(menuName[lastPos]);
      if (menuNumeric[lastPos]==Y) {
        Serial.println(menuValue[lastPos]);
      } else {       
        if (menuValue[lastPos]==N) {
          Serial.println("N");      
        } else {
          Serial.println("Y");      
        }
      }           
    }
  }
  
  // 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;
            // Set the delay
            intervalMillis=menuValue[Delay_MS];
            Serial.println("Menu Exited!");
          }
          
        } 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;          
      Serial.print("Menu: ");
      Serial.print(menuName[lastPos]);
      if (menuNumeric[lastPos]==Y) {
        Serial.println(menuValue[lastPos]);
      } else {       
        if (menuValue[lastPos]==N) {
          Serial.println("N");   
        } else {
          Serial.println("Y");      
        }
      }

    }
  } else if (menuLevel==Set) { // Set/edit menu item value
    if (menuValue[lastPos]!=EncoderPos) {
      if (EncoderPos>menuValue[lastPos]) {
        EncoderPos=EncoderPos+menuValueStep[lastPos]-1;
      } else {
        EncoderPos=EncoderPos-menuValueStep[lastPos]+1;        
      }
      if (EncoderPos>menuValueMax[lastPos]) EncoderPos=menuValueMin[lastPos];
      if (EncoderPos<menuValueMin[lastPos]) EncoderPos=menuValueMax[lastPos];
      menuValue[lastPos]=EncoderPos;
      Serial.print("Set:  ");
      Serial.print(menuName[lastPos]);
      if (menuNumeric[lastPos]==Y) {
        Serial.println(menuValue[lastPos]);
      } else {       
        if (menuValue[lastPos]==N) {
          Serial.println("N");   
        } else {
          Serial.println("Y");      
        }
      }
      
    }
  }
  // Enable polling
  TIMSK0|=(1<<OCIE0B);
  
  return (menuLevel!=Top); // Return true if menu active
}

void setup() {
  // Setup for Keyes rotary encoder and push button
  pinMode(5,INPUT_PULLUP); // Rotary PinA or Clk
  pinMode(4,INPUT_PULLUP); // Rotary PinB or DT
  pinMode(3,INPUT_PULLUP); // Rotary SW
  pinMode(2,OUTPUT);       // Power for onboard pullup resistors
  digitalWrite(2,HIGH);    // Turn on power

  // Set up Blink
  pinMode(LED_BUILTIN,OUTPUT);
  
  // Turn on polling ISR
  OCR0B=0xA0;
  TIMSK0|=(1<<OCIE0B);

  // Initialise Serial
  Serial.begin(9600); // Stardard serial speed
  while (!Serial);    // Wait for the Serial system to come up
  
  // Print welcome messeage
  Serial.println("Rotary Blink");
  Serial.println("Hints:");
  Serial.println("  1 Turn the encoder to navigate the menu");
  Serial.println("  2 Push the button to change the setting");
  Serial.println("  3 Turn the encoder to change the setting");
  Serial.println("  4 Don't turn the the encoder too fast!");
  Serial.println("  5 Push the button to save the setting (i.e. Y)");
  Serial.println();
  Serial.println("  6 Select 'Exit Menu'");
  Serial.println("  7 Push the button to change the setting");
  Serial.println("  8 Push the button to save the setting");
  Serial.println("  9 You should have exited the menu and Blink is now running");
  Serial.println();
  Serial.println("  10 Push the button to re-enter the menu after 'Exit Menu'");
  Serial.println();
}

void loop() {
  static unsigned long previousMillis=0;
  unsigned long currentMillis;

  if (!processMenu()) {
    /* Run this when not in menu */
    
    // Blink without delay (intervalMillis is set by the rotary encoder)
    currentMillis=millis();
    if (currentMillis-previousMillis>=intervalMillis) {
      previousMillis=currentMillis;
      digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN));
    }
  }
}

Okay, its a bit long but it has:

A nice feature is that the delay increments/decrements in steps of 10 (programmanble).

Magic

Discussions