Virtual printer & more fun with AVR interrupts

In building my AVR ATMEGA328P-based ‘virtual parallel printer‘, there were two signals that required special treatment. So it was time to revisit interrupts.

The virtual printer prototype

On a Centronics-style parallel port, the host machine sends an ‘init’ or reset signal to the printer to tell it to flush its buffers and set itself to the default state. It also sends a ‘strobe’ signal to tell the printer each time there’s a byte’s worth of data sitting on the data lines.

In both cases, these signals are active low – that is, the lines normally sit at the high level (ie, 5V) but are taken low to indicate a signal. And also in both cases, the line is taken low only for a moment before going high again – ie, the signal is a pulse.

On the virtual printer, I could have the code look for the /STROBE or /INIT lines being low as part of the main loop. But there’s always a risk that a brief pulse might get overlooked if it happens at the wrong part of the loop. This is a classic use for an interrupt.

Luckily, using interrupts is really quite easy on the ATMEGA chips. I’ve already given a brief introduction. But that dealt with ‘pin change’ interrupts. Pretty much any pin on an ATMEGA can be configured to trigger a pin change interrupt. But the limitation is that they are triggered whenever there’s any change on the pin – ie, it goes from low to high or vice versa.

With these lines on the virtual printer, I specifically want to trigger only when the line goes from high to low – that is, a falling edge. I don’t want them triggering a second time when the line reverts to high.

On the ATMEGA328P, there are two pins that you can configure to act in this manner – INT0 and INT1 – which are also known as PD2 and PD3.

Registering an interest

It’s time to start talking registers. With these interrupts there are two you absolutely have to know about and one that can be handy.

EICRA: The External Interrupt Control Register A is where you configure how the interrupts are triggered. Each of the two interrupts gets two bits: INT0 uses bits 0 and 1 (named ISC00 and ISC01), while INT1 uses bits 2 and 3 (named ISC10 and ISC11). Think of these in pairs.

Binary Value Condition that triggers interrupt
00 Low level on the pin
01 Any logic change
10 Falling edge
11 Rising edge

Let’s say we want to use INT1, and we want it triggered by a falling edge. We can set the EICRA using a binary value like this:

EICRA = 0b00001000;

As it happens, I want both interrupts to be triggered by falling edges, so the value I’ve used is:

EICRA = 0b00001010;

I could have used the named bits in the following way (assuming EICRA is currently set to 0, which it is at startup):

EICRA |= (1 << ISC11 | 1 << ISC01);

That’s a very AVR way of doing it. But personally, in this case I find just setting the binary value is simple and clear.

EIMSK: The External Interrupt Mask is what actually turns the interrupts on and off. It couldn’t be simpler. Set bit 0 to 1 to turn on INT0 and bit 1 to 1 to turn on INT1. Those bits also have the names INT0 and INT1. As, in my case, I want both interrupts on, I can do either of the following:

EIMSK = 0b00000011;                         // or…
EIMSK |= (1 << INT0 | 1 << INT1);

It’s worth noting that these pins will now trigger interrupts even if they’ve been configured as outputs.

All that’s left is to call sei(); in the code to enable interrupts. We’ll need two Interrupt Service Routine (ISR) functions to actually handle the interrupts. ISR functions have no return type and take, as the parameter, the relevant interrupt vector. In my case, these ISRs just set a couple of global boolean variables.

ISR(INT0_vect)
{
    data_waiting = true;
}

ISR(INT1_vect)
{
    initialise = true;
}

And that’s it – we’re all set.

Whoah, not so fast!

Actually, in my application, things got a little more complicated. Let me walk you through it. In fact, here’s some code.

#include <avr/io.h>
#include <avr/interrupt.h>    // need this for interrupts
#include <util/delay.h>

bool data_waiting = false;    // our globals
bool initialise = false;

ISR(INT0_vect)                // interrupt handling for INT0
{
	data_waiting = true;
}

ISR(INT1_vect)                // interrupt handling for INT1
{
	initialise = true;
}

int main(void)
{
    EICRA = 0b00001010;                 // INT0 and INT1 will trigger on falling edges
    EIMSK = (1 << INT0 | 1 << INT1);    // enables both INT1 and INT0 interrupts
    EIFR = (1 << INTF0 | 1 << INTF1);   // clear the interrupt flags
    sei();                              // enable interrupts

    bool runloop = true;
    while (runloop) {
        if(data_waiting) {
            cli();                      // disable interrupts
            /* ... DO STUFF ... */
            data_waiting = false;
            EIFR |= (1 << INTF0);       // clear interrupt flag in case of bounce
            sei();                      // reenable interrupts
        }

        if(initialise) {
            cli();                      // disable interrupts
            /* ... DO STUFF ... */
            initialise = false;
            EIFR |= (1 << INTF1);       // clear interrupt flag in case of bounce
            sei();                      // reenable interrupts
        }
    }
}

That’s not the whole thing – just the relevant lines. In the two places where you see ‘do stuff’, that stuff takes a moderately long time – over a second in the case of the initialisation routine.

The ISRs contain very little code – in each case just setting the value of a global variable. This is considered good practice; you’re supposed to get out of ISRs as fast as you can. But in this case, I’m not convinced this brevity is wholly necessary. In each case, the code that deals with either data_waiting or initialise being true – the ‘do stuff’ blocks – is meant to be blocking. And I don’t want any other interrupts happening while I’m there.

To that end, each block starts with globally disabling interrupts – cli() – and ends with enabling them again – sei(). That wasn’t enough for my test rig, though. I was getting a lot of repeated calls to these routines, almost certainly due to switch bounce. The final device won’t have a switch for the /STROBE line and any switch I used for /INIT will be debounced (probably the Schmitt trigger way), but the bounces were annoying me.

So this is where we meet another register.

EIFR: The External Interrupt Flag Register notes whether an interrupt has been triggered. Bits 0 (aka INTF0) and 1 (known as INTF1) are set to 1 if the INT0 or INT 1 interrupts have been triggered. They get cleared again when the relevant ISR completes. But (and this is slightly odd and counterintuitive) you can clear each one manually by writing a 1 to it. It’s weird, but Microchip explains it thus:

Writing a logical 1 to it requires only a single OUT instruction, and it is clear that only this single interrupt request bit will be cleared. There is no need to perform a read-modify-write cycle (like, an SBI instruction), since all bits in these control registers are interrupt bits, and writing a logical 0 to the remaining bits (as it is done by the simple OUT instruction) will not alter them, so there is no risk of any race condition that might accidentally clear another interrupt request bit

So it’s all to do with the efficiency and safety of the underlying code.

This is what I do in each of the ‘do stuff’ blocks of code and it seemed to help a lot with debouncing the buttons (although not perfectly).

This, and the use of cli() and sei() in the code blocks, may become unecessary if I move all the ‘do stuff’ code into the ISRs, which I’m likely to at least try.

I also clear the EIFR flags in the setup routine because the interrupts were getting triggered when the code first ran, no doubt due to the signal levels having to settle down.

You can easily overdo the use of interrupts. By and large, I prefer polling in the main loop as it’s easy to see what’s going on and understand the structure of the program. But when interrupts are necessary, it’s good to know they’re so simple to use.

Leave a Reply

Your email address will not be published. Required fields are marked *