Now we have finished with the special things that have to be at the start, we can get back to our "main" program. The first thing that has to do is initialise various parts of the system.

Main program start - stack pointer

;==============================================================
; Main program
;==============================================================
main:
    ld sp, $dff0

Here's the "main" label we were coming to.

You may remember that sp is a special register called the Stack Pointer, and that I didn't explain the stack. I'm still not going to because we don't need to use it yet; but suffice to say that the stack takes up some of the available RAM, and we have to tell it which RAM to use. We tell it here to take some memory ending at offset $dff0. The amount it takes varies, but by telling it to be at the end of RAM, we can use memory at the start of RAM and be confident of not overlapping with it.

As a reminder, the SMS has 8KB of RAM, located from hex address $c000 to $dfff, and mirrored at $e000 to $ffff. We don't set sp to $dfff (the very end) for an important reason which I will come to later.

We load the register with a value using the ld <register>,<value> instruction. You can read it as "load stack pointer with $dff0".

Setting up the VDP registers - block transfer

Now we get a technical bit. The VDP (Video Display Processor) is the graphics chip in the SMS. It has a set of registers and some RAM inside it which we control through two ports, $be and $bf. Charles MacDonald's "Sega Master System VDP documentation" is a very good (advanced) document on how it works, but it's a lot to go into for now. Suffice to say that lower down in the program I've put a block of data we can use for setting these registers to suitable initial values:

; VDP initialisation data
VdpData:
.db $04,$80,$00,$81,$ff,$82,$ff,$85,$ff,$86,$ff,$87,$00,$88,$00,$89,$ff,$8a
VdpDataEnd:

.db is a WLA DX directive which instructs it to just put the (byte) data you write in the ROM with no modification. .db should be followed by a comma-separated list of values which are evaluated to bytes and stored.

In order to write to the VDP registers, we have to output this data to the VDP control port, which is port $bf. I'm going to do this using one of the Z80's block transfer instructions. These take values stored in certain registers and transfer (copy or output) a block of data according to those values. Here's the code:

    ;==============================================================
    ; Set up VDP registers
    ;==============================================================
    ld hl,VdpData
    ld b,VdpDataEnd-VdpData
    ld c,$bf
    otir

otir means "output the b bytes of data starting at the memory location stored in hl to the port specified in c". That's great - we can figure out all of those, as you can see. One trick is that we subtract two labels - "VdpDataEnd-VdpData". Because labels get turned into addresses, the difference between two labels will be the number of bytes between them.

There are other block transfer commands, most notably ldir which can be used for copying from one memory location to another.

Clearing VRAM - VRAM write access, looping, conditional jumps

We don't know what's in the VDP RAM and if we don't clean it up, it will make our screen ugly. (In actual fact it will contain the SEGA logo from the BIOS on a real system.) So let's do that, by setting every byte of it to zero.

    ;==============================================================
    ; Clear VRAM
    ;==============================================================
    ; 1. Set VRAM write address to 0 by outputting $4000 ORed with $0000
    ld a,$00
    out ($bf),a
    ld a,$40
    out ($bf),a
    ; 2. Output 16KB of zeroes
    ld bc, $4000    ; Counter for 16KB of VRAM
    ClearVRAMLoop:
        ld a,$00    ; Value to write
        out ($be),a ; Output to VRAM
        dec bc
        ld a,b
        or c
        jp nz,ClearVRAMLoop

Wow, look at that! That's quite a piece of code there, quite daunting really. But it's not that bad, honestly - you'll laugh at something like that in no time. Let's see what's there.

First, we have to communicate with the VDP and tell it that we want to write to VRAM. (VRAM is what we'll call the RAM inside the VDP.) To do this we have to tell it the address we want to write to, and tell it we want to write. Because there's 16KB of VRAM, we'll need 14 bits (214 = 16384 = 16KB) for the address. The last two bits to make it up to a 16-bit (2-byte) number are used to signal what our intentions are. To get the final number we can use an OR calculation - the number $4000 only contains the bits required to tell the VDP we want to write to VRAM, and if we OR it with the address we'll get the final number to send to the VDP. In out case, we want to start at address $0000 so the final number is $4000. (Try it on a calculator which supports hexadecimal.)

We have to output this to port $bf, the VDP control port as I told you a while back. However, we have to consider byte ordering.

Most modern computer systems are based around bytes as a unit of storage. However, they also often want to deal with larger numbers - typically 32-bit or 64-bit on modern computers - so when these numbers are being transferred as multiple bytes, there needs to be some standard to decide in what order to transmit them. On the Z80, and the SMS VDP, the order is that the least significant byte' comes first, so if you have a number like $4000 you will communicate it in the order $00, $40. This is also known as "little-endianness". So that's why in part 1 above we output $00 and then $40.

Why can't we write "out ($bf),$4000"? Because the Z80 doesn't know how to. There are restrictions on how you can handle data. In this case, we can only output one byte at a time, and the data has to come from register a. (There are other possible ways to do it but this is the easiest.)

Now we've set the VDP ready to receive data. We send it data by outputting to port $be, the VDP data port. When it gets it, it will write it to VRAM and then (rather handily) move to the next byte of VRAM, so we can just send it a stream of data bytes and it will write them consecutively to VRAM. So we need to send 16384 zeroes. The way to do this is to start at $4000 (=16384), then output a zero and decrease our counter. Then we'll repeat this until our counter is zero. That's what part 2 is doing.

First, we store $4000 in register pair bc. Then we come across another Z80 instruction we wish we had. We want to go in a loop, decreasing bc by one each time and checking if it's zero - but while there is an instruction to decrease a register pair by one (decrement it), it doesn't have a built-in check if the answer is zero. So we will check it ourselves, using the fast or instruction. This combines the current byte in a with another byte, giving a result with a binary 1 where either of the two inputs had a 1. So it can only give a result of zero if both inputs were zero.

The or instruction also affects the z flag. This is one bit in the f (flag) register, which will be 1 if the last calculation result was zero - but only for certain calculations. (Check the Z80 CPU User Manual documentation to see which flags are affected by each instruction.) We can then use this flag in a conditional jump to keep outputting until the counter gets to zero.

A conditional jump is the same as a regular jump, except that it only jumps if a specified condition is met. These are all based on the f register; the contents of the f register are affected by different instructions in different ways, and some instructions do not affect it at all. So the flag may be looked at some distance from where it was assigned.

Here is the list of available conditions:

ConditionStands forDescription
nzNot ZeroLast calculation result wasn't 0
zZeroLast calculation result was 0
ncNo Carry (overflow)Last calculation result didn't go over the boundary from $ff to $00, or vice versa
cCarryLast calculation result went over the boundary from $ff to $00, or vice versa
poParity OddLast calculation result had an odd number of bits set
peParity EvenLast calculation result had an even number of bits set
pPositiveLast calculation result was between $00 and $7f
mMinus (negative)Last calculation result was between $80 and $ff (signed)

I'll explain the other conditions later, as they are quite confusing.

So, we can output something bc times using a loop like the one shown above. Here's a version with comments describing what's happening more verbosely:

    ; 2. Output 16KB of zeroes
    ld bc, $4000    ; Counter for 16KB of VRAM
    ClearVRAMLoop:
        ld a,$00    ; Value to write
        out ($be),a ; Output to VRAM
        dec bc      ; Decrement counter
        ld a,b      ; Get high byte
        or c        ; Combine with low byte
        jp nz,ClearVRAMLoop ; Loop while the result is non-zero

So, this section has set all of VRAM to zero. Now we've got a blank space in which to start putting our data.


< Pause Handler | Lesson 1 | Setting up the graphics >