Machine Code |
This month, we are going to look at a number of previously unmentioned registers. Those are the index registers and the alternative register set.
Although they can be very useful at times, there is nothing they can do which is not equally possible with the registers we have been using in previous articles. Consequently, rather than giving an example program using the new instructions, we will look at a few other useful tricks.
Let's consider the index registers of which there are two, labelled IX and IY. They are both 16-bit registers similar to the BC, DE and HL register pairs, but unlike the latter, they cannot be split into individual byte sizes. The most common form of the index register is:
(IX + disp)
where disp is an eight-bit signed integer. That means that with lists, arrays and other table data, an index register can point to, say, a row of items, then: (IX+0) is the first item (IX+1) is the second item, (IX+2) is the third item, and so on. The main limitation is the size of the displacement value. Being only eight bits, it can only contain numbers in the range -128 to +127.
Figure five shows all the new instructions available using those registers. In general, the index registers are not used very frequently because of the extra time and memory required when using them. That is because an extra byte is required in each instruction simply to tell the computer that an index register is being used.
During the past few months, I have included a number of machine code routines which require a parameter to be passed to them. For instance, the first article - May 1985 - had a routine which would set all the screen attribute bytes to a particular value. To do that, we had to tell the machine what value to use and then POKE the value into location 60000, then read it into register E.
That method has a number of disadvantages. To begin with it makes the Basic program less readable, with seemingly random POKEs scattered here and there. More importantly, it makes the code position dependent. In other words, if we were to move the code further down in memory to make way for some other data, then not only will the Basic POKE location have to change, but the machine code instruction which loads location 60000 into the E register will also have to be changed, then the assembly code reassembled at the new location.
Fortunately, using one of the hidden secrets of the Spectrum, we can devise a more satisfactory method of passing parameters to machine code. Our new method makes use of the normal parameter organisation in Basic function calls. Imagine you had a Basic function defined as:
DEF FN a(x) = 5*x
When you call that function, you have to supply a value for the argument x. Naturally, the computer has to keep a record of the value, so that it can use it to calculate 5*x in the function. That is where the Spectrum manual comes to the rescue.
Look at the section on system variables where you will find reference to a couple of bytes with the mnemonic DEFADD, which point to the "address of arguments of user-defined function if one is being evaluated; otherwise zero". That means if we get the contents of the two bytes 23563 and 23564, they will tell us exactly where the computer has stored the values passed to the function we have just called. It is all rather convenient, and can be used to pass values to position independent machine code.
The only other item of information we need is how to decipher floating point numbers, which is how the Spectrum will hold those values. That can be complicated, but we will only ever be dealing with numbers in the range 0 to 65535, since that is the maximum value which can be stored in a register pair. Having limited ourselves to that range, floating point numbers will always be of the five-byte form:
00 00 LL HH 00
where LL is the low-byte and HH is the high-byte to give the two-byte number HHLL.
In addition, there will be two bytes preceding that value, giving the ASCII code of the single character variable name, followed by the value 14 (0E in hex). There will also be a single byte after the number containing the ASCII code for a comma, unless it is the last argument of the function, in which case it will be the closing bracket.
That is all we need in order to write a function which will call a machine code routine and set values to all our registers simultaneously. The Basic function will look something like:
DEF FN a(A,B,C,D,E,H,L) = USR location
where location is the start location of our machine code routine. Figure one shows the assembly code to set the appropriate variables, and figure two shows the format of the numerical arguments, explaining the displacements from IX in the assembly code.
We are now in a position to rewrite our first ever machine code program. The assembly code is given in figure three. The major change is that IX is set to the contents of DEFADD, and the E register is then loaded with the function argument value, rather than from location 60000 as previously.
If we want to change the location of the machine code routine, all we need to do is move our machine code, then change the function definition in the Basic program, making it truly relocatable. Just to prove that all this really works, figure four gives a Basic loader with the appropriate function.
Before looking at the alternate register pairs, it is worth pointing out that the value of IY will be initially set to 23552 - 5C00 in hex - which just happens to be the start of the Spectrum's system variables. Those locations are important to many Spectrum routines, and it uses displacements from IY frequently. Consequently, if you are using the IY index register and also calling Spectrum ROM routines, then it is a good idea not to change its value.
There is nothing to stop you using displacements from IY to change system variables, and you can achieve some very interesting effects.
Now for the alternate registers. Those are simply a second set of the registers A, B, C, D, E, F, H and L. There is no way in which you can mix both the normal registers and the alternate registers, but you can easily access them independently. There are a number of advantages in doing that, concerned mainly with both speed and time. Imagine you want to save the values of some of the registers for use at a later date, say whilst calling another routine. The most common method of doing that is to stack the register pairs, then unstack them at the end of the CALL.
That method requires 11 bytes - one for each PUSH/POP and three for the CALL - and will take 101 clock cycles to execute - 11 for each PUSH, 10 for each POP, and 17 for the CALL. Now let's consider the same problem, saving the register values by exchanging to the alternate registers:
EX AF,AF' EXX CALL routine EX AF,AF' EXX
That now uses only seven bytes and takes only 33 clock cycles to complete.
Finally, a word of warning for anybody using the alternate registers and calling machine code from Basic. To call any machine code routine from Spectrum Basic, the USR instruction is used, either as part of a PRINT, or RANDOMIZE.
The problem is that the Spectrum ROM handles this line interpretation as an arithmetic calculation, and calls its calculating routines. On return to Basic, the routine to deal with the end of the calculation is called, and one of the things that does is to reset the IY index register to the start of the system variables.
Unfortunately, the very important value of the HL' alternate register pair is not reset to the hexadecimal number 2758 to allow a correct return to Basic. So, by all means use those registers but, if you have called the routine from Basic, make sure that the value 2758H is placed back into HL' before returning.
|
|
|
|
|
Previous article in series (issue 44)
Next article in series (issue 46)