Maxim's World of Stuff - SMS/Meka stuff

Getting started with SMS programming - Lesson 2

2.0 Introduction

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:

forceThis section must be put exactly where defined by the most recent .org directive. Use for location-sensitive code (boot code, pause button, etc).
freeThis section can be put anywhere in the current bank. Use this one normally.
semifreeThis section can be put anywhere in the current bank, but only somewhere after the point defined by the most recent .org directive.
overwriteThis 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:

-, --, --- etcfor backward jumps
+, ++, +++ etcfor 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 jump jp. The location to jump to is stored in the code as an absolute address. The second type is the relative jump jr. 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:

  1. Load a screenful of text
  2. Wait for a short delay
  3. Scroll it up a bit
  4. Load more text if necessary
  5. 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

.include "..\Useful functions.inc"
.include "..\Fonts\BBC Micro font.inc"
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:

  1. 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.
  2. 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,NameTableAddress
    call 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,NameTableAddress
    call 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:

 1286432168421  
%00000111 = 710
%00001000 = 810
%00001001 = 910
%10100110 = 16710
%10101000 = 16810
%10101001 = 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:

%10101000 =16810
AND
%00000111 =710
=
%00000000 =010

%10101001 =16910
AND
%00000111 =710
=
%00000001 =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):

If we increase the length of the message, it gets even worse:

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:
  1. Assemble (compile) your code
  2. Disassemble the output .sms file
  3. Search through it to find the code section you want
  4. Note the address of the code
  5. Open the rom in a debugging emulator
  6. Make it stop execution at the point you are interested in
  7. Step through execution until you find the problem
  8. 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:

  1. Output a blank
  2. Subtract 1 from c
  3. 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?
  • If you're shy, you can post on the SMS Power development forum rather than emailing me...