9

I use the interrupt function to fill an array with values received from digitalRead().

void setup() {
    Serial.begin(115200);
    attachInterrupt(0, test_func, CHANGE);
}

void test_func() {
    if (digitalRead(pin) == HIGH) {
        test_array[x] = 1;
    } else if(digitalRead(pin) == LOW) {
        test_array[x] = 0;
    }
    x = x + 1;
}

That problem is that when I print test_array there are values like: 111 or 000.

As I understand it, if I use the CHANGE option in the attachInterrupt() function, then the data sequence should always be 0101010101 without repeating.

The data changes quite fast since it comes from a radio module.

6
  • 2
    Interrupts don't debounce the button. Are you using hardware debouncing? Feb 20, 2015 at 1:07
  • 1
    Please post the complete code, including pin, x and test_array definition, and also loop() method; it would enable us to see if this can be a concurrency issue when accessing variables modified by test_func.
    – jfpoilpret
    Feb 20, 2015 at 5:36
  • 3
    You should not digitalRead() twice in the ISR: think about what would happen if you get LOW on the first call and HIGH on the second. Instead, if (digitalRead(pin) == HIGH) ... else ...; or, better yet, this single-line ISR: test_array[x++] = digitalRead(pin);. Feb 23, 2015 at 16:54
  • @EdgarBonet nice! +1 to that comment. I hope you don't mind I've added something to my answer to include what you've mentioned here. Also if you decide to put up your own answer including this detail then I'll remove my addition and give yours an up vote so you get the rep for it. Feb 27, 2015 at 11:53
  • 1
    @Clayton Mills: I am preparing an (overly long and marginally tangential) answer, but you can keep your edit, it is perfectly fine with me. Feb 27, 2015 at 18:16

2 Answers 2

29
+50

As a sort of prologue to this overly long answer...

This question got me deeply captivated with the problem of interrupt latency, to the point of losing sleep in counting cycles instead of sheep. I am writing this response more for sharing my findings than for just answering the question: most of this material may actually not be at a level suitable for a proper answer. I hope it will be useful, however, for readers that land here in search of solutions for latency problems. The first few sections are expected to be useful to a wide audience, including the original poster. Then, it gets hairy along the way.

Clayton Mills already explained in his answer that there is some latency in responding to interrupts. Here I will focus on quantifying the latency (which is huge when using the Arduino libraries), and on the means to minimize it. Most of what follows is specific to the hardware of the Arduino Uno and similar boards.

Minimizing the interrupt latency on the Arduino

(or how to get from 99 down to 5 cycles)

I will use the original question as a working example, and restate the problem in terms of interrupt latency. We have some external event that triggers an interrupt (here: INT0 on pin change). We need to take some action when the interrupt is triggered (here: read a digital input). The problem is: there is some delay between the interrupt being triggered and our taking the appropriate action. We call this delay "interrupt latency". A long latency is detrimental in many situations. In this particular example, the input signal may change during the delay, in which case we get a faulty reading. There is nothing we can do to avoid the delay: it is intrinsic to the way interrupts work. We can, however, try to make it as short as possible, which should hopefully minimize the bad consequences.

The first obvious thing we can do is take the time-critical action, inside the interrupt handler, as soon as possible. This means calling digitalRead() once (and only once) at the very beginning of the handler. Here is the zeroth version of the program upon which we will build:

#define INT_NUMBER 0
#define PIN_NUMBER 2    // interrupt 0 is on pin 2
#define MAX_COUNT  200

volatile uint8_t count_edges;  // count of signal edges
volatile uint8_t count_high;   // count of high levels

/* Interrupt handler. */
void read_pin()
{
    int pin_state = digitalRead(PIN_NUMBER);  // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (pin_state == HIGH) count_high++;
}

void setup()
{
    Serial.begin(9600);
    attachInterrupt(INT_NUMBER, read_pin, CHANGE);
}

void loop()
{
    /* Wait for the interrupt handler to count MAX_COUNT edges. */
    while (count_edges < MAX_COUNT) { /* wait */ }

    /* Report result. */
    Serial.print("Counted ");
    Serial.print(count_high);
    Serial.print(" HIGH levels for ");
    Serial.print(count_edges);
    Serial.println(" edges");

    /* Count again. */
    count_high = 0;
    count_edges = 0;  // do this last to avoid race condition
}

I tested this program, and the subsequent versions, by sending it trains of pulses of varying widths. There is enough spacing between the pulses to ensure that no edge is missed: even if the falling edge is received before the previous interrupt is done, the second interrupt request will be put on hold and eventually serviced. If a pulse is shorter than the interrupt latency, the program reads 0 on both edges. The reported number of HIGH levels is then the percentage of correctly read pulses.

What happens when the interrupt is triggered?

Before trying to improve the code above, we will take a look at the events that unfold right after the interrupt is triggered. The hardware part of the story is told by the Atmel documentation. The software part, by disassembling the binary.

Most of the time, the incoming interrupt is serviced right away. It may happen, however, that the MCU (meaning "microcontroller") is in the middle of some time-critical task, where interrupt servicing is disabled. This is typically the case when it is already servicing another interrupt. When this happens, the incoming interrupt request is put on hold and serviced only when that time-critical section is done. This situation is hard to avoid completely, because there are quite a few of those critical sections in the Arduino core library (which I will call "libcore" in the following). Fortunately, these sections are short and run only every so often. Thus, most of the time, our interrupt request will be serviced right away. In the following, I will assume that we do not care about those few instances when this is not the case.

Then, our request is serviced immediately. This still involves a lot of stuff that can take quite a while. First, there is a hardwired sequence. The MCU will finish executing the current instruction. Fortunately, most instructions are single-cycle, but some can take up to four cycles. Then, the MCU clears an internal flag that disables further servicing of interrupts. This is intended to prevent nested interrupts. Then, the PC is saved into the stack. The stack is an area of RAM reserved for this kind of temporary storage. The PC (meaning "Program Counter") is an internal register holding the address of the next instruction the MCU is about to execute. This is what allows the MCU to know what to do next, and saving it is essential because it will have to be restored in order for the main program to resume from where it was interrupted. The PC is then loaded with a hardwired address specific to the request received, and this is the end of the hardwired sequence, the rest being software-controlled.

The MCU now executes the instruction from that hardwired address. This instruction is called an "interrupt vector", and is generally a "jump" instruction that will bring us to a special routine called an ISR ("Interrupt Service Routine"). In this case, the ISR is called "__vector_1", a.k.a. "INT0_vect", which is a misnomer because it is an ISR, not a vector. This particular ISR comes from libcore. Like any ISR, it starts with a prologue that saves a bunch of internal CPU registers on the stack. This will allow it to use those registers and, when it is done, restore them to their previous values in order not to disturb the main program. Then, it will look for the interrupt handler that was registered with attachInterrupt(), and it will call that handler, which is our read_pin() function above. Our function will then call digitalRead() from libcore. digitalRead() will look into some tables in order to map the Arduino port number to the hardware I/O port it has to read and the associated bit number to test. It will also check whether there is a PWM channel on that pin that would need to be disabled. It will then read the I/O port... and we are done. Well, we are not really done servicing the interrupt, but the time-critical task (reading the I/O port) is done, and it is all that matters when we are looking at latency.

Here is a short summary of all the above, together with the associated delays in CPU cycles:

  1. hardwired sequence: finish current instruction, prevent nested interrupts, save PC, load address of vector (≥ 4 cycles)
  2. execute interrupt vector: jump to ISR (3 cycles)
  3. ISR prologue: save registers (32 cycles)
  4. ISR main body: locate and call user-registered function (13 cycles)
  5. read_pin: call digitalRead (5 cycles)
  6. digitalRead: find the relevant port and bit to test (41 cycles)
  7. digitalRead: read the I/O port (1 cycle)

We will assume the best case scenario, with 4 cycles for the hardwired sequence. This gives us a total latency of 99 cycles, or about 6.2 µs with a 16 MHz clock. In the following, I will explore some tricks that can be used to lower this latency. They come roughly in increasing order of complexity, but they all need us to somehow dig into the internals of the MCU.

Use direct port access

The obvious first target for shortening the latency is digitalRead(). This function provides a nice abstraction to the MCU hardware, but it is too inefficient for time-critical work. Getting rid of this one is actually trivial: we just have to replace it with digitalReadFast(), from the digitalwritefast library. This cuts the latency almost by half at the cost of a small download!

Well, that was too easy to be any fun, I will rather show you how to do it the hard way. The purpose is to get us started into low-level stuff. The method is called "direct port access" and is nicely documented on the Arduino reference at the page on Port Registers. At this point, it is a good idea to download and take a look at the ATmega328P datasheet. This 650-page document may seem somewhat daunting at first look. It is, however, well organised into sections specific to each of the MCU peripherals and features. And we only need to check the sections relevant to what we are doing. In this case, it is the section named I/O ports. Here is a summary of what we learn from those readings:

  • The Arduino pin 2 is actually called PD2 (i.e. port D, bit 2) on the AVR chip.
  • We get the whole port D at once by reading a special MCU register called "PIND".
  • We then check bit number 2 by doing a bitwise logical and (the C ‘&’ operator) with 1 << 2.

So, here is our modified interrupt handler:

#define PIN_REG    PIND  // interrupt 0 is on AVR pin PD2
#define PIN_BIT    2

/* Interrupt handler. */
void read_pin()
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Now, our handler will read the I/O register as soon as it is called. The latency is 53 CPU cycles. This simple trick saved us 46 cycles!

Write your own ISR

The next target for cycle-trimming is the INT0_vect ISR. This ISR is needed for providing the functionality of attachInterrupt(): we can change interrupt handlers at any time during program execution. However, although nice to have, this is not really useful for our purpose. Thus, instead of having the libcore's ISR locate and call our interrupt handler, we will save a few cycles by replacing the ISR by our handler.

This is not as hard as it sounds. ISRs can be written like normal functions, we just have to be aware of their specific names, and define them using a special ISR() macro from avr-libc. At this point it would be good to have a look at the avr-libc's documentation on interrupts, and at the datasheet section named External Interrupts. Here is the short summary:

  • We have to write a bit in a special hardware register called EICRA (External Interrupt Control Register A) in order to configure the interrupt to be triggered on any change of the pin value. This will be done in setup().
  • We have to write a bit in another hardware register called EIMSK (External Interrupt MaSK register) in order to enable the INT0 interrupt. This will also be done in setup().
  • We have to define the ISR with the syntax ISR(INT0_vect) { ... }.

Here is the code for the ISR and setup(), everything else is unchanged:

/* Interrupt service routine for INT0. */
ISR(INT0_vect)
{
    uint8_t sampled_pin = PIN_REG;            // do this first!
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

void setup()
{
    Serial.begin(9600);
    EICRA = 1 << ISC00;  // sense any change on the INT0 pin
    EIMSK = 1 << INT0;   // enable INT0 interrupt
}

This comes with a free bonus: since this ISR is simpler than the one it replaces, it needs less registers to do its job, then the register-saving prologue is shorter. Now we are down to a latency of 20 cycles. Not bad considering that we started close to 100!

At this point I would say we are done. Mission accomplished. What follows is only for those who are not afraid of getting their hands dirty with some AVR assembly. Otherwise you can stop reading here, and thank-you for getting so far.

Write a naked ISR

Still here? Good! For proceeding further, it would be helpful to have at least some very basic idea of how assembly works, and to take a look at the Inline Assembler Cookbook from the avr-libc documentation. At this point, our interrupt entry sequence looks like this:

  1. hardwired sequence (4 cycles)
  2. interrupt vector: jump to ISR (3 cycles)
  3. ISR prologue: save regs (12 cycles)
  4. first thing in the ISR body: read the IO port (1 cycle)

If we want to do better, we have to move the reading of the port into the prologue. The idea is the following: reading the PIND register will clobber one CPU register, thus we need to save at least one register before doing that, but the other registers can wait. We then need to write a custom prologue that reads the I/O port right after saving the first register. You have already seen in the avr-libc interrupt documentation (you have read it, right?) that an ISR can be made naked, in which case the compiler will emit no prologue or epilogue, allowing us to write our own custom version.

The problem with this approach is that we will probably end up writing the whole ISR in assembly. Not a big deal, but I would rather have the compiler write those boring prologues and epilogues for me. So, here is the dirty trick: we will split the ISR in two parts:

  • the first part will be a short assembly fragment that will
    • save a single register to the stack
    • read PIND into that register
    • store that value into a global variable
    • restore the register from the stack
    • jump to the second part
  • the second part will be regular C code with compiler-generated prologue and epilogue

Our previous INT0 ISR is then replaced by this:

volatile uint8_t sampled_pin;    // this is now a global variable

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    push r0                \n"  // save register r0
    "    in r0, %[pin]          \n"  // read PIND into r0
    "    sts sampled_pin, r0    \n"  // store r0 in a global
    "    pop r0                 \n"  // restore previous r0
    "    rjmp INT0_vect_part_2  \n"  // go to part 2
    :: [pin] "I" (_SFR_IO_ADDR(PIND)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges >= MAX_COUNT) return;     // we are done
    count_edges++;
    if (sampled_pin & (1 << PIN_BIT)) count_high++;
}

Here we are using the ISR() macro to have the compiler instrument INT0_vect_part_2 with the required prologue and epilogue. The compiler will complain that "‘INT0_vect_part_2’ appears to be a misspelled signal handler", but the warning can be safely ignored. Now the ISR has a single 2-cycle instruction before the actual port read, and the total latency is only 10 cycles.

Use the GPIOR0 register

What if we could have a register reserved for this specific job? Then, we would not need save anything before reading the port. We can actually ask the compiler to bind a global variable to a register. This, however, would require us to recompile the whole Arduino core and libc in order to make sure the register is always reserved. Not really convenient. On the other hand, the ATmega328P happens to have three registers that are not used by the compiler nor any library, and are available for storing whatever we want. They are called GPIOR0, GPIOR1 and GPIOR2 (General Purpose I/O Registers). Although they are mapped in the I/O address space of the MCU, these are actually not I/O registers: they are just plain memory, like three bytes of RAM that somehow got lost in a bus and ended up in the wrong address space. These are not as capable as the internal CPU registers, and we cannot copy PIND into one of these with the in instruction. GPIOR0 is interesting, though, in that it is bit-addressable, just like PIND. This will allow us to transfer the information without clobbering any internal CPU register.

Here is the trick: we will make sure that GPIOR0 is initially zero (it is actually cleared by hardware at boot time), then we will use the sbic (Skip next instruction if some Bit in some I/o register is Clear) and the sbi (Set to 1 some Bit in some I/o register) instructions as follows:

sbic PIND, 2   ; skip the following if bit 2 of PIND is clear
sbi GPIOR0, 0  ; set to 1 bit 0 of GPIOR0

This way, GPIOR0 will end up being 0 or 1 depending on the bit we wanted to read from PIND. The sbic instruction takes 1 or 2 cycles to execute depending on whether the condition is false or true. Obviously, the PIND bit is accessed on the first cycle. In this new version of the code, the global variable sampled_pin is not useful anymore, since it is basically replaced by GPIOR0:

/* Interrupt service routine for INT0. */
ISR(INT0_vect, ISR_NAKED)
{
    asm volatile(
    "    sbic %[pin], %[bit]    \n"
    "    sbi %[gpio], 0         \n"
    "    rjmp INT0_vect_part_2  \n"
    :: [pin]  "I" (_SFR_IO_ADDR(PIND)),
       [bit]  "I" (PIN_BIT),
       [gpio] "I" (_SFR_IO_ADDR(GPIOR0)));
}

ISR(INT0_vect_part_2)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

It should be noted that GPIOR0 has to always be reset in the ISR.

Now, the sampling of the PIND I/O register is the very first thing done inside the ISR. Total latency is 8 cycles. This is about the best we can do before getting stained with terribly sinful kludges. This is again a good opportunity to stop reading...

Put the time-critical code in the vector table

For those still here, here is our current situation:

  1. hardwired sequence (4 cycles)
  2. interrupt vector: jump to ISR (3 cycles)
  3. ISR body: read the IO port (on 1st cycle)

There is obviously little room for improvement. The only way we could shorten the latency at this point is by replacing the interrupt vector itself by our code. Be warned that this should be immensely distasteful to anyone who values clean software design. But it is possible, and I will show you how.

The layout of the ATmega328P vector table can be found in the datasheet, section Interrupts, subsection Interrupt Vectors in ATmega328 and ATmega328P. Or by disassembling any program for this chip. Here is how it looks like. I am using avr-gcc and avr-libc's conventions (__init is vector 0, addresses are in bytes) which are different from Atmel's.

address │ instruction     │ comment
────────┼─────────────────┼──────────────────────
 0x0000 │ jmp __init      │ reset vector 
 0x0004 │ jmp __vector_1  │ a.k.a. INT0_vect
 0x0008 │ jmp __vector_2  │ a.k.a. INT1_vect
 0x000c │ jmp __vector_3  │ a.k.a. PCINT0_vect
  ...
 0x0064 │ jmp __vector_25 │ a.k.a. SPM_READY_vect

Each vector has a 4-byte slot, filled with a single jmp instruction. This is a 32-bit instruction, unlike most AVR instructions which are 16-bit. But a 32-bit slot is too small to hold the first part of our ISR: we can fit the sbic and sbi instructions, but not the rjmp. If we do that, the vector table ends up looking like this:

address │ instruction     │ comment
────────┼─────────────────┼──────────────────────
 0x0000 │ jmp __init      │ reset vector 
 0x0004 │ sbic PIND, 2    │ the first part...
 0x0006 │ sbi GPIOR0, 0   │ ...of our ISR
 0x0008 │ jmp __vector_2  │ a.k.a. INT1_vect
 0x000c │ jmp __vector_3  │ a.k.a. PCINT0_vect
  ...
 0x0064 │ jmp __vector_25 │ a.k.a. SPM_READY_vect

When INT0 fires, PIND will be read, the relevant bit will be copied into GPIOR0, and then the execution will fall through to the next vector. Then, the ISR for INT1 will be called, instead of the ISR for INT0. This is creepy, but since we are not using INT1 anyway, we will just "hijack" its vector for servicing INT0.

Now, we just have to write our own custom vector table to override the default one. It turns out it is not so easy. The default vector table is provided by the avr-libc distribution, in an object file called crtm328p.o that is automatically linked with any program we build. Unlike library code, object-file code is not meant to be overridden: trying to do that will give a linker error about the table being defined twice. This means we have to replace the whole crtm328p.o with our custom version. One option is to download the full avr-libc source code, do our custom modifications in gcrt1.S, then build this as a custom libc.

Here I went for a lighter, alternative approach. I wrote a custom crt.S, which is a simplified version of the original from avr-libc. It lacks a few rarely used features, like the ability to define a "catch all" ISR, or to be able to terminate the program (i.e. freeze the Arduino) by calling exit(). Here is the code. I trimmed the repetitive part of the vector table in order to minimize scrolling:

#include <avr/io.h>

.weak __heap_end
.set  __heap_end, 0

.macro vector name
    .weak \name
    .set \name, __vectors
    jmp \name
.endm

.section .vectors
__vectors:
    jmp __init
    sbic _SFR_IO_ADDR(PIND), 2   ; these 2 lines...
    sbi _SFR_IO_ADDR(GPIOR0), 0  ; ...replace vector_1
    vector __vector_2
    vector __vector_3
    [...and so forth until...]
    vector __vector_25

.section .init2
__init:
    clr r1
    out _SFR_IO_ADDR(SREG), r1
    ldi r28, lo8(RAMEND)
    ldi r29, hi8(RAMEND)
    out _SFR_IO_ADDR(SPL), r28
    out _SFR_IO_ADDR(SPH), r29

.section .init9
    jmp main

It can be compiled with the following command line:

avr-gcc -c -mmcu=atmega328p silly-crt.S

The sketch is identical to the previous one except that there is no INT0_vect, and INT0_vect_part_2 is replaced by INT1_vect:

/* Interrupt service routine for INT1 hijacked to service INT0. */
ISR(INT1_vect)
{
    if (count_edges < MAX_COUNT) {
        count_edges++;
        if (GPIOR0) count_high++;
    }
    GPIOR0 = 0;
}

To compile the sketch, we need a custom compile command. If you have followed so far, you probably know how to compile from the command line. You have to explicitly request silly-crt.o to be linked to your program, and add the -nostartfiles option to avoid linking in the original crtm328p.o.

Now, the reading of the I/O port is the very first instruction executed after the interrupt triggers. I tested this version by sending it short pulses from another Arduino, and it can catch (although not reliably) the high level of pulses as short as 5 cycles. There is nothing more we can do to shorten the interrupt latency on this hardware.

4
  • 2
    Good explanation! +1
    – Nick Gammon
    Jul 21, 2015 at 7:55
  • (For a future edit: 1) "loosing sleep""losing sleep" 2) "Skip next""skip next". Otherwise excellent writing. The (misleading) indentation in the code in the question could be fixed at the same time.) Aug 22, 2022 at 16:22
  • @PeterMortensen: Thanks for the correction! I kept the Uppercase “S” in “Skip next” in order to show where the acronym sbic comes from. Aug 22, 2022 at 20:31
  • ...Some say he's still optimizing that interrupt vector to this day...
    – user515655
    May 22, 2023 at 16:05
6

The interrupt is being set to fire on a change, and your test_func is set as the Interrupt Service Routine (ISR), called to service that interrupt. The ISR then prints the value of the input.

At first glance you would expect the output to be as you have said, and alternating set of high lows, as it only gets to the ISR on a change.

But what we're missing is that there is a certain amount of time that it takes for the CPU to service an interrupt and branch to the ISR. During this time the voltage on the pin may have changed again. Particularly if the pin is not stabilised by hardware de-bouncing or similar. Because the interrupt is already flagged and hasn't been serviced yet, this extra change (or many of them, because a pins level can change very quickly relative to the clock speed if it has low parasitic capacitance) will be missed.

So in essence without some form of de-bouncing, we have no guarantee that when the input changes, and the interrupt gets marked for servicing, that the input will still be at the same value when we get to reading its value in the ISR.

As a generic example the ATmega328 Datasheet used on the Arduino Uno details interrupt times in section 6.7.1 - "Interrupt Response Time". It states for this microcontroller the minimum time to branch to an ISR for servicing is 4 clock cycles, but can be more (extra if executing multi-cycle instruction at interrupt or 8 + sleep wake time if the MCU is in sleep.)

As @EdgarBonet mentioned in comments, the pin could also similarly change during ISR execution. Because the ISR reads from the pin twice, it would not add anything to the test_array if it encountered a LOW on the first read and a HIGH on the second. But x would still increment, leaving that slot in the array unchanged (possibly as uninitialised data depending on what was done to the array earlier).

His one line ISR of test_array[x++] = digitalRead(pin); is a perfect solution to this.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.