Classic Computer Magazine Archive ANTIC VOL. 1, NO. 4 / OCTOBER 1982

Systems Guide
On Having a Good Time

By Pete Goodeye

Most enlightened users know the ATARI computers have a crystal-controlled dock that reports to the CPU with an interrupt each time a vertical TV scan begins. The scan rate is a constant sixty times per second, so this looks like an ideal basis for an accurate time-of-day dock.

But anyone who has tried this will have stumbled across a few obstacles. For a start, crystal-controlled accuracy isn't that good. Run your program for an hour or so and you will find that it's about five seconds slow. Also, an accurate clock should keep running independently of other computer activities. It is not very helpful if, when the program stops, so does the dock. It would be even more disastrous if the dock module code was erased.

What is wrong with our "precise" 60 Hz scan rate? It is not, as you might think, simply sloppy engineering! In fact the frequency is a tightly maintained 59.92 Hz. The reason for this odd value goes back in electronic history.

A twisty tale of time

In the days before color TV, the vertical scan rate was indeed exactly 60 Hz. Color meant that a lot more information now had to be packed into the space originally allocated for black and white transmission, without upsetting older receivers tuned to the same signal. A color frequency with its own set of harmonics and sidebands would, given a chance, insinuate themselves onto the other signals, causing "herringbone" patterns in the picture. These effects were minimized by careful juggling of the various frequencies involved.

Unfortunately, the ATARI gets a little more complicated. Unlike a standard color TV transmission, where the color signal is carefully kept unrelated to the line frequency, the Atari wants an exact number of color clocks per line, so that it can generate colors digitally. The color clock itself must adhere to the standard pretty closely, because this is critical for proper color, but now all the other frequencies are divided down from this, ending up with that vertical scan frequency of 59.92 a second (compared with a broadcast TV rate of 59.94).

We have a simple cure for this tardiness. Every time we compute that our time-keeping has slipped one "tick" (i.e. one vertical scan period) out of step, just add an extra one into the count. The proper interval between corrections is almost thirteen seconds. With this correction we get well within the accuracy of the crystal, and should keep time within a couple of seconds per day.

Hanging on to the reins

What about keeping the clock running and on time, independent of other activities? The main hangup is that most processing related to the vertical-scan interrupt is inhibited when urgent tasks -like servicing the disk-have to be done. Normally our clock must be treated in the same way. There is a critical path for the dock interrupt which is not blocked at these critical times, but if we did all our timekeeping there we would quickly run into serious trouble, by interfering with all our peripheral communications.

The solution is to split our processing, doing only the essential counting of seconds in the unblockable path, and all the rest in the non-critical way. If the main process finds that it has missed some seconds while it was blocked, it just does some extra cycles to catch up. (By the way, if you knowledgeable readers know that the ATARI operating system already has several clocks and timers based on the same interrupt, and are wondering why we don't just use one of them, the reason is that they are all either used for something else, or are cleared by "RESET"; so constructing one of our own is a necessity.)

The ability to postpone updating our clock, that we have gained with this split-processing approach, turns out to be useful in another way. The time that is so diligently kept by our dock has to be read at some point-usually by a BASIC program. The trouble is that more than one number is involved (hours, minutes and seconds). At the speed with which we can do things in BASIC there is a good possibility that by the time the last number is read, the first is no longer valid! To correct this problem we simply add a flag that, when set, freezes the clock. We set this before picking up the time values and clear it again afterwards, getting a nice uncorrupted reading.

The timekeeping routine needs to be inserted in the interrupt service chain, and we also have to ensure that it is not erased. A number of countermeasures are necessary, including, a harmless patch to DOS.

Puting the pieces together

I have placed the module in the cassette buffer. If you don't run the cassette, this area is unused. Two points of caution about its present location: although the cassette buffer extends from 3FD hex to 4FF, "SYSTEM RESET" clears all of pages two and three so our code can't actually begin below 400; also the module slightly overflows the top end of the buffer into locations used by BASIC. Fortunately the initilization code - which only runs when the module is first loaded-can be put there, so there is no great problem.

We can partition the code as follows. The user communication area [TIMLOK, SECS...DAYS] is placed at the very beginning, so that it can be easily referenced. Then, after some local storage, comes the code to handle the RESET button, followed by the interrupt handler itself [CLOCK]. At the end-and extending into BASIC's space - is the initialization code [WINDIT]. Notice the start-address is location 2E2 (not 2EO).

Now, I'll define some of the terminology. First of all, clock "ticks" are actually VBLANK interrupts. When a VBLANK interrupt occurs, the operat ing system jumps to the service routine through the "immediate VBLANK vec tor" location VVBLKI. The service routine is normally within the if operating system [SYSVBV], but we can change the contents of the vector to point to our own routine. The vector is a two-byte address, and there would be a reasonable possibility of an interrupt happening just after one byte of the two had been changed, with disastrous results. To circumvent this hazard, the operating system provides a special routine SETVBV that should always be used to alter this. The particular vector to change is selected by the value passed in the accumulator (6 in this case). The operating system will always restore the vector to its original value on RESET, so we in turn must immediately re-fix it.

The reset code in the module is executed after the operating system's reset sequence has been initiated. This sequence occurs whenever the system has been bootstrapped (not simply loaded) from cassette. This CASINI vector points to the code that performs the reset sequence, and may be used for our own purposes. If the CASINI vector does happen to be already in use, the startup-time code [WINDIT] will store the current value as part of a JSR instruction [INIT( IN] in the reset code so that it is still executed. The operating system is informed that CASINI is enabled by setting bit 1 of the flag BOOTF.

Caution! Don't try to load the module more than once without a coldstart! If you do so it will find its own address in CASINI and go into an infinite loop at the next RESET.

Once the VBLANK vector is set up, our interrupt service routine [CLOCK] will go into action at each clock tick. There is little more than needs to be said about this section, except to note how the CRITIC flag is used. This flag is set non-zero by the system to inhibit deferred VBLANK processing, and we too must bypass most of the normal sequence at such times. We actually combine the CRITIC flag with our our own TIMLOK, freezing the clock if either is nonzero, so that we can avoid reading a running clock. Don't leave TIMLOK set for more than ten minutes, though. If the number of 13second adjustments needed become greater than 60, the count-down timer [CNT60] will overflow and take several seconds to get back in step. TIMLOK is set initially to 255, as a signal to the user that the clock has never been run. But the user program, when setting the time of day, should always initialize the seconds-counter CSECS to zero and the count-down timer CNT60 to sixty immediately before releasing TIMLOK, thus ensuring that the clock starts in sync.

Doing it to DOS

That about covers the code itself. Now all we have to do is make the DOS patch. Under DOS 2, when you return to a cartridge program from DUP.SYS with the "B" command, the VBLANK interrupt vectors will be reset to their original system values. I have never fathomed the intention behind this. In any case, suppressing the action has absolutely no detrimental effect on normal usage.

The patch is trivial, a jump to avoid that section of code, but a little messy to install, because DUP.SYS usually goes away when not in use. It seems best to present it as a recipe; the one that follows is probably the shortest reasonable path.

1. Install the Assembler/Editor cartridge and boot up with DOS-2

2. Insert the disk you intend to patch. It should have DUP.SYS on it - and I suggest it be a scratch disk!

3. Use the editor to generate the patch:

10 * = $272A
20 JMP $1912
30 .END

4. Assemble it to a disk file with:

ASM,,#D:DOSPATCH.OBJ

5. Got to the DOS menu.

6. Give the "C" command, and in response to the file-spec query enter: DOSPATCH.OBJ,DUP.SYS/A

At this point the patch has been tagged on to the end of the save file, making it one sector longer.

7. Use the "B" command to return to the editor, and then go back to DOS. This brings in the modified system.

8. Re-install the new system on your disk drive with the "H" command. DUP.SYS will return to its original length.

You should, of course, copy the modified system onto any disk you are going to use while the clock module is running. (In fact I never use anything but the patched version.) For this you can use either the "H" command to install a complete DOS, or "C" or "O" to update DUP.SYS alone.

Where we came in

If you've stuck with me this far, you probably don't need to ask "What can I use it for?" You must have desperate need for it. However, as an example of how to couple the module to BASIC, a simple digital clock is given in listing 2. Take it and go from there.

10 ;"ACCURATE" CLOCK MODULE
20 ;Copyright Pete Goodeve, 1982
30 ;=========================
40 ;
50 ;occupies cassette buffer
60 ;
70 ;Atari OS references:
80 SETVBV=$E45C set-vector entry
90 SYSVBV=$E45F OS VBLANK service
0100 VVBLKI=$222 immed. VBLANK vector
0110 CRITIC=$42 critical section flag
0120 CASINI=$2 "cassette" init vector
0130 BOOTF=9 boot mode flag for init
0140 ;
0150 ;
0160  *=$400 cass. buffer(1024 ded
0170 ;
0180 TIMLOK .BYTE $FF
0190 SECS .BYTE 0
0200 MIN .BYTE 0
0210 HRS .BYTE 0
0220 DAYS .BYTE 0
0230 ;
0240 CNT60 .BYTE 60 VBLANK ticks
0250 CSECS .BYTE 0 contin. count
0260 ASECS .BYTE 13 adjustment count
0270 ;
0280 ;
0290 INITON=*+1
0300 ;Comes this way on RESET Button
0310 ;via "Cassette Init" vector:
0320 RESET
0330  JSR NUTHIN -- filled before use
0340 SETINT
0350  LDX #CLOCK/256
0360  LDY #CLOCK&$FF
0370  LDA #6 "immediate VBLANK" code
0380  JSR SETVBV set up interr# vect,
0390 NUTHIN
0400  RTS
0410 ;
0420 ;
0430 ;Immed VBLANK interrupt service
0440 ;comes through here first:
0450 ;
0460 CLOCK
0470  DEC CNT60 count 60 ticks
0480  BNE XIT before doing anything
0490  INC CSECS keep track of seconds
0500  LDX #60 (kept around for later)
0510  STX CNT60 reset count
0520  LDA CRITIC check if critical
0530  ORA TIMLOK or if locked by user
0540  BNE XIT gotta stop here
0550 ; continue on if not critical
0560 ; or locked ...:
0570 ; repeats if seconds were missed
0530 CLKLP
0590  DEC ASECS 13 second count down
0600  BNE TICK
0610  LDA #13
0620  STA ASECS reset 13-sec count
0630  DEC CNT60 and skip one tick
0640 TICK
0650  INC SECS user's time
0660  CPX SECS reached 60 yet!
0670  BNE TOK nops
0680  LDY #0
0690  STY SECS reset seconds
0700  INC MIN and bump minutes
0710  CPX MIN over the hour?
0720  BNE TOK not yet
0730  STY MIN and so on
0740  INC HRS
0750  LDA #24
0760  CMP HRS
0770  BNE TOK
0780  STY HRS
0790  INC DAYS
0800 ;...etc. if needed
0810 TOK
0820  DEC CSECS were any missed?
0830  BNE CLKLP round again if so
0840 VVON=*+1
0850 ;continue with VBLANK chain:
0860 XIT
0870  JMP SYSVBV altered at setup
0880 ;
0890 ;
0900 ;
0910 ;*** INITIAL ENTRY HERE
0920 ;gets overwritten by BASIC
0930 WINDIT
0940  LDX CASINI+1 Cassette Init vect
0950  BEQ NOINI zero if not used
0960  LDY CASINI rest of current vect
0970 SETON
0980  STX INITON+1 set up JSR address
0990  STY INITON so stuff gets done
1000  LDA #RESET/256 plug in our own
1010  STA CASINI+1 reset sequence
1020  LDA #RESET&$FF
1030  STA CASINI
1040  LDA VVBLKI current immed VBLANK
1050  STA VVON will be done after us
1060  LDA VVBLKI+1
1070  STA VVON+1
1080  LDA BOOTF bootstrap mode flag
1090  ORA #2 must include "cassette.'
1100  STA BOOTF
1110  JMP SETINT go set VBLANK vector
1120 ;
1130 NOINI
1140  LDY #NUTHIN&$FF dummy for JSR
1150  LDX #NUTHIN/256
1160  BNE SETON
1170 ;
1180 ;
1190 ;Autostart addr.
1200  *=$2E2 "init" vector
1210  .WORD WINDIT
1220 ;
1230  .END

Listing: ACLOCK.SRC Download / View