|Lesson M1 - Z80 Monitor/Debugger|
|Lesson M2 - String Reading and Memory Dumping!|
|Lesson M3 - String Matching for command reading|
| Lesson M1 - Z80 Monitor/Debugger
Because we're developing for multiple end systems, we won't be able to rely on having debugging tools as good as Winape has given us...
Lets rely only on what we can do! So today we'll do some clever Z80 tricks, and make some simple tools that will help us check what the processor is doing, and work out if anything is going wrong!
|Monitor||Show the status of all the registers|
|Monitor_PushedRegister||Push a register pair... this function will show it's contents|
|Monitor_BreakPoint||Show the program counter at this point - you can then use the emulator debugger to check the location and memory|
|Monitor_BreakPointOnce||Same as above... but this version deletes itself - designed to be put in a loop , the breakpoint will only occur once|
require the ShowHex function... this will show a byte as Hex chars
onscreen, and not modify any registers.
The Monitor is split into two parts... the SimpleMonitor one for breakpoints and PushedRegsisters.. and the Monitor for general register viewing... you can use the simple version only when memory is limited
There are also 2 extra options... Monitor_Full if defined will show all registers in the Monitor... Monitor_Pause will cause a wait for keypress
|The code for this lesson is really complex... so don't try to type it in!... See MonitorTest.ASM for the usage example
It's in the Sources.7z file archive... so just download it and use it from there...
That said, it's worth knowing how it works... there's some clever tricks in there... and you'll want to know how it works, so you can customize it and make it better if you need more functionality!
|call Monitor_BreakPointOnce and
Monitor_BreakPoint both work the same... they will show
counter to the screen
The Program Counter is the location in memory the code ended up when it ran, you can then check this memory location in the debugger if you want!
|Want to see the value of a register?... just push it onto the stack and Call Monitor_PushedRegister... it will show the value of the register pair, and take it off the stack... so your program can continue!
|If you really need to know everything...use Call Monitor... it will show the state of all Registers!
if you define Monitor_Full it will show IX/IY and Shadow registers... otherwise it'll just show the basics, but use less memory!
|Need to debug, but short on memory? remove the definition of Monitor_Full...
Still too big... well you can always just use Monitor_Simple to just show breakpoints and single register pairs!
|This is not called by the user directly, it's called by the other modules and shows HL as hex, surrounded by ** symbols
If needed it will pause the system after showing the Breakpoint or registrer
|This module will pop a register and show it to screen...
because we need HL, but don't want to use the stack we back it up with
self modifying code...
Then we pop a pair of the stack... this will be the return address... Next we swap HL with the pair on the stack... HL will now be the pair pushed before the call, and the return address will be at the top of the stack...
All that's left to do is show HL... then restore HL and return to our program!
|This module is super simple! all it does is get the return address off the stack.. show it, and return!|
|This can help you check why your program isn't working right!
Once you know the Program Counter, you can use the emulator's internal Disassembler or memory monitor to check what's going on in the code!
Maybe selfmodifying code has gone wrong... or the memory is corrupted....
Knowing where in memory a command is ending up after compiling is the first stage of solving the problem!
|More complex than the last version... this one overwrites the 3 bytes preceeding the return address...
These will be the CALL command that called the monitor - the end result is that the monitor is only called once... so it can be used in a repeating loop to find out the program counter location (or if the loop even ran - if your program is crashing) without slowing down the program with lots of pauses.
|Now this is a weird trick! (That won't help you lose
weight!!)... When we do LD A,I... something secret happens... the PO
flag is altered... if Interrupts were disabled,the PO will be set!
Here we use this to allow us to detect if EI or DI is set, show it to screen... and restore the interrupt state at the end of our Monitor function!
in this block B is the letter 'D' or 'E' for the onscreen label... and &F3 is the DI , and &FB is EI for a selfmodifying command!
|While this works on many Z80's... some may misbehave!
On the Enterprise NMOS Z80 if a firmware interrupt fires before the PO, then the flag may be changed!
There is a more complex alternative to fix this below!
|on the Enterprise's NMOS Z80 ... LD A,I may not be enough!
We need to check if an interrupt JUST fired... we do this by pushing &00 onto the stack... if the interrupt fired, that &00 will be replaced with &38... then we use LD A,R to check the Parity status.
once we've done our check, we just need to make sure PO wasn't set by an interrupt that just fired, so we POP A and check if it's still &00.. if it isn't then an interrupt must have just fired (so Interrupts were enabled).
The result is this function returns no carry (NC) if interrupts are enabled (EI)... and carry (C ) if they are disabled.
|The first thing we need to do is
push all the registers we're interested in onto the stack, this also
backs them up, so we don't need to worry about changing them!
|Next we work out what the stack pointer was before we started pushing.. and put that on the stack too...
We load DE with the address of the stack (and all the register values)...
HL points to the text labels of our registers... and we use B as our counter register!
|Now we're going to show each of the registers.. we show a 2 character label,
then we display the two bytes - they're stored in Little Endian (Low-High) ... so we have to INC DE then DEC DE to show them as the user expects...
We repeat this for each register we pushed!
|DI/EI were stored in a string... they have no matching byte-pair on the stack... but we need to show them anyway, so we do it here!|
|We pushed the stack pointer onto the stack - so we get rid of it with a POP HL...
Next we Pause for the user if needed...
Then we restore all the registers ... using two INC SP's to skip over the pushed I and R register that we don't restore.
|Last, but not least... we restore the interrupt state... and return to the program that called us!|
Monitor covers all the Z80 basics... but you could go even further!...
Maybe we want to check the status of the memory bank mapping, or rom
paging... or perhaps we need to show the contents of the stack, or dump
Anything is poissible! but of course we don't want to end up with a 'Monitor' that uses up all the ram, leaving no room for our actual program!
| Lesson M2 - String Reading and Memory Dumping!
We Made a nice debugging tool, but sometimes we may want to check the contents of memory, so it would be nice to export a range so we can look at it...
But we're tired of coding useless stuff, right? so lets read in the address to export in a string from the user, and convert it to Hex!
|Here is our WaitString Command, we need to CALL it with two parameters, HL needs to be a destination address, and B is the maximum
number of characters to read... of course the function will overwrite that many characters plus 1 (for the end of string Char 255)
The function will return some useful parameters too! HL will now be the end of the string, DE will be the start... B will be the remaining unused characters... and C will be the number of characters entered!
How's it work? pretty simple!
DE is used to back up the start of the string - we need that for later!
Each character is read in, and if it's not DELETE or ENTER key, we store it in the string, and repeat if B is still >0
When we're done, we store a 255 as the last character in the string.
|Now we need to handle the backspace key!
First we need to check if any keys were pressed, we could do LD A,C then OR A... but lets try a different trick!
By doing INC C... DEC C... we can set the Zero flag in the same way, but leave A untouched - a trick worth bearing in mind!
If the keycount is zero, we give up - there's nothing to delete! otherwise we INC B - set the last character to a space (to delete the one onscreen!
next we get the current cursor position, subtract the length of the string from the Xpos in H, and redraw it (with the new space at the end)... the line will be redrawn
Finally we DECrease the key count in C and return back to wait for more keys!
|These examples all use 255 as
the end-of-string byte... while 0 is more common in many examples, this
was used as an end-of-message command by ChibiAkuams
Some examples even add 128 (&80) to the last character in the string, to save a byte from the string length, but this causes problems for character sets that need more than A-Z in their alphabet
|We're going to create a String to Hex processor soon, but we're going to assume all the characters are uppercase,
So lets create a ToUpper function that will do that for us, it's pretty simple!
we process all the characters in the string until we get to a char 255... if a character is between 'a' and 'z' then we add 32 to it, which will convert it to 'A'-'Z'
|This function will take a pair of characters from HL, convert them from HEX, and return them as a byte in A
We use the funtion AsciiToHex to convert as single character, then we do 4 RLCA's to move the bottom nibble to the top, and back it up in B
Then we run AsciiToHex again to get the second character, and OR the top nibble in B in... we're done now, so we just return!
AsciiToHex reads in a character from HL, we subtract the character '0' to convert the numbers from Ascii a number... then we check if our symbol is below 'A' and return if it is (we have to subtract '0' to counter the previous change to A)
if it's not, then we need to alter A, so that it's correct - we do this by subtracting the difference between 'A' and 'O', less the amount we need to add (which is Hex A)
|Beware, we're not being very thourough with AsciiToHex... it will return weird values for charactrers other than 0-F!...
We're going to add more String processing functions in later lessons, as we use our text reader for more interesting things!
|This is the main call routine for adding test code to our program.
Because we don't want to use any of the registers our program uses, we're going to do a little trick, and store the parameters after the call! - so a call to our dumper will look like this:
Where the first byte after the call... '33' is a one-byte count of the number of bytes to dump to screen
and the following word '&1234' is the address to dump from!
First we get the address that called us, by swapping HL with the top of the stack pointer... next we back up all our registers...
Now we read in C (bytecount) and HL (address) from the following bytes... and call our actual display routine!
finally we restore the registers... and skip over the 3 bytes of parameters... last we reswap HL with the top of the stack pointer, and all the registers will be restored the way they were before the call!
|We can Call MemDumpDirect directly with an address in HL and bytecount in C
First we show HL to the screen as a label, so we can tell what we're viweing...
Next we need to decide how many bytes to show onscreen... on TI-83 we can only show 4... but on everything else we show 8!
Now we check if we have a whole line of bytes left to show, and limit the amount to show if we have more than a line left.
Next we subtract the number of bytes we're going to show from the remaining count C
|We're ready to start displaying it...
First we show HEX, but we'll show ASCII later too, so we back up the variables in BC and HL
we show each byte as a hexadecimal pair with a space afterwards (eg 'FF ')... we repeat for the count in B
We've backed up HL, and BC, because we'll do the same to show the characters as Ascii
|Now we show the characters to the screen as Ascii.
Characters below 32 have special meanings, so we convert them to '.' characters to keep things working ok
Also, on ZX Spectrum or Sam Coupe we can't use characters above 128 either.
I've defined BuildZXSv and BuildSAMv... they will be 1 only on those systems, and 0 otherwise.
IF statements on WinApe will compile lines where the condition is nonzero... by using BuildZXSv+BuildSAMv the following lines will compile if EITHER of these systems are being used.
On TI, or 32 character systems, we're all done!, but on 40 character wide systems we need to do a newline...
Finally we check C - and if there are any more lines to draw, we repeat the whole procedure again.
|By adding BuildSAMv an BuildZXSv together, and using that as a condition in an IF statement, we can define a block that compiles on multiple systems... if our assembler didn't support it, we could just use a pair of IFDEF blocks, but it could get quite messy!|
|We define a buffer of 16 characters labeled TestString for the user input (We'll actually only need 5 chars)
We read in up to 4 characters using our WaitString Function (5th char is 255)
Next we want to check if the first character is 255 (if the user has entered no characters)... and return if they have
we do a trick here, rather than doing CP 255.. we can do INC A - if A becomes zero, it was 255... INC A saves one byte of code over CP 255... but of course A is changed in the process!
Next we convert the string to Uppercase using ToUpper
Then we convert the first 2 characters to hex, and store them in D
Then we convert the next 2 characters to hex, and store them in E
Now we swap the memory address from DE to HL
On the TI we can only show 6 lines... on the everything we'll show 128.. we store this into C
finally we call our MemDumpDirect routine, and repeat the whole procedure.
thing to note is that the AsciiToHexPair and WaitString functions are
not restricting the keys pressed, and WaitString is not checking if 4
characters were entered... both these factors mean 'weird' resulting
memory addresses if, for example the address 'AZZZ' was entered.
Because we're only showing memory addresses, and this is only a test it doesn't really matter, but you may want to add some code to limit the keys Waitstring allows to be preseed, and check 4 characters were really entered if you want to make it better!
| Lesson M3 - String Matching for command reading
So, we're doing pretty well, but lets do more with our string reader! lets read in strings, and try to recommend individual commands in them!
We're going to read in a string, and look for up to two commands (separated by a space)... we'll match them to a list of commands, and if we manage it, remember which command numbers the user entered for future processing!
|We want to split a string up by spaces, to do this we'll convert 'Space' (Char 32) into Char 255
This Replace char function will process the string pointed to by HL, and any characters that match register D will be swapped to E
|CompareString is our other function, We pass two string pointers, one at HL, and the other at DE... this function will set the Carry flag if the strings don't match, and Clear it if they do!
We check each character in the strings, to see if they match, and if we find one that doesn't before BOTH strings end... then they don't match!
This function doesn't check which is greater, or less than... so you'd need something else if that's what you need!
SCF is Set carry flag, and this is done if the strings don't match.
We load A from the string at DE... and we AND it with the character from DE... this means A will be 255 if BOTH are 255... then we do inc A, and ret Z as a quick way of checking if A is 255... also AND and OR clear the carry flag, so we don't need to do anything to clear it if the strings match
|We're going to process contents of a user entered string, and match it to a list of words.
First we'll need to define all the strings, and then we will create a table of pointers to the addresses of those strings... the list will end with &FFFF as an 'end of table' marker.
Using a list of pointers will make the data neatly structured, and make things easier for us to work with!
a list of pointers to the strings will give us far more flexibility...
if we want to print String 2 we just set DE to 2, and set HL to the
wordlist, add DE twice, and we'll be pointing to the word!
Lists of pointers are used all the time, so while they are confusing, you'll have to try to get used to it!
|First we load in the address of the next string in the list into DE.
Like before, we AND both D and E together... and add one - this effectively checks if both are 255, if they are, we've no words left to check, and we never found a match.
otherwise, we compare the word to the one we're searching for, and repeat if it's not found...
We return the number of the match in A, but if it was never found we return 255
|By converting a string to a integer, we'll be able to get assembly to process the user's input easily!
We're going to process up to two commands... so we'll be able to do things like 'North' or 'Take C64' and have the program respond to those commands!
|First we print a prompt to the user, then we read up to 20 characters from the user.
We're going to read in two strings, but the user may have only entered one, so for safety we add an extra 255 to the end of the string!
|We need to prepare our string,
First we change all the characters to capital letters,
Then we replace any spaces in the string to char 255's... this means our string code will treat it as two separate strings
Finally we start a new line, we'll show the matches numerically soon!
|We now compare the user's string (TestString) with the contents of the wordlist, and show the result with ShowHex
Because we converted Spaces to Char 255, we'll only match the first word
|Now we want to find the second word... this is a rare chance to use CPIR (compare inc repeat)
We need scan up to 255 characters, so we load BC with 255..
We want to find 255 so we need A to be set to 255... we know C contains this value, so we load A with C... then we run our CPIR
Now we need to check if the user has entered multiple spaces, not just one, so we check if the next char is also a 255 (using our inc a jp z trick) and skip if it is.
|Now we shift the starting searchpos into DE, and do another Scan... again we show the result with ShowHex
Finally we start a new line,
We'll repeat the whole procedure again until the user gets bored!
contents of todays lesson may not be *That* interesting, but we're
going to extend it into the template for a little text adventure! so
tune in next week for... "Majokko Sakuya in the land of the
Chibi-Chibi's'... or something like that!... ok it's only going to be a
little 'joke game' but it'll show us how we could create a real text