LCD fail after attachInterrupt

Hello all. I cam across some odd behaviour on a Leonardo. Basically, after I call...

  attachInterrupt(digitalPinToInterrupt(clk), isr, LOW);

Any interaction with the LCD causes the board to lock up. All of the interaction with the LCD prior to attaching the interrupt work fine. The goal here is to turn an encoder and increment a counter, then use the counter value to move a cursor up or down on the LCD. This is a menu system. The encoder code works fine, increments the counter and prints results to the serial monitor. As soon as I uncomment this code...

      //lcd.setCursor(0, currentPosition);
      //lcd.write(0);

The counter freezes on 1, the program locks up and nothing happens on the LCD. If I comment these 2 lines and upload again, the program runs, encoder works and counter updates.

The full code is below. Any ideas on what could be going on? The LCD library I'm using is LiquidCrystal_I2C from Arduino.

Thanks

#include <LiquidCrystal_I2C.h>
#include <Wire.h>
//#include <SD.h>

const int clk = 3;  // Used for generating interrupts using CLK signal 
const int dt = 2;  // Used for reading DT signal 
const int btn = 4;  // Used for the push button switch
const int linePos = 1; // Start position for menu line items
const int cursorPos = 0; // Start position for menu cursor
int lastPosition = 0;  // Keep track of last rotary value
volatile int currentPosition = 0;  // Updated by the ISR 
String menu[8] = { "Hurricane","Mojito","Bahama Mama","Painkiller","Orange Crush","Adios MF","Liquid Marijana","Alabama Slammer" };

// Set the LCD address to 0x27 
LiquidCrystal_I2C lcd(0x27, 20, 4);

// Custom menu cursor character 
byte menuCursor[] = {
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F
};

// ------------------------------------------------------------------
// INTERRUPT --------------------------------------------------------
// ------------------------------------------------------------------
void isr ()  {
  static unsigned long lastInterruptTime = 0;
  unsigned long interruptTime = millis();

  // If interrupts come faster than 5ms, assume it's a bounce and ignore
  if (interruptTime - lastInterruptTime > 5) {
    if (digitalRead(dt) == LOW)
    {
      currentPosition-- ; // Could be -5 or -10
    }
    else {
      currentPosition++ ; // Could be +5 or +10
    }

    // Restrict value from 0 to +100
    currentPosition = min(20, max(0, currentPosition));
  }
  // Keep track of when we were here last (no more than every 5ms)
  lastInterruptTime = interruptTime;
}

// ------------------------------------------------------------------
// SETUP ------------------------------------------------------------
// ------------------------------------------------------------------
void setup() {
  int i; // Loop variable

  // Just whilst we debug, view output on serial monitor
  Serial.begin(9600);

  //Init the LCD and create the cursor character
  lcd.begin();
  lcd.createChar(0, menuCursor);
  lcd.home();
  //lcd.write(0);
 
  // Write first 4 menu items to display
  //  lcd.print("test2");
  for (i = 0; i < 4; i++) {
    lcd.setCursor(linePos, i);
    lcd.print(menu[i]);
  }
  // Write cursor to disay
  lcd.setCursor(cursorPos, 0);
  lcd.write(0);

  // Switch is floating so use the in-built PULLUP so we don't need a resistor
  pinMode(btn, INPUT_PULLUP);
  pinMode(clk, INPUT_PULLUP);
  pinMode(dt, INPUT_PULLUP);

  // Attach the routine to service the interrupts
  attachInterrupt(digitalPinToInterrupt(clk), isr, LOW);
}

// ------------------------------------------------------------------
// MAIN LOOP --------------------------------------------------------
// ------------------------------------------------------------------
void loop() {

  // Is someone pressing the rotary switch?
  if ((!digitalRead(btn))) {
    currentPosition = 0;
    while (!digitalRead(btn))
      delay(10);
    Serial.println("Reset");
  }

  // If the current rotary switch position has changed then update everything
  if (currentPosition != lastPosition) {

    // Write out to serial monitor the value and direction
    if (currentPosition > lastPosition) {
      Serial.print("UP: ");
      Serial.println(currentPosition);
      //lcd.setCursor(cursorPos, currentPosition - 1);  
      //lcd.print(" ");   
      //lcd.setCursor(0, 1);
      //lcd.write(0);
    } else {
      Serial.print("DOWN: ");
      Serial.println(currentPosition);
    }
    // Keep track of this new value
    lastPosition = currentPosition ;
  }

  //delay(100);
}

Your sketch mostly worked for me. I did alter the cursor code a bit to get it to move past line 1. But even without that change, attachInterrupt didn't cause a lock up (verified by blinking the onboard LED in the loop).

  // If the current rotary switch position has changed then update everything
  if (currentPosition != lastPosition) {
    lcd.setCursor(0, lastPosition);
    lcd.write(' ');

    // Write out to serial monitor the value and direction
    if (currentPosition > lastPosition) {
      Serial.print("UP: ");
    } else {
      Serial.print("DOWN: ");
    }
    Serial.println(currentPosition);
    lcd.setCursor(0, currentPosition);
    lcd.write(0);
    // Keep track of this new value
    lastPosition = currentPosition ;
  }

It might depend on which LiquidCrystal_I2C library you used. I pulled this one.

You should rethink your program. You do not want to do anything in an interrupt routine (ISR) that can be done elsewhere. The best thing is to set a flag and then process it in the main loop. Delay() is what is causing you to lock up. I believe the LiquidCrystal_I2C uses a delay.

Realize the delay() function uses the milliseconds interrupt, part of the system design. While inside any interrupt handler, all other interrupts are suspended so the millis() counter doesn't count and delay() never ends. delayMicroseconds().

Good Luck!

attachInterrupt(digitalPinToInterrupt(clk), isr, LOW);

LOW is not a good choice for a mode from an encoder. It is most used for waking from sleep.
What happens if the encoder stops in a position where clk is at ground.
The code will keep calling the isr and the i2c lcd will not work properly.

Have you tried an algorithm to read the encoder which triggers clk FALLING or RISING or CHANGE,

2 Likes

I wondered about that. I didn't have an encoder wired up at hand so I was just grounding pin 3 manually.

Please check the schematic diagram and/or pinout diagram of the Leonardo.

Pins 2 and 3 are the I2C bus. You will have to find other pins for the clk and the dt.

2 Likes

I totally missed that it was a Leonardo. Good catch!

if you are using a QEI encode try the td_libs_Encoder library

on a Leonardo connected to pins 2 and 3 as encoder is rotated gives

1
2
3
4
5
6
7
8
7
8
7
6
5
4
3
2
1
0
-1
-2
-3
-4
-3
-4
-5
-6
-7
-8

Thanks guys. I did not realize that pins 2 and 3 were on the I2C bus. Looks like the Leonardo can implement an interrupt on 0, 1, 2, 3, 4 and 7. That alone could be the issue. Maybe just using pins other than 2 and 3 will do it.

I'll also try taking the millis delay out of the ISR. That was an effort to debounce. The final destination for this project will be a Mega so maybe I should be developing on that as well. My reasoning for using a Leonardo for development is that I have a bunch of them and if I blow it up somehow, I can just grab another one.

I see the logic behind not using LOW as a trigger state. Would the interrupt just keep firing endlessly if the encoder landed on LOW? Seems plausible. That should be changed to FALLING.

Thanks again for the great suggestions guys. I was about to give up on the encoder and just use buttons.

Looks like the Leonardo can implement an interrupt on 0, 1, 2, 3, 4 and 7.

Not pin 4.

From attachInterrupt() documentation.

Micro, Leonardo, other 32u4-based 0, 1, 2, 3, 7

We have some success here. I changed the clk and dt pins to 7 and 8. The isr attaches to clk (pin 7). The dt pin is not tied to an interrupt so I moved it to pin 8. Then I changed the interrupt to trigger on FALLING. The result was really severe debounce that didn't exist before, but the program didn't lock up.

Next I changed the trigger back to LOW and got some stable encoder readings again, so I uncommented the LCD code and it works great. I left the millis delay in the isr for now because it appears to be working. If it were more critical than a menu system I might be motivated to build a hardware debounce circuit.

Also need to add code to auto scroll the menu up and down and read the menu list from an SD card. I've done projects with the SD card module so that should be straight forward. The code that works is below. I think the whole issue was a conflict on pins 2 and 3 between the isr and the LCD on the I2C bus.

#include <LiquidCrystal_I2C.h>
#include <Wire.h>
//#include <SD.h>

const int clk = 7;  // Used for generating interrupts using CLK signal 
const int dt = 8;  // Used for reading DT signal 
const int btn = 9;  // Used for the push button switch
const int linePos = 1; // Start position for menu line items
const int cursorPos = 0; // Start position for menu cursor
int lastPosition = 0;  // Keep track of last rotary value
volatile int currentPosition = 0;  // Updated by the ISR 
int menuStartIndex = 0;
int menuEndIndex = 3;
String menu[8] = { "Hurricane","Mojito","Bahama Mama","Painkiller","Orange Crush","Adios MF","Liquid Marijana","Alabama Slammer" };

// Set the LCD address to 0x27 
LiquidCrystal_I2C lcd(0x27, 20, 4);

// Custom menu cursor character 
byte menuCursor[] = {
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F,
  0x1F
};

// ------------------------------------------------------------------
// INTERRUPT --------------------------------------------------------
// ------------------------------------------------------------------
void isr ()  {
  static unsigned long lastInterruptTime = 0;
  unsigned long interruptTime = millis();

  // If interrupts come faster than 5ms, assume it's a bounce and ignore
  if (interruptTime - lastInterruptTime > 5) {
    if (digitalRead(dt) == LOW)
    {
      currentPosition-- ; // Could be -5 or -10
    }
    else {
      currentPosition++ ; // Could be +5 or +10
    }

    // Restrict value from 0 to +100
    currentPosition = min(20, max(0, currentPosition));
  }
  // Keep track of when we were here last (no more than every 5ms)
  lastInterruptTime = interruptTime;
}

// ------------------------------------------------------------------
// SETUP ------------------------------------------------------------
// ------------------------------------------------------------------
void setup() {
  int i; // Loop variable

  // Just whilst we debug, view output on serial monitor
  Serial.begin(9600);

  //Init the LCD and create the cursor character
  lcd.begin();
  lcd.createChar(0, menuCursor);
  lcd.home();
  //lcd.write(0);
 
  // Write first 4 menu items to display
  //  lcd.print("test2");
  for (i = menuStartIndex; i <= menuEndIndex; i++) {
    lcd.setCursor(linePos, i);
    lcd.print(menu[i]);
  }
  // Write cursor to disay
  lcd.setCursor(cursorPos, 0);
  lcd.write(0);

  // Switch is floating so use the in-built PULLUP so we don't need a resistor
  pinMode(btn, INPUT_PULLUP);
  pinMode(clk, INPUT_PULLUP);
  pinMode(dt, INPUT_PULLUP);

  // Attach the routine to service the interrupts
  attachInterrupt(digitalPinToInterrupt(clk), isr, LOW);
}

// ------------------------------------------------------------------
// MAIN LOOP --------------------------------------------------------
// ------------------------------------------------------------------
void loop() {

  // Is someone pressing the rotary switch?
  if ((!digitalRead(btn))) {
    while (!digitalRead(btn))
      delay(10);
    Serial.print("Current Position: ");
    Serial.println(currentPosition);
  }

  // If the current rotary switch position has changed then update everything
  if (currentPosition != lastPosition) {

    // Write out to serial monitor the value and direction
    if (currentPosition > lastPosition) {
      //Serial.print("UP: ");
      //Serial.println(currentPosition);
      lcd.setCursor(cursorPos, currentPosition - 1);  
      lcd.print(" ");   
      lcd.setCursor(0, currentPosition);
      lcd.write(0);
    } else {
      //Serial.print("DOWN: ");
      //Serial.println(currentPosition);
      lcd.setCursor(cursorPos, currentPosition + 1);  
      lcd.print(" ");   
      lcd.setCursor(0, currentPosition);
      lcd.write(0);
    }
    // Keep track of this new value
    lastPosition = currentPosition ;
  }
}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.