Interrupts 101

Contents

Code - Sender Interrupt Driven - Fully commented listing

This is the fully commented version of the interrupt driven sender program. In the setup() function the register settings are copiously commented to explain how they configure the Timer 2 hardware to generate an interrupt once every millisecond.

While I have included plenty of comments. The full details of the Timer 2 hardware can be found in chapter 18 of the ATMega328P datasheet.

This program is covered in the video starting at about 1:09:34.

/*
 * Sender - Interrupt driven
 *
 * This program resolves the problem with the non-interrupt driven
 * version of the program.
 * 
 * It resolves it by using Timer2 to generate an interrupt once every
 * millisecond. Then we count 200 invocations (= 200ms). Once we reach
 * our target interval (i.e. 200ms) we toggle the system status.
 * 
 * This works reliably - despite the regular execution of the "big task".
 * 
 */
// Values defining our hardware environment
const int signalLedPin= 4;
const int heartbeatLedPin =  7;// the number of the LED pin

// Establish values to manage our signal and
// "Big task" operations.
const long heartbeatInterval = 200;
const long reportInterval = 8000;

// Setup our environment.
void setup() {
  Serial.begin(115200);
  pinMode(heartbeatLedPin, OUTPUT);
  pinMode(signalLedPin, OUTPUT);
  Serial.println("Sender - ISR");

  noInterrupts();   // Turn interrupts off.
  /* The following "variables" represent hardware registers in the
   * ATMega328P Microcontroller (MCU).
   * 
   * By modifying these registers we are configuring the hardware,
   * in this case the Timer2 subsystem.
   * 
   * To get a full understanding of these registers, it is necessary
   * to refer to the datasheet which is the reference guide for the MCU.
   * The datasheet can be found here:
   *   https://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf
   * 
   * In this manual, Timer2 is described in Chapter 18. The registers are
   * described in section "18.11 Register Description".
   */
  TCCR2A = 0;       // set the main timer 2 control registers 0
  TCCR2B = 0;       // to turn off all functions.
                    // initialize the timer 2 counter value to 0
                    // TCNT2 is the register that counts time
  TCNT2  = 0;       // when the timer is activated.

                // set compare match register for 1khz increments
                    // For a 1ms rate, the value is calculated as follows:
                    //  (clock speed (16MHz) / prescaler / 1000) - 1.
                    // = (16*10^6) / (1000*64) - 1
                    // = 16 MHz clock / (1000 hz  * 64x prescaler) - 1
                    // = 16,000,000 / 64,000 - 1
                    // = 250 - 1
                    // = 249
                    // OCR2A is a single byte (and thus must be < 256)
                    // Thus, we must adapt the calculation above to ensure
                    // that our "count to" value is < 256.
                    // Thus a combination of "prescaler" and frequency is used
                    // to determine how high to count before generating an interrupt
  OCR2A = 249;      // Count match value
  
                // Set CS22 bit for 64x prescaler
                    // - Refer to section 18.10 of the datasheet
                    // Basically this divides the 16MHz clock by 64 (= 0.25MHz)
                    // for the purposes of driving Timer2.
                    // (CS22 = 1, CS21 = 0, CS20 = 0)
  TCCR2B |= (1 << CS22);    // Set 64x prescaler
                    // The above two settings cause the counter (TCCNT2) to be incremented by
                    // one every time the scaled clock (16MHz / 64x) ticks.
                    // this will occur roughly once every 16,000,000 / 64,000 = 1/250th of a second
                    // or put another way, 250,000 times per second.
                    // In combination with OCR2A which requires we count 249 of them (including 0
                    // so the total is 250), we get a 1ms cycle.

                // Turn on CTC (Clear Timer on Compare match) mode
                    // Basically, when TCNT2 reaches OCR2A (i.e. 249) TCNT2 is reset to zero
                    // and resumes counting upwards.
                    // (WGM22 = 0, WGM21 = 1, WGM20 = 0: waveform generation mode 2)
  TCCR2A |= (1 << WGM21);   // Clear the counter (TCNT2) when it reaches the match value (OCR2A).
  
                // Enable timer compare (Timer 2 Output Compare Match A) interrupt.
                    // This is "the good bit", it is what we have been working towards.
                    // Basically when the counter (TCNT2) reaches our limit (OCR2A) an interrupt
                    // will be generated if we enable it.
                    // The interrupt is the Timer 2 Output Compare Match A Interrupt (TIMER2_COMPA).
                    // This interrupt is interrupt vector #8 (address 0x0007) on an Arduino Uno.
                    // In our code, the Interrupt Service Routine (ISR) is nominated by the rather
                    // curious looking function definition as follows:
                    //   SIGNAL(TIMER2_COMPA_vect) {
                    //     // ISR code goes here
                    //   }
  TIMSK2 |= (1 << OCIE2A);  // Enable the the Timer2 Compare Match A Interrupt.

  interrupts();     // Turn interrupts on.
}

/*
 * A "Big Task" that takes a "long time" to run.
 * The idea of this is to run it periodically to illustrate how
 * it interferes with our signal generation.
 * 
 * This task is possibly taking an excessive amount of time, but the
 * idea is that it takes enough time to see from a human perspective.
 * As such it is long enough so that we, as humans, can see what is going
 * on. 
 * At MCU speeds, a shorter task can interfere with our signal in
 * exactly the same way. So the important part here is the principle that
 * sometimes things can interfere with other activity and that that can be
 * problematic if one of those activities is critical in some way such as
 * our signal which we need to be regular and consistant.
 */
 void bigTask() {
  Serial.println("Starting Big Task(ISR): ");
  for(int i = 0; i < 40; i++) {
    Serial.print(".");
    delay(100);
  }
  Serial.println("\nBig Task Complete");
}

/*
 * This is our Timer ISR (Interrupt Service Routine).
 * It is triggered once ever millisecond (i.e. 1000
 * times per second).
 * Since we want our heartbeat to be 200 milliseconds
 * in duration (as specified by the heartbeatInterval),
 * we will count that many ticks before toggling the system
 * state and outputing it to the two DIO pins.
 */
//void heartBeat() {
SIGNAL(TIMER2_COMPA_vect) {
static int cnt = heartbeatInterval;

    // Count this clock tick.
    // We are counting down from heartbeatInterval.
    // If the counter is non-zero, we simply return (i.e. do nothing this time round).
  if (--cnt) {
    return;
  }
    // Otherwise, our counter has reached zero. This means the desired interval
    // has passed, so we reset the counter to our interval...
  cnt = heartbeatInterval;
    // Toggle the system state and output it.
  int sysStatus = !digitalRead(heartbeatLedPin);
  digitalWrite(heartbeatLedPin, sysStatus);
  digitalWrite(signalLedPin, sysStatus);
}

/*
 * Our main loop. Checks for and acivates:
 * - The big Task.
 * The non ISR also managed the heartbeat, but in this
 * version, the heartbeat (and thus the signal) is
 * managed by the timer ISR.
 */
void loop() {
static unsigned long reportPreviousMillis = 0;

  unsigned long currentMillis = millis();

  if (currentMillis - reportPreviousMillis >= reportInterval) {
    reportPreviousMillis = currentMillis;
    bigTask();
  }  
}

Jump to: Top

Please support me if you can

Please help support these projects: Buy me a coffee - gm310509 Patreon - gm310509