# What's the maximum time an interrupt service routine can take to execute on ATmega328P?

I have an ATmega328P that checks if a button was pressed via pin change interrupts. Now, I want to turn on an LED for 200 ms. Can I just turn the LED on, wait 200 ms and turn it back off in the ISR like in the following code?

ISR(PCINT1_vect)
{
if(PINB & 0b1)
{
PORT = 0b10;
_delay_ms(200);
PORT = 0;
}
}


In a few forum posts on AVR Freaks, I've read that you shouldn't spend much time in an ISR, but I've never seen any exact numbers. I sadly can't find those posts anymore, so I can't link them. As far as I can remember, they all said, that if you spent to much time in the ISR, the microcontroller might crash. Is that true? And if so, is there an exact time limit after that this might happen?

## 5 Answers

If nothing else is running in the MCU, then you are free to take as long as you like in the ISR. But, this is a bad habit to get into, and it means that if you want to do anything else, you'll likely have to rework the code.

A particular case is if the MCU is using a serial library, that expects interrupts to be working often enough to service individual characters received. At 115,200 baud (a high serial speed often used to minimise download time), there is less than 100 µs between characters. If you block the interrupts for longer than that, you risk losing input characters.

As a general rule, do the absolute minimum in an ISR. In your application, a reasonable design would be to have an interrupt every ms, which increments and checks a counter value. I'm sure you can work out some suitable logic to set and test the counter to get 200 ms between turn on and turn off events.

In the worst case, an ISR can run until the next interrupt of the same type occurs again.

But in general, it's poor design practice to spend more time in an ISR than absolutely necessary, because it prevents all other code from running at all. This is a big issue for anything other than trivial programs.

• I'd say in the worst case an ISR can run indefinitely, preventing the next IRQ from occurring again. – Dmitry Grigoryev Jul 12 at 8:57
• @DmitryGrigoryev: In this case, I meant "worst case" in terms of NOT having the system fail immediately. – Dave Tweed Jul 12 at 11:27

While the practice is to allocate the minimum possible execution cycles inside an interruption, and beside other general hardware specifications, there are not technical limitations for increasing them, if there are not any other interruption to be executed.

At attachInterrupt() Arduino Reference:

Generally, an ISR should be as short and fast as possible. If your sketch uses multiple ISRs, only one can run at a time, other interrupts will be executed after the current one finishes in an order that depends on the priority they have. millis() relies on interrupts to count, so it will never increment inside an ISR. Since delay() requires interrupts to work, it will not work if called inside an ISR. micros() works initially but will start behaving erratically after 1-2 ms. delayMicroseconds() does not use any counter, so it will work as normal.

Having 25 possible interruptions in this processor family, it is encouraged to deal with them like punctual events, for allowing other interruptions to happen.

• While delay in an ISR is generally a bad idea, the question uses what appears to be avr-gcc's _delay_ms() which is cycles based unlike the Arduino delay() function which relies on the timer interrupt. – Chris Stratton Jul 11 at 14:08

Technically, it's not forbidden to even start an infinite loop inside an interrupt, so there is no top limit on ISR execution time. However, interrupts are most useful when you want to, well, interrupt your normal program flow to take a short action which must be carried out immediately, and the qualifiers "short" and "immediately" are naturally related: if your longest ISR takes 1ms, then an incoming interrupt of the same priority will have 1ms response time. So in the essence your ISR execution time is limited by the desired IRQ response time.

If your program spends a long time waiting in an interrupt, it may be easier to drop interrupts completely and use polling, which makes programming substantially easier.

There are cases where you can abuse interrupts to run more or less regular code. One example would be to implement task priority: a low-priority task is started from the main loop, while a higher-priority task is a periodically-triggered timer interrupt. This is usually done on MCUs with several IRQ priority levels, so that the system can still have regular ISRs when needed.

You have a single-core, single-threaded CPU. This means that at any given time, it's doing exactly one thing. If your application requires it to do several things, then the code has to be designed to switch between all of them. This can be as trivial or as complex as you like.

Normally, the CPU putters around the main loop, doing whatever is in there, and that's all well and good until an interrupt occurs. The interrupt hardware basically forces a function call to the appropriate ISR, even though there's no call instruction for it in the main loop. This unpredictability is where most of the rules come from for writing ISR's.

Whatever time you spend in an ISR is time that the main loop is paused, waiting for the ISR to return. If the main loop is the only one to reset an active watchdog timer (very good practice), then the watchdog will not be reset during that time. If the watchdog times out, it gives you a hard reset. Just like the external reset, but with different flags that you can check on startup. This is probably the "crash" that you heard of.

It's very good practice to use the watchdog, and to only reset it once each trip around the main loop. This forces you to write code that stays responsive. If you need to wait for something, you can set up an event (timer finished, next character received, etc.), and move on. Check periodically for that event or set another interrupt for its completion, and get back to it then. Meanwhile, you continue with whatever else you were doing.

My main structure is typically something like this:

#include "module1.h"
#include "module2.h"

void main(void)
{
//overall
//chip
//setup

mod1_init();
mod2_init();

//clear interrupt flags
//global interrupt enable

while(1)
{
//clear watchdog

mod1_run();
mod2_run();
}
}


And my modules are like this:

void modX_init(void)
{
//hardware and variable init for this module only
//don't use interrupts if polling is good enough
}

void modX_run(void)
{
if (POLLED_INTERRUPT_FLAG)
{
POLLED_INTERRUPT_FLAG = 0;

//non-blocking "ISR" code
}
}

void ISR modX_ISR(void)
{
//okay, this does require an *immediate* response
//spend the absolute minimum time here and get out
}


The function signatures don't have to be void, but most of them are. Sometimes I'll have some broad timing in one module that is also used by another, and it's handy to use the return value of one modX_run() and the arguments of another (or some basic logic) to make that connection. For example:

if (DMX_run())      //includes its own timing, and returns true at the start of each 30Hz interval, otherwise false
{
I2C_start();    //I2C frames are sync'ed to DMX
}
I2C_run();          //once started, an I2C frame runs freely until finished


If you study the datasheet, you may also find that the hardware peripherals can be massaged to do what you want without any CPU intervention at all.

Output pulse generation, for example, is a common one. Turn it on, set the peripheral to turn it off some time later, and forget about it. It's typically in the same general area as PWM.