Skip to main content
Engineering LibreTexts

13.2: We Interrupt Our Regularly Scheduled Code…

  • Page ID
    25628
  • \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)\(\newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    The solution is the use of an interrupt. Think of an interrupt as a scheme whereby a special bit of code called an interrupt handler or interrupt service routine (ISR) is triggered to run under special conditions. The execution of the existing code is suspended until the ISR is done. The interrupt can be triggered by either software or hardware events. A typical example would be to have the interrupt triggered by a signal change on an input port pin. Most microcontrollers have many different levels of interrupts and these can be sorted by importance. That is, some events can take precedence over other events. This means that it may be possible to have one ISR interrupted by a higher priority interrupt. Generally, ISRs should be short, quickly executing chunks of code so as to not clog-up the operation of the main code. In the case of our event counter, the item switch could be connected to an input pin that triggers an interrupt. The ISR would do nothing more than increment a global variable that keeps track of the item count. The main body of the code would then periodically check this global and respond accordingly (in this case, updating the display). We can think of the ISR scheme as “running in the background” constantly checking the input pin. This frees us from having to sprinkle port checking code throughout the main body of the program.

    Ordinarily, none of the available interrupts are active with the exception of the system reset. These have to be enabled individually before they can be used. The development system defines a name for each interrupt routine so all we have to do is enable the proper bit(s) in the interrupt control register(s) and fill out the code for the ISR.

    For the event counter, we’re going to reuse a considerable amount of code and hardware from the Reaction Timer Redux exercise, most notably the seven segment displays and multiplexing code. The player’s switch will be traded for the event counting switch. We shall make use of the ATmega 328P’s pin change interrupt (PCINT) capability. When programmed, each of the input port pins can trigger an interrupt when its input signal changes from high to low or low to high. These are arranged into banks which correspond to the ports, for example bank 0 corresponds to port B. Each bank has an ISR associated with it. The bank 0 ISR is called PCINT0_vect. This stands for “Pin Change Interrupt Bank 0 Vector” (technically, a vector is a function address so this is really the starting address of the interrupt service routine). Each of the pins of a particular port is activated through its own interrupt bit. For bank 0, these bits are PCINT0 through PCINT7. So, if port pins 0 and 1 are enabled for interrupts (via PCINT0 and PCINT1), a change on either of their pins will generate the bank 0 pin change interrupt and cause the PCINT0_vect ISR to execute. Once the ISR is done, normal execution of the main code will continue where it left off.

    Before we look at hardware let’s consider some code. As mentioned, we can modify the existing Reaction Timer Redux code. We will no longer need the special “go” message so we can delete some items from the numeral array. Other modifications will be minimal if we keep the wiring the same. We’ll use the generic term “sensor” in place of the former FSR as we have yet to determine how the event counting switch will be implemented. Note that having some documentation inside the original code as to how the circuit is wired saves us from re-inventing the wheel. All good programmers, technicians and engineers do this!

    /* Event counter with common anode 7 segment displays X 3. Displays off of 
    D1:7 (segment a=7), Mux off of B0:2 (hundreds on B.2), w/ PNP drivers. Sensor 
    on B.3. Values > 999 show up as "Err" ALL SEGMENTS AND MUX ACTIVE LOW */
    
    // Port B.0:2 for 7 segment mux
    #define DIGIT1   0x01
    #define DIGIT10  0x02
    #define DIGIT100 0x04
    #define DIGIT111 0x07
    
    #define SENSORMASK  0x08
    
    unsigned char numeral[]={
       //ABCDEFG,dp
       0b00000011,   // 0
       0b10011111,   // 1 
       0b00100101,   // 2
       0b00001101,   // 3
       0b10011001,
       0b01001001,
       0b01000001,
       0b00011111,
       0b00000001,
       0b00011001,   // 9
       0b11111111,   // blank
       0b01100001,   // E
       0b01110011    // r
    };
    
    #define LETTER_BLANK  10
    #define LETTER_E      11
    #define LETTER_R      12
    

    At this point we need to declare the afore-mentioned global variable and modify the setup() routine.

    unsigned int g_count = 0;
    
    void setup()
    {
       // set Arduino pins 0-7, port D.0:7, for output to 7 segment LEDs
       DDRD = 0xff;
    
       // set Arduino pins 8-10, port B.0:2, for output to mux 7 segment LEDs
       DDRB |= DIGIT111;
    
       // Displays are active low so turn them off initially
       PORTB |= DIGIT111;
    
       // Arduino pin 11, port B.3 for input from sensor
       DDRB &= (~SENSORMASK);   // initialize the pin as an input.
       PORTB |= SENSORMASK;     // enable pull-up  
    
       // enable pin change interrupt bank 0
       bitSet(PCICR,PCIE0);
    
       // enable pin change interrupt on port B.3
       bitSet(PCMSK0,PCINT3);
    }
    

    The last two lines of the code above are particularly important. The first line enables the pin change bank 0 interrupt (bit PCIE0 or Pin Change Interrupt Enable bank 0) in the Pin Change Interrupt Control Register (PCICR). The second line then specifies which pin(s) in bank 0 will be used (Pin Change INTerrupt pin 3 or PCINT3) in the Pin Change MaSK for bank 0 (PCMSK0) register.

    Once enabled, we now need to write the ISR itself. As mentioned, this routine already has a name (PCINT0_vect) and all it needs to do is increment the global count variable.

    /* This is the bank 0 interrupt handler. It triggers on any change to the appropriate pin(s) of bank 0 (port B), either H->L or L->H. */
    
    ISR(PCINT0_vect)
    {
       // increment only on sensor going low to avoid double counting
       if( !(PINB & SENSORMASK) )
          g_count++;
    }
    

    The former loop() function can be tossed. In its place we use the contents of the former DisplayValue() function that controlled the display multiplexing. Unlike the original, this does not render the display for a specified time. Rather, it does a single scan of the three displays for a total of 15 milliseconds. loop() will then be called again and the process repeats.

    At the start, the contents of the global count variable are copied to a local variable which is then taken apart to determine the indices into the numeral array as before. Note a minor refinement, namely the snippet of code added to “blank” leading zeros. A question might remain though, and that is, why bother to make a local copy of the global variable? The reason is because an interrupt can happen at any time. That means that partway through the h, t, u index code an interrupt might occur and the global variable will change. If this happens the computed indices will be incorrect because the code will be operating on two (or possible more) different values. If we make a local copy this won’t happen. Only the global will change and the display will be updated appropriately the next time loop() is called.

    void loop()
    {
       unsigned int v;
       unsigned char h, t, u; // hundreds, tens, units
    
       // grab a local in case another interrupt comes by
       v = g_count;
    
       if( v > 999 ) // error code
       {
          h = LETTER_E;
          t = u = LETTER_R;
       }
       else
       {
          u = v%10;
          v = v/10;
          t = v%10;
          h = v/10;
      
          // blank leading zeros
          if( !h )
          {
              h = LETTER_BLANK;
              if( !t )
                 t = LETTER_BLANK;
          }
       }
    
       // clear all displays then activate the desired digit
       PORTB |= DIGIT111;
       PORTD = numeral[h];
       PORTB &= ~DIGIT100;
       delay(5);
    
       PORTB |= DIGIT111;
       PORTD = numeral[t];
       PORTB &= ~DIGIT10;
       delay(5);
    
       PORTB |= DIGIT111;
       PORTD = numeral[u];
       PORTB &= ~DIGIT1;
       delay(5);   
      
       // clear display
       PORTB |= DIGIT111;
    }
    

    At this point we have some workable code. If the earlier hardware is not still in place, rewire it. Enter the code above, compile and transfer it. Each press of the player button should show an increment on the display. Note: If the switch was not debounced you might get an extra count or two on random presses.


    This page titled 13.2: We Interrupt Our Regularly Scheduled Code… is shared under a CC BY-NC-SA 4.0 license and was authored, remixed, and/or curated by James M. Fiore via source content that was edited to the style and standards of the LibreTexts platform; a detailed edit history is available upon request.