Gammon Forum - Electronics - Microprocessors - Interrupts
Gammon Forum - Electronics - Microprocessors - Interrupts
Entire forum
Electronics
Microprocessors
Interrupts
Interrupts
Refresh page
Message This article discusses interrupts on the Arduino Uno (Atmega328) and similar processors, using the Arduino IDE. The concepts however are very
general. The code examples provided should compile on the Arduino IDE (Integrated Development Environment).
TL;DR :
Keep it short
Don't use delay ()
Don't do serial prints
Make variables shared with the main code volatile
Variables shared with main code may need to be protected by "critical sections" (see below)
Don't try to turn interrupts off or on
Most processors have interrupts. Interrupts let you respond to "external" events while doing something else. For example, if you are cooking dinner
you may put the potatoes on to cook for 20 minutes. Rather than staring at the clock for 20 minutes you might set a timer, and then go watch TV.
When the timer rings you "interrupt" your TV viewing to do something with the potatoes.
Interrupts are not for simply changing your mind and doing something different. For example:
The above are examples of doing something different. They should not be done in an interrupt routine. The most that an interrupt (service)
routine might do would be to remember that the red button was pressed. This fact would then be tested in the main program loop.
Example of interrupts
void setup ()
{
pinMode (LED, OUTPUT); // so we can update the LED
digitalWrite (BUTTON, HIGH); // internal pull-up resistor
attachInterrupt (digitalPinToInterrupt (BUTTON), switchPressed, CHANGE); // attach interrupt handler
} // end of setup
void loop ()
{
// loop doing nothing
}
This example shows how, even though the main loop is doing nothing, you can turn the LED on pin 13 on or off, if the switch on pin D2 is pressed.
To test this, just connect a wire (or switch) between D2 and Ground. The internal pullup (enabled in setup) forces the pin HIGH normally. When
grounded, it becomes LOW. The change in the pin is detected by a CHANGE interrupt, which causes the Interrupt Service Routine (ISR) to be called.
In a more complicated example, the main loop might be doing something useful, like taking temperature readings, and allow the interrupt handler to
detect a button being pushed.
digitalPinToInterrupt function
To simplify converting interrupt vector numbers to pin numbers you can call the function digitalPinToInterrupt, passing a pin number. It returns
the appropriate interrupt number, or NOT_AN_INTERRUPT (-1).
For example, on the Uno, pin D2 on the board is interrupt 0 (INT0_vect from the table below).
However the second one is easier to read and more portable to different Arduino types.
Available interrupts
1 Reset
2 External Interrupt Request 0 (pin D2) (INT0_vect)
3 External Interrupt Request 1 (pin D3) (INT1_vect)
4 Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
5 Pin Change Interrupt Request 1 (pins A0 to A5) (PCINT1_vect)
6 Pin Change Interrupt Request 2 (pins D0 to D7) (PCINT2_vect)
7 Watchdog Time-out Interrupt (WDT_vect)
8 Timer/Counter2 Compare Match A (TIMER2_COMPA_vect)
9 Timer/Counter2 Compare Match B (TIMER2_COMPB_vect)
10 Timer/Counter2 Overflow (TIMER2_OVF_vect)
11 Timer/Counter1 Capture Event (TIMER1_CAPT_vect)
12 Timer/Counter1 Compare Match A (TIMER1_COMPA_vect)
13 Timer/Counter1 Compare Match B (TIMER1_COMPB_vect)
14 Timer/Counter1 Overflow (TIMER1_OVF_vect)
15 Timer/Counter0 Compare Match A (TIMER0_COMPA_vect)
16 Timer/Counter0 Compare Match B (TIMER0_COMPB_vect)
17 Timer/Counter0 Overflow (TIMER0_OVF_vect)
18 SPI Serial Transfer Complete (SPI_STC_vect)
19 USART Rx Complete (USART_RX_vect)
20 USART, Data Register Empty (USART_UDRE_vect)
21 USART, Tx Complete (USART_TX_vect)
22 ADC Conversion Complete (ADC_vect)
23 EEPROM Ready (EE_READY_vect)
24 Analog Comparator (ANALOG_COMP_vect)
25 2-wire Serial Interface (I2C) (TWI_vect)
26 Store Program Memory Ready (SPM_READY_vect)
Internal names (which you can use to set up ISR callbacks) are in brackets.
Warning: If you misspell the interrupt vector name, even by just getting the capitalization wrong (an easy thing to do) the interrupt routine will
not be called, and you will not get a compiler error.
Summary of interrupts
The "data transfers" can be used to let a program do something else while data is being sent or received on the serial port, SPI port, or I2C port.
External interrupts, pin-change interrupts, and the watchdog timer interrupt, can also be used to wake the processor up. This can be very handy, as in
sleep mode the processor can be configured to use a lot less power (eg. around 10 microamps). A rising, falling, or low-level interrupt can be used to
wake up a gadget (eg. if you press a button on it), or a "watchdog timer" interrupt might wake it up periodically (eg. to check the time or temperature).
Pin-change interrupts could be used to wake the processor if a key is pressed on a keypad, or similar.
The processor can also be awoken by a timer interrupt (eg. a timer reaching a certain value, or overflowing) and certain other events, such as an
incoming I2C message.
The "reset" interrupt cannot be disabled. However the other interrupts can be temporarily disabled by clearing the interrupt flag.
Enable interrupts
You can enable interrupts with the function call "interrupts" or "sei" like this:
Disable interrupts
If you need to disable interrupts you can "clear" the interrupt flag like this:
Either method has the same effect, using "interrupts" / "noInterrupts" is a bit easier to remember which way around they are.
The default in the Arduino is for interrupts to be enabled. Don't disable them for long periods or things like timers won't work properly.
There may be time-critical pieces of code that you don't want interrupted, for example by a timer interrupt.
Also if multi-byte fields are being updated by an ISR then you may need to disable interrupts so that you get the data "atomically". Otherwise one byte
may be updated by the ISR while you are reading the other one.
For example:
noInterrupts ();
long myCounter = isrCounter; // get value set by ISR
interrupts ();
Temporarily turning off interrupts ensures that isrCounter (a counter set inside an ISR) does not change while we are obtaining its value.
Warning: if you are not sure if interrupts are already on or not, then you need to save the current state and restore it afterwards. For example, the
code from the millis() function does this:
return m;
}
Note the lines in bold save the current SREG (status register) which includes the interrupt flag. After we have obtained the timer value (which is 4
bytes long) we put the status register back how it was.
Tips
Function names
The functions cli/sei and the register SREG are specific to the AVR processors. If you are using other processors such as the ARM ones the
functions may be slightly different.
However if you just want to disable a particular interrupt then you should clear the interrupt-enable flag for that particular interrupt
source. For example, for external interrupts, call detachInterrupt().
Since there are 25 interrupts (other than reset) it is possible that more than one interrupt event might occur at once, or at least, occur before the
previous one is processed. Also an interrupt event might occur while interrupts are disabled.
The priority order is the sequence in which the processor checks for interrupt events. The higher up the list, the higher the priority. So, for example, an
External Interrupt Request 0 (pin D2) would be serviced before External Interrupt Request 1 (pin D3).
Interrupts events (that is, noticing the event) can occur at any time, and most are remembered by setting an "interrupt event" flag inside the
processor. If interrupts are disabled, then that interrupt will be handled when they are enabled again, in priority order.
Variables shared between ISR functions and normal functions should be declared "volatile". This tells the compiler that such variables might change
at any time, and thus the compiler must reload the variable whenever you reference it, rather than relying upon a copy it might have in a processor
register.
For example:
void setup ()
{
attachInterrupt (digitalPinToInterrupt (2), isr, CHANGE); // attach interrupt handler
} // end of setup
void loop ()
{
if (flag)
{
// interrupt has occurred
}
} // end of loop
You write an ISR (interrupt service routine). This is called when the interrupt occurs.
You tell the processor when you want the interrupt to fire.
Writing an ISR
Interrupt Service Routines are functions with no arguments. Some Arduino libraries are designed to call your own functions, so you just supply an
ordinary function (as in the examples above), eg.
// Interrupt Service Routine (ISR)
void pinChange ()
{
flag = true;
} // end of pinChange
However if a library has not already provided a "hook" to an ISR you can make your own, like this:
In this case you use the "ISR" define, and supply the name of the relevant interrupt vector (from the table earlier on). In this case the ISR is handling
an SPI transfer completing. (Note, some old code uses SIGNAL instead of ISR, however SIGNAL is deprecated).
For interrupts already handled by libraries, you just use the documented interface. For example:
void setup ()
{
Wire.onReceive(receiveEvent);
}
In this case the I2C library is designed to handle incoming I2C bytes internally, and then call your supplied function at the end of the incoming data
stream. In this case receiveEvent is not strictly an ISR (it has an argument) but it is called by an inbuilt ISR.
void setup ()
{
attachInterrupt (digitalPinToInterrupt (2), pinChange, CHANGE); // attach interrupt handler for D2
} // end of setup
In this case the attachInterrupt function adds the function pinChange to an internal table, and in addition configures the appropriate interrupt flags in
the processor.
The next step, once you have an ISR, is to tell the processor that you want this particular condition to raise an interrupt.
As an example, for External Interrupt 0 (the D2 interrupt) you could do something like this:
EICRA (External Interrupt Control Register A) would be set according to this table from the Atmega328 datasheet (page 71).
That defines the exact type of interrupt you want:
Fortunately you don't need to remember those numbers because attachInterrupt does that for you. However that is what is actually happening, and for
other interrupts you may have to "manually" set interrupt flags.
To simplify your life some common interrupt handlers are actually inside library code (for example INT0_vect and INT1_vect) and then a more user-
friendly interface is provided (eg. attachInterrupt). What attachInterrupt actually does is save the address of your wanted interrupt handler into a
variable, and then call that from INT0_vect / INT1_vect when needed. It also sets the appropriate register flags to call the handler when required.
When an ISR is entered, interrupts are disabled. Naturally they must have been enabled in the first place, otherwise the ISR would not be entered.
However to avoid having an ISR itself be interrupted, the processor turns interrupts off.
When an ISR exits, then interrupts are enabled again. The compiler also generates code inside an ISR to save registers and status flags, so that
whatever you were doing when the interrupt occurred will not be affected.
However you can turn interrupts on inside an ISR if you absolutely must, eg.
} // end of pinChange
Normally you would need a pretty good reason to do this, as another interrupt now could result in a recursive call to pinChange, with quite possibly
undesirable results.
According to the datasheet, the minimal amount of time to service an interrupt is 4 clock cycles (to push the current program counter onto the stack)
followed by the code now executing at the interrupt vector location. This normally contains a jump to where the interrupt routine really is, which is
another 3 cycles.
Then an ISR routine (declared with the ISR define) does something like this:
That's another 19 clock cycles (1.1875 µS). So in total, an ISR using the ISR define will take you 2.625 µS to execute, plus whatever the code itself does.
However the external interrupts (where you use attachInterrupt) do a bit more, as follows:
SIGNAL(INT0_vect) {
182: 1f 92 push r1 (2)
184: 0f 92 push r0 (2)
186: 0f b6 in r0, 0x3f (1)
188: 0f 92 push r0 (2)
18a: 11 24 eor r1, r1 (1)
18c: 2f 93 push r18 (2)
18e: 3f 93 push r19 (2)
190: 4f 93 push r20 (2)
192: 5f 93 push r21 (2)
194: 6f 93 push r22 (2)
196: 7f 93 push r23 (2)
198: 8f 93 push r24 (2)
19a: 9f 93 push r25 (2)
19c: af 93 push r26 (2)
19e: bf 93 push r27 (2)
1a0: ef 93 push r30 (2)
1a2: ff 93 push r31 (2)
if(intFunc[EXTERNAL_INT_0])
1a4: 80 91 00 01 lds r24, 0x0100 (2)
1a8: 90 91 01 01 lds r25, 0x0101 (2)
1ac: 89 2b or r24, r25 (2)
1ae: 29 f0 breq .+10 (2)
intFunc[EXTERNAL_INT_0]();
1b0: e0 91 00 01 lds r30, 0x0100 (2)
1b4: f0 91 01 01 lds r31, 0x0101 (2)
1b8: 09 95 icall (3)
}
1ba: ff 91 pop r31 (2)
1bc: ef 91 pop r30 (2)
1be: bf 91 pop r27 (2)
1c0: af 91 pop r26 (2)
1c2: 9f 91 pop r25 (2)
1c4: 8f 91 pop r24 (2)
1c6: 7f 91 pop r23 (2)
1c8: 6f 91 pop r22 (2)
1ca: 5f 91 pop r21 (2)
1cc: 4f 91 pop r20 (2)
1ce: 3f 91 pop r19 (2)
1d0: 2f 91 pop r18 (2)
1d2: 0f 90 pop r0 (2)
1d4: 0f be out 0x3f, r0 (1)
1d6: 0f 90 pop r0 (2)
1d8: 1f 90 pop r1 (2)
1da: 18 95 reti (4)
I count 82 cycles there (5.125 µS in total at 16 MHz) as overhead plus whatever is actually done in the supplied interrupt routine. That is, 2.9375 µS
before entering your interrupt handler, and another 2.1875 µS after it returns.
This various somewhat. The figures quoted above are the ideal ones where the interrupt is immediately processed. A few factors may delay that:
If the processor is asleep, there are designated "wake-up" times, which could be quite a few milliseconds, while the clock is spooled back up to
speed. This time would depend on fuse settings, and how deep the sleep is.
If an interrupt service routine is already executing, then further interrupts cannot be entered until it either finishes, or enables interrupts itself.
This is why you should keep each interrupt service routine short, as every microsecond you spend in one, you are potentially delaying the
execution of another one.
Some code turns interrupts off. For example, calling millis() briefly turns interrupts off. Therefore the time for an interrupt to be serviced would
be extended by the length of time interrupts were turned off.
Interrupts can only be serviced at the end of an instruction, so if a particular instruction takes three clock cycles, and has just started, then the
interrupt will be delayed at least a couple of clock cycles.
An event that turns interrupts back on (eg. returning from an interrupt service routine) is guaranteed to execute at least one more instruction. So
even if an ISR ends, and your interrupt is pending, it still has to wait for one more instruction before it is serviced.
Since interrupts have a priority, a higher-priority interrupt might be serviced before the interrupt you are interested in.
Performance considerations
Interrupts can increase performance in many situations because you can get on with the "main work" of your program without having to constantly be
testing to see if switches have been pressed. Having said that, the overhead of servicing an interrupt, as discussed above, would actually be more than
doing a "tight loop" polling a single input port. If you absolutely need to respond to an event within, say, a microsecond, then an interrupt would be too
slow. In that case you might disable interrupts (eg. timers) and just loop looking for the pin to change.
Some set a flag and they are handled in priority order, even if the event that caused them has stopped. For example, a rising, falling, or changing
level interrupt on pin D2.
Others are only tested if they are happening "right now". For example, a low-level interrupt on pin D2.
The ones that set a flag could be regarded as being queued, as the interrupt flag remains set until such time as the interrupt routine is entered, at which
time the processor clears the flag. Of course, since there is only one flag, if the same interrupt condition occurs again before the first one is processed, it
won't be serviced twice.
Something to be aware of is that these flags can be set before you attach the interrupt handler. For example, it is possible for a rising or falling level
interrupt on pin D2 to be "flagged", and then as soon as you do an attachInterrupt the interrupt immediately fires, even if the event occurred an hour
ago. To avoid this you can manually clear the flag. For example:
However the "low level" interrupts are continuously checked, so if you are not careful they will keep firing, even after the interrupt has been called.
That is, the ISR will exit, and then the interrupt will immediately fire again. To avoid this, you should do a detachInterrupt immediately after you know
that the interrupt fired. An example of this is going into sleep mode:
#include <avr/sleep.h>
void sleepNow ()
{
set_sleep_mode (SLEEP_MODE_PWR_DOWN);
sleep_enable (); // enables the sleep bit in the mcucr register
attachInterrupt (digitalPinToInterrupt (2), wake, LOW); // wake up on low level on D2
sleep_mode (); // here the device is actually put to sleep!!
detachInterrupt (digitalPinToInterrupt (2)); // stop LOW interrupt on D2
} // end of sleepNow
Note that the interrupt is detached immediately after we wake up. However we should be cautious about putting the detachInterrupt actually inside
the "wake" ISR, because it is possible the interrupt might fire between attaching the interrupt and going to sleep, in which case we would never wake
up.
The improved version below solves this problem by disabling interrupts before doing the attachInterrupt call. Since you are guaranteed that one
instruction will be executed after re-enabling interrupts, we are sure that the sleep_cpu call will be done before the interrupt occurs.
#include <avr/sleep.h>
void sleepNow ()
{
set_sleep_mode (SLEEP_MODE_PWR_DOWN);
noInterrupts (); // make sure we don't get interrupted before we sleep
sleep_enable (); // enables the sleep bit in the mcucr register
attachInterrupt (digitalPinToInterrupt (2), wake, LOW); // wake up on low level on D2
interrupts (); // interrupts allowed now, next instruction WILL be executed
sleep_cpu (); // here the device is put to sleep
} // end of sleepNow
These examples illustrates how you can save power with a LOW interrupt, because the LOW interrupt is activated even if the processor is asleep
(whereas the other ones - RISING, FALLING, CHANGE - are not). (Amended 15 Dec 2013 : We now believe all four external interrupt types will wake
the processor).
Tip:
I received this confirmation from Atmel that the datasheet is wrong about only LOW interrupts waking the processor.
Quote:
Hello Nick,
Our design team has confirmed that “Note-3 mentioned under Table 10-1” is a datasheet bug. So you can use any type of
interrupt (Rising edge/ Falling edge / Low level / Any logical change) to wake up from sleep mode. Sorry for the inconvenience
caused.
Best Regards,
Manoraj Gnanadhas
In brief, keep them short! While an ISR is executing other interrupts cannot be processed. So you could easily miss button presses, or incoming serial
communications, if you try to do too much. In particular, you should not try to do debugging "prints" inside an ISR. The time taken to do those is likely
to cause more problems than they solve.
A reasonable thing to do is set a single-byte flag, and then test that flag in the main loop function. Or, store an incoming byte from a serial port into a
buffer. The inbuilt timer interrupts keep track of elapsed time by firing every time the internal timer overflows, and thus you can work out elapsed time
by knowing how many times the timer overflowed.
Remember, inside an ISR interrupts are disabled. Thus hoping that the time returned by millis() function calls will change, will lead to
disappointment. It is valid to obtain the time that way, just be aware that the timer is not incrementing. And if you spend too long in the ISR then the
timer may miss an overflow event, leading to the time returned by millis() becoming incorrect.
A test shows that, on a 16 MHz Atmega328 processor, a call to micros() takes 3.5625 µS. A call to millis() takes 1.9375 µS. Recording (saving) the
current timer value is a reasonable thing to do in an ISR. Finding the elapsed milliseconds is faster than the elapsed microseconds (the millisecond
count is just retrieved from a variable). However the microsecond count is obtained by adding the current value of the Timer 0 timer (which will keep
incrementing) to a saved "Timer 0 overflow count".
Warning: Since interrupts are disabled inside an ISR, and since the latest version of the Arduino IDE uses interrupts for Serial reading and writing,
and also for incrementing the counter used by "millis" and "delay" you should not attempt to use those functions inside an ISR. To put it another way:
However there are also "pin change" interrupts for all pins (on the Atmega328, not necessarily all pins on other processors). These act on groups of
pins (D0 to D7, D8 to D13, and A0 to A5). They are also lower priority than the external event interrupts. You could make an interrupt handler to
handle changes on pins D8 to D13 like this:
ISR (PCINT0_vect)
{
// one of pins D8 to D13 has changed
}
Clearly extra code is needed to work out exactly which pin changed (eg. comparing it to a "before" value).
Pin change interrupts each have an associated "mask" byte in the processor, so you could actually configure them to only react to (say) D8, D10 and
D12, rather than a change to D8 to D13. However you still then need to work out which of those changed.
#include <avr/sleep.h>
#include <avr/wdt.h>
#define LED 13
// watchdog interrupt
ISR (WDT_vect)
{
wake ();
} // end of WDT_vect
void setup ()
{
digitalWrite (2, HIGH); // pull-up on button
} // end of setup
void loop()
{
pinMode (LED, OUTPUT);
digitalWrite (LED, HIGH);
delay (5000);
digitalWrite (LED, LOW);
delay (5000);
} // end of loop
The above code, running on a "bare bones" board (that is, without voltage regulator, USB interface etc.) uses the following:
It also illustrates how you can recover from sleep in two different ways. One is with a button press (ie. you ground pin D2), the other is waking up
periodically (every 8 seconds), although you could make it every 1, 2 4, or 8 seconds (or even shorter if you consult the data sheet).
Warning: after being woken the processor might need a short time to stabilize its clock. For example, you may see "garbage" on a serial comms port
while things are synchronizing. If this is a problem you may want to build in a short delay after being woken.
Note re brownout detection: The figures measured above are with brownout detection disabled. To generate a reference voltage for the brownout
detection takes a bit of current. With it enabled, the sleep mode uses closer to 70 uA (not 6 uA). One way of turning off the brownout detection is to use
avrdude from the command line, like this:
That sets the "efuse" (extended fuse) to be 7, which disables brownout detection. This example assumes you are using the USBtinyISP programmer
board.
A "normal" Arduino environment already is using interrupts, even if you don't personally attempt to. The millis() and micros() function calls make use
of the "timer overflow" feature. One of the internal timers (timer 0) is set up to interrupt roughly 1000 times a second, and increment an internal
counter which effectively becomes the millis() counter. There is a bit more to it than that, as adjustment is made for the exact clock speed.
Also the hardware serial library uses interrupts to handle incoming serial data (and in the more recent library versions, outgoing serial data as well).
This is very useful as your program can be doing other things while the interrupts are firing, and filling up an internal buffer. Then when you check
Serial.available() you can find out what, if anything, has been placed in that buffer.
After a bit of discussion and research on the Arduino forum, we have clarified exactly what happens after you enable interrupts. There are three main
ways I can think of that you can enable interrupts, which were previously not enabled:
In all cases, the processor guarantees that the next instruction after interrupts are enabled (if they were previously disabled) will always be executed,
even if an interrupt event is pending. (By "next" I mean the next one in program sequence, not necessarily the one physically following. For example, a
RETI instruction jumps back to where the interrupt occurred, and then executes one more instruction).
sei ();
sleep_cpu ();
If not for this guarantee, the interrupt might occur before the processor slept, and then it might never be awoken.
Empty interrupts
If you merely want an interrupt to wake the processor, but not do anything in particular, you can use the EMPTY_INTERRUPT define, eg.
EMPTY_INTERRUPT (PCINT1_vect);
This simply generates a "reti" (return from interrupt) instruction. Since it doesn't try to save or restore registers this would be the fastest way to get an
interrupt to wake it up.
Read the data sheet!
More information about interrupts, timers, etc. can be obtained from the data sheet for the processor.
http://atmel.com/dyn/resources/prod_documents/8271S.pdf
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
Message
Example code of a pump timer
This was posted on the Arduino forum (by me) of an example of how to make a pump timer. The hardware consisted of a switch (the on/cancel switch)
connected to D2 and a "pump time" rotary switch (not a rotary encoder) connected to pins 3 to 11. For example, if the switch was in the 3rd position
you would get the pump running for 10 minutes.
// Pump timer
// Author: Nick Gammon
// Date: 7th January 2012
// Released into the public domain.
#include <avr/sleep.h>
const int pumpPin = 13; // output pin for the pump and solenoid (goes to the relay)
const int buttonPin = 2; // input pin (for a pushbutton switch)
const int firstSwitch = 3; // first switch pin for time rotary button
const int lastSwitch = 11; // last switch pin
const int debounceTime = 20; // debounce in milliseconds
const unsigned long delayTime [] = { 2, 5, 10, 15, 20, 30, 45, 46, 120}; // Pump run times in minutes
unsigned long startPumpTime = 0; // when pump started
unsigned long pumpTime; // how long to run pump
volatile boolean buttonPressed; // set when button pressed
void sleepNow ()
{
set_sleep_mode (SLEEP_MODE_PWR_DOWN);
sleep_enable (); // enables the sleep bit in the mcucr register
attachInterrupt (digitalPinToInterrupt (buttonPin), wake, LOW);
sleep_mode (); // here the device is actually put to sleep!!
detachInterrupt (digitalPinToInterrupt (buttonPin)); // stop LOW interrupt
} // end of sleepNow
void deBounce ()
{
unsigned long now = millis ();
do
{
// on bounce, reset time-out
if (digitalRead (buttonPin) == LOW)
now = millis ();
}
while (digitalRead (buttonPin) == LOW ||
(millis () - now) <= debounceTime);
} // end of deBounce
void setup()
{
pinMode(pumpPin, OUTPUT);
digitalWrite (buttonPin, HIGH); // pull-up on button
for (int i = firstSwitch; i <= lastSwitch; i++)
digitalWrite (i, HIGH); // pull-up on switch
} // end of setup
void loop ()
{
// pump is not running and we woke up, work out how long to run it
deBounce ();
pumpTime = 0;
if (pumpTime == 0)
return; // no time selected
// start pump
startPumpTime = millis ();
digitalWrite (pumpPin, HIGH);
} // end of loop
Of interest is the deBounce function - that handles the inevitable "bounces" of switch contacts as you open or close them. Without some sort of
debouncing handler you might interpret a single button press as 20 presses, making it very hard to actually do anything.
Above is an image of a switch bouncing, captured on the logic analyzer. You can see from the above, that a simple keypress might result in a dozen or
so transitions. On the image each bounce is around 5 mS apart. So we really need to wait for a longer interval to elapse, in which the switch doesn't
open/close again.
The debounce handler above waits for 20 milliseconds for the switch to stop bouncing, and if the switch closes again, resets the time interval so it waits
another 20 milliseconds. This seemed to work quite well.
The code also demonstrates putting the processor to sleep if it isn't needed (eg. at midnight) so it uses less power. It also shows how the "low" interrupt
can be used to wake it up, and the falling interrupt to notice if the switch is pressed while the pump is running (eg., you have watered your plants
enough already).
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
Message
Timer interrupts
The "normal" Arduino IDE uses timer 0 to provide a "clock" being a number that is incremented, and adjusted, to give you a count per millisecond.
You can read that by doing something like this:
You can also find a more accurate timer value by using micros (), which takes the current value from the millis counter, and adds in the current
reading from timer 0 (thus correcting for "right now"). eg.
ISR(TIMER1_COMPA_vect)
{
static boolean state = false;
state = !state; // toggle
digitalWrite (LED, state ? HIGH : LOW);
}
void setup() {
pinMode (LED, OUTPUT);
// set up Timer 1
TCCR1A = 0; // normal operation
TCCR1B = bit(WGM12) | bit(CS10); // CTC, no pre-scaling
OCR1A = 999; // compare A register value (1000 * clock speed)
TIMSK1 = bit (OCIE1A); // interrupt on Compare A Match
} // end of setup
void loop() { }
This uses CTC (Clear Timer on Compare), so it counts up to the specified value, clears the timer, and starts again. This toggles pin D13 faster than you
can see (once every 62.5 µS). (It turns on every second toggle, that is every 125 µS, giving a frequency of 1/0.000125 = 8 KHz).
Note: The counter is zero-relative. So if the counter is 1, then it actually counts two times the clock_speed/pre-scaler (0 and 1). So for an interval of
1000 slower than the internal clock, we need to set the counter to 999.
If you change the prescaler it toggles every 64 mS, slow enough for you to see it flashing. (That is, the LED lights every 128 mS, being 1/0.128 = 7.8125
Hz).
ISR(TIMER1_COMPA_vect)
{
static boolean state = false;
state = !state; // toggle
digitalWrite (LED, state ? HIGH : LOW);
}
void setup() {
pinMode (LED, OUTPUT);
// set up Timer 1
TCCR1A = 0; // normal operation
TCCR1B = bit(WGM12) | bit(CS10) | bit (CS12); // CTC, scale to clock / 1024
OCR1A = 999; // compare A register value (1000 * clock speed / 1024)
TIMSK1 = bit (OCIE1A); // interrupt on Compare A Match
} // end of setup
void loop() { }
And even more simply, you can output a timer result without even using interrupts:
void setup() {
pinMode (LED, OUTPUT);
// set up Timer 1
TCCR1A = bit (COM1A0); // toggle OC1A on Compare Match
TCCR1B = bit(WGM12) | bit(CS10) | bit (CS12); // CTC, scale to clock / 1024
OCR1A = 4999; // compare A register value (5000 * clock speed / 1024)
} // end of setup
void loop() { }
Plug an LED (and resistor in series, say 470 ohms) between D9 and Gnd, and you will see it toggle every 320 mS. (1/(16000000 / 1024) * 5000).
http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=50106
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
Message
Ignition timing with timers and interrupts
Below is another example of interrupts and timers from the Arduino forum. The basic problem was to turn something on when an interrupt occurred
(eg. on pin D2) but after a delay. The code below uses an interrupt to start the process, sets a timer for 0.5 mS (as an initial delay). Then when that
timer fires, the spark is turned on for a different interval (2 mS) and then when that time is up, the spark is turned off, and the process resumes.
#include <digitalWriteFast.h>
void activateInterrupt0 ()
{
EICRA &= ~(bit(ISC00) | bit (ISC01)); // clear existing flags
EICRA |= bit (ISC01); // set wanted flags (falling level interrupt)
EIFR = bit (INTF0); // clear flag for interrupt 0
EIMSK |= bit (INT0); // enable it
} // end of activateInterrupt0
void deactivateInterrupt0 ()
{
EIMSK &= ~bit (INT0); // disable it
} // end of deactivateInterrupt0
} // end of TIMER1_COMPA_vect
// set up Timer 1
TCCR1A = 0; // normal mode
TCNT1 = 0; // count back to zero
TCCR1B = bit(WGM12) | bit(CS11); // CTC, scale to clock / 8
// time before timer fires - zero relative
// multiply by two because we are on a prescaler of 8
OCR1A = (sparkDelayTime * 2) - (isrDelayFactor * 2) - 1;
TIMSK1 = bit (OCIE1A); // interrupt on Compare A Match
void setup()
{
TCCR1A = 0; // normal mode
TCCR1B = 0; // stop timer
TIMSK1 = 0; // cancel timer interrupt
activateInterrupt0 ();
} // end of setup
void loop()
{
// read sensors, compute time to fire spark
The graphic shows (ignore the switch bounce) that after the switch is closed there is a delay of 500 µS (between the green flags) and then a delay of
1999 µS while the spark is "on".
The sketch uses the digitalwritefast library to do quick writes to the spark pin. It also uses the ISR (INT0_vect) function rather than attachInterrupt,
because attachInterrupt takes longer to run.
https://code.google.com/p/digitalwritefast/
There is a tweaking figure of 4 µS (isrDelayFactor) which you can determine empirically (by measuring) to compensate for the few microseconds that it
takes to enter the ISR. By subtracting that from the required timer interval, the timing is more accurate.
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
Message
Camera shutter speed example
This example detects how long a camera shutter is open by using a change interrupt. At the first transition it gets the time, and at the second one it
gets the new time.
Of course this general concept could be applied to anything where you want to time a brief pulse. Tested down to a 50 µS pulse, but it could probably
go a bit shorter, as it takes around 5 µS to enter and leave an ISR.
started = !started;
} // end of shutter
void setup ()
{
Serial.begin (115200);
Serial.println ("Shutter test ...");
attachInterrupt (digitalPinToInterrupt (2), shutter, CHANGE);
} // end of setup
void loop ()
{
if (endTime)
{
Serial.print ("Shutter open for ");
Serial.print (endTime - startTime);
Serial.println (" microseconds.");
endTime = 0;
}
} // end of loop
Another example uses a laser beam to shine on a LDR (light dependent resistor). The laser is so powerful this simple circuit seemed to work OK:
The sketch below times intervals between RISING interrupts (the resistance goes up when less light shines on the LDR):
void isr ()
{
// wait until we noticed last one
if (triggered)
return;
void setup()
{
digitalWrite (2, HIGH); // pull-up
attachInterrupt(digitalPinToInterrupt (2), isr, RISING);
Serial.begin (115200);
Serial.println ("Started timing ...");
} // end of setup
void loop()
{
if (!triggered)
return;
lastTriggerTime = triggerTime;
triggered = false; // re-arm for next time
ms = elapsed / 1000;
elapsed -= ms * 1000;
Serial.print (minutes);
Serial.print ("m ");
Serial.print (seconds);
Serial.print ("s ");
Serial.print (ms);
Serial.println ("ms.");
} // end of loop
Example output:
I shone a cheap 1 mW laser pointer onto the LDR. The resistance is low with the light on it (I read about 1.9V on pin D2) which registers as a LOW
input. When you break the beam the resistance is high and the pin gets about 3V (being a HIGH). Thus the beam is broken on a RISING interrupt.
The code times the intervals between rising interrupts and reports them to the serial monitor. Of course you could use a LCD or something. There is a
test for short intervals (which might be the light passing through a part of the robot) so that if the time is less than a second it doesn't start timing
again.
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
Message
Read the Analog-to-Digital converter asynchronously
The code example below demonstrates reading the ADC converter (pin A0 in this case) whilst doing something else at the same time.
Each ADC conversion takes around 104 uS, and since that would be 1664 clock cycles (something like 832 instructions) it would be useful to do
something (like process the previous reading) while you are doing it.
void setup ()
{
Serial.begin (115200);
// set the analog reference (high two bits of ADMUX) and select the
// channel (low 4 bits). this also sets ADLAR (left-adjust result)
// to 0 (the default).
ADMUX = bit (REFS0) | (adcPin & 0x07);
} // end of setup
void loop ()
{
// if last reading finished, process it
if (adcDone)
{
adcStarted = false;
// do something with the reading, for example, print it
Serial.println (adcReading);
delay (500);
adcDone = false;
}
} // end of loop
Once the reading has started you continue to execute the main loop, checking to see if it completed in the ISR. Once it does you can process it, print it,
or whatever.
Another approach is to go to sleep during the reading. In this case, of course, you cannot do other things, but being asleep reduces digital noise inside
the chip, leading to a better analog conversion. The code below is similar to the above, but adds in a "SLEEP_MODE_ADC" sleep for a better
conversion.
#include <avr/sleep.h>
void setup ()
{
Serial.begin (115200);
// set the analog reference (high two bits of ADMUX) and select the
// channel (low 4 bits). this also sets ADLAR (left-adjust result)
// to 0 (the default).
ADMUX = bit (REFS0) | (adcPin & 0x07);
} // end of setup
void loop ()
{
// if last reading finished, process it
if (adcDone)
{
adcStarted = false;
adcDone = false;
}
} // end of loop
Also consider putting a 0.1 uF capacitor between the AREF and GND pins (adjacent to each other on the Uno board) to reduce noise in the analog
reference voltage.
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
Message
Pin Change Interrupts
Pin change interrupts let you detect changes to any of the Arduino pins. However they are a bit more fiddly to use than the external interrupts because
they are grouped into batches. So if the interrupt fires you have to work out in your own code exactly which pin caused the interrupt.
Example code:
ISR (PCINT0_vect)
{
// handle pin change interrupt for D8 to D13 here
} // end of PCINT0_vect
ISR (PCINT1_vect)
{
// handle pin change interrupt for A0 to A5 here
} // end of PCINT1_vect
ISR (PCINT2_vect)
{
// handle pin change interrupt for D0 to D7 here
} // end of PCINT2_vect
void setup ()
{
// pin change interrupt (example for D9)
PCMSK0 |= bit (PCINT1); // want pin 9
PCIFR |= bit (PCIF0); // clear any outstanding interrupts
PCICR |= bit (PCIE0); // enable pin change interrupts for D8 to D13
}
Specify which pin in the group. This is the PCMSKn variable (where n is 0, 1 or 2 from the table below). You can have interrupts on more than
one pin.
Enable the appropriate group of interrupts (0, 1 or 2)
Supply an interrupt handler as shown above
#include <avr/sleep.h>
ISR (PCINT1_vect)
{
// handle pin change interrupt for A0 to A5 here
// toggle LED
digitalWrite (LEDWAKE, !digitalRead (LEDWAKE));
} // end of PCINT1_vect
void setup ()
{
pinMode (LEDWAKE, OUTPUT);
pinMode (LEDLOOP, OUTPUT);
digitalWrite (A0, HIGH); // enable pull-up
} // end of setup
void loop ()
{
set_sleep_mode (SLEEP_MODE_PWR_DOWN);
sleep_mode ();
} // end of loop
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
Message
Critical sections ... accessing volatile variables
There are some subtle issues regarding variables which are shared between interrupt service routines (ISRs) and the main code (that is, the code not in
an ISR).
Since an ISR can fire at any time when interrupts are enabled, you need to be cautious about accessing such shared variables, as they may be being
updated at the very moment you access them.
A variable should only be marked volatile if it is used both inside an ISR, and outside one.
eg.
Atomic access
ISR (TIMER1_OVF_vect)
{
count = 10;
}
void setup ()
{
}
void loop ()
{
count++;
}
There are two danger points here, as marked. If the interrupt fires after the lds (load register 24 with the variable "count"), but before the sts (store
back into the variable "count") then the variable might be changed by the ISR (TIMER1_OVF_vect), however this change is now lost, because the
variable in the register was used instead.
We need to protect the shared variable by turning off interrupts briefly, like this:
ISR (TIMER1_OVF_vect)
{
count = 10;
} // end of TIMER1_OVF_vect
void setup ()
{
} // end of setup
void loop ()
{
noInterrupts ();
count++;
interrupts ();
} // end of loop
Now the update of "count" is done "atomically" ... that is, it cannot be interrupted.
Multi-byte variables
Let's make "count" a 2-byte variable, and see the other problem:
ISR (TIMER1_OVF_vect)
{
count++;
} // end of TIMER1_OVF_vect
void setup ()
{
pinMode (13, OUTPUT);
} // end of setup
void loop ()
{
if (count > 20)
digitalWrite (13, HIGH);
} // end of loop
OK, we are not changing count any more, so is there still a problem? Sadly, yes. Let's look at the generated code for the "if" statement:
Imagine that count was 0xFFFF and was about to "wrap around" back to zero. We load 0xFF into one register, but before we load the second 0xFF the
variable changes to 0x00. Now we think count is 0x00FF which is neither the value it had before (0xFFFF) or now (0x0000).
So again we have to "protect" the access to the shared variable, like this:
void loop ()
{
noInterrupts ();
if (count > 20)
digitalWrite (13, HIGH);
interrupts ();
} // end of loop
There is a final "gotcha" here. What if interrupts might be off already? Then turning them back on afterwards is a Bad Idea.
In that case you need to save the processor status register like this:
This "safe" code saves the current status of interrupts, turns them off (they may already be off), gets the shared variable into a temporary variable,
turns interrupts back on - if they were on when we entered the function - and then returns the copy of the shared variable.
Summary
Code may "appear to work" even if you don't take the above precautions. That is because the chances of the interrupt occurring at the exact wrong
moment is fairly low (maybe 1 in 100, depending on the code size). But sooner or later it will happen at the wrong moment, and your code will either
crash, or occasionally return the wrong results.
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
The various models of Arduino have somewhat confusing mappings of interrupt numbers to pins, and the correlation between attachInterrupt (0) is
not always to INT0.
The table below shows the ways they are mapped in the internal libraries:
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
Message
ATtiny85 sleep mode and wake on pin change
The sketch below illustrates putting the ATtiny85 to sleep, and then waking on a pin-change interrupt on D4 (pin 3 on the chip). The pin is put into
input-pullup mode so all you have to do is ground the pin to wake the chip.
Also see http://www.gammon.com.au/forum/?id=11497&reply=6#reply6 for an example of similar code which also wakes up on a watchdog timer
event.
ISR (PCINT0_vect)
{
// do something interesting here
}
void setup ()
{
pinMode (LED, OUTPUT);
pinMode (SWITCH, INPUT);
digitalWrite (SWITCH, HIGH); // internal pull-up
} // end of setup
void loop ()
{
digitalWrite (LED, HIGH);
delay (500);
digitalWrite (LED, LOW);
delay (500);
goToSleep ();
} // end of loop
void goToSleep ()
{
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
ADCSRA = 0; // turn off ADC
power_all_disable (); // power off ADC, Timer 0 and 1, serial interface
sleep_enable();
sleep_cpu();
sleep_disable();
power_all_enable(); // power everything back on
} // end of goToSleep
With the brownout disable turned off, the above sketch uses 500 nA of current when asleep, as predicted by the datasheet, page 183:
Fuses were:
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
Message
Timing an interval
I have seen example code suggesting you count things for a second by turning interrupts off for a second and then on again. I don't recommend this,
because for one thing, you can't do serial prints if interrupts are off.
void eventISR ()
{
events++;
} // end of eventISR
void setup ()
{
Serial.begin(115200);
attachInterrupt (digitalPinToInterrupt (2), eventISR, FALLING);
} // end of setup
void loop ()
{
events = 0; // reset counter
interrupts (); // allow interrupts
delay (INTERVAL); // wait desired time
noInterrupts(); // stop interrupts
That may work for simple situations, but turning interrupts off means that the Serial prints won't work until they are turned on again (which they may
not be in a more complex sketch).
It is better to use the millis() result and just detect when the time limit is up.
Improved sketch:
void eventISR ()
{
if (counting)
events++;
} // end of eventISR
void setup ()
{
Serial.begin (115200);
Serial.println ();
attachInterrupt (digitalPinToInterrupt (2), eventISR, FALLING);
} // end of setup
void showResults ()
{
Serial.print ("I counted ");
Serial.println (events);
} // end of showResults
void loop ()
{
if (counting)
{
// is time up?
if (millis () - startTime < INTERVAL)
return;
counting = false;
showResults ();
} // end of if
noInterrupts ();
events = 0;
startTime = millis ();
EIFR = bit (INTF0); // clear flag for interrupt 0
counting = true;
interrupts ();
} // end of loop
In loop() here we wait for the interval (1 second in this case) to be up, otherwise we return, effectively doing nothing. You could of course do other
things instead of just returning.
If counting is not currently active (and presuming we want it to be active) we remember the start time, reset the counter to zero, and let it start
counting up.
That code seemed to work OK up to 100 kHz, although the counts were getting a bit inaccurate. You can do more precise timings by using the
hardware counters / timers. Details here:
http://www.gammon.com.au/timers
- Nick Gammon
www.gammon.com.au, www.mushclient.com
top
The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).
To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.
769,989 views.
Refresh page
top
Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.
Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.
Comments to: Gammon Software support
Forum RSS feed ( https://gammon.com.au/rss/forum.xml )