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:

Posted Sat Aug 9 00:23:18 2025 Tags: