health check on an ISR please - COMPLETE - THANKS

Hi guys

I'm developing a kiln controller. I'm experimenting with using an interrupt driven routine for reading the temperatures of the kiln using an Ocean Controls thermocouple shield. I've fallen into and climbed back out of several ISR traps already, and I'm wary that there may still be some lurking in the code. If you're expert in ISRs, can you please take a look over it ?

#include <SPI.h>
#include <EEPROM.h>
#include <TimerOne.h>

void setup(){ 
  // other stuff
 Timer1.initialize(1000000);
 Timer1.attachInterrupt( readTemperatures ); // attach the service routine here 
 }

// the ISR
void readTemperatures(){
    // pick up last temperature asked for, and request the next one
    // divides the standard MUX read in two parts: initiate, and read
    // this way eliminates the delay(220) in the standard Ocean readfunction, leaving maximum time for other activity, 
    // while maintaining readings for each channel every tempRedaingPeriod

  // declare, initialise temperature working variables
  static int st_temp[mcp1]; 
  static int st_slope[mcp1]; 
  static byte n_ramp[mcp1]; 
  static int rp=-1; // data byte record pointer
  static unsigned long reading_count=0;

  int temperature;
  int slope;  // change from previous temp
  int slope_difference_limit=2;
  int slope_difference=0;   // difference from initial slope
  static int channelToRead=1;
  unsigned long time;  // to calculate delays (in microseconds)

  if (readTemperaturesSuspended) return; 

  //temperature=Read_Temperature_take_reading();          // inserted code in-line to avoid function call - necesssary?
  //int Read_Temperature_take_reading(void){              // old declaration    
  // unsigned int temp_reading;                           // using temperature instead
  // read result (by virtue of the timer, it has been over 250ms since the read was initiated (time for 'conversion' on the chip)
  digitalWrite(CS_TEMP,LOW); // Set MAX7765 /CS Low

  //delay(1);                      // replaced with loop
  time=Timer1.read(); while (Timer1.read()-time>1*1000) {};

  temperature = SPI.transfer(0xff) << 8;  
  temperature += SPI.transfer(0xff);  
  digitalWrite(CS_TEMP,HIGH); // Set MAX7765 /CS High

  //delay(1);                      // replaced with loop
  time=Timer1.read(); while (Timer1.read()-time>1*1000) {};

  // check result
  if(bitRead(temperature,2) == 1) temperature=-1; // Failed / No Channel Error  (KD presumably no thermocouple attached, broken lead wire, etc)
  else temperature=(int)(temperature >> 5); //Convert to Degc

  slope=temperature-temp[channelToRead]; // during last interval
  slope_difference=slope-st_slope[channelToRead];  // difference from starting slope, not from last slope reading

  if ((abs(slope_difference)>slope_difference_limit) ||   // if the slope is too different from the original, or
    (n_ramp[channelToRead]==255) || //if we've run out of space in intervals byte, or
    (forceTempLog)) {  // if we've been forced to log an interval, as when closing a session, or doing a data upload
      
    recordCount+=1;       // recordCount increments for each reading for each channel

    // store the latest interval data
    EEPROM.write(rp+1,n_ramp[channelToRead]);  // byte 1 = number of intervals
    if (temperature < 0) {  // usually a broken channel
      EEPROM.write(rp+2,0); 
      EEPROM.write(rp+3,0); 
    }
    else {
      // channel goes in left most 3 bits, high bits of temp in balance
      EEPROM.write(rp+2,(channelToRead<<5)+(st_temp[channelToRead]>>8));
      EEPROM.write(rp+3,st_temp[channelToRead] & 255);  // least significant byte
    }
    rp += 3;  // update record buffer pointer

     // record the new starting points
    st_temp[channelToRead]=temperature;
    st_slope[channelToRead]=slope;
    n_ramp[channelToRead]=0;  // will be incremented to 1
  } // if a new record is required

  forceTempLog=OFF;  // reset in case it was used 
  temp[channelToRead]=temperature;
  n_ramp[channelToRead] += 1;  // number of intervals associated with this reading (roughly speaking the time at the current rate of temperature change)
  
  // find next channel to read 
  do {
    channelToRead=(((channelToRead-1)+1)% 8) +1; // -1 to return to zero base, +1 to increment to next channel, % to loop back to 0, +1 to return to 1-base
    if (channelToRead==1) {
      reading_count += 1;  // reading count incremented only after all active channels have been read
    }
  } while (!(bitRead(channelsInUse,channelToRead-1)));   // using 0-based bit reader
  // now pointing at next channel requiring servicing

  // Set_Mux_Channel(channelToRead);
  byte chanZeroBased=channelToRead-1;
  // set the MUX channel
  // place address bits on pins  
  digitalWrite(MUX_A0,(chanZeroBased & B00000001)>>0); 
  digitalWrite(MUX_A1,(chanZeroBased & B00000010)>>1);
  digitalWrite(MUX_A2,(chanZeroBased & B00000100)>>2);

  //  Initiate next read
  digitalWrite(CS_TEMP,LOW); // Set MAX7765 /CS Low

  //delay(5);                      // replaced with loop
  time=Timer1.read(); while (Timer1.read()-time>5*1000) {};

  digitalWrite(CS_TEMP,HIGH); // Set MAX7765 /CS High
}

Are EEPROM reads and writes ssafe in an ISR? (there will be EEPROM activity in the main programme: there will be reads from the EEPROM addresses written by the ISR, and there will be Writes, but these will never be to the addresses used in the ISR)

I have eliminated all user-defined functions from the ISR, but the following function calls remain:
digital.Write()
SPI.transfer()
bitRead()
Are these safe?

Was it necessary to avoid user-defined function calls? (I had problems when these contained Serial.prints, so decided to remove them entirely)

I have replaced both uses of delay() with a do-nothing loop, but the loop makes use of calls on Timer1 - is this safe?

The ISR updates a global array temp[] so I have made this volatile in the main programme .

How does the compiler know that this is an ISR - is it clever enough to determine this through the Timer1 class invocation?

Many thanks
Kenny

I'm developing a kiln controller.

Kilns aren't noted for their sub-microsecond latency requirements.

Why do you think you need an interrupt-driven temperature reading?

Are EEPROM reads and writes ssafe in an ISR?

Safe? Yes.
Desireable? Probably not.

Serial.print (in fact the whole serial subsystem) relies on interrupts, and so cannot be (safely) used inside an ISR or any function called by an ISR. There is no reason to not use your own function though.

Delays should be avoided inside an ISR - in fact, I'm not sure they will even work correctly given that they use millis() and the millis() function is driven by an interrupt.

Any variables which are shared between your main program and the ISR must be declared "volatile" - you haven't shown us those, so I can't see if they are or not. Also, any non-atomic access to any of those variables (i.e., anything that's not a single ASM code) should be protected by disabling the interrupts before the access and re-enabling them afterwards - again, the code as shown is lacking, so I can't see if you have done that or not.

Conventional wisdom with interrupts is that they should do little more than set a flag to tell you that something happened. That flag is detected in loop and acted upon. Of course, there may be a need to do a little more than that. The aim though is to get out as quickly as possible to avoid missing other interrupting events. In this case though, you have a huge routine which I can see little justification for. To echo AWOL, why do you feel the need for an interrupt at all?

On second thoughts, no, doing EEPROM read and writes in interrupt and background context is probably a really bad idea, unless you disable interrupts during the background EEPROM operations.

A much more sensible way would be to not do everything in the ISR, but rather on the main loop.

In this, the ISR sets a boolean variable to true each time the interrupt occurs. The loop() checks if the variable is true, and if so reads the temperatures.

#include <SPI.h>
#include <EEPROM.h>
#include <TimerOne.h>

volatile boolean interruptOccured = false;

void setup(){ 
  // other stuff
 Timer1.initialize(1000000);
 Timer1.attachInterrupt( timerInterrupt); // attach the service routine here 
 }

void loop(){
  if(interruptOccured){
    readTemperatures(); //Temperatures read on main loop, NOT in ISR. When the ISR triggers, 'interruptOccured' is set to true, and this is executed.
    interruptOccured = false; //Don't keep reading temperatures until next interrupt.
  }
}

// the ISR

void timerInterrupt(){
  interruptOccured = true;
}

void readTemperatures(){
    // pick up last temperature asked for, and request the next one
    // divides the standard MUX read in two parts: initiate, and read
    // this way eliminates the delay(220) in the standard Ocean readfunction, leaving maximum time for other activity, 
    // while maintaining readings for each channel every tempRedaingPeriod

  // declare, initialise temperature working variables
  static int st_temp[mcp1]; 
  static int st_slope[mcp1]; 
  static byte n_ramp[mcp1]; 
  static int rp=-1; // data byte record pointer
  static unsigned long reading_count=0;

  int temperature;
  int slope;  // change from previous temp
  int slope_difference_limit=2;
  int slope_difference=0;   // difference from initial slope
  static int channelToRead=1;
  unsigned long time;  // to calculate delays (in microseconds)

  if (readTemperaturesSuspended) return; 

  //temperature=Read_Temperature_take_reading();          // inserted code in-line to avoid function call - necesssary?
  //int Read_Temperature_take_reading(void){              // old declaration    
  // unsigned int temp_reading;                           // using temperature instead
  // read result (by virtue of the timer, it has been over 250ms since the read was initiated (time for 'conversion' on the chip)
  digitalWrite(CS_TEMP,LOW); // Set MAX7765 /CS Low

  //delay(1);                      // replaced with loop
  time=Timer1.read(); while (Timer1.read()-time>1*1000) {};

  temperature = SPI.transfer(0xff) << 8;  
  temperature += SPI.transfer(0xff);  
  digitalWrite(CS_TEMP,HIGH); // Set MAX7765 /CS High

  //delay(1);                      // replaced with loop
  time=Timer1.read(); while (Timer1.read()-time>1*1000) {};

  // check result
  if(bitRead(temperature,2) == 1) temperature=-1; // Failed / No Channel Error  (KD presumably no thermocouple attached, broken lead wire, etc)
  else temperature=(int)(temperature >> 5); //Convert to Degc

  slope=temperature-temp[channelToRead]; // during last interval
  slope_difference=slope-st_slope[channelToRead];  // difference from starting slope, not from last slope reading

  if ((abs(slope_difference)>slope_difference_limit) ||   // if the slope is too different from the original, or
    (n_ramp[channelToRead]==255) || //if we've run out of space in intervals byte, or
    (forceTempLog)) {  // if we've been forced to log an interval, as when closing a session, or doing a data upload
      
    recordCount+=1;       // recordCount increments for each reading for each channel

    // store the latest interval data
    EEPROM.write(rp+1,n_ramp[channelToRead]);  // byte 1 = number of intervals
    if (temperature < 0) {  // usually a broken channel
      EEPROM.write(rp+2,0); 
      EEPROM.write(rp+3,0); 
    }
    else {
      // channel goes in left most 3 bits, high bits of temp in balance
      EEPROM.write(rp+2,(channelToRead<<5)+(st_temp[channelToRead]>>8));
      EEPROM.write(rp+3,st_temp[channelToRead] & 255);  // least significant byte
    }
    rp += 3;  // update record buffer pointer

     // record the new starting points
    st_temp[channelToRead]=temperature;
    st_slope[channelToRead]=slope;
    n_ramp[channelToRead]=0;  // will be incremented to 1
  } // if a new record is required

  forceTempLog=OFF;  // reset in case it was used 
  temp[channelToRead]=temperature;
  n_ramp[channelToRead] += 1;  // number of intervals associated with this reading (roughly speaking the time at the current rate of temperature change)
  
  // find next channel to read 
  do {
    channelToRead=(((channelToRead-1)+1)% 8) +1; // -1 to return to zero base, +1 to increment to next channel, % to loop back to 0, +1 to return to 1-base
    if (channelToRead==1) {
      reading_count += 1;  // reading count incremented only after all active channels have been read
    }
  } while (!(bitRead(channelsInUse,channelToRead-1)));   // using 0-based bit reader
  // now pointing at next channel requiring servicing

  // Set_Mux_Channel(channelToRead);
  byte chanZeroBased=channelToRead-1;
  // set the MUX channel
  // place address bits on pins  
  digitalWrite(MUX_A0,(chanZeroBased & B00000001)>>0); 
  digitalWrite(MUX_A1,(chanZeroBased & B00000010)>>1);
  digitalWrite(MUX_A2,(chanZeroBased & B00000100)>>2);

  //  Initiate next read
  digitalWrite(CS_TEMP,LOW); // Set MAX7765 /CS Low

  //delay(5);                      // replaced with loop
  time=Timer1.read(); while (Timer1.read()-time>5*1000) {};

  digitalWrite(CS_TEMP,HIGH); // Set MAX7765 /CS High
}

Thanks as always for useful feedback.

wildbill:
The aim though is to get out as quickly as possible to avoid missing other interrupting events. In this case though, you have a huge routine which I can see little justification for. To echo AWOL, why do you feel the need for an interrupt at all?

There are definitely other ways of skinning this particular cat, but...
a) I wanted to learn about interrupts and this was an opportunity
b) the temperatures need to be read every 3 seconds or so. There are potentially eight sensors. Each takes 220ms to "convert" apparently, so we are already in the region of 2 seconds out of the 3 available. I have therefore split the "read" into two halves, one to initiate the read, the other to complete it after the 220 ms, giving me more time to get on with other stuff. However I am shredding the 3 second period into 3/8 second chunks and this puts a (not insurmountable, but annoying) restriction on the other things I want to do. Of course I could break this other stuff into small chunks, but I thought that putting the regular temp reading routine in an ISR was one way of avoiding this restriction.

Be that as it may, I'm interested in understanding the art of interrupts more thoroughly.

majenko:
Also, any non-atomic access to any of those variables (i.e., anything that's not a single ASM code) should be protected ...

Can you give me an example in C of a "non-atomic access" - I'm not very keen (nor sufficiently skilled) to get down into ASM to examine the code.

AWOL:
unless you disable interrupts during the background EEPROM operations.

Have found cli() and sei() for disable/enable interrupts - is this what you're referring to?

kenny_devon:

majenko:
Also, any non-atomic access to any of those variables (i.e., anything that's not a single ASM code) should be protected ...

Can you give me an example in C of a "non-atomic access" - I'm not very keen (nor sufficiently skilled) to get down into ASM to examine the code.

volatile int a;

...

a++;

A is an integer variable, it takes two bytes to store the number. Any operation on it will therefore require multiple operations to manipulate both bytes. If the ISR is called part way through that operation then the results could very well be corrupted. You should wrap anything like that in cli() and sei().

Hi TC World - thanks - your reply came in as I was writing my previous response, which gives a bit more background.

Your approach is my fall-back, but it puts a restriction on the other things I do in the main loop, several of which could potentially take many seconds to accomplish, in any case longer than my 3 seconds limit. The ISR solution I was suggesting accomplishes two things: it gives priority to the time-critical reading (and subsequent resetting of the relays), and relieves me of the job of splitting my other processes down into sub-second chunks.

Unless I have misunderstood, your current proposal won't do this - is there a variant of it that addresses my requirements?
Thanks

Thanks Makenko - understood

kenny

it gives priority to the time-critical reading

Unless your kiln is very, very tiny, temperature readings are anything but time-critical and certainly not in the sub-microsecond domain of interrupts.

Serial.print (in fact the whole serial subsystem) relies on interrupts, and so cannot be (safely) used inside an ISR or any function called by an ISR.

Doesn't SPI also use interrupts? If so, using SPI in an ISR would be ruled out, too.

b) the temperatures need to be read every 3 seconds or so. There are potentially eight sensors. Each takes 220ms to "convert" apparently, so we are already in the region of 2 seconds out of the 3 available. I have therefore split the "read" into two halves, one to initiate the read, the other to complete it after the 220 ms, giving me more time to get on with other stuff.

Is there any reason why these eight sensors cannot be triggered all at once (albeit serially)? A bit of blink without delay methodology to do other things while you wait for the capture would mean that hardly any processor time would be spent on the temperature processing.

What are you doing in loop that consumes so much time?

Hi AWOL

"Time critical" is i think a relative term, and not restricted to the realm of microseconds, even if that's where interrupts are normally employed. My criticality is measured in seconds, not microseconds. It's just that I have some high priority processes measured in tenths of a second, and other lower priority processes measured in several seconds. What attracts me to the interrupt mechanism is not its lightening speed, but the implicit prioritisation that it gives. Am I missing something?

kenny

Wow - can hardly keep up with the response - thanks again!

Hi WildBill

wildbill:
Is there any reason why these eight sensors cannot be triggered all at once (albeit serially)?

My understanding of the Ocean Controls thermocouple multiplexor shield is that there is only one chip that does the "conversion", and so the process of reading all eight is a serial one. Initiate for T1, 220 ms, read T1, initiate for T2, 220 ms, read T2, etc. I have combined the read for one thermocouple with the initiate for the next, to get

Initiate for T1,
220 ms,
read T1 and initiate for T2,
220 ms,
read T1 and initiate for T3,
etc

So my choice is I think, either slice my other processess into approximately 250 ms chunks, or use the interrupt system. (Other routes?)

Hi PaulS
Now you've got me worried. Does that mean anyone using SPI is essentially barred from employing their own interrupts?

At this point, for best results, you might want to post (or attach) your code and an explanation of what you are trying to do.

Hi guys

I have included the cli / sei wrappers and the routine appears to be working fine for the moment (with SPI calls included).

Still have an outstanding question:

How does the compiler know that a routine is an ISR - is it clever enough to determine this through the Timer1 class invocation?

Thanks all for your help.

Kenny

How does the compiler know that a routine is an ISR - is it clever enough to determine this through the Timer1 class invocation?

There are specific names for the various interrupt vectors. The Timer1::attachInterrupt method adds code to a specific vector to call the specified function. There is nothing magic about how it works, nor does the compiler do anything clever to make a routine into an ISR.

Hi PaulS

PaulS:
nor does the compiler do anything clever to make a routine into an ISR.

The reason I asked was because somewhere along the line I came across this article

which suggested that compilers do indeed take account of the peculiarities of an ISR. Perhaps only for more sophisticated compilers?

PaulS:
Doesn't SPI also use interrupts? If so, using SPI in an ISR would be ruled out, too.

I believe not. The important routine is SPI.transfer, as follows:

byte SPIClass::transfer(byte _data) {
  SPDR = _data;
  while (!(SPSR & _BV(SPIF)))
    ;
  return SPDR;
}

No interrupts there.


How does the compiler know that a routine is an ISR

You tell it with the ISR define. That adds a "signal" attribute to the function:

#ifdef __cplusplus
#  define ISR(vector, ...)            \
    extern "C" void vector (void) __attribute__ ((signal,__INTR_ATTRS)) __VA_ARGS__; \
    void vector (void)
#else
#  define ISR(vector, ...)            \
    void vector (void) __attribute__ ((signal,__INTR_ATTRS)) __VA_ARGS__; \
    void vector (void)
#endif

signal
Use this attribute on the AVR to indicate that the specified function is an interrupt handler. The compiler will generate function entry and exit sequences suitable for use in an interrupt handler when this attribute is present.


You may want to read my fairly lengthy page about interrupts:


Perhaps only for more sophisticated compilers?

You don't get much more sophisticated then g++. It's the same (general) compiler used to build OS/X and iPhone apps (different architecture of course).

kenny_devon:
There are potentially eight sensors. Each takes 220ms to "convert" apparently, so we are already in the region of 2 seconds out of the 3 available. I have therefore split the "read" into two halves, one to initiate the read, the other to complete it after the 220 ms, giving me more time to get on with other stuff. However I am shredding the 3 second period into 3/8 second chunks and this puts a (not insurmountable, but annoying) restriction on the other things I want to do. Of course I could break this other stuff into small chunks, but I thought that putting the regular temp reading routine in an ISR was one way of avoiding this restriction.

I'm not sure interrupts will help in this respect. Sounds to me like you simply need to keep track of time.