How to store data in Z80 code; Marcus Jeffery fills in on assemblers and colours in shapes
One of the biggest problems when using Z80 machine code is finding places to put numbers. In Basic, there are plenty of variables and arrays which can be used to store data, but in Z80 there are just a few registers.
One way around that is to use an instruction:
LD (address), register
which would place the value of the register into the contents of the specified address. A better method, however, is known as the 'stack'.
A stack is a type of data structure, in much the same way as an array, list, queue and so on. With an array, you tell the computer which element you want by giving the array subscripts. With a list, you start at the 'head' and move towards the 'tail'. A queue is known as a First In First Out (FIFO) structure because you add items to the end of the queue and take them away from the start.
A stack is a Last In First Out (LIFO) structure. Imagine a stack of plates. Trying to take a plate from the middle of the stack is likely to have disastrous results. You will probably also have problems if you try to put a plate into the middle. You can only safely place and remove plates from the top. In Z80 machine code, those operations are known as PUSH and POP.
Z80 only allows you to PUSH and POP two registers at a time, giving the instructions:
PUSH AF ; POP AF PUSH BC ; POP BC PUSH DE ; POP DE PUSH HL ; POP HL
So, for example, the code
LD HL,1234 PUSH HL LD HL, 4321
would leave HL containing the number 4321. However, if we now execute the instruction
then the number 1234 will be restored to HL. One interesting point is that the stack does not know which registers it has stacked; all it has done is place two bytes onto the stack. Therefore, instead of the last instruction, we could just as easily use, say,
which would overwrite the contents of the BC registers with the number 1234.
The main point to remember when using PUSHes and POPs is that they must always be a matching pair. If you allow values to be stacked without POPping, the area set aside for the stack will eventually run out of room. The location of the top of the stack is held in a register pair called, surprisingly, the Stack Pointer, which is abbreviated to SP. Bearing that in mind, another useful instruction is
EX (SP), HL
which will exchange the last two bytes placed on the stack with the contents of the HL register pair.
Remember that the brackets indicate that we want the 'contents of the location addressed by' the contents of the Stack Pointer, not the contents of the Stack Pointer. Those instructions are shown at the end of the article in figure four. That also shows a few other instructions which operate on the Stack Pointer, though you're unlikely to find those very useful as yet.
Though your use of the stack is likely to be limited to the above instructions under most circumstances, it is far more flexible. For example, you will have noticed the instructions CALL and RET in recent programs. Those act in the same way as GO SUB and RETURN in Spectrum Basic. The instruction
transfers execution to the specified address, and
returns to the instruction immediately following the CALL. However, when that happens, the computer has got to know where to return to. Consequently, when the CALL instruction is executed, the computer PUSHes the value of the location following the CALL statement onto the stack. Then, when a RETurn statement is found, the stack is POPped to tell the computer which code to execute next.
Now we are ready for this month's example program, which will allow you to fill in any shape on the screen. To run this just type in and run the Basic program in figure one - the Hex Loader is the same as usual. To use the routine generally, POKE the x coordinate into location 23728 and the y coordinate into 23729. Those two locations among the system variables - User Guide, page 130 - are normally unused.
Figure two shows the equivalent in assembly code. The PBYTE routine takes the screen coordinates 'x' and 'y' - in C and B, respectively - and calculates the coordinates of the screen byte, placing the result into the HL register pair. In addition, a single bit is set in the A register, corresponding to the bit within the HL byte which represents the (x,y) coordinate. The letters in brackets, given in the comments on the right-hand side of the listing, refer to figure three, which should help to explain how the BC coordinates are converted into the HL location.
The routine works by filling in the (x,y) pixel - assuming that it is not already filled - then doing the same for the pixels to the North, South, East and West. Those in turn, if filled, will fill in four directions, and so on.
In that way, the whole shape is filled. By writing a general routine - FBYTE - to carry out the operation, it can 'call itself' to fill to the North, South, East and West!
Remember that the return locations from the CALLs are all stacked, so the routine always knows where to return to. The only problem is that the (x,y) coordinates in BC would be overwritten, so we must stack those too. The comments marked with asterisks show which instructions are connected with saving and restoring values - imagine how messy that would be without the stack!
That is by no means the quickest way of filling shapes, but is probably quick enough for most purposes. You might like to try it out in Basic. You can easily implement a stack, as follows:
1 DIM s(1000) 2 LET sp = 0 PUSH: 1000 LET s(sp) = xcoord 1010 LET s(sp+1) = ycoord 1020 LET sp = sp+2 1030 RETURN POP: 2000 LET sp = sp-2 2010 LET ycoord = s(sp+1) 2020 LET xcoord = s(sp) 2030 RETURN
The major problem with this method of filling shapes is that it uses enormous amounts of stack space. If you try to fill large shapes on the screen, you may find the whole computer crashing as the stack runs out of space. The easy way to avoid that is to split large spaces into a number of smaller shapes.
If you are sufficiently attracted to the advantages of using machine code, then you will probably consider purchasing an Assembler package. The assembly code listings given in these articles use Spectre-Mac-Mon from Oasis Software, but other assemblers may have slightly different formats.
Assembly code is usually divided into a number of fields. Those are
LABEL MNEMONIC OPERAND COMMENT
In some cases, such as the Zeus Assembler from Sinclair Research, the source listing also contains line numbers, as in Basic. The first thing to look for is the method of entering those formatted instructions. On- screen editors are obviously very useful. One very important feature is the use of labels - don't buy it if it doesn't use them.
In addition to the normal Z80 mnemonic instructions, most assemblers include 'pseudo-operations'. Those include such mnemonics as ORG, to tell the computer where in memory to assemble the code. EQU allows constants to be assigned label names, making the listing more readable. DB - or similar - standing for Define Byte, allows memory locations to be initialised to particular values.
It is often possible to define 'macros', which allow you to assign a name to a sequence of instructions. Conditional assembly allows you to decide whether certain pieces of code are assembled at assembly-time. Many more features may be found, varying between assemblers.
Many assemblers include 'monitors'. Those allow you to examine memory, while running machine code. Though not absolutely necessary, the more serious machine code user will find those invaluable. Some important additional features to look for are the setting of 'break points', single-stepping, examination of registers, intelligent copy, modify memory and disassembly.
When converting listed programs for your own assembler, the two main points to check are:
The Format: assemblers differ in the fields, length of labels, format of numbers, and so on;
The Memory: assemblers are likely to use different areas of memory. For instance, the routines given have all started from location 60000. However, the Zeus Assembler uses that as workspace, so the routines should be changed to start at, say, 30000.
In all cases, check with the documentation given with your assembler.
Previous article in series (issue 40)
Next article in series (issue 42)