Accessing the Real Time Clock - FujiNetWIFI/fujinet-firmware GitHub Wiki
Accessing the FujiNet's Real-Time Clock
[!NOTE] This page uses Atari BASIC examples only.
A Brief History of Time
Back in the 1980s, ICD created the "RTime8" cartridge for the Atari 8-bit line of computers, which was a pass-through cartridge that provided a battery-backed-up real-time clock to the Atari. SpartaDOS took advantage of this, to allow timestamps on files (something not available in, e.g., Atari DOS), and it was popular with Atari users who ran Bulletin Board Systems (BBSes).
An optional device handler, "ZHAND
", was available which made the clock accessible to the OS (and users and programmers) via a new "Z:" device.
Today (the 2020s) a drop-in replacement for ICD's cartridge is available from The Brewing Academy, and real-time clock features are included in various Atari upgrades -- e.g., the Ultimate 1MB upgrade, and the SIDE2 and SIDE3 mass-storage cartridges.
AtariMax's "Atari Peripheral Emulator" (APE) system -- which allows an Atari to use a modern PC as floppy disk drives and other devices, via a simple cable (similar to the SIO2PC system that first appeared in the early 1990s) -- provides the Atari with access to the PC's clock via an SIO protocol called "APETIME".
The FujiNet also responds to the APETIME protocol used by APE, allowing a stock Atari to access the current time and date without any extra cartridges or upgrades, and without even booting into a DOS!
As an extension to the protocol, FujiNet also allows you to specify the timezone for which you wish to receive the current time. (For example, if you want to know the time in UTC, you can do so.) See below.
Implementation Overview
First, set up the SIO Device Control Block (DCB):
- Set
DDEVIC
($300) to $45, the APETIME device ID (akaPOKE 768,69
) - Set
DUNIT
($301) to $01 (akaPOKE 769,1
) - Set
DCOMND
($302) to $93, the APETIME "GETTIME" request (akaPOKE 770,147
) - Set
DSTATS
($303) to $40 (bit 6 set), stating that we want to receive data (akaPOKE 771,64
) - Set
DBUFLO
/DBUFHI
($304,$305) to the location of 6 bytes where the results should be stored- For example, if you just want to use the first 6 bytes of 'Page 6' user memory ($600 through $605, aka 1536 through 1541), do
POKE 772,0
andPOKE 773,6
- For example, if you just want to use the first 6 bytes of 'Page 6' user memory ($600 through $605, aka 1536 through 1541), do
- Set
DBYTLO
/DBYTHI
($308,$309) to $06 and $00 respectively, denoting that we're going to retrieve 6 bytes (akaPOKE 776,6
andPOKE 777,0
)
In the example below, the following are also set, but apparently do not actually matter:
- Set
DRESVD
(akaDUNUSE
in Mapping the Atari) to $00; it is unused - Set
DTIMLO
to $0F, setting the timeout to 15 seconds (or, whatever you want) - Set
DAUX1
($30A) to $EE - Set
DAUX2
($30B) to $A0
Then, call "SIOV", the Atari OS's Serial Input/Output (SIO) utility entry point. In assembly, this would simply be:
PLA
JSR $E459
RTS
Calling SIOV from BASIC
You can use Atari BASIC's (and variants') USR()
function to call a machine language routine.
- The 6502 machine language for the assembly code is
$68,$20,$59,$E4,$60
in hex, or104,32,89,228,96
in ASCII. - It's possible to
POKE
those values into memory (e.g., somewhere in 'Page 6')... - ...or to assign them into a string variable in BASIC manually (
A$(1)=CHR$(104)
)... - ...or via a
FOR
/NEXT
loop, usingREAD
to fetchDATA
values (FOR I=1 TO 5:READ N:A$(I)=CHR$(N):NEXT I
)... - ...or even simply assign the values directly as ATASCII characters! (
A$(I)="h Y{d}◆"
-- lowercase 'h', space, uppercase 'Y', inverse-video lowercase 'd', diamond ([Control]+[.]).
BASIC code
The following was taken from a post by Stuart Simpson to the FujiNet Facebook group, in a thread that started with a query by Jeff Thiele, and included with help from Thomas Cherryhomes. (Slight modifications during wikification by Bill Kendrick.)
5 GOSUB 900
7 GRAPHICS 2+16
8 POKE 65,0:REM TURN OFF SIO SOUND
10 REM FUJINET SHOW TIME/DATE
20 POKE 768,69:REM DDEVIC=$45
30 POKE 769,1:REM DUNIT=$01
40 POKE 770,147:REM DCOMND=$93
50 POKE 771,64:REM DSTATS=$40
60 POKE 772,0:REM DBUF=$0600 (DBUF LO BYTE)
61 POKE 773,6:REM ...(DBUF HI BYTE)
62 POKE 774,15:REM DTIMLO=$F
63 POKE 775,0:REM DRESVD=0
64 POKE 776,6:REM DBYTL=6 (HI BYTE)
65 POKE 777,0:REM ... (LO BYTE)
66 POKE 778,238:REM DAUXL=$EE
67 POKE 779,160:REM DAUXH=$A0
68 X=USR(ADR(A$))
70 DD=PEEK(1536)
71 MO=PEEK(1537)
72 YY=PEEK(1538)
73 HH=PEEK(1539)
74 MM=PEEK(1540)
75 SS=PEEK(1541)
76 POSITION 0,0
77 REM -----------------------------
78 REM TAKE CARE OF SINGLE DIGIT CASES
79 REM -----------------------------
80 IF DD<10 THEN DD$(1)="0":DD$(2)=STR$(DD)
81 IF MO<10 THEN MO$(1)="0":MO$(2)=STR$(MO)
82 IF YY<10 THEN YY$(1)="0":YY$(2)=STR$(YY)
83 IF HH<10 THEN HH$(1)="0":HH$(2)=STR$(HH)
84 IF MM<10 THEN MM$(1)="0":MM$(2)=STR$(MM)
85 IF SS<10 THEN SS$(1)="0":SS$(2)=STR$(SS)
87 IF DD>=10 THEN DD$=STR$(DD)
88 IF MO>=10 THEN MO$=STR$(MO)
89 IF YY>=10 THEN YY$=STR$(YY)
90 IF HH>=10 THEN HH$=STR$(HH)
91 IF MM>=10 THEN MM$=STR$(MM)
92 IF SS>=10 THEN SS$=STR$(SS)
100 REM ------------------------------
130 ? #6;DD$;"/";MO$;"/";YY$
140 ? #6;HH$;":";MM$;":";SS$
150 GOTO 10
900 REM ******SET UP STRINGS
905 NUMDATA=5
907 DIM A$(NUMDATA)
908 DIM DD$(2),MO$(2),YY$(2),HH$(2),MM
$(2),SS$(2)
909 REM ROUTINE TO LOAD A$ WITH ASM
910 REM CODE WITHOUT HAVING TO USE
920 REM FUNNY CHARACTERS
930 FOR LOOP=1 TO NUMDATA
940 READ VALUE
950 A$(LOOP)=CHR$(VALUE)
960 REM PRINT VALUE
970 NEXT LOOP
980 REM PRINT A$
990 RETURN
999 END
1000 DATA 104,32,89,228,96
1010 REM *****************
1020 REM ASM ROUTINE IS
1025 REM ----------------------
1030 REM PLA
1040 REM JSR $E459
1050 REM RTS
1055 REM ----------------------
1060 REM $68,$20,$59,$E4,$60
1065 REM *****************
C code
The OS
struct used here is defined in cc65's _atarios.h
, and the siov()
function should contain code similar to sio.h
and sio.c
found here: https://github.com/FujiNetWIFI/fujinet-nhandler/tree/master/test-programs/sio/nopen/src
#include "sio.h"
...
char time_buf[6];
unsigned char y, m, d, h, m, s;
memset(time_buf, 0, 6);
OS.dcb.ddevic = 0x45; /* APETIME protocol */
OS.dcb.dunit = 0x01; /* unit 1 */
OS.dcb.dcomnd = 0x93; /* GETTIME request */
OS.dcb.dstats = 0x40; /* receive */
OS.dcb.dbuf = (void *) time_buf;
OS.dcb.dtimlo = 15; /* timeout (seconds) */
OS.dcb.dunuse = 0;
OS.dcb.dbyt = (unsigned int) 6; /* reading 6 characters */
OS.dcb.daux1 = 0xee;
OS.dcb.daux2 = 0xa0;
siov();
if (OS.dcb.dstats == 1 /* no error */) {
y = time_buf[2]; /* Year minus 2000 */
m = time_buf[1]; /* Month (1-12) */
d = time_buf[0]; /* Day (1-31) */
h = time_buf[3]; /* Hour (0-23) */
m = time_buf[4]; /* Minute (0-59) */
s = time_buf[5]; /* Second (0-59) */
} else {
/* error occurred */
}
Other examples
Wade Ripkowski (of Inverse ATASCII (🕭) podcast) posted examples in BASIC, Action!, and C (CC65) on his "Unfinished Bitness" blog: https://unfinishedbitness.info/tag/apetime/
Changing timezone
It's possible to set your FujiNet's time zone via the device's web interface.
You can also send a "SETTZ" ($99) request, using the following Device Control Block:
- Set
DDEVIC
($300) to $45, the APETIME device ID (akaPOKE 768,69
) - Set
DUNIT
($301) to $01 (akaPOKE 769,1
) - Set
DCOMND
($302) to $99, the APETIME "SETTZ" request (akaPOKE 770,153
) - Set
DSTATS
($303) to $80 (bit 7 set), stating that we want to send data (akaPOKE 771,128
) - Set
DBUFLO
/DBUFHI
($304,$305) to the location of a buffer containing the timezone (e.g. "EST5EDT
")- See https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
- e.g.
DIM A$(7):A$="EST5EDT":BHI=INT(ADR(A$))/256:BLO=ADR(A$)-(BHI*256):POKE 772,BLO:POKE 773,BHI
- Set
DBYTLO
/DBYTHI
($308,$309) to the length of the buffer (e.g.,POKE 776,LEN(A$):POKE 777,0
) Then, call "SIOV", as described above.
FIXME
- Is there a
Z:
handler equivalent that works with FujiNet? - Is there a better way to discuss the non-necessity of
DAUX1
/DAUX2
,DUNUSE
(akaDRESVD
), andDTIMLO
?