My Pi Description

My Experiences With the Raspberry Pi -- Tracking My Learning -- My Pi Projects

Friday, March 11, 2016

Interrupts and the Arduino Uno


This post is a logical follow-up on my last post: "Programming the ATmega328P Registers From the Arduino IDE". This discussion is about using the Uno's interrupts. You have two ways to monitor the Uno's I/O pins (to monitor switches, for example). You can poll for inputs to change state, or you can use interrupts to sense when a change of state occurs.
Polling means you are just going to check the state of Uno I/O pins. In a simple system this if fine. Polling means your sketch does nothing else while you check the state of pins. Interrupts are a way to check pins without your sketch diverting its attention from its main tasks.
Here is an example taken from a project I have been reporting on in this blog: I have an enclosure that accepts a large number of temperature sensors. Measurements are made and the results are transmitted to a Raspberry Pi for storage and graphing. The enclosure has a 2 line by 16 character display. I use the first line to display a sensor's description, and the second line to display the sensor's latest temperature measurement. Since the display can only show one sensor at a time, I installed a switch that allows me to switch the display between sensors.
I first handled the switch by polling. I included a small routine in my code in a convenient location. This routine looks at the logic level of the pin connected to the switch, and if low, causes the display to go to the next sensor. A lot goes on in my main code, so the routine to check the switch repeats after several seconds have passed. If I'm not pressing the switch when the routine looks, the routine misses the change of state.
The operation just felt awkward. I now monitor the switch by interrupt. Whenever the switch is pressed, an interrupt occurs so my routine in the loop just looks to see if an interrupt occurred, not if my finger is on the switch. Much better operation.
When the condition for an interrupt occurs, a small function, you write, will run immediately. When that function has done all it needs to do, operation is returned to the task your sketch was doing at the time before the interrupt. Since you don't know when the interrupt will occur, you have to look closely at your sketch to assure there are no instances when an interrupt will impact some critical timing. There are ways to handle that situation. Interrupts can be turned on and off at any place in your sketch.

Interrupts and the Uno

If you want to use the Uno's interrupts, the Arduino Language Reference states there are only two pins, pin 2 and pin 3 you can use. I'm here to tell you it is possible to generate an interrupt from every I/O pin on the Uno. Unless you are using pins 2 and/or 3, however, you will not be able to use the Arduino IDE functions attachInterrupt() and detachInterrupt(). You can, however, use Register Programming to program interrupts on any I/O pin.
The interrupt capabilities of pins 2 and 3 are, however, more robust. All of the external interrupts, when enabled, will trigger an interrupt whenever a change of logic level is detected. Pins 2 and 3 have three additional capabilities.
The four capabilities available for pins 2 and 3:
  1. trigger an interrupt whenever the logic level of the pin changes (high to low or low to high).
  2. only trigger an interrupt whenever the logic level changes from a low to a high.
  3. only trigger an interrupt whenever the logic level changes from a high to a low.
  4. trigger an interrupt whenever the logic level of the pin is low.
Every pin operates with choice 1. Only pins 2 and 3 have the additional three choices.
If you are designing a new project, and you know you are going to use interrupts to monitor sensors or switches, certainly plan to use pins 2 and 3. If you need to utilize interrupts from other pins, or would like to learn the advantages of register programming interrupts, read on.

A Little Review

From my last post, we will need knowledge of the ATmega328P's datasheet, the AVR library for the ATmega328P, and how we handle pin names. To use register programming, we shift our focus from the Arduino Uno to the ATmega328P, itself. Indeed, you can remove a programmed ATmega328P from your Arduino and use it on a circuit board of your own design. With an FTDI adaptor, you can program the ATmega328P while it is on your circuit board.
To use register programming, we will not be using the pin numbers printed on the Uno (like pins 2 and 3). We will use the pin nomenclature found on the ATmega328P microcontroller datasheet. I'm going to repeat a graphic from my last post. Its a partial schematic showing how the Uno's pins connect to the ATmega328P:
All of the ATmega328P pins that connect to the Uno I/O pins have multiple functions. For example, the ATmega328P pin 2 has this label inside the graphic for the chip: "PD0 PCINT16/RXD". That means this pin has three functions:
  1. PD0 - Standard I/O function, Port D, Pin 0
  2. PCINT16 - External Interrupt Number 16. One of 24 External Interrupts. ATmega documentation calls them Pin Change Interrupts
  3. RXD - UART Receive Pin. For Serial Input From External Devices

External Interrupt Register Programming - Register Definitions From the ATmega328P DataSheet

I cobbled together the following from the ATmega328P datasheet:
Pin Change Interrupt Control Register, PCICR: As I described in my last post, the ATmega328P has three ports, Port B, Port C, and Port D. PCICR lets you to enable or disable interrupts on any of the three ports. You can enable interrupts on more than one port. To enable interrupts on a specific port, write a "1" to the applicable bit in the PCICR register. Writing a "0" disables interrupts on the applicable port. Use the following when writing to PCICR:
  • PCIEO (bit 0) controls the interrupts on Port B, pins PCINT0 - PCINT7. PCINT6 and PCINT7 are not available as those pins connect to the crystal.
  • PCIE1 (bit 1) controls the interrupts on Port C, pins PCINT8 - PCINT14. PCINT14 is not available as it reserved for the RESET function.
  • PCIE2 (bit 2) controls the interrupts on Port D, pins PCINT15 - PCINT23.
Pin Change Mask Registers, PCMSK0, PCMSK1, PCMSK2: These three registers let you enable or disable individual pins in a port whose interrupts have been enabled with the PCICR register.

External Interrupt Register Programming - The ATmega328P Arduino Library

As mentioned in the last post, you do not have to consult the library. I include it to show how the datasheet, pin names, and the library come together to help you to write your sketches. The "#defines" in the library show what names you can use in your sketches. These names relate directly to the names in the datasheet register definitions and the ATmega328P pin names, as shown in the datasheet.

Examples Using Interrupts

From my setup in my last post, I'm going to use the red and green switches, and the Arduino Uno. I/m not using the LEDs. If a switch is pressed, a message to the serial monitor will appear.
In my first script, below, I will connect the green switch to PC4 (PCINT12, Uno Pin A4), and the red switch to PC5 (PCINT13, Uno pin A5). In this example, both switches connect to the same port. In setup you can see where I have setup my registers. Remember, from my last post I use the ATmega328p's internal pull-up resistors. They are enabled in line 28. Lines 32 and 33 enable interrupts for the two switches. That should be self-explanatory from the preceding discussion.
Here is where the magic happens: Note lines 13 to 20. This function is called the "Interrupt Service Routine" or ISR. Most of the time we will be in the while loop in setup. If either switch is pressed, processing diverts to the interrupt service routine. When this routine is done, processing goes back to where it was in the while loop.
It is important that you write line 13, the function definition, exactly as shown in my sketch (except for the comment, of course). This ISR will react to any enabled interrupt on port C. If you enabled interrupts on port B, or port D you would substitute "(PCINT0_vect)", or "(PCINT2_vect)", respectively, for "(PCINT1_vect)".
You want your interrupt service routine to be as short as possible. Do not use delay() in the ISR, delay(), itself, uses interrupts. If you use millis() in your code, millis() will not continue to increment while you are in the ISR, resulting in errors.
Any variable declared outside of the ISR, but changed inside the ISR should be declared with the modifier "volatile". This changes where the values of these variables are stored within the ATmega328P. Declaring them as volatile assures the values will be stored in RAM and not in a register - more important is the fact the values will not be lost. In our case that refers to the variasbles "data" and "foundInterrupt".
What does the ISR do? First, it reads PINC (the port C input register), masks out all bits but those related to PC4, and PC5, and stores the value in the variable "data". Second, it disables the port C interrupts - this is a way to debounce the switch (further key presses will not trigger an interrupt). Lastly, it makes the Boolean variable "foundInterrupt" true. When it has done all of these things, it exits back to the while() loop.
Back in the while()n loop, The first "IF" statement will now be true. We now look at the variable "data" to see which switch was pressed (The bit will be low for the switch that was pressed). Variable "foundInterrupt" is set to false and the masked value of PINC is check. If neither switch is pressed, the interrupt for port C is enabled again. This last is another way to effect switch debounding. This assures that only one message will be displayed for each switch press. In this scenario, it is not possible to detect if both switches are pressed at the same time.
The sketch below illustrates connecting the two switches to different ports. The green switch connects to PB0 (PCINT0, Uno pin 8). The red switch connects to PC5 (PCINT13, Uno pin A5). As with the last sketch, look at setup() to see how the ports, pullup resistors, and interrupts are setup.
Since we would like to detect interrupts on both the B and C ports, we need an interrupt service routine for both ports. The two ISRs are very simple. When a switch is pressed, variable "foundInterrupt_PortB", or "foundInterrupt_PortC", is made true. Also, the interrupt for that port is disabled. This is for switch debounce to avoid multiple interrupts (without this provision, an interrupt would occur whenever the switch is pressed and whenever the switch was released).
I think the code in the while() loop is pretty clear. Note that this sketch allows the possibility to detect both switches pressed. Since the while() loop has a 2 second delay, if both switches are pressed within the same two second period, the serial monitor will show that both are pressed. To avoid that situation, you should disable both interrupts in the two ISRs. The last part of the while() loop enables the two interrupts if both switches are released.

No comments:

Post a Comment