Using I2C within an interrupt. And Works!! How?

Hi there.
I have a pretty large program (kinda) working. It uses an Arduino Mega, I2C RTC clock, Ethernet Shield (w5100), relay module, etc...
I recently added it a LCD+Keypad panel (LINK). It connects thru I2c too (different address from the RTC so no issues here)

Since I added the LCD panel, I am having some random restarts. It usually takes 1 or 2 days to restart, but it happens...

I tried to investigate what could be wrong, read lots about interrupts and found that...
I SHOULDN'T BE USING I2C AT ALL WITHIN AN INTERRUPT SERVICE ROUTINE...

And I do use it a lot in my program. Let's see. As the program has several thousand lines, I will try to summarize and use pseudocode for the the key blocks:

void setup () {
  bv4242.init();
  Set up periodic Timer Interrupt (4 times a second, INT0);
  }

ISR (INT0)  {
  If (key pressed) {//This is I2c driven
    char x=key.read(); //I2c
    lcd.print ("Key"+ c + " pressed"); //I2C everywhere...
    store keypress locally;
    }
  if (time since we wrote something in LCD > 4seconds)  
    lcd.clear() //I2c
}

void loop ()  {
  if (more than 30s since last connection)
    Send keypresses over http connection //This may take several seconds
  if (more than 2mins before last time)
    Connect to other server to update rules //This may take several seconds too
}

Ok, here you see I use a lot of i2c communication (actually much more than in this summarization) from within the ISR routine, which, technically, shouldn't work. I2C calls rely on interrupts and once I'm already inside an ISR, interrupts are disabled.

So my first question is, how on earth is this (normally) working? It perfectly takes keystrokes and shows them on screen, connects periodically to the server to update it and everything is working fine for hours.

Second question would be, as you can see I need to connect to a server pretty often and 1) if the server is not responding it takes several seconds to timeout. 2)Even when it is available, my connection takes a while. During this time I can't miss any key stroke on the keypad and it has to be shown right away. I mean, the lcd has to be 100% responsive to key presses 100% of the time. Can you figure out a good strategy to take care of these 2 things in a right manner?

Any help would be much much appreciated!

So my first question is, how on earth is this (normally) working?

The ONLY way to answer that is to see ALL of your REAL code.

Can you figure out a good strategy to take care of these 2 things in a right manner?

The ONLY useful strategy is for you to develop realistic expectations.

PaulS:
The ONLY way to answer that is to see ALL of your REAL code.

or a subset of your code that demonstrates the behavior that you observe.

I SHOULDN'T BE USING I2C AT ALL WITHIN AN INTERRUPT SERVICE ROUTINE...

Nothing to stop you using I2C as such within an ISR! Just don't use the I2C interrupts.

which, technically, shouldn't work. I2C calls rely on interrupts

Which I2C lib are you using?

Mark

OP:
So my first question is, how on earth is this (normally) working? It perfectly takes keystrokes and shows them on screen, connects periodically to the server to update it and everything is working fine for hours.

Crazy things happen during interrupts and communication routines(i2c,serial,LCD etc.). avoiding doing interrupts with comm requests within will save you headaches like you are mentioning.

Here is some of the code I use with explanations:
The Strategy I use is to create a char array that holds the the 32 characters to be displayed on my LCD Array

char S[33]; // To be displayed

I populate the char array instead with memcpy() to be used later outside the interrupt.
I've creates several functions that mimic how the LCD positions the text I also handle clearing the outdated left over data Basic

void LCDArray(char * StrX, byte c , byte r) {
	byte Shift;
	byte SLength;
	byte CpyLenght;
	Shift = c + (r * LCDColums);
	SLength = strlen(StrX);
	CpyLenght = min(Shift + SLength, (LCDColums * LCDScrollRows) + (LCDColums * (LCDNumRows - 1))) - Shift;
	memcpy ( SMX + Shift, StrX, CpyLenght);
}

void LCDPrintFloat(float val, int len, int prec, byte c , byte r){ // Negitive len (Length) Aligns to the left
	len = min(LCDColums - c,len);
	LCDclearTempStr();
	dtostrf(val,len,prec,S);
	LCDArray(S, c , r);
}

void LCDPrintInt(long val, int len, byte c , byte r){// Negitive len (Length) Aligns to the left
	LCDPrintFloat((float) val,  len, 0, c , r);
}
void LCDPrintDate(int len, byte c , byte r)
{
	len = min(LCDColums - c,len);
	LCDclearTempStr();
	char sz[32];
	for (int i=0; i<LCDColums; ++i)memcpy ( S + i, " ", 1);
	S[LCDColums] = '\0';
	sprintf(sz, "%02d/%02d/%02d ", month(), day(), year());
	memcpy ( S, sz, len);
	S[len] = '\0';
	LCDArray(S, c , r);
}
void LCDPrintTime(int len,byte c , byte r){
	len = min(LCDColums - c,len);
	LCDclearTempStr();
	char sz[32];
	for (int i=0; i<LCDColums; ++i)memcpy ( S + i, " ", 1);
	S[LCDColums] = '\0';
	if (len >7)	sprintf(sz, "%02d:%02d:%02d ", hour(), minute(), second());
	else sprintf(sz, "%02d:%02d ", hour(), minute());
	memcpy ( S, sz, len);
	S[len] = '\0';
	LCDArray(S, c , r);
}
void LCDprintStr(const char *str, int len,byte c , byte r){// extends a string to len length
	len = min(LCDColums - c,len);
	LCDclearTempStr();
	len = min(strlen(str),len);
	memcpy ( S, str, len);
	S[len] = '\0';
	LCDArray(S, c , r);
}

now in the main loop using a blink without delay type timing control routine I populate the LCD display with new data after 200 ms and only if it has changed.
I populate the entire LCD;s 32 characters

void LCDDisplay(){
	//static int MSCtr;
	static char CopyS[33];
	 static unsigned long _ATimer;
         if ((millis() - _ATimer) >= (200)) {
                _ATimer = millis();
		if ( strcmp ( CopyS, S ) == 0) return;// no need to resend to LCD both strings match
		lcd.clear();
		for(int col = 0;col<16;col++)lcd.write((uint8_t)S[col]);
		//lcd.print(S);
		lcd.setCursor(0, 1);
		for(int col = 0;col<16;col++)lcd.write((uint8_t)S[col+ 16]);
		//lcd.print(S + 16);
		strcpy ( CopyS, S );
	}
}

Second question would be, as you can see I need to connect to a server pretty often and 1) if the server is not responding it takes several seconds to timeout. 2)Even when it is available, my connection takes a while. During this time I can't miss any key stroke on the keypad and it has to be shown right away. I mean, the lcd has to be 100% responsive to key presses 100% of the time. Can you figure out a good strategy to take care of these 2 things in a right manner?

you are trapped in a blocking while loop which is waiting for 2 conditions 1 for proper returning of data, 2 elapsed time. I have had to re-write all my server code to not be blocking. This may be difficult due to the complexity of some server communication code. Adding additional conditions to the offending while loop to abandon the server comm attempt on key press or decreasing the wait time may be the easiest options.
Z

Crazy things happen during interrupts and communication routines(i2c,serial,LCD etc.). avoiding doing interrupts with comm requests within will save you headaches like you are mentioning.

No they are not crazy they are in the datasheet - use paper and pen to help you understand what is really happening.

Mark

holmes4:
No they are not crazy they are in the datasheet - use paper and pen to help you understand what is really happening.

Mark

:slight_smile: That was intended for OP. You are absolutely correct. but most people here on this forum haven't read the datasheet or understand what else happens outside of their sketch. so unless you go through the entire code sequence and understand what is happening, wouldn't you recommend NOT using any communication type requests inside of an interrupt.
Z

Ok, some advance here.
First of all thanks everyone for their replies. (TL;DR, btw...)

I realized my code wasn't actually getting frozen somewhere. It was actually (sw) resetting at a certain point due to something wrongly programmed, and then the software reset (basically running "setup ()" again) wansn't working fine after a second boot and got stuck there.

Don't ask me why but I started suspecting about concurrent uses of lcd+keypad commands. Like, being in the middle of main code writting something in the LCD (RIGHT in the middle of the i2c command to send it to the lcd) and triggering the timer interrupt right there, which first thing it does is polling for key strokes (uses another i2c command to the lcd+keypad). So this was probably rebooting my device.

What I did to allow it to completly reboot (better starting over to work than getting stuck) was to disable the TIMER interrupt mask right away first thing in the setup (). During a normal (hw) reboot, the timer ints were disabled until they were actively enabled, but after a sofware interrupt, they were still enabled!!! This is what screw my (re)booting process, as it was calling lots of i2c commands all together at setup (lcd clear, buffer clear, mode 2, startup msg...)

Ok at this point it started to reboot ok after a hung up.

Then I had to make sure an i2c command during normal "main" loop was never interrupted in the middle by a timer interrupt (which uses i2c cmds as well). For this I made noInt() doInt() functions, which enabled and disabled only the timer interrupt mask, and wrapped all my lcd commands in the main loop by them, so the interrupts were disabled during lcd+key calls.

This way there's no way a timer interrup happens when I am using the lcd+key somewhere else. For these tests I forced the timer to trigger 8 times a second instead of 2, and after 5hs it hasn't still had any reboot, seems like I'm good now.

Answers to comments: I had read that I shouldn't use i2c commands during an ISR, but I don't really know why. I opened my bv4242.c file (lcd+keypad library) and it uses i2c commands everywhere, checked the wire.c library and it does deal with interrupts. I use the built in wire.h library. I don't really know if there's a way to use them WITH o WITHOUT interrupts, tbh...

Anyway, in my case, my code does work fine with i2c calls within the ISR...That's for sure

Again thanks everyone for checking this out (and thanks for reading this loooong comment)

edugimeno:
Answers to comments: I had read that I shouldn't use i2c commands during an ISR, but I don't really know why. I opened my bv4242.c file (lcd+keypad library) and it uses i2c commands everywhere, checked the wire.c library and it does deal with interrupts. I use the built in wire.h library. I don't really know if there's a way to use them WITH o WITHOUT interrupts, tbh...

Anyway, in my case, my code does work fine with i2c calls within the ISR...That's for sure

possibly because the library 1) is using a buffer when it "writes" to I2C and 2) is not waiting for any response on the bus. I suspect that you may see issues if you were managing a device with communications in both directions, (like your clock). However, in your example, since you clock is not at the same address, no collisions there either!

If you study how the library is using the buffer, you may glean some understanding as to why your particular example works, though it may cause you an issue when you are actually writing to the buffer... What are the odds that you press the button at that exact moment?

Well actually no...I checked it and most communication is bidirectional like (can't remember the actual commands, but something like:)

wire.send (command,address);
result=wire.receive();
return (result);

most of the time, like, really interactive communication.

The lcd keypad has its own processor and it buffers up to 70 keystrokes (nice panel btw in case anyone is looking for an all in one LCD+capacitive keypad, customizable with your own overlays) so I don't need to be reading the panel while the key is being pressed, I just check the buffer from time to time to see if "ui.key()>0".

edugimeno:
wire.send (command,address);
result=wire.receive();
return (result);

I'm not familiar withe the lib, but that looks more like an ack which would be nearly synchronous, vs a dallas temperature sensor (for example) where the sensor takes its sweet time sending back its data.

I know this is topic hasnt seen the light of day for 2 years, but could the OP please post the code of his ISR routine? I've been looking around for examples on how to use I2C within a Timer ISR but so far, I've been rather unsuccessful..

teknomage:
I know this is topic hasnt seen the light of day for 2 years, but could the OP please post the code of his ISR routine? I've been looking around for examples on how to use I2C within a Timer ISR but so far, I've been rather unsuccessful..

I'm still here:) but unfortunately I left this project behind 2 years ago, as I left the company I was working for
And I can barely remember how this all ended. I want to believe I added a "timer interrupt disable" and "timer interrupt enable" before and after wach call to i2c, but I couln't really tell now

Also I was told some days ago they ended up using a different keyboard+LCD module, profbably not using i2c anymore...

Sorry

teknomage:
I know this is topic hasnt seen the light of day for 2 years, but could the OP please post the code of his ISR routine? I've been looking around for examples on how to use I2C within a Timer ISR but so far, I've been rather unsuccessful..

My suggestion is to have the ISR set a flag and then in the main program outside the ISR check that flag for true and then execute the i2c request.

// ================================================================
// ===               INTERRUPT DETECTION ROUTINE                ===
// ================================================================

volatile bool xInterrupt = false;     // indicates whether MPU interrupt pin has gone high
void dmpDataReady() {
    xInterrupt = true;
}



// ================================================================
// ===                      INITIAL SETUP                       ===
// ================================================================

void setup() {
        attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING);
}




// ================================================================
// ===                    MAIN PROGRAM LOOP                     ===
// ================================================================

void loop() {
  if (xInterrupt ) {
    xInterrupt = false;
// run all your I2C code here
  }
// avoid delay() at all costs 
// use one of these blink without delay timers to keep the main loop as fast as possible
unsigned long t = 100;
  static unsigned long _ETimer; // Exact Timer ... Flaw is that it can wind up triggering several times to catch up
  if ( millis() - _ETimer >= (t)) {
    _ETimer += (t);
    
  }
  static unsigned long _ATimer; // After Timer // flaw is that it is not an accurate delay as the other code execution time is added to the total delay time
  if ((millis() - _ATimer) >= (t)) {
    _ATimer = millis();

  }
}

@ edugimeno: Thank you for replying, even it's been a while since this topic came up. I did try out calling Time Interrupt clear and enable within the ISR but it just doesnt work. Bummer though, it would've been useful to have seen your code. Oh well..

@ zhomeslice: Yes, that would be great, except that in my project, I'm trying to call an I2C DAC like MCP4725 from within the Timer ISR, so that I can generate accurate sinewaves (right now, it's just sines for testing purposes). The ISR code works alright if I send the DAC output directly to Ports D & B, i.e., IF I use a Parallel DAC like DAC0808 or AD7541.. but fails miserably when using ANY form of I2C or Serial calls from within the ISR.. so, Serial or I2C DACs are out of the question?

Sorry am new here, so I guess I should post this up as a new thread maybe?

teknomage:
@ edugimeno: Thank you for replying, even it's been a while since this topic came up. I did try out calling Time Interrupt clear and enable within the ISR but it just doesnt work. Bummer though, it would've been useful to have seen your code. Oh well..

@ zhomeslice: Yes, that would be great, except that in my project, I'm trying to call an I2C DAC like MCP4725 from within the Timer ISR, so that I can generate accurate sinewaves (right now, it's just sines for testing purposes). The ISR code works alright if I send the DAC output directly to Ports D & B, i.e., IF I use a Parallel DAC like DAC0808 or AD7541.. but fails miserably when using ANY form of I2C or Serial calls from within the ISR.. so, Serial or I2C DACs are out of the question?

Sorry am new here, so I guess I should post this up as a new thread maybe?

The biggest difficulty with using any of the communication requests is they need interrupts also, and so you would need to free up and allow additional interrupts to be used before leaving the current interrupt call. if your interrupt triggers again during the communication process the interrupt would create a second call to the same process.
I have created a library that uses interrupts on any port on the Arduino. This code may help answer your questions.

My example code prints within the callback. Most consider this a big no-no but it what you are asking for.

This code uses "Lambda" Anonymous Functions extensively sorry if it is confusing.

Lambda functions are being sent to callback variables which hold the pointer to the function in a variable instead of a name as we are used to. Lambda functions start with [] or [=] instead of a name and then you have the variables you wish to receive within ( ) and then the function as normal within { }

So the first callback onInterrupt is called on each and every pin change that has been enabled on any pin on the Arduino. This stores the time and checks for pin changes then calls a Lambda function which receives the time, what pins changed and all the pin states at that moment in time.
We have captured all the information needed and so we can allow additional interrupts:
sei ( ); // re-enable other interrupts at this point
****** Now we can use Serial.print() *******
Then we execute callbacks for every pin that has changed with the PinCallBack function.

  Interrupt.onInterrupt([ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    sei(); // re enable other interrupts at this point,
    Interrupt.PinCallBack(Time, PinsChanged, Pins);
  });

More details on how to use my code below if you would decide to use it:
How does it configure the pin?
To set pins active and give them a function to use in the startup we assign a Lambda function to that pins callback

  Interrupt.onPin(4, INPUT_PULLUP, [ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    if (Interrupt.CheckPin(4)) { // rising
      Serial.println(" Pin 4 Rising \t");
    } else { // Falling
      Serial.println(" Pin 4 Falling \t");
    }
  });

The onPin function does all the work sets the pin enables the pullup if needed and assigns it a function to trigger when the callback occurs similar to the attachInterrupts() function.

If you hate lambda functions or if they are confusing you, you could easily create a function to insert it into the spot

void MyFunction (uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    if (Interrupt.CheckPin(4)) { // rising
      Serial.println(" Pin 4 Rising \t");
    } else { // Falling
      Serial.println(" Pin 4 Falling \t");
    }
  }
Interrupt.onPin(4, INPUT_PULLUP, MyFunction);

just like attachInterrupts() the only difference the function must have a way to receive 3 variables: uint32_t Time, uint32_t PinsChanged, uint32_t Pins

One last note all my functions return a pointer to the class they are from so you can do call another function with just a period between.

Interrupt.onPin(7,*** Code ***). onPin(10,*** Code ***). onPin(16,*** Code ***);

Here is my example code:
This code can handle Ping ultrasound sensors, encoders RC remote control input from the RC receivers, tachometers and I'm sure more.

Hope this helps
Z

#include "Interrupts.h"
InterruptsClass Interrupt;
volatile long EncoderCounter = -2;
float microsecondsToInches(long microseconds)
{
  // According to Parallax's datasheet for the PING))), there are
  // 73.746 microseconds per inch (i.e. sound travels at 1130 feet per
  // second).  This gives the distance travelled by the ping, outbound
  // and return, so we divide by 2 to get the distance of the obstacle.
  // See: http://www.parallax.com/dl/docs/prod/acc/28015-PING-v1.3.pdf
  return (float) microseconds / 74 / 2;
}

float microsecondsToCentimeters(long microseconds)
{
  // The speed of sound is 340 m/s or 29 microseconds per centimeter.
  // The ping travels out and back, so to find the distance of the
  // object we take half of the distance travelled.
  return (float)microseconds / 29 / 2;
}

void setup() {
  Serial.begin(115200); //115200
  Serial.println(" testing");
  // put your setup code here, to run once:
  pinMode(9, OUTPUT);
  Interrupt.onInterrupt([ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    sei(); // re enable other interrupts at this point,
    Interrupt.PinCallBack(Time, PinsChanged, Pins);
  });
  Interrupt.onPin(4, INPUT_PULLUP, [ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    if (Interrupt.CheckPin(4)) { // rising
      Serial.println(" Pin 4 Rising \t");
    } else { // Falling
      Serial.println(" Pin 4 Falling \t");
    }

  }).onPin(5, INPUT, [ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    uint16_t RCTime =  Interrupt.RCRemote(5, Time, Pins, true);
    if (RCTime) {
      Serial.print(" RC Time:");
      Serial.print(RCTime);
    }
  }).onPin(6, INPUT, [ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    unsigned int PingTime = Interrupt.Ping(6, Time, Pins);
    if (PingTime) {
      Serial.print("Ping \t");
      Serial.print(microsecondsToCentimeters(PingTime));
      Serial.println("cm");
    }
  });
  Interrupt.onPin(9, INPUT_PULLUP, [ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    Serial.print("Switch \t");
    Serial.println(Interrupt.Switch(9, Time, 1000, false));

  }).onPin(10, INPUT_PULLUP, [ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    Serial.print("Switch \t");
    Serial.println(Interrupt.Switch(10, Time, 1000, false));

  }).onPin(11, INPUT_PULLUP, [ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    Serial.print("Switch \t");
    Serial.println(Interrupt.Switch(11, Time, 1000, false));

  }).onPin(12, INPUT, [ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    EncoderCounter += Interrupt.Encoder(12, 13, Pins, true);
    Serial.print("Count ");
    Serial.print(EncoderCounter);
    Serial.print("\t Encoder \t");
    Serial.println(Interrupt.Encoder(12, 13, Pins, true));

  }).onPin(13, INPUT, [ = ](uint32_t Time, uint32_t PinsChanged, uint32_t Pins) {
    EncoderCounter -= Interrupt.Encoder(12, 13, Pins, true);
    Serial.print("Count ");
    Serial.print(EncoderCounter);
    Serial.print("\t Encoder \t");
    Serial.println(-1 * Interrupt.Encoder(12, 13, Pins, true));
  });


}

void loop() {
  // put your main code here, to run repeatedly:
  int t = 100;
  static unsigned long _ETimer;
  if ( millis() - _ETimer >= (t)) {
    _ETimer += (t);

    Serial.print(".");
    digitalWrite(9, LOW);
    delayMicroseconds(1);
    digitalWrite(9, HIGH); // Trigger another pulse
    delayMicroseconds(10);
    digitalWrite(9, LOW);
  }
}

Interrupts.cpp (6.5 KB)

Interrupts.h (5.54 KB)

1 Like

This alternate Wire library claims to have removed the interrupts and uses a polling approach. It's probably worth a try.

Rev 5 Changes

  • Removed the use of interrupts from the library so all TWI state changes are polled

It's often forgotten that Wire.Endtransmission is the function that starts the actual transmission.

If that is the last call in an ISR, I think (but never tested) that it's safe to use I2C in the ISR.

@ zhomeslice : Wow, thank you for that detailed reply! I'm not really sure I'd want to use Lambda functions though, only because I'd want to keep my code as simple as possible. And this requires a lot more coding ninjitsu than I'm currently used to ;D

@ cattledog : OK, now that looks right up my alley. Thank you so much for pointing me to this I2C Library, I'm definitely gonna give it a shot!

@ sterretje : Thank you for that lil tip, I've actually added your note to my code comments for future reference :slight_smile:

I'm gonna finally put up a post about the I2C issues I'm facing, but thanks for all your inputs so far, guys!