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 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.

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.

` ````
// 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();
}
```

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.

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.ba*t for Windows, and
*set_environment.sh* for Linux) that may be run to configure the include and library directory paths.

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

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

`
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.

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.

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.

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.

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.

This website is powered using ultra low power green locally based servers.

Copyright © i4cy 2000-2024. All rights reserved.