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 (aka POKE 768,69)
  • Set DUNIT ($301) to $01 (aka POKE 769,1)
  • Set DCOMND ($302) to $93, the APETIME "GETTIME" request (aka POKE 770,147)
  • Set DSTATS ($303) to $40 (bit 6 set), stating that we want to receive data (aka POKE 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 and POKE 773,6
  • Set DBYTLO/DBYTHI ($308,$309) to $06 and $00 respectively, denoting that we're going to retrieve 6 bytes (aka POKE 776,6 and POKE 777,0)

In the example below, the following are also set, but apparently do not actually matter:

  • Set DRESVD (aka DUNUSE 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, or 104,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, using READ to fetch DATA 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 (aka POKE 768,69)
  • Set DUNIT ($301) to $01 (aka POKE 769,1)
  • Set DCOMND ($302) to $99, the APETIME "SETTZ" request (aka POKE 770,153)
  • Set DSTATS ($303) to $80 (bit 7 set), stating that we want to send data (aka POKE 771,128)
  • Set DBUFLO/DBUFHI ($304,$305) to the location of a buffer containing the timezone (e.g. "EST5EDT")
  • 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 (aka DRESVD), and DTIMLO?