By Pete Goodeye
On Having a Good Time
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 timeIn 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 reinsWhat 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 togetherI 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 DOSThat 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
4. Assemble it to a disk file with:
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 inIf 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