Timer v3.05 is a program written in BASIC and 6502 assembly language for the BBC Microcomputer. The source code is available, but is not supported; use it at your own risk.
The Timer program is actually a collection of routines based on a software counter which counts in milliseconds. If two separate events are used to start and stop the counter, it is possible to time the interval between the events in milliseconds. The counter can be programmed to count up or down.
The count-up mode is normally used for timing external events such as reaction time. At the onset of the stimulus, the counter is set to zero. When a reaction is detected, the counter value at that instant is saved. This value gives the reaction time in milliseconds.
When using a count-down, it may be necessary to provide an initial value from which to decrement. The count-down mode may be used for providing computer generated events at set intervals. The required duration of the interval is loaded into the counter and the first event is generated, starting the count-down. When the value of the counter reaches zero, the second event is triggered. Although this is the easiest method for timing pre-defined intervals, it is also possible to use the count-up mode for the same purpose.
The software timer operates by virtue of a hardware interrupt
from Timer1 of the USER 6522 VIA chip. While the Timer1 interrupts
are running, the operating system interrupts are disabled, in order
to ensure accurate timing. All OS routines such as OSBYTE and OSWRCH
are, therefore, inoperative. In order to circumvent this problem, a
number of routines have been written to perform useful functions
independently of the operating system. These routines are
Byte2VDU which will be explained later.
NOTE: After being called from BASIC, if the machine code
StartTimer, it is vital that, before
returning to BASIC, the
StopTimer routine is also
called. This is to replace the operating system interrupt vectors
that were removed by the
StartTimer routine. Failure to
go so will result in errors such as keyboard hang-up on return to
not be called if
StartTimer has not
been called first. The two routines can be thought of as left and
right brackets. The left bracket (
always precede and be accompanied by the right bracket
StopTimer) and neither should appear alone.
The actual counts (
Seconds) are incremented in the Interrupt Service
Routine (ISR) called
IntHandle. This routine is called
every time the USER VIA generates a Timer1 interrupt. When the 6502
processor receives this interrupt, it completes execution of the
current instruction (in the main program) and then changes the
Program Counter to the location of the interrupt handler or ISR. The
processor then executes the ISR code as normal. Upon completion of
the ISR, the processor resets the Program Counter back to its
original value and, thus, commences execution of the main program
where it left off.
Due to the fact that the ISR interferes (benignly) with the execution of the main program, a precaution within the ISR is necessary: the main processor registers must be saved before they are used by the ISR. The Stack is used to temporarily save the Accumulator, Status Register, Zero-Page address &00FC, X register, and Y register. These are replaced at the end of the ISR.
After completion of our ISR (
processor then jumps to the location of its own interrupt handler.
It is necessary to pass interrupt control over to the system's ISR
after our own has been performed, to ensure that interrupts are
In addition to the above measures, it's important that the main program is coded such that it is not 'confused' by the interrupts. The problem is that, even though the interrupts occur at regular intervals, they occur asynchronously with the execution of the main program. An example of how an interrupt can confuse the main program is described below:
Assume that somewhere in the main program, the two bytes of
RealMilisec are transferred to two other bytes in
memory via the Accumulator. Problems will arise if the interrupt
occurs between the reads of the low and high bytes. Say the
value of the time counter is &01FF when the lo-byte is read,
giving &FF. Then, an interrupt occurs. After completion of the
ISR, the value of the time counter is &0200. When the processor
returns to the main program, it reads the high byte of the timer.
The value of this byte is now &02. Thus, when the values of the
two reads are combined, &02FF is obtained. This is obviously not
the true value of the time counter when either byte was read.
The way to avoid this problem is to disable the interrupts with
SEI command immediately before reading the first
byte. Turn them back on with the
immediately after reading the second byte. Adopting these measures
ensures that the ISR is not performed if an interrupt occurs while
the two bytes are being read.
It should be noted that interrupts still occur while they are disabled, the processor simply ignores them. It is important that the interrupts should not be disabled unless absolutely necessary. Also, if they must be disabled, then it should be for as short a period as possible. These measures prevent any interrupts from being missed and, consequently, improve the reliability of the timer.
If an interrupt occurs while they are disabled, the corresponding flag in the Interrupt Flag Register (IFR) is set and remains set so that the ISR will be performed as soon as interrupts are re-enabled. The ISR then clears the IFR. If, however, two or more consecutive Interrupts from the same source (in this case, Timer1) occur while it is disabled, the processor will perform the ISR once only. That is, the other Timer1 interrupts will be ignored and the timer may lose a few milliseconds.
This routine sets up the interrupt system. Firstly, all interrupts are disabled while the interrupt system is adjusted. Next, the SYSTEM VIA's Interrupt Enable Register (IER) is saved and replaced with a value which disables all operating system interrupts. The SYSTEM IFR, containing any interrupts presently active (before they were disabled) is then cleared.
Once the SYSTEM IER status has been stored and replaced, it is
necessary to set up the subsequent operation of the interrupt system
for our own purposes. Firstly, the interrupt vector held in
IRQ2V is saved in
OldIRQ2V and replaced
with the vector (location) of our own ISR (
Next, the inter-interrupt period (
microseconds is passed to the two byte Timer1 latch of the USER VIA
(it is not necessary to save the original contents of this latch).
Then, the USER Auxiliary Control Register (ACR) is set up to provide
continuous interrupts from Timer1 (with user port B bit 7 disabled).
Finally the USER IER is set up such that only Timer1 interrupts can
occur. It is now only necessary to reset the value of
RealMilisec before turning on the interrupts.
This routine reverses the action of the
routine. It is necessary to do so before returning to BASIC as the
interpreter requires that the operating system runs normally. All
interrupts are disabled first. Then, the original SYSTEM IER is
replaced, followed by the original interrupt vector
IRQ2V (held in
OldIRQ2V). Finally, Timer1
is disabled in the USER IER, and de-selected in the USER ACR.
Interrupts are then re-enabled, allowing normal OS operations to
This is the Interrupt Service Routine which provides the basis
for the timing operation. This routine is performed once every
Period microseconds). Hence, to provide
a millisecond count, the routine increments
by one. The millisecond counter is assigned the symbol
RealMilisec contains the low
order byte, while
RealMilisec+1 is the high-order-
byte. These two bytes can be thought of as a one word (16 bit)
variable, which may be any value from 0 to 65535.
RealMilisec counter is also used to provide a
count of seconds. This is assigned the symbol
and is incremented once every 1000 milliseconds. It is a single byte
variable allowing a count of up to 255 seconds. Of course, it is
possible to add counts of larger time periods (e.g. a
Minute counter incremented once every 60
In order to provide a seconds count, a further routine is
included after the
RealMilisec increment. If the value
RealMilisec is equal to 1000, then
RealSec is incremented by one and
RealMilisec is reset to zero. If events are being timed
which are asynchronous with the seconds count then it is necessary
to use a separate symbol, incremented along with
RealMilisec, for this count. Otherwise, the resetting
RealMilisec by the
may corrupt the event timing.
This process is carried out by the routine
DenaryConv and its associated sub-routine
Divide. The conversion is achieved by dividing the
specified number by 10,000. The number of times 10,000 can be
divided into the original number, i.e. the quotient, gives the
number of ten-thousands. The remainder is then divided by 1,000 and
the process is repeated with divisors of 100 and, finally, 10. The
remainder from this last division gives the number of units.
The denary digits are stored in the indexed symbol
Answer,4 contains ten-thousands,
contains thousands, and so on to
This routine only works when the computer is in screen mode 7. It
will display, on screen, the denary value of a two byte number
specified by the programmer. The position at which the denary number
is printed is specified by the symbol
Screen's value 3 corresponds to the memory location at
which the right-most digit should be printed. In this case, the
number is printed at the centre of the screen.
This routine produces a steady tone. It writes to the sound processor IC via the SYSTEM VIA. The tone produced is a steady 200Hz at volume 15 (maximum) from channel 1.
This routine is used to turn off the tone produced by
ToneOn. It also writes to the sound processor via the
SYSTEM VIA simply changing the volume (set up by
ToneOn) to 0 (off).
This routine will only work in screen mode 7. It displays the
value of a byte, in hex, on screen. A one byte temporary store with
ConvByte is used, The entry conditions are
The MSB will, of course, be displayed to the left of the LSN. The actual address of the screen location, provided by the X and Y registers is stored in &70 and &71. These zero-page addresses act as indirection registers for the screen address and should not be used elsewhere in the program.
|Home||About Me||Copyright © Neil Carter|
Original document: 1992-04-16
Content last updated: 2008-07-18