Machine Code |
In the penultimate article of this series, we're going to take a more detailed look at the CALL and RETurn statements, and also be looking at places to store machine code programs.
This month's routine allows you to clear rectangles to a particular colour, either for simple graphics or pseudo windowing.
CALL and RET statements have been included in past programs, but we haven't discussed how and where to use them. Those statements effectively create a subroutine program in much the same way as GO SUB and RETURN are used in Basic. AH that is connected with the theories of program structure and top-down programming.
Suppose you want to develop a line drawing routine for the Spectrum using a top-down design, we would have the main program looking like:
main CALL crds ; Get start and end ; coordinates of the line CALL draw ; Draw the line on screen RET ; Return to the calling code ; or Basic
The next step is to move down a level, and divide the two routines into further sub-divisions. If we were inputting the coordinates from the keyboard, then the ~get_coords' routine would look something like:
crds CALL get ; Get first x-coordinate CALL store ; Store first x-coordinate CALL get ; Get first y-coordinate CALL store ; Store first y-coordinate CALL get ; Get second x-coordinate CALL store ; Store second x-coordinate CALL get ; Get second y-coordinate CALL store ; Store second y-coordinate RET
Similarly, the 'get' routine will be divided:
get CALL print ; Print the input message CALL input ; Input the coordinate RET
and so on.
There are a number of advantages to this method of programming - you are less likely to make mistakes if you split the program into a number of menial tasks. However, you still have to take care of those registers which are corrupted, where the results will be stored, and so on. One failsafe method of handling the registers is to stack - PUSH - the registers which will be corrupted at the start of each routine, then POP them again at the end.
Another advantage is that it is easy to build a library of subroutines for general use, especially if you have documented them properly. If PUSHing takes too long, you could always try using the alternative registers, which we covered last month.
When you come to develop the actual line-drawing part of this routine, you're going to need a subroutine which plots points. Easy, just pinch the plotting routine which was used in the shape-filling program - August issue. Having got a completed line-drawing routine, it can be placed in the library, and used again if you want to draw squares, or build an adventure graphics program.
Now that you know how the CALL and RET statements should be used, let's look at the tricks which can performed with them. One obvious space- saving device which many programmers overlook is adding conditions to CALL and RET statements. It's all too easy to write something like:
. . CP byte JR Z,loc1 CALL routine loc1 . . CP (HL) JR Z,loc2 . . loc2 RET
when it would be far easier to write:
. . CP byte CALL NZ,routine loc1 . . CP (HL) RET Z . . loc2 RET
Hardened structuralists would have a fit if they saw that kind of programming, arguing that routines should have only single input and output locations to avoid mistakes and making the program more readable. That is reminiscent of the GO TO spaghetti programming arguments. Although those jumbled programs should be avoided, multiple input/output locations should be used. After all, when using machine code, you'll have to make a jump at some point.
There are quite a number of conditions which may be attached to CALL and RETurn statements, listed in figure five.
In previous articles, machine code routines have been stored from location 60000 upwards. That isn't necessarily the best place to hold machine code. All you have to do is choose a series of locations, starting at 'loc' near the top of memory, sufficient to hold your machine code program and data. Then use the Basic CLEAR 'loc'-1 instruction to ensure that the area of memory is safe. You can still corrupt machine code in that area using POKEs, but Basic won't affect it, nor will the NEW instruction erase any of your code.
The major disadvantage with this method of lowering RAMTOP with the CLEAR instruction, is that the machine code is separate to any Basic program which uses it. You either have to include a loader in the Basic program, as we have in previous articles, or save the machine code separately using a SAVE "name" CODE location, length instruction. If you don't want to load it separately each time you use it, then it must be saved on tape after the Basic program, which should include the instructions:
CLEAR location-1 LOAD "name" CODE
to load the machine code into memory.
There are alternatives to that method of storing machine code programs. One of the most popular of those is to embed the code inside a Basic REMark statement. REM statements are typically of the form:
10 REM This is line ten of the Basic program. 20 REM REM statements allow programmers to 30 REM add comments to their programs, and 40 REM are ignored by the Basic interpreter
Any information which appears after a REM statement is ignored when the program is running. That information can be anything you like, including machine code. The only problems are putting the machine code after the REM statement to begin with, and knowing where the machine code routine starts so that you can call it from Basic.
Those are both solved by a handy couple of bytes in the system variables area. If you type
PRINT 256 * PEEK 23636 + PEEK 23635
you'll get a figure telling you where your Basic program starts. If we add five to that number, we'll get the location of the first character after a REM statement, assuming that the REM is the first statement of the program. We can check that with the program:
10 REM ABCDEFG 20 LET bc = 256 * PEEK 23636 + PEEK 23635 + 5 30 FOR i=0 TO 6 40 PRINT CHR$(PEEK (loc + i)); 50 NEXT i
which should pick the characters out of the REM statement and print them.
We'll now store this month's routine in the same way. The assembly code, shown in figure one, is 96 bytes long. If you look carefully, you'll notice that nowhere in the code does it refer to any specific locations in other parts of the code. That means that we can easily place the code anywhere in memory without having to change any bytes, as would be the case with a CALL or JP instruction.
This relocatable Z80 is more useful than location specific machine code. For instance, it allows you to build up libraries of routines and load them anywhere in memory, tying them together with CALLs from the main routine.
The Basic loader program, figure two, shows how that method can be used. The Hex Load Routine - lines 1000 onwards - is slightly different from normal. Instead of reading the start location, it assigns it the first location after the initial REM - line one - in line 1040. Be careful when typing line one, to ensure that there are at least 96 characters after the REM statement, not including the automatic space, otherwise you'll find the machine code overwriting your program.
When you run the program, it will initially cover the screen with X characters then overwrite those with pseudo windows. True windows will clear an area of the screen to a particular colour, then allow you to write specifically to that window without affecting anything outside the window. That program simply does the clearing and you must be careful where you print. You could just as easily use the routine to draw coloured rectangles very quickly, as when drawing a bar chart. If you want to see how fast the program really is, just take out line 60.
Having run the program once, try listing it. You may have a few problems. The initial REM will be followed by garbage, and possibly a system error. That is due to the machine code now embedded in the program. The advantage is that you can now delete lines 1000 onwards, and type 'RUN 10' to run the program as before - no need to reload the machine code routine. By typing 'LIST 2', you'll be able to list the program normally. You can also SAVE and LOAD the program with the routine still embedded.
When using the routine in your own programs, delete everything but lines one and two. To call the machine code use FN w, of the form: FN w(x,y,w,h,i) where x - the x-coordinate of the top-left of the rectangle; y - the y-coordinate of the top-left of the rectangle; w - width of rectangle; h - height of rectangle. i - the new ink and paper attribute - see figures three and four.
The 'FN w ...' can be preceded by a number of commands, such as RANDOMIZE, RESTORE, or just LET X=. You should ensure that the window fits onto the screen - 0 to 31 columns, and 0 to 23 lines, inclusive.
Although you may have set the INK and PAPER in a window to particular colours when specifying a value for 'i', you'll still have to set those colours when PRINTing. Otherwise, you'll merely alter the attributes in the PRINTed character squares. That is shown quite clearly in the included Basic program.
In the final article, next month, we'll take an in-depth look at how to use the Spectrum ROM.
|
|
|
|
|
Previous article in series (issue 45)
Next article in series (issue 47)