A PROGRAM TO HELP YOU DEBUG | |||||||||||||||||||||||
[HEXLD3][Thunor: Before going any further, it is important that you read my addendum at the bottom of the page. You will also find links to downloadable versions of HEXLD3 there.]Now we know more or less what machine language is, it's about time we learned a bit more about how to handle it. What we shall do now is to write a new program - HEXLD3 - which will allow us to do five things:
More to the point - you'll need HEXLD2 in order to help you load it. The addresses used in this chapter assume that the machine code is being loaded into a REM statement in line one of a NEW ROM machine. If this is so you'll actually need 255 characters after the word REM. However, you don't have to use the same addresses as me if you don't want to. OLD ROM folk are specifically advised NOT to use a REM statement, since the machine code contains newline characters. To store machine code at different addresses to those I've used simply change the listed addresses to yours. [APRINT]Let's create it one part at a time. First of all a special subroutine for OLD ROM people - designed to AUTOMATICALLY print a character to the screen, in much the same way that the NEW ROM PRINT routine does. The routine will also protect all of the registers. Study this listing:
Note that HL needs to be stacked, since CALL PRINT changes the value of HL'. The next subrotuine we'll need is a mechanism for printing to the screen the value of the A register in hexadecimal. This subroutine will INCLUDE a subroutine-call to APRINT, at least for OLD ROM people. NEW ROM people in fact already have an automatic print routine which protects all of the registers, since there is one in the ROM itself. It is not quite the same as the PRINT routine, since it also preserves the values of all the registers - this is something that CALL PRINT will not do. CALL PRINT will erase the values of B, C, D, E, H, and L. The address at which APRINT begins in the NEW ROM is 0010, and so CALL 0010 would print a character without changing any registers. This is very useful indeed. One of the Z80 instructions designed to speed things up a bit is RST. It is in effect the same as CALL except that only one of the eight addresses may be called. It just so happens that 0010 is one of these possible addresses. RST is better than CALL for two reasons: 1) it is faster to execute, and 2) it is only one byte in length. The code for RST 10 is D7. D7 then has precisely the same effect as CD1000, that is, to print a character. OLD ROM users should note that although D7 still produces a call to 0010, it will not print a character, since in the OLD ROM there is no PRINT subroutine located at this point. RST is short for RESTART. [HPRINT]
By the way, did you understand all those ANDs and RRAs? If you didn't I'll explain exactly what's going on. In binary, F0 is 1111 0000. This means that when you apply AND to F0 and another number, then the first four binary digits of A will be unchanged, and the second four binary digits will all become zero. Do you remember how to change from binary to hex? You have to look at it four bits at a time. The first four representing the first digit, and the second four the second digit. Thus all we have done is to change the second digit to zero. If A were 36 then it would become 30. If it were 99 it would become 90. If it were D5 it would become D0. And so on. This is not what we want. We must shift A four bits to the right. RRA moves A one bit to the right, replacing bit 7 (the leftmost bit) by the value of the carry. In this case the carry is zero, since we have just done an AND instruction. The new value of the carry will be the previous value of bit 0 (the rightmost bit). This will also be zero since there are now four zeroes at the right of A. RRA then, repeated four times, will change A from 30 to 03, from 90 to 09, and from D0 to 0D. All that remains now is to add 28 (decimal) to this number and print it. We print it using the instruction RST 10. Back to our new program. The BASIC part of the List routine will look like this:
To obtain the keyword LIST in line 10, either type THEN LIST (NEW ROM only) and delete the word THEN, or type the whole line as 10 LIST quote back
What about this USR routine at 16539 then? What will that do? And what about this business of POKEing 16533 and 16534? What's that all about? Well using my address, 16539 is the start of a routine called HLIST, which we haven't yet written. It is designed to actually LIST a machine code program in hexadecimal (hence H-List). The address 16533 is the number I've used to hold a "variable" called ADDRESS. That is to say, it is a place at which we can store a two-byte number. Any address may be used for this purpose provided that BASIC will not change that two-byte number. This program demands four such "variables", or two-byte memory locations. They will be called BEGIN, ADDRESS, ADD2, and LIMIT. They will be used by the program as follows:
I ought to explain here what is meant by "subject-program". The program we are writing is a replacement for HEXLD2. As such it is to be called HEXLD3. This is the "object-program" - the one we are writing now. But the purpose of HEXLD3 is to enable us to be able to create and examine machine code programs. The program that HEXLD3 will be used to examine is called the "subject-program". These distinctions are clearly necessary in order to avoid confusion between the two different concepts. It is of course possible to use HEXLD3 to examine itself, in which case it becomes both the object and the subject, but for the time being keep these two ideas seperate in your mind. The addresses which I've used to store the "variables" BEGIN, ADDRESS, ADD2, and LIMIT are as follows:
Lines 640 and 650 POKE into the variable ADDRESS - giving the address at which our listing (input in hex as A$) is to begin. This idea of using part of the RAM in machine-code-area to store numbers is a very useful one. You can use it in many different programs. The numbers will be safe there even after the program ends and you are in command mode. You can type RUN or CLEAR and they won't be wiped out. They will even SAVE and reLOAD. [HLIST]Now for the subroutine HLIST (short for Hexadecimal List). It is a very very simple routine indeed, and should be no trouble for you to follow.
The above program will run as listed on a NEW ROM machine. OLD ROM users should replace every RST 10 instruction by CALL APRINT as before, and are reminded that the JR byte-count must be changed accordingly at There are several things we can note about this program. Firstly, two new instructions have been used - BIT 6,(HL) and RST 08. Here's what they do. BIT 6,(HL) tests the value of bit 6 of the address (HL). The result will either be 1 (if bit 6 is 1) or 0 (if bit 6 is 0). This result is not stored in any of the registers, but we can still check it with the next line JR NZ,NOPRINT, which says jump to NOPRINT if the last result (that is bit 6 of (HL)) is not zero. Why do we need to do this? Take a look at the character set. In particular look at their character codes in hex. Notice that all of the expandable characters lie between C0 and FF (except for RND, INKEY$, and PI on the NEW ROM - these are treated slightly differently by the ROM) and that all of the characters between 40 and 7F are not printable at all (again, except for RND, INKEY$, and PI on the NEW ROM. The machine has to make a special check for these. You could argue that the NEW ROM cursor 7F was printable, but of course it looks different depending on what mode the machine is in). In fact all of the printable characters are either between 00 and 3F, or between 80 and BF, and conversely every character between 00 and 3F, or 80 and BF, is printable. What have all these in common? The fact that BIT 6 of the character code is zero. In binary thse codes run between 0000 0000 and 0011 1111, and then from 1000 0000 to 1011 1111. So The other new instruction is RST 08. This will cause an immediate return to BASIC, stopping the program with an error code. The byte immediately after the RST 08 instruction tells it which error code to use. An error code 1 needs the data 00, since this byte has to be one less than the report code. If we wanted to be really flash we could have used 1C and got an error code of T! Now follow the program through carefully and see what it does. Note the way we check whether or not the address ADD2 has been reached (it is stored in DE) - especially the use of AND A to reset the carry flag. You can check that this program works by POKEing the address at which HPRINT starts into both BEGIN and ADDRESS, and by POKEing the address at which HPRINT ends into LIMIT. Then, if you type RAND USR HLIST (this is the location 16539 using my addresses) you should end up with a more or less instant listing of the subroutine HPRINT. [Thunor: The easy way to do this is with HEXLD2 -- since you are already using this to enter the machine code subroutines above -- and writing to 4093h, entering "8240824093409340", "S" to stop, and then RAND USR 16539. Because the ADDRESS variable is modified by the machine code, you'll have to do the same again if you want to repeat the listing.] [WRITE]Now for the second part of our program, HEXLD3. The BASIC part is to look like this:
This part calculates the length of the string A$, which because of the CLEAR statement in subroutine 600 is the first (and only) item in the variable store.
This routine leaves the length of the string divided by two (since it needs two characters to specify one byte of machine code) in the B register and leaves HL pointing to the byte immediately before the start of the contents of the string. Notice how LD A,(HL)/INC B/DEC A/JR Z is used to check for a character 1 (a quote mark, or end of string character) as well as counting the number of characters so far (in B). Can you also see how SRA B will divide B by two? Strings are stored differently in the NEW ROM. This actually makes things easier, not harder! Look at the corresponding NEW ROM routine which does the same job.
This works because the NEW ROM works by storing the length of a string immediately before the string itself. It takes two bytes for this, but notice that in both of our versions we are only using one byte for the length, so don't input more than 255 characters in one go. Here's the rest of the [WRITE] routine:
You can learn several things from this routine. Firstly, notice that if you input the empty string the program will jump back to the RST 08 instruction in the previous section. This is so that you can end the program without actually having to break out. Now look at the first few lines from CHECK onwards. What they do is this - if the end of the program (the program that WRITE is editing) is greater than the current address, do nothing, otherwise make a note of the fact that the program has got longer by altering our variable LIMIT. [DELETING HEXLD2]You now have two segments of machine code which, if you've typed them in properly, will work first go. Now delete the WHOLE of HEXLD2 (except of course line 1) but be very careful not to attempt to list line one. The first line now contains more characters, when the keywords in the REM are expanded, than will fit on the screen. In this circumstance the ROM will go into an infinite loop if it tries to list it - this is a design fault - the ROM should not be capable of making infinite loops. You won't be able to break out if it happens. To avoid it, type POKE 16403,10 (OLD ROM) or POKE 16419,10 (NEW ROM). Then type in lines 10 to 30, then delete the rest of the program one line at a time, lowest line number first. Now type in the rest of the program and SAVE it before you do anything else.For NEW ROM users, it should be made clear that the REM statement will, when keywords are expanded, be longer than will fit on the screen, thus although the command LIST is acceptable (the result of which is that part of line one is listed and an error 4 message displayed), if you LIST 10, to ensure that line 10 is always at the top of the screen (sometimes this doesn't work - if not type POKE 16419,10 which always works) be warned never to delete line 10. If you do the ROM will go into an infinite loop trying to reshuffle the lines so that it can list them. In SLOW this can be quite amusing to watch, but it is always irritating because the only way you can get out of it is by pulling the plug. [Now if you simply type RUN and enter 4082 the program will instantly list out the start of this program. In other words we are using it to examine itself. Typing CONT or CONTINUE repeatedly will continue the listing until the end of the program is reached, when you will get a report code of Now to complete the transition from HEXLD2 to HEXLD3 let's rewrite the section that will SAVE things in upper memory. The BASIC:
[Thunor: OLD ROM users note that the above BASIC line 500 won't work as RETRIEVE won't reside in a line 1 REM statement, it'll be in the O array. See my BASIC lines 500 to 504 patch in appendix one.] As you can see there are three different parts of machine code. The first, in line 400, alters nothing, but returns a numerical value to BASIC, which is then used by BASIC to reserve the correct amount of space using a DIM statement. Let's look at that part first: Using my addresses, ARRAY is 16635, STORE is 16651, and RETRIEVE is 16669. [ARRAY]
The first part is obvious. The beginning address is subtracted from the end address. Again we see AND A being used to zero the carry flag so that SBC gives the right answer. Now, for OLD ROM users, this number is divided by two, because arrays use two bytes per element. For NEW ROM users we move the answer into the BC register because this is what will return to BASIC. Now for the machine code that accompanies line 410. Use RUN 100 to load it in the first place. You may be wondering why ADD2 was loaded with the number of bytes in the code to be SAVEd. Well ADD2 is just a convenient place to store it, since it will be needed in line 410. [STORE AND RETRIEVE]
In case you're beginning to lose track, here's a quick round up of all the addresses we've used so far:
Briefly, STORE moves machine-code from upper memory and stores [it] in an array. RETRIEVE moves it back from the array to its previous position. Both of the routines start off by working out the address of the first free byte in the array. The array is the first item in the variable store, but because the OLD and NEW ROMs think differently, we have to add two to this location in the OLD ROM, and six on the NEW ROM. Can you spot the different ways in which this is done? This is also the first time we've used the instruction LDIR. What it does is to automatically move a block of elements from address (HL) to address (DE), assuming that the number of elements contained in this block is BC. This is of course precisely what we want to do. LDIR does alter the value of each of the register pairs BC, DE, and HL, but that doesn't concern us since the next thing we do is RET. LDIR is very, very useful indeed, but you must remember which way round it goes. It loads from (HL) into (DE). Have you ever pressed 'record' instead of 'play' when trying to load programs from tape? Well that's exactly what will happen to your machine code if you get DE and HL the wrong way round for LDIR - it will just be wiped out - and there's no going back. As long as you can see exactly what's happening you're OK. If you can't then get a piece of paper and write down the values of each register at each stage. Work through until you're convinced you know exactly what's happening all the way through. [INSERT]We now have a BASIC program called HEXLD3 which contains a fair number of machine code subroutines. As it stands it will both LIST and WRITE machine code, and can also be used to SAVE any machine code or data which is stored in spare RAM space high in memory. This is all that HEXLD2 did. You have now the ability to enter your own machine code programs very easily, but what you can't yet do is edit them if you make a mistake. That is what the next section is for - it is called INSERT, and will insert whatever you input between the surrounding code, without overwriting it. The BASIC part of the routine is this:
And the machine code which goes with it (which NEW ROM users should write to address 16687) is as follows:
Now exactly how this works is quite complicated, so think carefully. The part between INSERT and COPYUP finds the length of the string A$. As you can see it required a completely different method for each ROM. See WRITE on this, since it is very similar here. Between COPYUP and NOTEMPTY the length of the string is divided by two, and if it is zero returns to BASIC with error code 9. This is the job of the RST 08/DEFB 08 sequence. From then on we are concerned with moving part of the program being edited. Look at the diagram below.
As you can see, we need to load a complete block of elements from one point to another, but unlike before the new and old positions overlap. This is a slight problem, and we have to be very careful how we load it. If we were to simply assign HL to ADDRESS(before) and DE with ADDRESS(after), and then use LDIR as before (having assigned BC to the number of elements in the block first) then since LDIR moves things one byte at a time the first few elements would end up in the middle of the block, only to be copied up for a second time. The program would be completely corrupted. We can get round this flaw by sneaking up on the problem sideways while it's not looking. What we do is we block load it from the other end! This means loading HL with LIMIT(before) and DE with LIMIT(after) and Having found the length of the new section, this length is pushed onto the stack. BC is then loaded with the length of the block to be moved. See how this is worked out. Then HL and DE are correctly assigned, making use of the fact that the length of the new section is at the top of the stack, and the new limit is stored in our "variable" LIMIT. After the block load is successfully carried out we call the WRITE subroutine to fill the shaded area in the diagram with the contents of the input string. This will work because the above program does not change the value of the variable ADDRESS. WRITE will simply overwrite the shaded region, moving the current address pointer to its new position. We then return to BASIC for the next input. To test the program, use WRITE to write "9D9E9FA0A1A2A3A4A5" to the point just beyond where our program currently ends. This will list as . Now use INSERT. Give it the address of the inverse five, and input "00"/"201E"/"00". Here / means newline. When you list it you'll find four new characters have been inserted. Notice that the routine allows you to input as many characters as you like in one go, and that it allows you to press newline as many times as you like. Newline on its own (i.e. inputting the empty string) will break out of the program. [DELETE]The final section to add to our program is DELETE. This will look (in BASIC) like this:
The first four lines load the initial and final addresses into the variables ADDRESS and ADD2. Line five calls the machine language routine that will do the task for us. Here's what the machine code has to do. Look at the diagram below. Here the shaded region must be removed.
This is quite simple - we just use LDIR quite straightforwardly. You might think there would be some effort involved in calculating the new limit, but not so. LDIR alters the value of HL and DE for us in quite an advantageous way - as we shall see.
As LDIR moves from one end of the blocks being shifted to the other, HL and DE move with it, so HL ends up to the right of the original block, and DE ends up to the right of the copy. Thus a simple DEC DE after the LDIR will set it to exactly the right place for our new limit. Load this routine to address 410D (OLD ROM) / Now SAVE this program permanently. This is the final version. All you have to do in order to use it in future is to type RUN 100 and enter the address of the variable BEGIN (403C or 4093). Then input the address to which the program you are about to write will begin, then simply newline on its own. RUN 100 a second time to actually begin inputting a program. Download available for 16K ZX81 -> chapter09-hexld3.pDownload available for 16K ZX80 -> chapter09-hexld3.o[ADDENDUM][Thunor: Having worked through this chapter and written HEXLD3 for both ROMs, I can help to clear up some confusions.
|