C Programming on Sinclair ZX Machines

Generating a Mandelbrot set fractal in C for the ZX80, ZX81 and ZX Spectrum vintage computers with Z88DK

Written by Guy Fernando

Created May 2023 - Last modified Sep 2024


Sinclair ZX Computers

Sinclair ZX80, released in 1980, was a home computer conceived by British inventor Sir Clive Sinclair. It was one of the first affordable personal computers available to the general public. The ZX80 featured a black and white display, a membrane keyboard, and used a BASIC programming language. It had 1 KB of RAM and used a cassette tape recorder for data storage. The ZX80's low price and simplicity made it popular among computer enthusiasts, despite its limited capabilities.

Sinclair ZX81, introduced in 1981, was an upgraded version of the ZX80. It had a similar design but offered a few enhancements. The ZX81 featured a more responsive keyboard and also had 1 KB of RAM. It also introduced a better display system, capable of generating graphics and text in black and white. The ZX81 was highly successful, becoming one of the best-selling computers of its time.

Sinclair ZX Spectrum, launched in 1982, was a significant improvement over its predecessors. It featured a more advanced design and introduced colour graphics, making it a popular choice for gaming. The ZX Spectrum came in multiple models, including 16 KB, 48 KB, and later versions with up to 128 KB of RAM. It had a rubber keyboard, which was sometimes criticised for its lack of responsiveness but became an iconic feature. The ZX Spectrum had a large library of games and applications. It became one of the most successful home computers in the UK and played a significant role in popularising computer gaming.

Overall, these Sinclair ZX machines were early home computers that contributed to the rise of personal computing. They were affordable, accessible, and helped introduce many people to the world of programming and gaming.

Z88 Development Kit

The Z88DK or Z88 Development Kit Small-C cross compiler enables the creation of software for various Z80 based computers and platforms, including the Sinclair ZX80, ZX81 and ZX Spectrum. The use of a cross compiler is particularly beneficial in scenarios where developing software directly on the target platform is not practical or efficient. Z88DK code development is carried out on a host computer running a modern powerful operating system such as Windows, macOS or Linux, but generates executable code for a Z80 based target.

One of the limitations of the ZX80 is that it is unable to perform numerical calculations using real or floating-point numbers essential for generating fractals. Since the Z88DK can link in a maths library using the –lm compiler switch, real number (float or double) can now be used in ZX80 programs that were not originally possible on a computer having only integer BASIC.

The ZX80 and ZX81 only have a text mode via its character generator. Nevertheless rudimentary drawing is possible, since a range of the character set is graphical. This was deliberately incorporated by Sinclair for creating finer shapes and for improving the 32 x 22 text resolution into a reasonable 64 x 44 pixels. The Z88DK graphics library takes advantage of this contained and handled by a suite of graphic functions for drawing points, lines and circles. The same graphic primitives can be used for building ZX Spectrum programs, in fact for any Z88DK supported machine that has graphic capability.

Here we will use Z88DK to compile a single C source file (mandelbrot.c) for drawing the Mandelbrot set fractal on the ZX80, ZX81 and ZX Spectrum machines.

The Mandelbrot Set Fractal

          
            
// Mandelbrot fractal in C for the Sinclair ZX80, ZX81 and ZX Spectrum.
// Guy Fernando (2023)
// 
#include "conio.h"
#include "graphics.h"
#include "math.h"
#include "stdio.h"

// Math definitions for 16-bit or 32-bit floating point maths.
#ifdef __MATH_MATH16
    #define FLOAT       _Float16
    #define SQRT        sqrtf16
#else
    #define FLOAT       float
    #define SQRT        sqrt
#endif

// Complex number.
typedef struct {
    FLOAT real;
    FLOAT imag;
} complex;

// Adds two complex numbers and returns the result as a complex number.
complex* complex_add(complex* a, complex* b)
{
    static complex result;
    result.real = a->real + b->real;
    result.imag = a->imag + b->imag;
    return &result;
}

// Multiplies two complex numbers and returns the result as a complex number.
complex* complex_mul(complex* a, complex* b)
{
    static complex result;
    result.real = a->real * b->real - a->imag * b->imag;
    result.imag = a->real * b->imag + a->imag * b->real;
    return &result;
}

// Returns the real absolute value of a complex number.
FLOAT complex_abs(complex* z)
{
    return SQRT(z->real * z->real + z->imag * z->imag);
}

// Returns a point on the Mandelbrot fractal plane.
uint8_t mandelbrot(FLOAT x, FLOAT y, int max_iter)
{
    static complex c, z;

    c.real = x;
    c.imag = y;
    z.real = 0;
    z.imag = 0;

    uint8_t iter;
    for (iter = 0; iter < max_iter && complex_abs(&z) < 2.0; iter++)
    {
        complex* z_squared = complex_mul(&z, &z);
        complex* z_new = complex_add(z_squared, &c);
        z.real = z_new->real;
        z.imag = z_new->imag;
    }

    if (iter == max_iter)
        return 1;
    else
        return 0;
}

// The program main entry point.
void main()
{
#if __SPECTRUM    
    const uint16_t WIDTH = 256, HEIGHT = 192;
#elif __ZX80__ || __ZX81__
    const uint8_t WIDTH = 64, HEIGHT = 48;
#endif
    const FLOAT x_min = -2.0, x_max = 1.0;
    const FLOAT y_min = -1.0, y_max = 1.0;
    const uint8_t max_iter = 14;

    // Clear screen.
    clg();

    // Plot fractal.
    for (uint16_t y = 0; y < HEIGHT; y++)
    {
        for (uint16_t x = 0; x < WIDTH; x++)
        {
            FLOAT x0 = x_min + (x_max - x_min) * x / (WIDTH - 1);
            FLOAT y0 = y_max - (y_max - y_min) * y / (HEIGHT - 1);
            if (mandelbrot(x0, y0, max_iter))
                plot(x, y);
        }
    }

    // Print title.
#if __SPECTRUM
    printf("MANDELBROT - ZX SPECTRUM");
#elif __ZX81__
    printf("MANDELBROT - ZX81");
#elif __ZX80__
    printf("MANDELBROT - ZX80");
#endif
    getch();
}

          
        
mandelbrot.c


The complex_add function takes two complex numbers as input (a and b) and returns their sum as a new complex number. It performs the addition of the real and imaginary parts separately and stores the result in a static variable result. The function then returns a pointer to result, allowing the result to be accessed outside the function.

The complex_mul function takes two complex numbers as input (a and b) and returns their product as a new complex number. Similar to complex_add, it performs the multiplication of the real and imaginary parts separately and stores the result in a static variable result. The function returns a pointer to result.

The mandelbrot function generates the Mandelbrot set image by iterating over each pixel in the specified width and height. For each pixel, it calculates the corresponding complex number in the complex plane based on the pixel's position.

The main loop of the mandelbrot function iterates the Mandelbrot formula for each pixel until either the maximum number of iterations is reached or the point escapes to infinity. The formula is:

    \( \begin{aligned} z(n+1) = z(n)^2 + c \end{aligned} \)

where z(n) is the current value of z, c is the complex number for the current pixel.

Inside the loop, the variables x and y are used to represent the real and imaginary parts of z. The variables x0 and y0 represent the real and imaginary parts of c, respectively, which are calculated based on the pixel's position and the scaling factors.

The loop performs the iteration until either the escape condition \( \begin{aligned} |z| < 2 \end{aligned} \) or the maximum number of iterations is reached. It updates the values of x and y in each iteration according to the Mandelbrot formula.

After the loop, the variable iter holds the number of iterations performed. If iter equals the maximum number of iterations, it means the point did not escape to infinity and is considered to be inside the Mandelbrot set. In this case, the pixel is drawn black.

Overall, the complex_add and complex_mul functions provide the necessary operations to perform arithmetic with complex numbers, while the mandelbrot function uses these operations to iterate the Mandelbrot formula for each pixel.



Compiling the Program

Once Z88DK has been installed and the build environment configured, the following command lines are used to build the respective target programs. There exist files (z88dk_prompt.bat for Windows, and set_environment.sh for Linux) that may be run to configure the include and library directory paths.

Compiling for the ZX80

zcc +zx80 --math16 -create-app -Cz--audio mandelbrot.c -o mandelbrot


Compiling for the ZX81

zcc +zx81 --math16 -create-app -Cz--audio mandelbrot.c -o mandelbrot


Compiling for the ZX Spectrum

zcc +zx --math16 -lndos -create-app -Cz--audio mandelbrot.c -o mandelbrot


The compact 16-bit floating point library is selected using the --math16 switch and is perfectly acceptable for generating fractal plots while improving the overall execution speed of the program. In addition to creating binary files using the -create-app switch, a WAV file is also generated using the -Cz—audio switch. For just the Spectrum build, the -lndos switch is required to include the standard input output library.


Loading the Program

A modern MAXDuino device was used here to load the code into each Sinclair. There are many variants of this device available. The one pictured below was assembled as a kit and is based on the Arduino Nano. After compiling the source file for each machine, the target binary files (.O, .P and .TAP) are loaded from the host computer and on to an SD card which is inserted into the MAXDuino. When the MAXDuino audio output socket is connected to the machine's EAR socket, it emulates the behavior of a cassette tape deck and allows a Z88DK compiled program to be loaded into the Sinclair.

Before the audio is played, the LOAD “” command is entered into the Sinclair.

Load command

If a MAXDuino or similar device is not available, it is still possible to use the WAV audio file that was created by Z88DK, by playing it from a suitable device such as a smartphone’s headphone socket plugged into the Sinclair’s EAR socket.



It was found that the standard 1K RAM (1024 bytes) built into the ZX80 and ZX81 is insufficient to load even a null C program, so additional memory is required. Here a Sinclair 16k RAM Pack was first plugged into the ZX80 and ZX81 expansion edge connector before powering on these machines.



MAXDuino device used for loading

MAXDuino device used for loading

Executing the Program

The fractal program takes approximately 3 minutes to complete on both the ZX80 and ZX81 when run in “fast mode”, and 50 minutes to complete on the ZX Spectrum owing to the more pixels to plot. Bear in mind that the Z80 is an 8-bit processor and is being clocked at only 3.25MHz, a snail pace in today’s gigahertz race. Also the code contained in mandelbrot.c is not optimized for performance. The code has been written for clarity and for ease of comprehension, and to illustrate the ease by which mathematical calculations can be performed in C on these machines of the early 1980s.

Of course the program may still be run without having a physical Sinclair machine to hand by using a ZX emulator such EightyOne or Fuse.

ZX80 screenshot (64px x 48px)
ZX80 screenshot
ZX81 screenshot (64px x 48px)
ZX81 screenshot
Spectrum screenshot (256px x 192px)
Spectrum screenshot


Conclusion

Overall, Z88DK offers a powerful and flexible development environment for programming Sinclair ZX machines. Its cross-platform capabilities, efficient code generation, rich library ecosystem, and active community support make it an attractive choice for both hobbyists and professionals interested in retro computing or embedded systems development.


External References