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
ToneOn
, ToneOff
, ReadADC
and
Byte2VDU
which will be explained later.
NOTE: After being called from BASIC, if the machine code
program calls 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
BASIC. Conversely, StopTimer
should
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 (StartTimer
) should
always precede and be accompanied by the right bracket
(StopTimer
) and neither should appear alone.
The actual counts (RealMilisec
and
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 (IntHandle
), the
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
handled cleanly.
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
the SEI
command immediately before reading the first
byte. Turn them back on with the CLI
command
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 (IntHandle
).
Next, the inter-interrupt period (Period
), in
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 StartTimer
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
resume.
This is the Interrupt Service Routine which provides the basis
for the timing operation. This routine is performed once every
millisecond (or Period
microseconds). Hence, to provide
a millisecond count, the routine increments RealMilisec
by one. The millisecond counter is assigned the symbol
RealMilisec
, 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.
The RealMilisec
counter is also used to provide a
count of seconds. This is assigned the symbol RealSec
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
RealSecC
).
In order to provide a seconds count, a further routine is
included after the RealMilisec
increment. If the value
of 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
of RealMilisec
by the RealSec
increment,
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,X
(or Answer,Y
).
Answer,4
contains ten-thousands, Answer,3
contains thousands, and so on to Answer,0
which
contains units.
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
.
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
the symbol ConvByte
is used, The entry conditions are
as follows:
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