I’ve been having a play around with the networking features on my Spectrum Next and felt it would be useful at various points to be able to log timestamps or use timed delays between operations.
This led me to take a look at the machine’s Real Time Clock.
While they became standard on PCs, RTCs were a bit of a rarity in the 8-bit world. Any software that did need to know the time typically required you to type it in, and would then keep somewhat accurate time for as long as the machine was powered up by counting display cycles.
Although not standard, they were available - Dk'tronics produced one amongst their plethora of expansion cards for the CPC, for example.
The Next, for its part, has a DS1307 RTC module, essentially a quartz clock complete with battery that the CPU can talk to, optional on the early models and standard on the KS2 unit that I have.
In NextBASIC there are helper functions to get nicely formatted dates and times, however in assembly or C there is some construction required.
In C (I’m using Z88DK), the simplest way is to use the esx_m_getdate
function which is a wrapper around the M_GETDATE
system call, and returns data in a dos_tm structure comprising integer values for date and time. These are described in the API documentation as being in MS-DOS format.
This isn’t a format I was already familiar with, but repeatedly calling the function produced a stable date value and a time value that was progressively increasing - a ticking clock.
A bit of digging led me to discover that this is the format used in the FAT filesystem and is described here.
In short, the date and time are each stored in two bytes, with years, months, days, hours, minutes and seconds represented by strings of bits long enough to hold each number. To save a bit, the seconds field is divided by two, resulting in times that are always an even number of seconds. If you do need per-second accuracy, there are other ways to call M_GETDATE which provides the full seconds value separately.
Although this isn't as space-efficient as, say, UNIX's "seconds since 1970" timestamp mechanism, there is some eleance in being able to go from a timestamp to a readable time without having to do a lot of calculation, particularly on resource-constrained machine - surely a consideration when DOS was being written for the IBM PC.
<---------------------> <--------------------->
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
h h h h h m m m m m m x x x x x
In order to produce a readable date, each field needs to be extracted. There’s probably a few ways this can be done but the most common I expect, and what I’ve done, is to make use of C’s bitwise operations, particularly the ability shift the bits in a number.
Before we get to that, the first thing we can do is to extract the seconds value. As it only occupies the 5 least significant bits, we can use bit masking to get rid of the rest.
This is done by performing and AND operation between the time field and a number where the unwanted bits are set to zero and the wanted bits are set to one, resulting in an integer where only bits that are set in both the mask and the value will be set in the result.
Although you can statically define bit-masks, for this exercise I’m calculating them on the fly as I think it helps explain the operation. A refinement of what I’ve done could be to write a function that takes a arbitrary number of bits and returns a suitable bitmask.
unsigned int mask5 = (1 << 5) - 1;
This first takes the number 1 (0000 0001) and shifts it five bits to the left, resulting in 0010 0000, or 32 in decimal. Then it subtracts 1, arriving at 31 in decimal and giving us our five-bit mask - 0001 1111.
This leads us to the shift operation. By shifting the value 5 bits to the right, the 6-bit minutes field is now in the least significant bits position, and can be extracted in the same way, using a 6-bit mask again.
Finally we shift the value by 11 (5+6) bits, and use a 5-bit mask to extract the hours.
The date field is similarly structured, with five bits for the day, four bits for the month and seven for the year, which is rather a count of the number of years since 1980.
<---------------------> <--------------------->
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
y y y y y y y m m m m d d d d d
The complete function looks like this:
#include <stdio.h>
#include <arch/zxn.h>
#include <arch/zxn/esxdos.h>
static struct dos_tm nrtc;
void display_clock(){
unsigned int date, time, hh,mm,ds,dd,mo,yy;
esx_m_getdate(&nrtc);
unsigned int mask4 = (1 << 4) - 1;
unsigned int mask5 = (1 << 5) - 1;
unsigned int mask6 = (1 << 6) - 1;
unsigned int mask7 = (1 << 7) - 1;
ds=(nrtc.time & mask5)*2;
mm=(nrtc.time >> 5) & mask6;
hh=(nrtc.time >> 11) & mask5;
dd=nrtc.date & mask5;
mo=(nrtc.date >> 5) & mask4;
yy=((nrtc.date >> 9) & mask7)+1980;
printf("%04u-%02u-%02u %02u:%02u:%02u", yy,mo,dd, hh,mm,ds);
}
C Library function
After initially writing this, I noticed that time.h
has a tm_from_dostm()
function.
This takes the dos_tm
structure and returns a unix-style tm
structure:
struct tm
{
uint8_t tm_sec; // 0-59 seconds (leap seconds may be accommodated)
uint8_t tm_min; // 0-59 minutes
uint8_t tm_hour; // 0-23 hour (since midnight)
uint8_t tm_mday; // 1-31 day of month
uint8_t tm_mon; // 0-11 month (since January)
int16_t tm_year; // years since 1900 (signed)
// following is not filled in by some time functions
uint8_t tm_wday; // 0-6 day (since Sunday)
uint16_t tm_yday; // 0-365 day (since January 1)
int8_t tm_isdst; // daylight savings time <0 (not avail), 0 (no), >0 (yes)
// following is bsd/gnu extension and may not be filled in by some targets
// int32_t tm_gmtoff; // seconds to add to UTC for local time
// const unsigned char tm_zone[6]; // name of timezone
};
Using this, my function can be reduced to:
#include <time.h>
#include <stdio.h>
#include <arch/zxn.h>
#include <arch/zxn/esxdos.h>
static struct esx_drvapi rtc;
static struct dos_tm nrtc;
void display_clock(){
esx_m_getdate(&nrtc);
tm_from_dostm(&tm, &nrtc);
printf("%04u-%02u-%02u %02u:%02u:%02u", 1900+tm.tm_year, 1+ tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
}
Seconds
As mentioned above, dos_tm
timestamps only record seconds with 2-second precision. If we want seconds, we can get that by calling M_GETDATE
directly through the ESXDOS driver API rather than using the esx_m_getdate
library function.
Helpfully, the ESXDOS API provides a shortcut in the form of calling esx_m_drvapi
with driver and function numbers set to zero, which has the same results as M_GETDATE
This removes a bit of abstraction and gives us the time and date as the contents of the CPUs DE and BC register pairs, and seconds in the L register. In principle the H register can contain 100ths of seconds, but not with the RTC fitted to my Next.
To display it, we can load the time and date into the dos_tm
structure, use tm_from_dos
, replace the seconds element with the contents of he L register, and then print it as before.
#include <time.h>
#include <stdio.h>
#include <arch/zxn.h>
#include <arch/zxn/esxdos.h>
static struct esx_drvapi rtc;
static struct dos_tm nrtc;
void display_clock(){
rtc.call.function=0;
rtc.call.driver = 0;
esx_m_drvapi(&rtc);
nrtc.time=rtc.de;
nrtc.date=rtc.bc;
tm_from_dostm(&tm, &nrtc);
tm.tm_sec = (rtc.hl >> 8);
printf("%04u-%02u-%02u %02u:%02u:%02u", 1900+tm.tm_year, 1+ tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
}
References:
In 2024, the Spectrum Next team released a working Sinclair QL core for the Spectrum Next.
The QL is a rather interesting (and peculiar) machine that followed the ZX Spectrum.
Plenty has been written about the machine itself elsewhere so I'm not going to go over that ground here.
I've been slowly exploring the system delivered by the Next team as it's not a platform I've encountered before, and aside from the slight familiarity of being in a BASIC environment with SuperBASIC, it's quite different from anything else I've used.
The software distribution include XChange, which bundles together the four core applications that originaly shipped with the QL - Quill, Easel, Archive, and Abacus.
I wanted to take a look at these to see what the experience would have been like for a user taking a QL out of it's box in 1984.
Unfortunately, some assembly was required. As shipped, the software doesn't quite work, but fortunately fixing it isn't too difficult.
(This applies to the SYSTEM/NEXT 24.11 software distribution - these issues may have been fixed in later releases.)
As this particular software was originally shipped on floppy disk, the boot program needs altered to tell it to use a location in the virtual hard disk image on the Next.
DATA_USE win1_xchange
Take a backup for safety:
COPY boot to boot_bak
LOAD boot
Replace flp1_
with win1_xchange_
on each of these lines:
EDIT 160
EDIT 265
EDIT 270
SAVE boot
You can now start XChange with RUN
.
Due to the QL's odd mutlitasking environment, you'll still be in BASIC even after XChange has started. Press CTRL
(GRAPH
on the Next Keyboard) and C
to switch focus to XChange.
If you move the cursor keys you'll see you're in a menu, but the screen layout is slightly broken. There's a patched version that fixes this, and other bugs.
To get out of XChange, press F3, then Q and return.
Head to Dilwyn's QL Quill, Archive, Abacus and Easel page and download the "Xchange 3.90M Update"
Don't unzip the file - just copy it to the root directory of your SD card. I found when I unzipped it first and transferred the contents, it didn't work on the QL. Later I learned that this is because QDOS executable files have some external metadata which can be stored in zip files but gets discarded if you unzip them on other platforms. Thanks to Janko for explaining that to me).
With the SD card back in the Next and booted:
DATA_USE win1_xchange
COPY xchange to xchange_bak
EW SCOPY;"xch390m.zip xch390m_zip"
PROG_USE win1_commander
EW UNZIP;xch390m
When prompted, confirm the overwrite of win1_xchange_xchange
Then LRUN boot
to get back into XChange, and again press CTRL-C to switch focus.
You should now have a better looking screen with a menu letting you chose which programme you want to start a new task in.
While playing around with the Next's QL Core I managed to mess something up, and rather than debug it in the moment I decided to just create a fresh OS card that I could keep working on.
This got me wondering what's actually needed, amongst all the software that comprises NextZXOS, to actually boot a usable system, and whether I could make a card that would boot the QL core automatically.
If you do want to use the process to make a card for the QL core, copy QXL.WIN
to the card immediately after formatting it, then follow the rest of the instructions.
Note that there isn't really a practical argument for doing this - any SD card you have to hand in 2025 is cavernously large by 1982 standards and there's no reason to not just drop the entire NextZXOS distribution onto it.
It is, however, interesting to see how the boot process works.
After the main core starts, it attempts to load the firmware - TBBLUE.FW from the SD card. It then loads the configuration from files in machines/nextzxos/ - config.ini, menu.ini, and menu.def.
MENU.DEF contains the default list of personalities and can be overridden by a menu.ini in the same format.
At this stage the keymap is also loaded from machines/next/keymap.bin
The first line of menu.def tells us the next files we need:
menu=ZX Spectrum Next (standard),2,8,enNextZX.rom,enNxtmmc.rom,enNextMF.rom
enNextZX.rom
contains the main operating system and BASIC,
enNxtmmc.rom
contains the disk operating system for the SD card, and
enNextMF.rom
contains the 'multiface' firmware that is invoked when the yellow NMI button is pressed.
Two more files are loaded after the system boots:
machines/next/enAltZX.rom
nextzxos/enSystem.sys
This brings us to the familiar NextZXOS menu.
To make the Next launch the QL core automatically from here, we need to add a couple of more things to the card:
The entire contents of machines/ql
:
machines/ql/
machines/ql/core.bit
machines/ql/core.cfg
machines/ql/core2.bit
machines/ql/greek.rom
machines/ql/Instructions.txt
machines/ql/js.rom
machines/ql/min198a1.rom
machines/ql/qlsdn.rom
and the .CORE command:
dot/CORE
Boot the machine, and to make sure it works, enter the Command Line and type .CORE ql
- the machine should reboot into the QL core.
Cycle the power to get back into NextZXOS, use the browser to navigate to /nextzxos
, hit BREAK
and return to the command line.
type 10 .CORE ql
followed by SAVE "AUTOEXEC.BAS" LINE 10
type RUN
to return to the QL core. In future when the machine is powered on with this card present it will briefly enter NextZXOS, then boot the QL core automatically.