I have a question about interrupts

X7123M3-256

New member
I'm working on a project involving an accelerometer which I would like to read values from 100 times per second. Up until now I've been using an IntervalTimer to do that, but now it's no longer working, and from what I've found elsewhere (e.ghttps://forum.pjrc.com/threads/28720-Sending-I2C-message-from-ISR) it seems like the problem is that the Wire library isn't meant to be used within an interrupt service routine (I'm not sure why it was working before). Instead, it seems that the recommended solution is to poll the sensor from within my main loop with a construction like


Code:
while(running)
{
     if(millis()>last_sensor_read+10)
     {
     read_sensor();
     last_sensor_read=millis();
     }
}


The problem I have is that I don't think I can guarantee that my main loop will always take less than 10ms to run - I've tried timing how long the loop currently takes, and it seems that it averages about 5us (which is plenty quick enough), but writing output to the serial occasionally takes >10ms. I thought I could use two nested loops like this:


Code:
while(running)
{
     for(int i=0;i<100;i++)
     {
     //Stuff that needs to run often
     }
//Stuff that needs to run less often
}


But this doesn't really solve the problem as if any one operation takes longer than 10ms - even if that operation only needs to happen occasionally - I'm still going to miss data points as a result. What I want to do is have the routine that reads the sensor pre-empt whatever else is currently running, and save the values to be processed later so that I don't get gaps in the data if some long-running operation is blocking the main loop. It wouldn't matter too much if the samples aren't exactly 10ms apart, but I do want the sensor to be read exactly 100 times per second.

I am wondering, is there an alternative way to accomplish this if I can't use an interrupt, or is there some workaround that might make it work? (it did seem to be working before and I'm not really sure what changed) Or is there a different way to approach this problem?
 
Last edited:
I think you should be able to do the sensor read in the IntervalTimer callback function, but only if that is the only place you are doing I2C stuff. The construct you've shown of doing the I2C read from loop() will work. If you know the I2C read takes 5 ms, you probably should structure the main loop as shown below, so you never try to do both an I2C read and the other loop() stuff in a single pass.

Code:
if (time to read sensor)
  read sensor
else
  do other stuff
 
The 10ms interrupt could just set a flag - and then check it often enough perhaps in loop and busy places to perform the i2c read often enough to see it done.

Is there only a single i2c device in use? No idea given what the i2c _isr() code looked like? USB prints or other things? Perhaps set the _isr() to lowest priority, 127?, so anything else normally done while in loop() code still processes. And don't do anything but read and store the data in the _isr() so the loop() code can process it as needed.
 
The 10ms interrupt could just set a flag - and then check it often enough perhaps in loop and busy places to perform the i2c read often enough to see it done.

I know, the problem is that if I have tasks which may take more than 10ms, I can't guarantee that the flag would be checked often enough, because it would not check the flag during the long-running operation. Ideally, I'd want to check that flag every few milliseconds, so that there's not too much delay between the interrupt triggering and the sensor actually being read.

Is there only a single i2c device in use? No idea given what the i2c _isr() code looked like? USB prints or other things? Perhaps set the _isr() to lowest priority, 127?

There is another one (on a separate I2C bus if that's relevant) - I tried removing it completely but it doesn't seem to affect it. Setting the interrupt priority to the minimum doesn't seem to help either.

However, I am writing to the USB serial port as well and I think that may indeed be the problem - if I remove that code, it does seem to work. The serial output takes place outside the ISR but maybe it's causing a problem if the ISR triggers during the serial write? I had noticed that the serial output was getting corrupted - I initially thought that my code was writing to a bad pointer but this wasn't the case - it's definitely the I2C calls that are the issue - so perhaps the I2C calls corrupt some internal state that the Serial library is also using?

Unfortunately, having the sensor read be able to pre-empt the serial output is really what I wanted to accomplish, as that operation is can take more than 10ms to complete. On the other hand, my code makes multiple calls to Serial.write(), so perhaps I could split it up into several smaller functions that would not take so long (or maybe if I combine them all into one Serial.write() call, it would complete faster). My end goal is to have the data written to an SD card, so that might prove to be faster than the serial port or at least not interfere with the sensor read in the same way. The serial output is useful for debugging though, it'd be a shame if it cannot work at all.
 
For what it's worth, this is the solution I ended up with. It seems that the problem is not that the I2C library cannot run from within the interrupt handler, but rather that it cannot run at the same time as a serial command is already executing. Presumably, they must share some internal state - I guess the better way to approach it would be to alter the library code so they don't clash, but I think I'd struggle with that so for now I have this workaround - I just set a flag before calling Serial.write() that causes the ISR to fail with an error. I'm sure there's a better solution to this problem but at least I've got it working again for now. Thanks for the responses, anyway.

Code:
enum
{
ISR_LIBRARY_IN_USE=0x4,
ISR_SUCCESS=0x2,
ISR_DONE=0x1
};

volatile uint8_t isr_flags=0;

bool library_in_use()
{
     if(!(isr_flags&ISR_LIBRARY_IN_USE))
     {
     isr_flags|=ISR_LIBRARY_IN_USE;
     return false;
     }
return true;
}

void library_not_in_use()
{
isr_flags&=~ISR_LIBRARY_LOCK;
}

void isr()
{
     if(!library_in_use())
     {
     read_sensor();
     library_not_in_use();
     isr_flags|=ISR_SUCCESS;
     }
     else isr_flags&=~ISR_SUCCESS;
isr_flags|=ISR_DONE;
}


I then wrote this wrapper around Serial.write() that waits until the ISR has just executed, so it should have 10ms to finish - which should be plenty of time to ensure that the interrupt doesn't trigger again while the write is still taking place, but if it does detect that an interrupt was triggered it will trigger it again once the problematic serial call is done (which means the measurement would be delayed, but at least it still happens). The downside of this is that now every call to the serial library has to go via this wrapper and it adds 5ms of delay to every serial write, but that's not a huge problem for me.

Code:
void wait_on_isr()
{
//This would be an infinite loop if the ISR timer isn't running
     if(timer_mode==TIMER_OFF)return;
isr_flags&=~ISR_DONE;
     while(!(isr_flags&ISR_DONE));
}

void isr_rerun_failed()
{
     if(!(isr_flags&ISR_SUCCESS))
     {
     isr();
     }
}

void serial_write(const char* buf,int len)
{
//Wait until ISR has just completed (so we have the maximum amount of time until the next one)
wait_on_isr();
    //This should always succeed
     if(!library_in_use())
     {
     Serial.write(buf,len);
     //If a sensor read was attempted during the write, do it now
     imu_isr_rerun_failed();
     imu_library_in_use();
     }
}
 
Back
Top