Millisecond Timer for the BBC Microcomputer

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.

Introduction

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.

How It Works

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.

Description of Routines

StartTimer

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.

StopTimer

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.

IntHandle

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.

Hexadecimal to Denary Conversion

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.

Display

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.

ToneOn

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.

ToneOff

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).

Byte2VDU

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