Staying in a subroutine

I've done a few projects, but I'm no pro with the Arduino. It seems on nearly every pseudo complicated project my biggest hurdle is the interface. I'm wondering if anyone knows a clean way to stay in a subroutine and not have loop() continuously call it?
I'm interfacing with several sensors and a couple small steppers. I'm using a rotary encoder with switch along with a 16x2 LCD for interfacing.
What I'd like to do is when I first power on, it defaults into 'run mode' where it just does it's thing (read sensors, pulse steppers when required...); but if I toggle a switch, I'd like to jump to 'setup mode' where I can select from a list of options. Depending on the option I'll run a subroutine, such as 'calibrate sensor #1', or 'manually jog stepper #3'. After I scroll through and adjust all of the setups, I can simply toggle the switch back and it'll jump back to 'run mode'

Basically:

void loop() {
if (digitalRead(setuppin) == HIGH){
setupmode();
}
else {
runmode();
}

where setupmode(); has the bulk of the code doing things like reading the encoder, calling subroutines etc... I can't seem to make this work cleanly. loop() will continuously call setupmode().

Is there a way to call setupmode() and stay there until I flip the switch back? Or is there a better way to handle this situation?

Sure. Use a "for" loop to drive the function and code the for loop to watch a boolean to change from true to false. Somewhere in your function, change the boolean to false and the loop will end.
Paul

Better yet, use a "while" loop.

By this snippet, I infer that you have a bistable switch, in addition to the pushbutton on the rotary encoder, is that correct? If you were referring to the pushbutton that comes with the encoder, that's a temporary switch, which stays closed only as long as you keep it pressed.

Either write setupmode() so that it works correctly while repeatedly called (this is possible, but I am not sure if you have the skills as you are asking the question)

OR

Put this inside setupmode();

while(digitalRead(setuppin) == HIGH) {
// Setup mode code
}

However, if you do the second of my suggestions you will find you have actually done most of what you need for the first.

bool setupMode;
void setup {
setupMode = digitalRead(setuppin);
}

void loop() {
if (setupMode == HIGH){
setupmode();
}
else {
runmode();
}
void loop() 
{
   if (digitalRead(setuppin) == HIGH)
   {
        setupmode();
        while (digitalRead(setuppin) == HIGH); //<========= add this
   }
   else 
   {
       runmode();
   }
}

loop() is a subroutine, that is continuously called by main(). Everything of interest can be done in loop(), so there is no obvious reason to shove the problem one level down.

Thanks all for the suggestions. I tried tinkering and still getting stuck.

Using the if or the while (tried both) seems to jump to the menuselect() portion. But it seems to be getting stuck there. The LCD readout in the code below displays "rotate 0" and the encoder nor the button seem to have any effect.

I've tried putting the rotate() and the buttonPressed() in several places, as it seems that neither is being called no matter where I put them. I had both working before when it was just the two, but when trying to integrate into a larger program, they seem to be getting ignored. That tells me it has something to do with where I put them or how I'm calling them.

If anyone has any insight, I'd greatly appreciate it.

/*
 * This is to be the basis for a menu select for a LCD screen and a rotary encoder with button select
 * 
 * Arduino pins
 * 0 - (FREE)
 * 1 - Rotary Encoder CLK
 * 2 - Rotary Encoder DT
 * 3 - Rotary Encoder SW
 * 4 - (FREE)
 * 5 - (FREE)
 * 6 - (FREE)
 * 7 - (FREE)
 * 8 - (FREE)
 * 9 - (FREE)
 * 10- (FREE)
 * 11- 2 Position Selector Switch
 * 12- (FREE)
 * 13- (FREE)
 * A0- (FREE)
 * A1- (FREE)
 * A2- (FREE)
 * A3- (FREE)
 * A4- (FREE)
 * A5- (FREE)
 * SCL - I2C / LCD SCL line
 * SDA - I2C / LCD SDA line
 * 
*/

#include <Wire.h> //library for LCD / I2C
#include <LiquidCrystal_I2C.h> //library for LCD / I2c

LiquidCrystal_I2C lcd(0x27,16,2); //I2C ADDRESS

#define CLK 1 //define pin 1 as CLK of rotary encoder
#define DT 2 //define pin 2 as DT of rotary encoder
#define SW 3 //define pin 3 as the select switch on rotary encoder
#define setupPin 11 //switch to select between setup mode and run mode

int ButtonCounter = 0; //counts the button clicks
int RotateCounter = 0; //counts the rotation clicks
bool rotated = true; //state of the rotation
bool ButtonPressed = false; //state of the button

//variables to track the encoder:
int CLKNow;
int CLKPrevious;
int DTNow;
int DTPrevious;

// Timers for tracking encoder clicks:
float TimeNow1;
float TimeNow2;

void setup() {
  
  lcd.begin(); //initalize LCD display
  lcd.backlight(); //turn on LCD backlight
  lcd.clear(); //clear the LCD for a fresh start
  lcd.setCursor(1,0); // set the cursor to position (2,0)
  lcd.print("menu test V0.1"); // print out start up message to LCD

  pinMode(CLK,INPUT); // setup rotary encoder CLK as an input
  pinMode(DT,INPUT); // setup rotary encoder DT as an input
  pinMode(SW,INPUT_PULLUP); // setup rotary encoder SW as an input
  pinMode(setupPin, INPUT);

  //Setup states for the encoder:
  CLKPrevious = digitalRead(CLK);
  DTPrevious = digitalRead(DT);
  attachInterrupt(digitalPinToInterrupt(CLK), rotate, CHANGE);
  attachInterrupt(digitalPinToInterrupt(SW), buttonPressed, FALLING);

  TimeNow1 = millis(); //Start timer 1

}

void buttonPressed(){
    
  //This timer is an attempt at a software debounce
  TimeNow2 = millis();
  if((TimeNow2 - TimeNow1 > 500) && (digitalRead(SW) == HIGH)){    
    ButtonPressed = true;
    lcd.clear();
    lcd.setCursor(1,0);
    lcd.print("button press");    
  }
  TimeNow1 = millis();  //reset timer for the next press
  }

void rotate(){
  CLKNow = digitalRead(CLK); //Read the state of the CLK pin

  // If last and current state of CLK are different, then a pulse occurred  
    if (CLKNow != CLKPrevious  && CLKNow == 1)
    {
    // If the DT state is different than the CLK state then
    // the encoder is rotating CCW so increase
      if (digitalRead(CLK) != CLKNow){        
      RotateCounter++; 
      if(RotateCounter > 4){
       RotateCounter = 0;
      }
      }
      else{        
      RotateCounter--;
      if(RotateCounter < 0){
        RotateCounter = 4;          
      }   
      }       
    }   

  CLKPrevious = CLKNow;  // Store last CLK state
  rotated = true;
  lcd.setCursor(0,1);
  lcd.print("rotate ");
  lcd.print(RotateCounter);
}


void menuSelect(){
  
  rotate();
  buttonPressed();
   
  if(ButtonPressed == true)
  {
    switch(RotateCounter)
    {
      case 0:      
      lcd.clear();
      lcd.setCursor(0,1); 
      lcd.print("menu 0");
                   
      break;
      
      case 1:  
      lcd.clear();    
      lcd.setCursor(0,1); 
      lcd.print("menu 1");
             
      break;
      
      case 2:  
      lcd.clear();    
      lcd.setCursor(0,1); 
      lcd.print("menu 2");
             
      break;
      
      case 3: 
      lcd.clear();     
      lcd.setCursor(0,1); 
      lcd.print("menu 3");
             
      break;
      
      case 4: 
      lcd.clear();     
      lcd.setCursor(0,1); 
      lcd.print("menu 4");
             
      break;
    }    
  }  
  ButtonPressed = false; //reset this variable
}

void loop() {
  //menu select programming:
  if (digitalRead(setupPin) == HIGH){
  menuSelect();
  while (digitalRead(setupPin) == HIGH);
  }
  else {
    //run mode program
  }
}

'Assuming' you're using an UNO, compare the pins you're using vs. the pins 0-3 on this schematic -

It's not a good idea to use the Tx/Rx pins for other than serial comms.

Can you confirm you're using an UNO?

Hi dougp,
That's an excellent summary. I'll be bookmarking that page, thanks.

Yes, I'm using Uno R3. The LCD display has an I2C board on it that I was wiring to through the SCA/SCL lines.

You reset the button press variable 'TimeNow1' every time ButtonPress() is called, not just when a button press is detected. That is not right.

Usually it would have a descriptive name like "previousPress" or "lastPress". There can only be one time now, this Now1 and Now2 obfuscates the meaning.

Also 500 milliseconds is far too long for a debounce interval.

Well, you only call menuselect() once, then you enter an endless loop. So no surprise there...

Here is how you might stay in a loop during setup:

bool runningSetup = false;
void loop() {
  //menu select programming:
  if (digitalRead(setupPin) == HIGH and runningSetup == false){
  runningSetup = true;

  if (runningSetup == true){
  menuSelect();
  }
  else {
    //run mode program
  }
}

If you want to remain frozen in menuSelect, you can perform that action there, or you have the option of running your program by setting

runningSetup = false;

again

Programmatically, it seems like you have designed things "bottom up" and then had an assortment of parts that don't fit together into what you want. By designing "top down" you can avoid that.

Actually, this solution was already given in reply #2.

Thanks all for contributions.
I started over and I think I have a reasonable template to use for menu selecting with an encoder / LCD.
The only thing that's not quite working right yet is when you switch back over to run mode (pin 11), it doesn't seem to be running the clearScreen(); inside loop(); but that's relatively minor at this point.

Below is the code, incase anyone can benefit from it:

/*
* Arduino pins
 * 0 - (FREE)
 * 1 - Rotary Encoder CLK
 * 2 - Rotary Encoder DT
 * 3 - Rotary Encoder Switch
 * 4 - (FREE)
 * 5 - (FREE)
 * 6 - (FREE)
 * 7 - (FREE)
 * 8 - (FREE)
 * 9 - (FREE)
 * 10- (FREE)
 * 11- Two Position Switch to select Run Mode or Setup
 * 12- (FREE)
 * 13- (FREE)
 * A0- (FREE)
 * A1- (FREE)
 * A2- (FREE)
 * A3- (FREE)
 * A4- (FREE)
 * A5- (FREE)
 * SCL - I2C / LCD SCL line
 * SDA - I2C / LCD SDA line
 * 
*/

#include <Wire.h> //library for LCD / I2C
#include <LiquidCrystal_I2C.h> //library for LCD / I2c

LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27,16,2); //I2C ADDRESS

#define CLK 1 //define pin 1 as CLK of rotary encoder
#define DT 2 //define pin 2 as DT of rotary encoder
#define SW 3 //define pin 3 as the select on rotary encoder
#define setupPin 11 //switch to select between setup and run mode

int counter = 0; //variable for counting the rotary encoder movement
int menu = 0; //vairable for redistributing the counter clicks (2 per detent) into menu items
int currentStateCLK; // records CLK current position on rotary encoder
int lastStateCLK; // records CLK last position on rotary encoder
unsigned long lastButtonPress = 0; //records button press on rotary encoder
bool clearscrn = false; // switch to clear screen only once in loop()
int numScrn = 4; // number of setup screens to use

void setup() {
  lcd.begin(); //initalize LCD display
  lcd.backlight(); //turn on LCD backlight
  lcd.clear(); //clear the LCD for a fresh start
  lcd.setCursor(1,0); // set the cursor to position (2,0)
  lcd.print("menu two V0.1"); // print out start up message to LCD
  delay(3000);
  lcd.clear();

  pinMode(CLK,INPUT); // setup rotary encoder CLK as an input
  pinMode(DT,INPUT); // setup rotary encoder DT as an input
  pinMode(SW,INPUT_PULLUP); // setup rotary encoder SW as an input
  pinMode(setupPin, INPUT_PULLUP); // setup selector switch 

  lastStateCLK = digitalRead(CLK); // read inital state of CLK, assign to previousStateCLK

}

void menuSelect(){
  currentStateCLK = digitalRead(CLK); // read current state of CLK on rotary encoder

  // the following compares last and current state of CLK to see if pulse occured
  if (currentStateCLK != lastStateCLK){

    // if the DT state is different than CLK state then CCW rotation, so decrement
    if (digitalRead(DT) != currentStateCLK) {
      counter ++;
      menu = counter/2;
      if(menu > numScrn){ //this rolls back over to zero if counter gets over number of screens
        counter = 0;
      }
    } else {
      // encoder is rotating CW so incriment
      counter --;
      menu = counter/2;
      if (menu <0){ //this rolls back over to the max number of screens if counter gets below zero
        counter = numScrn;
      }
    }

    // the following is to display the encoder state on the LCD screen
    lcd.clear(); // clear LCD screen
    lcd.setCursor(0,0); // set cursor to beginning
    lcd.print("Select Operation"); // print out rotation on line 1
    lcd.setCursor(1,1); // set cursor to second row
    switch(menu){
      case 0:
      lcd.print("operation 1");
      break;
      case 1:
      lcd.print("operation 2");
      break;
      case 2:
      lcd.print("operation 3");
      break;
      case 3:
      lcd.print("operation 4");
      break;
      case 4:
      lcd.print("operation 5");
      break;
    }

  }

  lastStateCLK = currentStateCLK; //remember last CLK state

  int btnState = digitalRead(SW); //read button state

  //if btnState LOW, then button is pressed
  if (btnState == LOW) {
    if (millis() - lastButtonPress > 50) {
      switch(menu){
        case 0:
        lcd.clear();
        lcd.setCursor(0,1);
        lcd.print("running op 1...");
        //run menu 1 program here
        break;
        
        case 1:
        lcd.clear();
        lcd.setCursor(0,1);
        lcd.print("running op 2...");
        //run menu 2 program here
        break;
        
        case 2:
        lcd.clear();
        lcd.setCursor(0,1);
        lcd.print("running op 3...");
        //run menu 3 program here
        break;

        case 3:
        lcd.clear();
        lcd.setCursor(0,1);
        lcd.print("running op 4...");
        //run menu 4 program here
        break;

        case 4:
        lcd.clear();
        lcd.setCursor(0,1);
        lcd.print("running op 5...");
        //run menu 5 program here
        break;
      }
    }

    lastButtonPress = millis(); // remember last button press
  }
  clearscrn = false;
}

void clearScreen(){
  lcd.clear();
  clearscrn = true;
}

void loop() {
  if (digitalRead(setupPin) == HIGH){
    menuSelect();
  }
  if (digitalRead(setupPin) == LOW){
    if (clearscrn = false){
      clearScreen();
    }
    //run mode
    lcd.setCursor(2,0);
    lcd.print("run mode"); // trouble shooting remove this and insert the run mode program call
  }
  }

I never miss an opportunity to look at debouncing code or strategy. Here we see something a bit different. The code in #14 stripped down to show just the debounce logic

#define SW 3 //define pin 3 as the select on rotary encoder

# define LED  4   // just an LED for showing

unsigned long lastButtonPress = 0; //records button press on rotary encoder

void setup() {

  pinMode(SW,INPUT_PULLUP); // setup rotary encoder SW as an input
  pinMode(LED, OUTPUT); // setup selector switch 
}

void menuSelect(){

  int btnState = digitalRead(SW); //read button state

  //if btnState LOW, then button is pressed
  if (btnState == LOW) {
    if (millis() - lastButtonPress > 500) {

      digitalWrite(LED, !digitalRead(LED)); // toggle the LED

    }
    lastButtonPress = millis(); // remember last button press
  }
}

void loop() {
    menuSelect();
}

is a demonstration of getting it right, albeit uncommon. The button is reacted to immediately, debouncing is handled by not reacting again until (at least) the debounce period has elapsed.

In the wokwi and above, I set the debounce period to an absurd 500 ms, just to show the effect. Most debouncing code waits that period before reacting. Replace 500 with 50 and you have an instant acting debounced button.

Try it.

a7