Z80 vs C

Introduction

I am learning Z80 assembler, with the intention of building my own Z80 based computer, and also learning how to write ZX Spectrum games. The two seem good projects that go well together. In this post I compare the similarities and differences between a low level programming language like Z80 and a high level language like C.

Types of programming language

There are many many programming languages, but they can generally be categorised into two main broad types

High level languages

These look a bit like English, tend to use words to express programming concepts and are easy for people to understand. If you’ve written code in Python, JavaScript or C, you’ve written it in a high level language.

Low level languages

These are a direct 1:1 translation of the binary the CPU understands, and use short mnemonics to list the steps necessary to make the CPU perform operations. Due to the low level nature, these are difficult for people to understand, but with only some translation low level languages are exactly what the CPU executes.

(Yes, sometimes we refer to C as being a mid-level language because it’s more low level than Python, but not as low level as assembler. And yes, you can mix assembler into some languages like C. We’re not talking about that today…)

Programming Constructs

Programming a computer is split into three basic types of operation

  • Sequencing - This is simply where each instruction is executed sequentially, one at a time.
  • Selection - This is where the program can make a decision and switch to a different piece of code.
  • Iteration - Iterating means repeating, so this is where the code loops.

You can also possibly add subroutines to the list, too.

Our example program

Here’s a familiar little program, anyone who’s owned a DVD player will have seen something similar

How it works

The code for this is relatively straight forward, once you know the trick. No, it’s not using any fancy physics engine, it’s using a very simple piece of logic

  • Store the X and Y position of the dot
  • Store an X and Y velocity vector
  • Calculate the new X and Y position by adding the relevant velocity vector
  • If the X or Y position is off the screen, change the relevant vector
  • Repeat

C version

In C the code would look something like this

#include <stdio.h>
#include <fake-graphics-lib.h>

#define SCREEN_W 100
#define SCREEN_H 100

int xPos, yPos, xVel, yVel;

int main()
{
    xPos = 10;
    yPos = 10;
    xVel = 1;
    yVel = -1;

    while (1) {
        if (xPos + xVel > SCREEN_W) xVel = -1;
        if (xPos + xVel < 0>) xVel = 1;
        if (yPos + yVel > SCREEN_H) yVel = -1;
        if (yPos + yVel < 0>) yVel = 1;
        
        xPos += xVel;
        yPos += yVel;

        clear_screen();
        draw_at(xPos, yPos, "*");
        wait_a_bit();
    }
}

Even if you’ve never seen C much before, it’s made from human readable words like “if” and “while”, and structured in a way that allows us to follow the flow of the code using indentation and curly brackets.

Z80 Assembler version

The Z80 version looks like this

(Disclaimer, this is my first ‘proper’ Z80 program, so don’t use this as an example of what good programming practises look like!)

; Makes a * bounce around the screen
; Code file
start: .org #8000
	.model Spectrum48
	ld a,2
	call 5633
	ld a,1
	ld (xPos),a
	ld a,10
	ld (yPos),a
	ld a,1
	ld (xVel),a
	ld a,-1
	ld (yVel),a

Loop:
	; Motion on X Axis
	ld a, (xPos)
	ld hl,(xVel)
	add a,l
	cp 32		; hit rh edge?
	jp z,decX
	cp 0		; hit lh edge?
	jp z,incX
MoveX:
	ld a,(xPos)
	ld hl,(xVel)
	add a,l
	ld (xPos),a


	; Motion on Y Axis
	ld a, (yPos)
	ld hl,(yVel)
	add a,l
	cp 20		; hit bottom edge?
	jp z,decY
	cp 0		; hit top edge?
	jp z,incY
MoveY:
	ld a,(yPos)
	ld hl,(yVel)
	add a,l
	ld (yPos),a

	call print
	jp Loop

decX:
	ld a,-1
	ld (xVel),a
	jp MoveX

incX:
	ld a,1
	ld (xVel),a
	jp MoveX

decY:
	ld a,-1
	ld (yVel),a
	jp MoveY

incY:
	ld a,1
	ld (YVel),a
	jp MoveY

print:
	call setxy
	ld a,'*'
	rst 16
	call delay
	call setxy
	ld a,32         ; ASCII code for space.
    rst 16          ; delete old asterisk.
    call setxy      ; set up our x/y coords.
	ret

setxy:
	ld a,22
	rst 16
	ld a,(yPos)
	rst 16
	ld a,(xPos)
	rst 16
	ret

delay:
	ld b,1
delay0:
	halt
	djnz delay0
	ret

xPos defb 0
yPos defb 0
xVel defb 0
yVel defb 0

The first thing you should have noticed is how long it is! Every instruction goes on its own line, and every instruction does one specific thing. In fact, some of these instructions do nothing more than storing a number in a part of the CPU called a “register” ready for the CPU to move that number into a piece of RAM for storage.

I won’t go through it all, that is left as an exercise for the reader, but let’s pick one simple looking piece of code from the C version

xPos += xVel;

That single line of code merely adds the contents of “xVel” into the variable called “xPos”. In some other languages it might be written as “xPos = xPos + xVel”.

In assembly code, it looks like this. I’ve added comments to explain what the code is doing

ld a,(xPos)             ; Copy the value stored at memory location "xPos" in the 'a' register
ld hl,(xVel)            ; Copy the value stored at memory location "xVel" in the 'hl' register
add a,l                 ; Add the contents of the 'l' register to the 'a' register, storing it in 'a'
ld (xPos),a             ; Copy the value stored in the 'a' register into the memory location "xPos"

(The Z80 CPU is restricted in how its registers work, you can’t just load any memory location into any register, you have to use specific ones, and some registers like ‘hl’ are actually two smaller registers ‘h’ and ‘l’ which can be accessed separately, or together depending on what you need.)

While the code is much more verbose, you can see it very precisely explains to the CPU how to add the two numbers together. This is (to my knowledge) the way it’s done, this is the simplest way of describing “add two numbers together” to a computer. It takes four steps.

  • Get the first number from memory, keep it safe
  • Get the second number from memory, keep it safe too
  • Add them together and remember the answer
  • Write the answer back to memory

This direct control over the CPU is exactly what makes assembly language so fast, and so interesting to write. It’s also what makes it so complicated, long winded and frustrating to debug. And those negative points are why high level languages exist. However CPUs only understand low level languages, so underneath a modern web browser running JavaScript is still a translation tool that’s converting the code into something the CPU can follow.

Something to try and follow is the pattern of jumps and function calls in the above code. The words like “Loop:” and “delay0:” on lines by themselves are labels. The commands “jp” “djnz” and “call” cause the program to move to a new section, and “ret” causes the program execution to return to the last place it was called.

There is, no doubt, a much shorter way of writing that code, too. Some of the adding of the velocities seems redundant. In a future post I might dig into how many CPU cycles it takes for code to run, to see if I can optimise the programs to run faster or to use less memory. For now, I’m just trying to train my brain to break down high level concepts like for loops into their actual component steps.

Did you like this post?

if you did, it'd be really nice if you shared it