In lesson 1 we learned how to make a "Hello world!" program on the SMS. It was
quite a difficult process but it worked out OK in the end. Now we can start
adding to our knowledge as we add features to this program. We'll also learn
about debugging. At the same time I'll introduce some good practices which will
help you to make your code better.
2.1 Some good practices
2.1.1 Sections and code re-use
WLA DX has a concept of sections which allow us to arrange bits of code
in blocks and tell WLA DX where the divisions are; WLA DX can then arrange them
the best it can, and even leave out blocks that aren't used (if we tell it to,
which we do). It can also be useful for keeping data with the code it relates
to. However, it is important to make sure everything is in one of these
blocks (sections), otherwise the output is likely to have errors.
We do this using WLA DX's .section and .ends
directives. .section takes a couple of parameters:
.section "Section name" type
"Section name" is just a descriptive name for what's in this
section. There are a few special things you can do with the name to affect it:
"!Section name"
An exclamation mark at the start tells WLA DX
not to discard this section even if it thinks it's OK to.
"Section name_1000" "Section name_$200"
Adding an
underscore and a number at the end tells it that you want this section to be
that many bytes in size. If what's inside won't fit then the section will be
bigger, though.
"__Unique Section"
Two sections may not have the same name;
but if two or more files in a multi-file project contain a section starting
with a double underscore, only the first will be kept and the rest discarded
without any error. (Rarely used.)
type tells WLA DX what kind of section this is:
force
This section must be put exactly where defined by
the most recent .org directive. Use for location-sensitive code
(boot code, pause button, etc).
free
This section can be put anywhere in the current
bank. Use this one normally.
semifree
This section can be put anywhere in the current
bank, but only somewhere after the point defined by the most recent
.org directive.
overwrite
This is like force except it
doesn't care about overwriting other data. (Rarely used.)
Be careful not to call a section "BANKHEADER". This is a special name, to fulfil
a function needed on other consoles but isn't wanted for the SMS.
The most useful thing about sections is having WLA DX discard the ones we don't
use. It does this by checking for labels inside the section and seeing if
they're mentioned in another section that is used (or forced). It
is useful for two reasons. First, we can remove a function and its data simply
by removing the call to it:
; call DisplayDebugInformation
; Removed because it takes 30KB of ROM space and imposes an annoying
; delay before starting the game
Second, it allows us to re-use code. For example, do you remember the
VRAMToHL function we put into "Hello world!"? Well, if it was
useful there then it might be useful in another program, so if we put it on its
own in a file called "Useful functions.inc" then we can .include it
in every project. We can put other useful functions in there too. But what if
in any particular project, we want to use just one of the useful functions? By
.includeing the whole file, we will insert them all, wasting some
of the available space. So, if we divide this included file into
.sections, all the functions that aren't used will be left out!
You might have noticed that I just mentioned code re-use, the other part of this
section of the guide. As a rule, try to always re-use existing code where
possible because not only will you save time, but you will also avoid
problems by using bug-free code. Do this by putting it into re-usable
functions, in their own private sections, with careful use of
push and pop to make sure that they don't overwrite
any registers.
2.1.2 Local and unnamed labels
You know what labels are already. But WLA DX is a clever beast and it knows
about different types of labels, whose purpose is to help with the problem that
you can't use the same label twice.
First are local labels. These are distinguished by the fact that they
start with an underscore, eg "_InnerLoop:". They are only
available within the same section so you should use them as "temporary"
labels in a section, for labels that don't need to be referenced by outside
code.
Second are unnamed labels. It's not convenient to always have to think
up a new name every time you need to use a label; in particular, you will find
that you tend to create a lot of loops in your code like this:
; Set 16 bytes starting at $c100 to zero
ld a,$00 ; value
ld hl,$c100 ; address
ld c,16 ; counter
Loop:
ld (hl),a
dec c
jp nz,Loop
Having to think of an original name for the Loop label every time
soon gets tiresome. ("Have I used Loop3 yet?") So instead you can use a group of
unnamed labels which can be re-used as many times as you like. They are:
-, --, --- etc
for backward jumps
+, ++, +++ etc
for forward jumps
__ (two underscores)
for bi-directional jumps: jp _b backwards, jp _f forwards
You should use unnamed labels for short jumps where it's not really necessary to
have a descriptive label name. By all means use a descriptive name if it helps
to make the code clearer - it has no detrimental effect on the output.
2.1.3 Absolute and relative jumps
There are two types of jump available with the Z80 - absolute and relative. The
one we've already seen was the absolute jumpjp. The
location to jump to is stored in the code as an absolute address. The second
type is the relative jumpjr. Instead of storing the
absolute address (a 16-bit number) it stores the relative offset of the
given address from the current point, as a byte giving a range of -126 to +129.
We don't need to figure it out, we just write the address again and WLA DX
calculates the distance (or tells us if it's out of range). There are two
reasons to use jr: because it takes up one less byte in the code
(not something we generally need to worry about), and because for conditional
jumps it can be faster (when the condition is not met). When the condition isn't
met and for unconditional jumps, it is very slightly slower than
jp.
2.2 A text scrolling program
2.2.1 Program plan
"Hello world!" was all very well, but it never did anything much. So, I have
decided that I want to display a lot more information. I want to display a long
text file, and I want it to smoothly scroll vertically.
So what I need to do is:
Load a screenful of text
Wait for a short delay
Scroll it up a bit
Load more text if necessary
Repeat from step 2
There are some important lessons to be learnt here...
2.2.2 Base program
Let's start with a simple codebase. Click here to
download a simple preliminary version of the program - so preliminary, in fact,
that it doesn't do anything yet, it's just a modified version of "Hello world!".
You should find that the zip file contains a directory structure. That's because
I want to have a single file containing reuseable functions that is accessible
by many different projects. My solution is to have a structure like this:
This way, Scroller.asm and Hello World.asm can both include the
lines
and refer to the same files. This should prove useful later on. Useful
functions.inc contains... useful functions, some of which we have already
seen (VRAMToHL), some are adapted from the Hello world!
source, and some are copied from my own personal collection. If you look at the
main program block in Scroller.asm you'll find it's doing basically the
same thing as Hello World, except that it's using a lot of calls to
functions instead of "inline" code. By doing this it makes it much quicker and
easier to figure out what's going on.
Compile and run this base program and check what it's doing.
Perhaps the most important thing to note is the VDP initialisation code I've put
in Useful functions.inc. It sets the VDP registers to the default values
I used before, but with one more thing added: I have .defined some
important constants with it:
.define SpriteSet 0 ; 0 for sprites to use tiles 0-255, 1 for 256+
.define NameTableAddress $3800 ; must be a multiple of $800; usually $3800;
; fills $700 bytes (unstretched)
.define SpriteTableAddress $3f00 ; must be a multiple of $100; usually $3f00;
; fills $100 bytes
We can then use these constants in place of numerical constants, increasing code
readability and allowing us to change their values (if necessary) in just one
place. I'll be using NameTableAddress instead of writing
$3800 from now on.
2.2.3 Adding scrolling
Let's add scrolling to this program so we can see how that works. If we look in
the documents we can find out that scrolling is achieved by writing a value to
VDP register 9; so let's try that. Replace the infinite loop used to stop the
program with this:
; Scrolling loop
ld a,0 ; a = current scroll value (start at 0)
Loop:
out ($bf),a ; Output to VDP register 9
push af
ld a,$89
out ($bf),a
pop af
inc a ; Increment
jp Loop ; Repeat loop
Compile and run and yikes! It's scrolling much too quickly!
2.2.4 Adding a delay - the VBlank and the VCount
The reason it's going so fast is because there's no delay being used - it's
changing the vertical scroll value very rapidly, in fact much more rapidly than
the VDP can handle. The VDP can only handle one change in the vertical scroll
position per frame - it takes what's in that register when it starts drawing it
and ignores any changes until the next frame is ready to be drawn, 1/60s later.
So what we need to do is just change the scroll value once every 1/60s. There
are two ways to do this - one is to use VBlank interrupts, which is kind of
tricky, and the other is with a VCount busy loop.
First, let's explain the concept of VBlanks. It's all because TVs are cheap, old
technology. TV has had to maintain backwards compatibility almost since it was
invented so the technological limitations are the same as they always were - an
NTSC TV still draws two interlaced half-frames every 1/30s (approximately). And
here's the thing - in between every half-frame there has to be a pause while
the electron beam scanner inside the tube goes back up to the top. (Search on
Google for more details if you're really interested.) During this delay, the
VDP basically sits there waiting. When it's time to start sending the picture it's busy
but during the "Vertical Blank period" (VBlank) it hasn't got much to do.
So during this time, two notable (so far) things happen:
We can access VRAM as fast as we like. During the "active display
period", outside the VBlank, we have to be careful not to access VRAM too
quickly because it will cause corruption on a real system (although not on most
emulators). I thought I'd mention this now because it is an important factor in
making your program work on a real system.
The VDP reads in the vertical scroll value at the end of the VBlank.
So if we can wait for the start of the VBlank before updating the scroll
value, we can make it just update once per frame, ie. every 1/60s. We'll achieve
this by using the VCount, which I suppose is short for Vertical Position
Counter (maybe).
This is a number which we may read from the VDP which tells us which line it is
currently drawing. It starts at 0 when the active display period starts, and
counts up 1 every time a line is drawn. For a regular SMS screen, when the
VBlank occurs this counter will be at 192 because there 192 lines in the
display. (Actually there are more, but the SMS only displays 192; outside that
area, only the coloured border is shown. We can treat all of this as the VBlank
even if that isn't strictly true.) We can get it by inputting from port
$7e:
in a,($7e) ; get VCount
So, we can find the point at which it's safe to change the display by making a
busy loop which will continually read the VCount until it gets to 192;
then it should update the scroll value, then return to the busy loop to wait for
the next VBlank. Here's how we're going to do it. First we put this in our loop
just after the Loop: label:
call WaitForVBlankNoInt
Then we put these two functions somewhere in the source. Hey, these functions
look pretty useful - so let's put them in Useful functions.inc instead.
Here they are:
;==============================================================
; V Counter reader
; Waits for 2 consecutive identical values (to avoid garbage)
; Returns in a *and* b
;==============================================================
.section "Get VCount" free
GetVCount:
in a,($7e) ; get VCount
-:ld b,a ; store it
in a,($7e) ; and again
cp b ; Is it the same?
jp nz,- ; If not, repeat
ret ; If so, return it in a (and b)
.ends
.section "Wait for VBlank without interrupts" free
WaitForVBlankNoInt:
push bc
push af
-:call GetVCount
cp 192
jp nz,-
pop af
pop bc
ret
.ends
Now try it. You should see it smoothly scrolling upwards most of the time, but
occasionally jerking. So why the occasional jerk (if you'll excuse the
phrasing)? Well, it turns out to be a side effect of the way the screen lines
are counted. You'll have noticed that some of the text is displayed that wasn't
before, because it was off the bottom of the screen. This is because the VDP
has a "virtual" screen size of 32×28 tiles and only the 32×24 tiles
below the current vertical scroll point are shown. In eSMS you can click on
Debugger -> View tilemap to see this "virtual" screen, with a
rectangle showing the portion shown on the screen. (Notice how, when the bottom
of the rectangle goes off the bottom of the "virtual" screen, it appears at the
top.) The top line is
number 0, the bottom number 223. If we set a scroll value of 224 then that's
the same as 0; 225 is the same as 1. The problem comes when we get to 255 ($ff),
which is the same as 31. If you add one to $ff you get $00, so scrolling will
jerk from line 31 to line 0.
So we really want our scroll counter to go from 0 to 223 (for the 28 rows of
8 pixels) and then go back to zero, missing out these troublesome values from
224 to 255. Let's implement that by adding these lines between
inc a and jp Loop:
cp 224 ; Is it 224 now?
jp nz,Loop ; If not, repeat loop
ld a,0 ; Otherwise, set it to zero
See if you can follow what it's doing. (Hopefully, the comments I've added have
made it easier to understand.) Now run it and see what happens - smooth
scrolling!
2.2.5 Showing more than 28 lines (part 1) - making a suitable function
So far we've only managed to display 28 lines of text, which smoothly scrolls up,
and then repeats from the start. That's pretty good, but
we really want to show more. The only way to do this is to have our code update
the name table, so by the time it gets around to displaying line 1 again, it has
been changed to show line 29 instead. By changing it while it's offscreen the
user won't see it and it will look very smooth and professional.
In order to do this we're going to have to re-write the code for displaying
text. What we used before is OK as far as it goes, but we're going to have to
split it off into a function that will let us draw one line at a time, and keep
track of what we've drawn. Let's try that. Here's what I've got:
;==============================================================
; Draw one line
;==============================================================
; Draws one line (32 characters maximum) of text, converting
; from ASCII, from the given address to VRAM (which must be
; set ready to accept it).
; Parameters:
; hl = location of text
; When it returns, hl points to the next character to be drawn.
;==============================================================
.section "Draw one line" free
DrawOneLine:
push ??
pop ??
ret
.ends
Well, that looks very nice, but oh dear! I haven't written the function yet!
Well, let's see. What I want is to draw some text but at the same time keep
count of how many letters I've drawn and stop when I get to 32. So I'll take my
text-drawing code from before and add in a counter and check:
DrawOneLine:
push af
push bc
ld c,32 ; Start counter
-:ld a,(hl) ; Get data byte
inc hl ; Point to next letter
cp $00 ; Is it zero?
jp z,+ ; If so, exit loop
sub $20 ; Convert to ASCII
jp c,- ; Skip letter if tile index < 0
out ($be),a ; Draw letter
ld a,$00
out ($be),a
dec c ; Decrement counter (doesn't count skipped)
jp z,+ ; If zero, exit loop
jp -
+:
pop bc
pop af
ret
If you look closely you might notice that I rearranged the code a little bit at
the same time, to make it more efficient. You will often find you can come back
to some code and see something to make it better.
OK, so let's use this function instead of the existing one for drawing text to
the screen. We need to remove the existing one but it seems a shame to just
delete it - and it might still be useful to use. So instead, we're going to
comment it out. This means I'm going to mark the whole thing as a
comment, making WLA DX totally ignore it. If you remember back to lesson
1, we can mark several lines as a comment by putting /* before it and
*/ after it, just like in C:
/*
;==============================================================
; Write text to name table
;==============================================================
(...)
jp -
++:
*/
Then, we add a call to our new function there:
;==============================================================
; Write first line of text
;==============================================================
ld hl,NameTableAddresscall VRAMToHL
ld hl,Message
call DrawOneLine
Compile and check if it works. Well, it does, but drawing only one line is
pretty lame. Let's make it draw 24, to fill the screen:
;==============================================================
; Write first screen of text
;==============================================================
ld hl,NameTableAddresscall VRAMToHL
ld hl,Message
ld a,24
-:call DrawOneLine
dec a
jp nz,-
OK, that's great, but it's just doing what it was doing before!
2.2.6 Showing more than 28 lines (part 2) - drawing the next line
Now we get to the clever bit. Whenever we detect that the bit
just about to come onto the screen is wrong, we have to draw the right thing in
the right place. We do this by putting in a check just before scrolling. How can
we tell if it's time to draw a new line, though? Well, it will be time to do
that every 8th scroll line because each row is 8 pixels tall. We already have a
count of the scroll line number in a, so we need to see if it's an
exact multiple of 8.
The Z80 can't do this automatically for us. Believe it or not, it has not got
any instructions for multiplication or division. So what we have to do is use
some clever bitwise operations. In this case, we want to see if a number
divides exactly by 8. We can do that by checking some of the bits used to
make the number:
128
64
32
16
8
4
2
1
%
0
0
0
0
0
1
1
1
= 710
%
0
0
0
0
1
0
0
0
= 810
%
0
0
0
0
1
0
0
1
= 910
%
1
0
1
0
0
1
1
0
= 16710
%
1
0
1
0
1
0
0
0
= 16810
%
1
0
1
0
1
0
0
1
= 16910
Here I've shown six numbers (7, 8, 9, 167, 168, 169) in binary form, and I've
labelled each binary digit with its value. (You might have done this in school.)
You might notice that the two exact multiples of 8 (8 and 168) have their last
three binary digits set to zero. This will hold for any multiple of
eight, so we can use this as our test. We can separate out just those bits using
an AND operation:
%
1
0
1
0
1
0
0
0
=16810
AND
%
0
0
0
0
0
1
1
1
=710
=
%
0
0
0
0
0
0
0
0
=010
%
1
0
1
0
1
0
0
1
=16910
AND
%
0
0
0
0
0
1
1
1
=710
=
%
0
0
0
0
0
0
0
1
=110
Try this out on a calculator. Basically, any number AND 7 will be zero if the
number is a multiple of 8, and non-zero otherwise. So we can add this check in
the scrolling loop:
; Scrolling loop
ld a,0 ; a = current scroll value (start at 0)
Loop:
call WaitForVBlankNoInt
; Check to see if we need to update the name table
push af
and 7
call z,DrawOneLine
pop af
; Scroll screen
out ($bf),a ; Output to VDP register 9
...
OK, let's see what we get:
Oh. That didn't work at all, did it? I wonder why? Well, the reason is
because the VRAM write address is wrong. DrawOneLine relies
on the calling code to set the VDP ready to accept data to the right place in
the name table; but all the in-between writes to the VDP registers switch it out
of "write data" mode. So we'd better amend DrawOneLine to handle
this itself:
;==============================================================
; Draws one line (32 characters maximum) of text, converting
; from ASCII, from the given address to VRAM (which must be
; set ready to accept it).
; Parameters:
; hl = location of text
; bc = name table address
; When it returns, hl points to the next character to be drawn
; and bc points to the following name table address.
;==============================================================
.section "Draw one line" free
DrawOneLine:
; Set VRAM write address to bc and add 64 to it
push hl ; save value in hl
ld h,b ; transfer bc into hl
ld l,c
call VRAMToHL ; set VRAM write address
ld bc,64 ; add 64
add hl,bc
ld b,h ; transfer hl into bc
ld c,l
pop hl ; restore hl
push af
push bc
ld c,32 ; Start counter
...
We add 64 to bc each time because that corresponds to how many
bytes 32 name table entries take up. We also modify the first screen drawing
code appropriately:
;==============================================================
; Write first screen of text
;==============================================================
ld bc,NameTableAddress
ld hl,Message
ld a,24
-:call DrawOneLine
dec a
jp nz,-
We also check that during the scrolling loop, bc (and
hl) are not modified. In this simple case, they are not, so we can
keep our values in those registers; in a more complex program we'd probably
transfer them into RAM (something we haven't learnt how to do... yet). Let's see
what we get now. Well - it seems pretty good, it's displaying more than 24 lines
now, except that after a while, the text starts to get corrupted (notice the
ls):
Why is this? Because the VRAM address we're keeping in bc is
continually being increased, so when it gets to the end of the name table it
just keeps going. Eventually it exceeds the VRAM size and the effect is to make
it start addressing VRAM from zero again - which is where the tiles are. Writing
name table data over the tiles results in the corrupted tiles we see. The
solution is to check the value in bc and when it gets past $3f00
(the end of the name table and start of the sprite table, which we can also
calculate as NameTableAddress+$700) we must subtract $700. So,
immediately after adding 64 to hl in the bit we just added to
DrawOneLine, we put:
push af ; Check value is still in name table
ld a,h
cp $3f
jp nz,+
ld h,$38
+:
pop af
What we're doing here is, we're making use of the fact that we know that
hl only increases by 64 each time; we therefore know that it won't,
for example, jump from $3eff to $4000 - it will have to have a value of $3fxx at
some time. We can check that by looking at h on its own; if it's
$3f then we change it to $38. I actually coded a much more complicated and slow
routine involving several 16-bit subtractions before I realised this...
One more thing, though: we ought to take out those "magic numbers" $3f and $38,
and replace them with something related to the constant
NameTableAddress. The way we do this is by using WLA DX's built-in
expression parser. Its syntax is basically the same as C; to get the value we
want we replace $38 with NameTableAddress>>8 (value shifted right by 8
bits) and $3f with (NameTableAddress+$700)>>8 (similarly).
Run it now and see what happens. I suggest you open the Tilemap window in eSMS
(Debugger -> View tilemap) and you'll see how, just before
appearing on the screen, the next line is drawn. If you open a proper game with
a similar scrolling effect, you'll see that's exactly how it does it too!
2.2.7 Showing more than 28 lines (part 3) - what about the end?
Well, it works OK now. Except there's a problem when it gets to the end of the
message. It just keeps on going past the end, interpreting whatever it finds as
ASCII text and drawing that to the screen:
You may remember that ages ago we decided that we'd use a zero byte to mark the
end of the text. That's still there, and it still does what we originally told
it to - when it's encountered, the DrawOneLine function stops
writing to the name table. Unfortunately, it doesn't communicate this to the
code in the scrolling loop, so next time around it just calls
DrawOneLine again! Let's start by letting DrawOneLine
communicate the end-of-text state to the calling code. We could do this using an
additional register but instead we can make use of the registers we're already
using. bc contains the VRAM address to be written to next; no
matter what the VRAM arrangement, this has to be in the range $0000 to $3fff. So
we can signal the end-of-text by setting it to a value outside this; I'll choose
b = $ff:
push af
push bc
ld c,32 ; Start counter
-:ld a,(hl) ; Get data byte
inc hl ; Point to next letter
cp $00 ; Is it zero?
jp z,_EndOfFile
sub $20 ; Convert to ASCII
jp c,- ; Skip letter if tile index < 0
out ($be),a ; Draw letter
ld a,$00
out ($be),a
dec c ; Decrement counter (doesn't count skipped)
jp z,+ ; If zero, exit loop
jp -
+:
pop bc
pop af
ret
_EndOfFile:
pop bc
ld b,$ff
pop af
ret
I've changed the jp z,+ line after cp $00 so it jumps
to an alternative loop-exiting point, which pops the registers properly and
also sets b = $ff. Then I need to add it into the calling code:
; Scrolling loop
ld a,0 ; a = current scroll value (start at 0)
Loop:
call WaitForVBlankNoInt
; Check to see if we need to update the name table
push af
and 7
call z,DrawOneLine
ld a,b ; Check if we should stop scrolling
cp $ff
jp z,_StopScrolling
pop af
; Scroll screen
out ($bf),a ; Output to VDP register 9
push af
ld a,$89
out ($bf),a
pop af
inc a ; Increment
cp 224 ; Is it 224 now?
jp nz,Loop ; If not, repeat loop
ld a,0 ; Otherwise, set it to zero
jp Loop ; Repeat loop
_StopScrolling:
jp _StopScrolling
Now try it and see how it works. Perfectly!
2.2.8 Processing line breaks
There's still something bad about our program. What happens if we give it
a text file that's not specifically designed with
32-character-long lines? Well, it looks terrible. When you look at the text file
in a text editor, it understands the line break characters inside it. Our
program should too...
So what we should do is add a check when we read the character; if it's a
special line feed character (ASCII code $0a) we should simulate that by
writing blanks until the end of the line:
-:ld a,(hl) ; Get data byte
inc hl ; Point to next letter
cp $00 ; Is it zero?
jp z,_EndOfFile
cp $0a ; Is it a line break?
jp z,_LineBreak
sub $20 ; Convert to ASCII
jp c,- ; Skip letter if tile index < 0
out ($be),a ; Draw letter
ld a,$00
out ($be),a
dec c ; Decrement counter (doesn't count skipped)
jp z,+ ; If zero, exit loop
jp -
_LineBreak:
; Output blanks until end of line
ld a,$00
-:out ($be),a
out ($be),a
dec c
jp z,+
jp -
_EndOfFile:
Try that:
It's much better. The only problem is that blank line in the middle... it
shouldn't be there, it's not in the original file. Why is it there? If you're
lucky, you can figure out the reason by looking at the output and thinking
carefully, but sometimes you can't tell just by looking. So let's have a go at
debugging.
2.2.9 Introductory debugging
Until fairly recently, debugging was a tricky procedure where you had to do the
following:
Assemble (compile) your code
Disassemble the output .sms file
Search through it to find the code section you want
Note the address of the code
Open the rom in a debugging emulator
Make it stop execution at the point you are interested in
Step through execution until you find the problem
Try to fix it
However, thanks to Ville Helin and Martin Konrad, we can streamline this process
a little bit. WLA DX can output a symbol file and eSMS can read it. Note
that if you downloaded the compiler batch file before January 2004, you probably
got an old version that won't make the symbol file;
here it is again, along with binaries.
While you're downloading things, it's probably worth updating the
syntax highligher again
because since WLA DX is in active development, new things appear from time to
time. You should also check if ConTEXT
has been updated - the highlighter works best with the latest version.
Anyway, recompile (F9) and run in eSMS (F11). Once you're there, switch
out of fullscreen mode (Esc) and open up the debugger (F9). Press its Enable
button and it will stop the program running.
Next, click on the Debugger menu and choose Add from NO$GMB symbol
file. You might notice that some of your labels appear in the debugger
window, depending on where the code stopped:
Now, click on Labels from the Debugger menu, and this window comes
up:
In this window we can see the addresses in the file corresponding to our labels.
(You may see different numbers, depending on how WLA DX compiled your code - the
actual numbers don't matter, just make sure to use your values if they're not
the same as mine.) It seems the problem we're having is related to when line
breaks appear, so we notice that our label _LineBreak corresponds
to address $3696.
Next, click on Debugging -> Breakpoints:
Enter the address and click on Add. Next, go to the main Debugger window; make
sure the Break at breakpoints button is highlighted and click on
Run. It will then run through your code until it gets to that point:
Now let's have a think. When the code gets to this point, it's because it has
found a line break character. Register c contains the number of
spaces that we need to add at the end of the line that is about to appear on the
screen. In the above screenshot, it contains the value $04 - you can see that
because it says "BC = 3C04" at the top. Press Run again and it will run until
the next time it gets to the same point. You should notice that
whatever the number in c was, that's how many spaces
there are at the end of the line that appears next time it runs. Keep
going until you notice something odd...
hint 1,
hint 2.
What's happening is this. Whenever it gets to a line with exactly 32
characters in it, followed by a line break, it is calling
DrawOneLine. The code there returns after drawing 32
characters, leaving the pointer to the next character pointing to the
line break at the end of that line. Next time it is called, it sees a
line break and draws a blank line as it is suppsed to.
So what's the solution? Simply remove the 32 character limit in
DrawOneLine. We can make sure that our input text file does not
contain any longer lines and it should be OK. Remember, we're not
trying to write code that can handle anything, we just want it to
handle the data we prepare for it! Let's comment out the line in
question:
dec c ; Decrement counter (doesn't count skipped)
; jp z,+ ; If zero, exit loop
jp -
Run it and see how it goes.
It's managing those 32 character lines OK now, but sometimes it's
blanking the top of the screen. A little thought and we think -
what happens with those 32 character lines? It's arriving at
_LineBreak with c set to $00. But if you
look at the code there...
_LineBreak:
; Output blanks until end of line
ld a,$00
-:out ($be),a
out ($be),a
dec c
jp z,+
jp -
It's doing this:
Output a blank
Subtract 1 from c
Repeat unless c=0
It will output a blank no matter what the value of c is;
it will also subtract 1 from 0, which will give a result of $ff! Then
it will repeat, until it has output another $ff = 255 blanks. This is
an error due to the difference between test at top and test
at bottom loops. We need to test the value in c and
possibly exit before outputting any blanks. Let's rearrange
the loop:
_LineBreak:
; Output blanks until end of line
-:dec c
jp c,+
ld a,$00
out ($be),a
out ($be),a
jp -
What I'm doing here is using the carry condition instead of
zero. Carry can be a tricky concept to understand, so I'll
try to explain. On the Z80, we have 8-bit registers (ignoring the
16-bit ones for now). That means they can hold a value from 0 to 255.
But what happens when we reach the boundaries? 255 + 1 = 256, but we
can't represent that. In hexadecimal, it's $ff + $01 = $100 - notice
how there's an extra digit. Well, that extra digit is called the
carry out, a lot like back in school when you learned
arithmetic. Likewise, $100 - $01 = $ff; since $100 and $00 look the
same, you'll find that $00 - $01 = $ff. This time it's called
carry in. Either way, it's important to know that it's
happened, so the Z80 sets the carry flag in the f
register, and we can use it much like we've used the z
flag.
Anyway, let's give it a go...
Oh dear... time to debug again. Let's try another method this time.
All that loading of labels and typing in numbers last time was a bit
tricky, so let's do it a different way. We still want to stop at
_LineBreak. Go to where we want it to stop and add two
lines, both saying nop:
_LineBreak:
nop
nop
; Output blanks until end of line
-:dec c
jp c,+
ld a,$00
out ($be),a
out ($be),a
jp -
nop stands for no operation - it does exactly
nothing. Pretty useless, huh? Well, it can be useful for creating
delays for timing-sensitive things (eg. writing to VRAM during the
active display period, or a sample player), and thanks to Martin
implementing a feature request in eSMS, it's a way of adding
pseudo-breakpoints too. Compile and run in eSMS, and enable the
debugger. Make sure the Break at nop nops button is pressed,
and press Run. It'll run until it gets to two consecutive
nop instructions and then pause for us - a lot like a
breakpoint, but a bit easier to create.
Anyway, let's see what's happening. Keep pressing Run until we
get a value of $00 in c, then press Step in to
execute the code one line at a time. It decrements to $ff, but it
doesn't exit when it's supposed to. If we open up the Z80 user manual
(you downloaded
it in Lesson 1, the filename was z80cpu_um.pdf)
and look it up (Z80 Instruction Set → 8-Bit Arithmetic Group
→ DEC m), we can shed light on the problem. Scroll down to this:
D'oh! It turns out dec and inc don't change
the carry flag! Yes, even with my years of experience, I forgot this. It's
another mildly embarrassing example of how we all make little errors, which will
hopefully make you happier when you make similar mistakes.
But what's that at the top, talking about the
sign flag? How can it be negative, when it's limited to 0-255?
It's all to do with something called two's complement notation.
This is a rather complicated thing to explain and understand, so I
won't go into it now. Suffice to say, you can treat $80-$ff as
negative numbers if you don't mind losing that part of the positive
number range; and thanks to clever electronics and number
representation, the Z80 doesn't need to know you're doing it and you
can use the same instructions with the numbers. So let's think of
c as a signed number from -128 to +127. When we
subtract 1 from zero, we will get -1, which is a negative number.
Then we can use the minus and plus conditions in jumps.
Let's change the c condition to m and
delete those two nops:
_LineBreak:
; Output blanks until end of line
-:dec c
jp m,+
ld a,$00
out ($be),a
out ($be),a
jp -
Run it and...
Success! You can download my final version
in case you had problems following all of that.
2.3 What next?
Some suggestions...
What if the text was some ASCII art?
What if the tiles weren't text, but were graphical? Then, if the text was
right, it could build up a picture...
It scrolls a bit fast... could I make it scroll one line every 2 or 3
frames? (easy)
How about letting me press a button to pause it? (quite easy once you know
how)
How about letting me scroll back up? (a lot harder!)
Do you have any suggestions for what
you'd like to see me show you?