Proton BASIC Compiler - The Beginners Guide to Proton® Hardware Interrupts


  • Pic® Basic


  • The Beginners Guide to Proton® Hardware Interrupts

    The Beginners Guide to Proton® Hardware Interrupts

    By Harm de Vries (hadv215 on the forum)

    Introduction

    This article was written for a number of reasons, the most important being that interrupts can be pretty scary.
    The Proton® compiler supports both hardware and software interrupts, this article deals with hardware interrupts only.

    The intended audience for this article is users that want to start using interrupts and already have some experience programming with Proton®.
    Knowing how to read a datasheet is important because most of the things you will need in this type of work are contained in these documents.
    Understanding the role of the Special Function Registers (SFR) is important, they do much of the work.

    The structure of this article is as follows:
    • It starts with an explanation of what an interrupt is and why you want to use it.
    • Next two important types of interrupts are discussed.
    • A word of caution to prevent things from getting ugly.
    • The basic principles of using interrupts.
    • Writing a program using interrupts including some examples.
    • The unavoidable tips & tricks.
    • A discussion of a number of details of the interrupt operation.
    • Interrupts on 16 bit devices: a summary
    Many thanks to John Drew and John G. Barrat for taking the time to read all this and give me feedback.

    What is an interrupt & why would I want to use it in the first place

    What

    An interrupt can best be described as a hardware event the program has to respond to immediately, outside the normal flow of operation.
    Handling an interrupt means that the normal flow of operation, the 'main' program, is temporarily suspended (hence the name Interrupt) and a dedicated routine, called an Interrupt Service Routine or ISR, is executed. After the ISR has ended the microcontroller resumes normal flow of operation directly after the point at which it was interrupted.
    There are basically two sources of events:
    - an internal source: this can be compared to you feeling hungry and eat something before going on. A typical example is a timer interrupt.
    - an external source: this can be compared to the telephone ringing when you are busy on something: you stop what you are doing, pick up the phone, have a conversation, put down the phone again and continue what you were doing. Typical examples are receiving a byte in the UART and pins changing state.

    Why

    Although the events the program has to respond to are known by the programmer, the programmer does not know at what point in the program this event will take place.
    The latter is crucial. There is a big difference between waiting for a button to be pushed and responding whenever a button is pushed.
    For many applications it is fine to test the state of a button constantly in the main program loop. The drawback of this approach is that the button press will only be detected when the program reaches the point where it is tested. This may lead to a user continually pressing the button because the program appears not to detect it.

    That's where the interrupt will do the trick by instructing the microcontroller to jump to a routine handling the press of the button at any moment it happens.

    Types of interrupts

    There are a number of types of interrupts, each serving a special purpose.
    I will discuss two of the most commonly used interrupts. There are three reasons for this:
    - they have no Proton® specific implementation
    - they are available on all devices I know
    - they are pretty useful.

    The External interrupt

    This type of interrupt is triggered when an input pin changes from high to low or from low to high.
    There are two versions of this interrupt : the 'original' external interrupt which will only fire if the pin changes to a specific state and the 'interrupt on change' that will fire with both high-to-low and low-to-high changes.

    This type of interrupt is very handy when the program has to react to an external source.
    Example: many devices have a 'Sleep' mode. When the device enters this mode it will almost stop operating and consume very little power. Using an External interrupt, the device can be awakened and resume the operation where it left off.
    I use this in a circuit that is battery powered and must be switched on or off by a simple momentary switch. I could have used a slide switch of course, but the switch is hidden under some fabric.
    Another application is a circuit monitoring a number of digital signals and responding on these signals.
    Of course it could be achieved within the main program loop, but that may not always be the best solution if the application has to perform additional and/or time consuming functions.

    The Timer interrupt

    A timer interrupt occurs when an internal timer overflows. When using an 8 bit timer this means that the interrupt will occur when the timer goes from 255 to 0, for a 16 bit timer it is when the timer goes from 65535 to 0.
    This means that, when a timer is 'on' it will overflow again and again and again and again, at regular intervals, until it is stopped.

    When is a timer interrupt useful?
    Suppose you want to dim a set of leds but your device does not have enough hardware PWM channels. The 'PWM' command will not do the trick because it will only emit a maximum of 255 pulses. A timer interrupt will continue to raise an interrupt independent of the normal program flow until the program specifically tells it to stop.
    Another use is to create an accurate real-time clock when a crystal oscillator is used for the time base.

    Other types of interrupts

    Depending on the device there may be a lot of other types of interrupts.
    A/D converters can use them as well as various types of serial communication.
    I will not go into these types of interrupts, mainly because the compiler offers very efficient implementations of the functionality that uses these interrupts meaning you don't have to bother with them.

    Caution!

    Before starting to program with interrupts you should be aware of the following.

    1 - the time used by an interrupt has to be minimal. The reason for this is simple: the interrupt temporarily stops executing the normal flow of operation to execute the ISR. If this takes a long time the normal flow of operation will appear not to work anymore. There will be tips at the end of this article on how to deal with this.
    As a rule of thumb:
    - NEVER put a 'Delay' in an ISR (well, at least not delays of more than a few microseconds)
    - avoid looping in an ISR

    2 - ISRs take time a finite time to execute. If you are using Delays in your time-critical program and start timing your code, you may find it takes more time than without interrupts. This cannot be helped, it's the "nature" of interrupts. (Unless you are willing to recalculate your delays depending on the time spent by an ISR...)

    3 - A PIC® has a hardware stack. This stack is used to store return addresses for 'GoSub'. But it is also used by interrupts since the microcontroller will need to know where it has to go to at the end of the ISR. What goes for any program goes for programs using interrupts: watch your stack. A program using interrupts will always need one extra entry on the stack. Since some 10/12/16 devices have only 8 levels, it may get crowded!

    Using interrupts - the basic principles

    In order to use interrupts, a number of basic principles must be understood.

    1 - There is one bit that determines if any interrupt will be fired at all, that's the GIE bit of the SFR INTCON (GIE= Global Interrupt Enable) and that some interrupts will only be fired when the PEIE bit of INTCON (PEIE = PEripheral Interupt Enable) is set.

    2 - Any interrupt that is to be used must be enabled separately, this is done using a bit that usually has "IE" in its name, like TMR0IE for the Timer0 interrupt. Any interrupt enabled in this way can also be disabled just by clearing that bit

    3 - Interrupts, when fired, will have a corresponding InterruptFlag bit set, like TMR0IF for the Timer0 interrupt. As far as I know all these flags must be cleared in the ISR in order to allow a following interrupt of this source to happen.

    The enable bits are spread around a number of SFRs, depending on the number of interrupts that are available. These SFRs are INTCON and PIEx.
    The flag bits are in INTCON and PIRx.
    Since it's never known what MicroChip® will call these registers you'll have to refer to the datasheet for the exact names.

    Writing a program to handle interrupts

    Consider a pretty standard Proton® Basic program with the following structure :

    Device identification (Device = , XTAL = )

    Compile time device configuration ('configs' aka 'fuses')

    Initial run time device configuration (SFRs like TRISx and PORTx)

    Declares (i.e. for an LCD, the USART, ...)

    Declaration of variables and symbols

    On_Hardware_Interrupt (GoTo) ISR*
    GoTo InitProgram* ; Skip over ISR and user subroutines

    ISR:
    Context Save
    ...
    Context Restore

    Your own subroutines

    InitProgram:
    Intialisation of variables
    Final initialisation of SFRs

    MainProgram:*
    While 1 = 1
    a lot of instructions
    Wend
    End

    *: these names are just examples

    Preparation - setting up the interrupt(s)

    Look back at the general structure of the program. You'll find 'Initial run time device configuration'.
    This is where you want to start setting and clearing the bits that control the way the interrupts work.
    I call this 'initial' because it is aimed at determining the basic way this interrupt will work. Since it's all done using registers you can always change the behaviour at any point in the program.
    First of all take note that all SFRs have a power-up default value, so in theory you should be safe to use these values. But also remember that a brown-out-reset default may not necessarily be the same as the power-up default.
    Personally, I don't assume anything anymore, so I always set and clear the bits the way I need them.

    ! Now there's something sneaky when setting and clearing bits and registers: the order in which you do it may make a difference and a write to a register may have side effects on other registers.
    Read the datasheet, there's no general rule for this.

    If you have to manipulate individual bits more than once, use Symbols since they're easier to understand.

    The easiest way to initialise an SFR is like this:
    REGISTER = 0 ; clear it just to be sure
    ;REGISTER, 7 = 0 ; explain what it does
    REGISTER, 7 = 1 ; explain what it does
    ...
    ;REGISTER, 0 = 0 ; explain what it does
    REGISTER, 0 = 1 ; explain what it does
    have this code and comment those bits that should stay zero.

    The last two bits I set in this part are PEIE and GIE and I may not even set them here, but at the end of the InitProgram block if I'm using timers.

    Example for a Timer0 interrupt for a 16F688.
    Timer0 is an 8 bit timer than can be controlled by an external source or the internal system clock. If used with an external source you can choose to have Timer0 incremented on either the rising or falling edge of that source.
    Like most timers, Timer0 has a prescaler. A prescaler is a hardware mechanism that divides the pulses driving the clock by a certain number.
    Why would you do this. Simple example: suppose the overflow of every 256 clock pulses is too fast, just divide them using the prescaler before incrementing.

    The Timer0 functionality for the 16F688 uses the following SFRs to control its working:
    - INTCON
    - OPTION_REG
    - TRISA
    - TMR0
    The relevant bits of each of these SFRs are described in the datasheet.
    Tip for reading datasheets
    Every functionality described contains a summary of the SFRs and their relevant bits involving this function. Very handy indeed, although 'older' datasheets don't always have them.
    INTCON
    This SFR has 4 bits for Timer0: GIE and PEIE (as described earlier, may come as no surprise), T0IE and T0IF.
    T0IE is the Timer0 Interrupt Enable bit and T0IF is the Timer0 Interrupt Flag bit.
    OPTION_REG
    This SFR contains no less than 6 bits for Timer0
    - T0CS: this bit determines the input for the Timer0 clock, an external source (1) or the internal oscillator (0)
    - T0SE: only relevant if an external source is used.
    - PSA: This bit determines if the prescaler is used by Timer0 or the WatchDogTimer (which, however interesting it may be, will not be explained in this article)
    - PS<2:0>: these 3 bits together determine the amount by which the source will divided.
    TRISA
    This SFR controls the direction of the I/O pins of PORTA. Since T0CKI is on the same pin as PORTA, 2 this setting is relevant.
    If you want to use an external clock, T0CS should be 1. This option requires the external clock to be connected to the T0CKI pin. Now take care that an external clock should always be an input, so the corresponding TRISx-bit should be 1. In this case this bit is TRISA, 2.
    If you use the internal clock you're free to give TRISA, 2 the value you want and that can be either 0 (output) or 1 (input) as with any general purpose I/O pin.
    TMR0
    The way this SFR determines what is happening will be explained later, for setting things up it is of less importance. For now it is sufficient to know that this register holds the value of Timer0.

    So we have everything together. What shall we do?
    1) Configure Timer0 running from the internal oscillator, no prescaler (this is my favorite)
    OPTION_REG = 0
    ;OPTION_REG, T0CS = 0
    OPTION_REG, PSA = 1
    TRISA, 2 = 0 or 1, whatever you want.

    2) Configure Timer0 running from an external oscillator at 32 Hz while we want a 1 Hz clock (who comes up with these silly examples?)
    OPTION_REG = 0
    OPTION_REG, T0CS = 1
    OPTION_REG, T0SE = 0 or 1, I don't care
    OPTION_REG, PSA = 0
    ;OPTION_REG, PS0 = 0
    ;OPTION_REG, PS1 = 0
    OPTION_REG, PS2 = 1 ; 1:32 prescaler
    TRISA, 2 = 1 ; T0CKI input

    Note: there is no way to start or stop Timer0 on many 10/12/16 devices. This means the timer will always increment if the clock is present. If you need Timer0 as a stable time-base you would like to be sure that TMR0 is zero before the main program starts.
    The last set of instructions in InitProgram therefore will be

    INTCON = 0
    INTCON, T0IE = 1 ; enable the interrupt
    INTCON, T0IF = 0 ; clear the interrupt flag so we're sure that Timer0 will have to count up until it overflows
    INTCON, PEIE = 1 ; enable Peripheral Interrupts (now this is a strange one. PEIE is never mentioned in the chapter on Timer0. But better safe than sorry)
    INTCON, GIE = 1 ; enable handling interrupts (this will put the whole thing in working)
    TMR0 = 0 ; so we are sure we know where we are starting from

    Programming the Interrupt Service Routine

    Programming the Interrupt Service Routine (ISR) is just like any piece of code, but with some extras of course.

    The first instruction you want to have is 'Context Save'.
    Why? During the execution of the ISR important system variables will change and those changes will almost certainly influence the way your main program loop continues after the interrupt is finished.
    'Context Save' will preserve the value of these important system variables. The manual contains a list of SFRs per family.

    Since a program may have a number of interrupt sources you should always test the xxxIF bits to determine which instructions to be executed.
    Context Save
    If TMR0IF = 1 then
    ....
    TMR0IF = 0
    Endif
    If TMR1IF = 1 then
    ....
    TMR1IF = 0
    Endif
    Context Restore

    Context Restore will put the values that were saved by Context Save back into these variables, so that's the last instruction in the handler.

    Is it really that simple? Yes, it is!

    The 10/12/16F family only has one level of interrupts, the 18F has two (High and Low) that can be assigned in the program. If you use High and Low priority levels you will need two separate ISR's.
    This is not discussed in this article since it falls outside the scope of a 'beginners' guide.

    Tips and tricks

    1: Use the simulator (SIM) in MPLab® 8.x to see how interrupts affect SFRs.
    MPLab® 8.x has a pretty powerful debugger/simulator called SIM to test your code without flashing it to a device.
    Now it will not do everything you need, AD conversions for example are not supported (makes sense to me), but Timers and External interrupts are a piece of cake.
    See those IF bits change in the Watch, it's the best learning environment you can imagine.

    When you're writing Timer interrupts you should get yourself acquainted with the Stopwatch. It displays the number of cycles and, assuming the speed in the settings matches the actual circuit, the time elapsed between breakpoints (and you can reset it every time it stops).
    When you're writing External interrupts you can not do without Stimulus. With this tool you can manipulate I/O pins in all kinds of ways, thus simulating external interrupts.
    The documentation that comes with Proton contains a document on how to use MPLab® together with Proton®.

    Ok, it may sound a bit tedious but I must confess: I'm a big fan of MPLab® 8.x and especially SIM, the Stopwatch and the Stimulus (the three S's).
    Probably that is because I started programming PIC®s with MPLab® (yes, you guessed right, an assembler programmer, and I'm grateful I started with that instead of a 3rd generation tool because I find I have more insight than users only knowing Proton® and its siblings).

    2: Adjust the duration between overflows of a timer
    Basically a Timer interrupt does nothing else but incrementing an SFR until it overflows.
    So if you're running at 4 MHz, since each instruction takes 1 uS, an overflow of an 8 bit timer will always take 256 uS.
    But if you need it to overflow more often, say every 200 uS, you have to interfere.
    This is actually extremely simple: assuming a Timer0, 8 bit timer. At the end of the ISR code for the Timer 'preload' the TMR0 register with the value 55. After overflowing and executing the ISR code TMR0 will start counting at 55, needing only 200 uS to overflow again.

    3: Testing the interrupt flags
    If you have a program that supports a number of interrupt sources you might be tempted to handle all flags before leaving the ISR, e.g. like this:
    ISR:
    Context Save

    Check_IFlags:

    If TMR0IF = 1 Then
    .....
    TMR0IF = 0
    GoTo Check_IFlags
    Endif

    If TMR1IF = 1 Then
    .....
    TMR1IF = 0
    GoTo Check_IFlags
    Endif

    Context Restore

    This is not necessary; the PIC is fully aware of all xxxIF bits and will enter the ISR if any of them is set after finishing the ISR. So this is sufficient:
    ISR:
    Context Save

    If TMR0IF = 1 Then
    .....
    TMR0IF = 0
    Endif
    If TMR1IF = 1 Then
    .....
    TMR1IF = 0
    Endif

    Context Restore
    Excursion into the land of GoTo

    In 1968 Turing award winner Edsger Dijkstra (who has a prize named after him) wrote a private paper called 'A Case against the Go To Statement', renamed to 'The Go To Statement Considered Harmful' by Niklaus Wirth. I'm a big fan of Edsger, not because he was a rather famous Dutchman (I'm a Dutchman that almost certainly never will be famous), but because he pointed out the dangers of unstructured code and gave guidelines to write structured code.
    Since then the 'Go To' statement has been considered the cause of all evil in programming.
    But that's overreacting. Using a GoTo does not have to result in unstructured code.
    Unstructured programming is jumping all over the place, like a flea on steroids, making debugging and maintenance (understanding the code) practically impossible.

    Look at the first piece of code above: there is only ONE label (Check_IFlags) that is referenced in GoTo statements and that's almost at the top of the routine.
    My first assignment after my IT-training was with an organisation that had very strict rules on programming, the 'GoTo the end of the routine/function/procedure' was the only GoTo allowed.
    Over the years I modified that to read: there is one and only one location within a routine/function/procedure that may be jumped to (and most of the time that's the end of the routine/function/procedure).

    Testing the IF bits is always good practice, even if you have only one interrupt source in your program.
    Before you know it you may have a second timer active and forget to test the IF bit of the first one.

    4: Split up your program in small parts that are easy to test.
    If this is your first interrupt program: forget about the rest of the program, start getting the interrupt handling working.

    5: Read the datasheet

    6: Document your program. Especially all configuration details and the way subroutines work.
    Take special care of inline documentation after making changes, the documentation may no longer be correct

    7: Read the datasheet again

    8: Document the things you think are obvious at this moment, they will not be that after six months.
    But don't overdo it: I've seen documentation like "Device = 16F628A ; we're using a 16F628A"

    9: Software derived tones could be interrupted by an ISR. If purity of tone is required you will need to turn off interrupts (GIE = 0) before starting to play a tone and turn interrupts on again after completion of the tone (GIE = 1).

    10: Use semaphores. A semaphore is a signal that 'something' has happened that the main program should take care of. What I usually do is use a bit variable to be set inside the ISR, leave the ISR as fast as possible and check the bit in the main program loop, clear the bit and take action. As you may understand this requires the main program loop to be as fast as possible.
    The effect is that the actions you want to happen as a result of the interrupt are moved from the ISR to the main program, giving you more control.

    Going into details

    This part contains a number of topics that are not strictly necessary to be able to use interrupts.

    ISR: Subroutine or not?

    The ISR that is executed looks exactly like a subroutine, but there are two important differences
    - the ISR should not be called by means of a 'GoSub' or 'Call'
    - the ISR does not contain a 'Return' but a 'RetFie' that is generated by the compiler when it encounters 'Context Restore'
    and there is a similarity
    - it uses an entry on the hardware stack like a subroutine does

    How does the microcontroller know where to go on an interrupt?

    The ISR is a routine anywhere in program memory. To reach that routine its address must be stored at a specific place, the 'Interrupt Vector'.
    For most 10/12/16 devices this is at address 0x004.
    18F devices have a priority mechanism where the High priority vector is at 0x0008 and the Low priority vector is at 0x0018.
    The compiler generates a 'Goto ISR' at this memory locations.

    When is an interrupt executed and where will the normal flow of operation continue?

    When the interrupt is to be handled, the microcontroller finishes the current instruction, pushes the return address on the stack, and jumps to the address stored in the interrupt vector.
    On completion the return address is popped from the stack and loaded into the Program Counter (PC) to continue the main program.

    The interrupt is finished when the microcontroller encounters a 'RetFie' instruction (Return From Interrupt).
    The compiler generates this instruction as part of 'Context Restore'.
    So if you're NOT using the Context save/restore mechanism you'll have to program the 'RetFie' yourself.

    Now take note of this: when I write 'finish the current instruction' I mean the assembler instruction, not the Basic instruction, and there's a big difference between the two.
    On the one side there is a very simple Basic instruction like 'variable = 0'. This will result in one assembler instruction 'clrf variable'.
    On the other side there are very complex Basic instructions like 'variable = ADIn 0' or 'HSerOut...'. The compiler will generate a subroutine for instructions like these.

    Assembler instructions are elementary; an interrupt can not interfere with an instruction like 'clrf variable'.
    But Basic instructions, containing lots of assembler instructions, can easily be interrupted.

    An interrupt will finish the current assembler instruction and the address of the NEXT assembler instruction will be pushed on the stack.

    If you understand this you see why 'DelayMs 500' is no problem when using interrupts although its period will be extended by the length of the interrupt routine.

    Interrupts on 16 bit devices

    Up to now this article dealt with 8 bit devices.
    Recently the compiler supports a number of 16 bit devices as well and, although this is the Beginners Guide, a little attention must be paid to these devices too.
    As you may suspect, things are different although some principles stayed the same.
    Main differences:
    • Where 8 bit devices have one interrupt vector (or 2 for the 18F devices), the 16 bit devices have a dedicated interrupt vector for each source of interrupt, these are in the 'Interrupt Vector Table' (IVT). Up to 118 vectors. And if this was not enough, they also have so-called 'Fail/Error Trap Vectors' and an 'Alternate Interrupt Vector Table' (AIVT). The 'Fail/Error Trap Vectors' deal with unmaskable interrupts like Oscillator failure, Stack Errors, Address Errors and Arithmetic Errors.
    • The interrupts can be nested. This is really a totally different way of working. This means that an ISR may be interrupted by a new interrupt.
    • The user can assign priority levels to interrupts.
    At this moment I have not gone into the 16 bit family enough to be able to present you with a concise Guide. That'll have to wait, but prepare for a few hours of reading the datasheet. The good thing is that you can refer to a so-called 'family' datasheet that discusses Interrupts for a specific family. The good news is that they handle interrupts in a similar way.