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