Bascom and AVR, Interrupts


Suppose you have a Bascom program that is in a Loop doing something complicated and time-consuming. You want to be able to stop this task and switch the program to do someting else. The obvious hardware solution is to add a STOP button to your AVR:



You would normally test if this button is pressed with the following program fragment:
...
Config Pind.2 as Input
...
Do
  ...
  ...someting complicated...
  ...
  If Pind.2 = 0 Then
    Lcd "Stop!"
    Goto Othertask
  End If
Loop

...
OtherTask:
...
The fundamental problem with this example is that we might spend so much time doing the complicated task that pressing the STOP button will often not be noticed once the program arrives at the Pind.2 test. Obviously we need another mechanism to react to pressing the STOP button in an independant way from the main program.
This is where interrupts are for. Interrupts are a to change the program flow to react to external as well as internal controller events. We could modify the example above to:
interrupt-stopbutton.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Pind.2 = Input
Config Int0 = Falling

Dim Wtime As Byte

On Int0 Stopbutton

Cls

Wtime = 255

Enable Interrupts
Enable Int0

Do
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
Loop

Stopbutton:
  Lcd "stop!"
Return

End
Pind.2 is configured as input, don't forget the 10k pull-up resistor!
Config Int0 Falling: Int0 is to happen only when the voltage level on Pind.2 is going from high to low.
When the Int0 interrupt occurs, the program will jump to the Stopbutton label.
Interrupts in general and the Int0 interrupt in particular are enabled.
In the Do Loop, the 'complicated task' is flashing a Led.
The Stopbutton routine will write "stop!" to the Lcd, then return to the program at the the point where is was interrupted.

What will happen is that the Led flashes on and off, most of the time will be spent in the Waitms commands. When the button is pressed, the program will jump to the Stopbutton label and write "stop!" to the Lcd and return to flashing the Led.

Is that what happens? Alas, no. You will see "stop!" on the Lcd, but that is not all. Why? You will find out later, first an overview of AT90S2313 interrupts:

The AT90S2313 interrupts

A. Interrupts with an external source:
Int0 external interrupt on PortD.2, pin 6
Int1 external interrupt on PortD.3, pin 7
Counter0 overflow interrupt, PortD.4, pin 8
Counter1 overflow interrupt, PortD.5, pin 9
Timer1 capture interrupt, PortD.5, pin 9
Timer1 output compare A interrupt, PortD.5, pin 9
Serial Rx complete interrupt
Analog comparator0 interrupt, PortB.0, pin 12
Analog comparator1 interrupt, PortB.1, pin 13

B. Interrupts with an internal source:
Timer0 overflow interrupt
Timer1 overflow interrupt
Serial data register empty interrupt
Serial Tx complete interrupt

AVR interrupts all have the same priority. This is different from a lot of other controller types where you can specify which interrupts get priority over others.

If you use another type of AVR controller, use the Bascom *.def file to check which types of interrupts are available. Also check the controller datasheet of course.

Interrupts on or off
If you start a Bascom program all interrupts are off. They have to be enabled. With the command:
Enable Interrupts
interrupts are enabled as a group. They can be disabled as well:

Disable Interrupts
This can be useful if you have a program segment where you do want to be interrupted at all:
Enable Interrupts
Enable Int0
Enable Timer0
...
Disable Interrupts
...
Something very important here...
...
Ok, ready...
...
Enable Interrupts
...
Here, all interrupts are enabled or disabled as a group. Individual interrupts must be enabled separately:
Enable Interrupts
Enable Int0
Enable RX0
Enable Counter0
...
And they can be disabled individually:
...
Disable Counter0
...no counter0 interrupts here...
Enable Counter0
...

Interrupt routines
Every interrupt has to be handled in a seperate routine. A routine is a program fragment with a label, program lines and a Return statement. For every interrupt you enable, you must specifiy which routine it has to jump to:
On Int0 Stopbutton
On Int1 LcdMenu
On Counter0 Revcalc
...
Enable Interrupts
Enable Int0
Enable Int1
Enable Counter0
...
Main program
...
Stopbutton:
  Lcd "stop!"
  ...
Return

LcdMenu:
  Cls
  Lcd "Calibrate: press A"
  ...
Return

Revcalc:
  Revs = Counter0 * Revfactor
  Counter0 = 0
  ...
Return
Note that all interrupt routines start with a label, a name ending with the ":" character.

No interrupts in an interrupt routine
As soon as the program is interrupted and jumps to the relevant interrupt routine, all enabled interrupts are disabled as long as the routine is busy. Once the routine reaches the Return statement and jumps back to the place where it left the main program, disabled interrupts are enabled again. This is to avoid routines from interrupting themselves and thus ending as the snake that ate himself. This behaviour that differs from most other controller brands, is in the AVR architecture, not in Bascom.

Keep it short

Keep all your interrupts routines as short and simple as you can. Remember that your program is >interrupted< from what is was doing and that it should probably not be interrupted for too long. Try to do not more in interrupt routines than keeping track of counters or flags that are processed in the main program once the program is ready to do so.
Compare this program:
On Int0 Stopbutton

Enable Interrupts
Enable Int0

Do 
  ...
Loop
End

Stopbutton:
  Lcd "stop!"
  ...
  Do something complicated...
Return
with this program:
Dim Stopflag as Bit

On Int0 Stopbutton

Enable Interrupts
Enable Int0

Do 
  ...
  If Stopflag = 1 Then
    Reset Stopflag
    ...
    Do something complicated...
    ...
  End If
Loop
End

Stopbutton:
  Set Stopflag
Return
The Stopbutton routine is now kept to its minimum, only bit Stopflag is set. This is handled in the main program. Be aware though, that in this way you may react later to an interrupt than desired.

More on Int0/Int1

Into and Int1 are external interrupts. They are typically generated by a pushbuttons, switches or pulses or level changes from other circuits. You can select how these interrupts are honoured:
Config Intx = Low Level
Config Intx = Falling
Config Intx = Rising
So, interrupts are generated as long as the Intx pin is low, for any level going high to low, or for any level going low to high.
Note that Low Level keeps generating interrupts for as long as the level is low. As an example:
$regfile = "2313def.dat"
$crystal = 4000000

Dim Cntr As Integer

On Int0 Button
Config Int0 = Low Level

Cls

Enable Interrupts
Enable Int0

Do
  Locate 1 , 1
  Lcd Cntr
  Waitms 250
Loop

Button:
  Incr Cntr
Return

End
In the interruptroutine a counter is incremented, the value of the counter is displayed four times per second on the Lcd.

Not documented in the Bascom help files (as far as I know) is the fact that Low Level seems to be the default for the Intx configuration.
Generating interrupts on the Falling or Rising edge of an input pulse is much more common.

Bouncing buttons

A perfect pushbutton will switch directly from open to close and back to open when released. Alas, real world pushbuttons have a phenomenon called 'bounce'. When pressed, a button may 'oscillate' between open a close for a short while before settling in the closed position. The 'short while' may take as long as 50 milliseconds for some types. I made a 'snaphot' of a button being pushed, pulling Vcc low through a 10k pull-up:



Actually this is a very good pushbutton: it takes only 0.5 milliseconds for the button to settle in the closed position.
Now, if your program would react to every falling edge, you would have to handle a lot of interrupts. The better approach is to use 'debouncing'. What we could do is simply add a wait in the interrupt routine:
interrupt-debounce-self.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Pind.2 = Input
Config Int0 = Falling
Config Debounce = 50

Dim Wtime As Byte
Const Debouncetime = 75

On Int0 Stopbutton

Cls

Wtime = 255

Enable Interrupts
Enable Int0

Do
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
Loop

Stopbutton:
  Lcd "stop!"
  Waitms Debouncetime
  Gifr = 64
Return

End
In the interrupt routine a simple wait is added. In the routine, interrupts are disabled, so the program will only react once to the button being pressed. Choose the value of Debouncetime so that is longer than the longest 'bounce' time of the pushbuttons you use.
Bascom also has a Debounce command which you can use in the interrupt routine.

What is this strange Gifr?

In the example above, the interrupt routine has a strange command: Gifr = 64. What is its purpose? Remove this command and see what happens: Almost always the text "stop!" is written twice on the Lcd. How is that possible?
Examine this program:
interrupt-deb-pre.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Pind.2 = Input
Config Int0 = Falling

Dim Wtime As Byte

On Int0 Stopbutton

Cls
Set Portd.6
Waitms 3000
Reset Portd.6
Wtime = 255

Enable Interrupts
Enable Int0

Do
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
Loop

Stopbutton:
  Lcd "stop!"
  Waitms 75
Return

End
After the Lcd is cleared, the Led is switched on. A 3 second wait follows, the Led is switched off, and the interrupts are enabled. Now, reset the controller and try to press the stop button during the time the Led is on. You have three seconds to do that, so that should be no problem. After the Led is off, Int0 is enabled and you will observe that immediately the program jumps to the Stopbutton routine. So, even if you press the button >before< the interrupt is enabled, the interrupt is apparently stored somewhere and when interrupts are enabled, it is immediately processed.

And that is what happens in the AT90S2313 controller (and in all the other AVR's). Even if the relevant interrupt is not enabled, the moment an Int0/Int1 interrupt occurs it is stored in the General Interrupt Flag Register. This is one of the rare moments where even Bascom users have to delve into the inner architecture of the AVR's. The Gifr register has eight bits where bits six and seven are reserved for Int0 and Int1 respectively:



And this is also what happens in our interrupt-debounce-self.bas program. In the interrupt routine, when interrupts are disabled, new interrupts arrive due to button 'bounce'. The interrupts are flagged as zero bit in the Gifr register. Int0 interrupts in bit six, Int1 in bit seven. All we have to do to solve the problem is 'set' the relevant bit in the Gifr register just before the Return statement in the interrupt routine. In this way, if the program resumes its normal course, no further (old) interrupts are honoured.

Reserved register names

We now notice, that apparently there are a number of 'special' register names. Avoid using these names in your Bascom programs. Consult the Bascom help (start Bascom help, choose find and type 'AVR internal registers') and the AT90S2313 datasheet (table 1, page 15 and 16)>

Reading a rotary encoder or keyboard on interrupt
Rotary encoders and small keyboards are typically read on interrupt. This is described separately in the encoder and keyboard chapters.

Timers and Counters
The AT90S2313 has two timer/counters (make sure you read from page 27 onwards). Timer0 is an eight-bit timer/counter. We can use it to measure time (by counting clock pulses) or to count external events on the T0 pin. As it is an eight-bit timer/counter, the value can be from 0 to 255.
With Config we can choose from all the possibilities:
Config Timer0 = Timer, Prescale = 1|8|64|256|1024
Here, Timer0 is configured as timer with the controllerclock as input. It counts the clock pulses as they are output by a prescaler with a division ratio of 1, 8, 64, 256 or 1024. With the division ratio you can controller the time it takes for Timer0 to go from 0 to 255 counts. Let us assume a 4MHz clock and a prescale ratio of 1024. Timer0 will be incremented every Prescaler-ratio/Fclock or 1024/4.000.000 = 0.256 milliseconds. It will overflow in 255 * 0.256 = 65 milliseconds.
Timer1 is identical to Timer0 but it as it is a 16-bit counter it can count to 65535 before overflowing to zero.
Timer0 and Timer1 start running as soon as they are configured.
An example of a free-running Timer0: (you only need an Lcd attached to the AT90S2313)
interrupt-timer0-free.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Pinb.1 = Output
Config Timer0 = Timer , Prescale = 1024

Dim Wtime As Byte
Dim Timercounter As Byte

Wtime = 100
Timercounter = 0

Do
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
  Cls
  Timercounter = Timer0
  Lcd "tmrcntr: " ; Timercounter
Loop

End
And a free running Timer1:
interrupt-timer1-free.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Pinb.1 = Output
Config Timer1 = Timer , Prescale = 1024

Dim Wtime As Byte
Dim Timercounter As Word

Wtime = 100
Timercounter = 0

Do
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
  Cls
  Timercounter = Timer1
  Lcd "tmrcntr: " ; Timercounter
Loop

End
Of course the main difference is that here Timercounter is dimensioned as Word.
The actual value of Timer0 and Timer1 can be read at any time and copied into a properly dimansioned variable.

The two examples above used free-running timers. They can be started and stopped at any time:
Start Timer0
Stop Timer0
Start Timer1
Stop Timer1
And as these are just normal AVR registers, you can not only read the current value but also write a value into the registers.:
Stop Timer1
...
Timer1 = 132
...
Start Timer1

Timer interrupts
One of the more common applications of timers is to interrupt the program at regular intervals to do something else. For example checking if an input pin has changed level, generate an output pulse etc.
An example for generating output pulses: (use the stopbutton schematic, monitor output on pin Portb.1)
interrupt-timer0-pulse.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Pinb.1 = Output
Config Timer0 = Timer , Prescale = 64

Dim Wtime As Byte

On Timer0 Pulse:

Wtime = 100

Enable Interrupts
Enable Timer0

Do
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
Loop

Pulse:
  Toggle Portb.1
Return

End
The prescaler ratio is set at 64, so Timer0 will be incremented every 16 microseconds. As it can count to 255, it will overflow after 256 * 16 = 4096 microseconds. At every overflow, a Timer0 interrupt occurs and Pulse: is called. In the interrupt routine the state of Portb.1 is 'toggled', meaning that if it was high, it is made low and v.v.
The result is a nice square wave with an on and off time of app. 4 milliseconds on Portb.1:



The AT90S2313 is pretty fast! Try a prescale value of 1 and observe the result on Portb.1. The on and off time is app. 60 microseconds:


All this is working fine as long as the time spent in the interrupt routine is smaller than the on/off time of the output! Try this out by inserting an Cls command in the interrupt routine. You will see that the on/off time jumps from 60 microseconds to 6 milliseconds!


Other timings
In the examples shown thus far, the interrupt was generated on a timer overflow. This means that the timings we can realise are limited to certain number dependant on clock speed, prescaler ratio and timer register width. One way of choosing exact timings is to not let the timer run free from zero to maximum to zero etc., but to preload the timer register after every overflow with a certain value. We can choose this value such that the time it takes to get from this value to overflow is exactly the time we need:
interrupt-timer1-preload.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Pinb.1 = Output
Config Timer1 = Timer , Prescale = 1
Const Timer1pre = 65100

Dim Wtime As Byte
Stop Timer1
Timer1 = Timer1pre

On Timer1 Pulse:
Start Timer1

Wtime = 100

Enable Interrupts
Enable Timer1

Do
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
Loop

Pulse:
  Stop Timer1
  Timer1 = Timer1pre
  Toggle Portb.1
  Start Timer1
Return

End
Timer1 is loaded with the value 65100 after every overflow. Timer1 will thus count from 65100 to 65535 before overflowing and generating an interrupt. This will take (65536 - 65100) * 0.25 = 109 microseconds. Using the constant Timer1pre you can time the on/off time of the output pulse exactly in 0.25 microsecond resolution.

Counting external pulses
Timer0 and Timer1 can be configured to count external pulses on the T0 and T1 input pins:
Config Timer0 = Counter, Prescale = 1|8|64|256|1024, Edge = Rising|Falling
Config Timer1 = Counter, Prescale = 1|8|64|256|1024, Edge = Rising|Falling
You can choose to count pulses on the falling or rising edge of an input pulse. You can also choose to have the input pulses prescaled before counting.
Especially Timer1 is interesting to act as external pulse counter as it can count to 65535 before overflowing.

The names Timerx, Counterx and Capturex in Bascom programs all refer to the same registers, so for example the names Timer1 and Counter1 can be mixed in a program, although that would not be good programming practice.

Note that when Timer0 and Timer1 are used to count external pulses, the controller will sample the input the level on the input pin at the controller clock rate. This means that to accurately count input pulses, the pulse frequency must never be higher than half the controller clock frequency. (Nyquist) To be on the safe side, keep the input pulse frequency below 40% of the controller clock. So, for a clock of 4MHz, do not try to count faster than 1.6MHz.
Try this out with a TTL pulse generator attached to T1 (PortD.5, pin 9 of the AT90S2313):
counter1.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Timer1 = Counter , Edge = Falling , Prescale = 1

Stop Counter1

Set Portd.6
Waitms 1000
Reset Portd.6
Waitms 1000

Cls

Do
  Counter1 = 0
  Start Counter1
  Waitms 25
  Stop Counter1
  Cls
  Lcd "Counter1: " ; Counter1
  Waitms 100
Loop

End
In the Do Loop, Counter1 is cleared and started. After a 25 millisecond wait, Counter1 is stopped and its value is written to the Lcd. Note that timing with a Waitms command is not very accurate, there are better ways.
Slowly increase the pulse generator frequency and observe what happens above 1.6MHz.

Timer1 can count to 65535. If that is not enough, you can generate an interrupt on Timer1 overflow and keep track of the number of overflows in an interrupt routine:
counter2.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Timer1 = Counter , Edge = Falling , Prescale = 1

Dim Wtime As Byte
Dim Timercounter As Word
Dim Overflcounter As Word
Dim Totalcounter As Long

On Counter1 Uphigh

Wtime = 100
Timercounter = 0
Totalcounter = 0

Enable Interrupts
Enable Counter1

Do
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
  Cls
  Timercounter = Counter1
  Lcd Timercounter ; " " ; Overflcounter
  Lowerline
  Totalcounter = Overflcounter
  Shift Totalcounter , Left , 16
  Totalcounter = Totalcounter + Timercounter
  Lcd "total: " ; Totalcounter
Loop

Uphigh:
  Incr Overflcounter
Return

End
Three variables are dimensioned:
Overflowcounter (16-bit Word) keeps track of the number of Timer1 overflows
Timercounter (16-bit Word) has the actual value of Timer1
Totalcounter (32-bit Long) gets the value of Overflowcounter shifted 16 places to the left plus the value of Timercounter.

Timer1 Capture

Timer1 can be configured in the 'Capture' mode. This means that Timer1 counts the controller clock through a prescaler, and when on the ICP input (PortD.6, pin 11) a pulse arrives, the contents of the Timer1 register is copied to the input capture register. In this way it is possible to measure the time between two pulse edges exactly:
Config Timer1 - Timer, Prescale = 1|8|64|256|1024, Capture Edge = Rising|Falling
interrupt-timer1-capture.bas
$regfile = "2313def.dat"
$crystal = 4000000

'Config Pind.6 = Output
Config Timer1 = Timer , Prescale = 64 , Capture Edge = Rising

Dim Wtime As Byte
Dim Timercounter As Word

On Capture1 Captmr

Wtime = 100
Timercounter = 0

Enable Interrupts
Enable Capture1

Do
  'Set Portd.6
  Waitms Wtime
  'Reset Portd.6
  Waitms Wtime
  Cls
  Lcd "pwidth: " ; " " ; Capture1
Loop

Captmr:
  Timercounter = Capture1
  Timer1 = 0
Return

End
In the interrupt routine the value of Timer1 (Capture1 is just another name for this register) is copied to Timercounter. Timer1 is then reset. The next time that a pulse on the ICP input arrives the same happens. So, Timercounter is a measure of the time between pulses on ICP.

Timer1 Compare

Timer1 has a compare register: CompareA. (Bascom also mentiones CompareB, but that register is not in the AT90S2313) This register can be loaded with a certain value. When the Timer1 value equals that of CompareA, a previously defined action can be performed on OC1. (PortB.3, pin 15):
Config Timer1 = Timer, Prescale = 1|8|64|256|1024, Compare A = Clear|Set|Toggle|Disconnect, Clear Timer = 0|1
Actions are:
- Set OC1
- Clear OC1
- Toggle OC1
- Disconnect OC1

With Clear Timer you can reset Timer1 when the CompareA occurs.

Especially the toggle function is much used to generate precise frequencies on OC1.
compare.bas
$regfile = "2313def.dat"
$crystal = 4000000

Config Pind.6 = Output
Config Timer1 = Timer , Prescale = 1 , Compare A = Toggle , Clear Timer = 1

Dim Wtime As Byte
Dim Compval As Word

Wtime = 100

Do
  For Compval = 100 To 10000 Step 100
    Compare1a = Compval
    Waitms 10
  Next Compval
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
Loop

End
In the Do Loop, the Compare1A register is preloaded with Compval, which varies between 100 and 10000. When Timer1 equals Compare1A, the pin OC1 is toggled and Timer1 is cleared.
Connect a small loudspeaker through a series resistor of several hundred ohm's to OC1 (PortB.3, pin 15) and listen to the music...

UART interrupts
The AT90S2313 UART has three interrupt possibilities:
1. Tx ready. When the last Tx bit has been sent and no more data is in the data buffer. This interrupt can be used when working half-duplex and you must know when to change from send to receive. In the default full-duplex mode this interrupts has no use.
2. Tx data buffer empty. This interrupt is generated when a character is written from the data buffer to the Tx buffer. It can be used to signal that the next character can be written in the data buffer. You will not often need this interrupt because Bascom arranges everything when sending a string to the UART.
3. Rx ready. When a complete character has been received by the UART and has been placed in the data buffer this interrupt is generated.This character has to be read from the data buffer as soon as possible so that the next character can be received. Bascom also deals with receiving strings through the UART, but it can be useful to interrupt a program when a character arrives, for example to change the course of the program:
interrupt-rs232rx.bas
$regfile = "2313def.dat"
$crystal = 4000000

$baud = 9600

Config Pind.6 = Output

On Urxc Getchar

Dim Wtime As Word
Dim Inchar As String * 1

Const Fastblink = 100
Const Slowblink = 500

Wtime = Slowblink

Enable Interrupts
Enable Urxc

Do
  Print Wtime
  Set Portd.6
  Waitms Wtime
  Reset Portd.6
  Waitms Wtime
Loop

Getchar:
  Inchar = Inkey()
  Select Case Inchar
    Case "f" : Wtime = Fastblink
    Case "s" : Wtime = Slowblink
  End Select
Return

End
When a character is received, Getchar is called. If the character is a "f", in the Do Loop the Led will flash fast, if a "s" is received it will flash slowly. All other characters are ingnored.

Analog Comparator

The AT90S2313 has an analog comparator on Ain0 (pin 12) and Ain1 (pin 13). The comparator can be configured such that when it triggers, the Timer1 Capture function is started. It is also possible to generate an interrupt:
Config ACI = On|Off, Compare = On|Off, Trigger = Rising|Falling|Toggle
When Compare is configured On, the Timer1 Compare is started when the comparator triggers. With Trigger the trigger moment can be set to rising (Ain0 > Ain1), falling (Ain0 < Ain1) or toggle (Ain0 > Ain1 and Ain0 < Ain1).
A possible application of the analog comparator is an eight-bit DAC, but then you need the complete PortB to construct a R-2R DAC. This is rather wasteful, a better solution would be an outside chip such as the I2C PCF8591 which has one DAC and four ADC's, all eight-bit.

TOC