Tuesday 21 February 2012

Introduction to Z80 assembly part III.

Here are the final four instalments of the Z80 assembly language tutorials that I wrote for Micro Mart magazine [now defunct] (www.micromart.co.uk) that were printed a couple of years ago now. Thanks for all of the kind comments about these articles here and elsewhere. Lets get on with it, shall we?

Return of the bedroom programmer

Part IX: Buzzer

You'll may recall from previous tutorials that we've spent a lot of time looking at how the screen works, and in the most recent part, there was the concept of the two 'channels' (of which there are three) used by the Sinclair ZX Spectrum to decide where to display or output your text. Other basic concepts have been gently introduced that will hopefully help you in building your project in the months to come, such as adding delays, moving bytes around and doing a basic keyboard scan. I know it has been at a rather sedate pace which may annoy some people, but the whole point is for those people who have been frustrated by assembly language tutorials in the past (including me) to try the now ancient art of programming at the machine's level. If you would like to move much quicker than this then please join the Spectrum Computing community forums at spectrumcomputing.co.uk/forums/ and look in the 'Development' sub-forums. If you're perfectly happy with this series so far then point your web browser at the Micro Mart forums (the specific thread is at tinyurl.com/Speccy-Coding a secret that only Archive.org can reveal) if you require help, but right now lets get coding.

This week, we'll move on to looking at the Speccy's infamous beeper. Don't worry to much about musical theory for now. Here is a quick example to play one note.

    org $8000       ; As usual, our code will
                    ; begin at 32768 (8*4096)
    ld hl,262       ; This is the pitch for
                    ; our note
    ld de,255       ; This will be the duration
                    ; it will play
    call 949        ; Play the note by calling
                    ; the relevant ROM routine
    ret             ; Return to BASIC
  

I'll apologies now because I'm delving into the world of mathematics again. If this isn't your strong point then don't worry about it, just concern yourself with the code and experiment, or use the built-in calculator on the 128K Spectrum.

As you can see from the above small routine, the pitch of the note that you want to play along with the duration it is to be played is set up using the register pairs hl and de respectively. On the Sinclair, when the beeper is accessed, all other processing is halted until the note has finished, but fortunately you are working in machine language rather than BASIC, which is many times quicker.

What you will need to know is the 'Hertz' value for the frequency of note that the beeper is to play, or more accurately 'emit'. This is the number of times the internal loudspeaker needs to be toggled each second to produce the desired pitch. This is your base line for working out each note:

  • Middle C is 261.63Hz
  • C# 277.18Hz
  • D 293.66Hz
  • D# 311.13Hz
  • E 329.63Hz
  • F 349.23Hz
  • F# 369.99Hz
  • G 392.00Hz
  • G# 415.30Hz
  • A 440.00Hz
  • A# 466.16Hzm
  • and finally B 493.88Hz

If you want to go an octave lower than this then you must halve the value, and to go an octave higher, it should be doubled.

So far, so good? Well, now comes the difficult bit. You've worked out what note or notes you want to hear, so now you need to calculate how long you should play it, remembering that the longer it is played, the less processor time you will have to do anything else. Lets say you want to play G for 0.2 seconds, well we look up the value of G and times that by 0.2, so we have 392.00*0.2, which equals 78.4. Now divide 437500 by 392.00 (the G note), which will give you 1116.07 (roughly). Now subtract 30.125 and you get approximately 1085.94, and round this to the nearest whole number, which is 1086. To put it another way, the register pair de is our duration, which is equal to Frequency*Seconds and hl is used for the pitch, which has an equation of 437500/Frequency-30.125. Each time, you should round to the nearest whole number, so in this instance, hl should be 1086 and de should be 78.

I urge you not to worry too much about this as it's fairly easy to accomplish simple sound effects for your project without doing much maths at all, but this certainly worth knowing so have a play and see if you can write a simple tune. See you next week.

Part X: A MOB

Let's be honest, most of you that have been following this series will have in the back of their minds creating a 'commercial-quality' game for the old Sinclair ZX Spectrum, using '100% machine code', and if you look at the software released for the 8-bit home computer, not just during the 1980s but to date, then the vast majority of it falls into this category. So, the most obvious thing to do is to draw, animate and control graphics. I've covered writing text to the screen fairly extensively, so with any luck you've had a program proclaiming that you are ace, or perhaps a similar message in a 'scrolly text'. Maybe you've played about with last weeks example too and played a tune along with your message. Now you can add to the mix some user defined graphics. Let's have a look at this little routine:

    org $9000       ; Our program will
                    ; start at 9*4096 (36864)
    ld hl,GFX       ; Points the HL register
                    ; at the area of memory
                    ; called GFX
    ld (23675),hl   ; Initialized system for
                    ; user defined graphics
    ld a,2          ; We want channel two
                    ; (upper screen)
    call 5633       ; Open channel two
                    ; (ie, write to screen)
    ld a,12
    ld (XCOORD),a   ; We'll put our graphic
                    ; character on row 12
LOOP
    call INITXY     ; This will set up the
                    ; X (row) and Y (column)
                    ; co-ordinates
    ld a,144
    rst 16          ; Display character 144 
                    ; (our UDG)
    call DELAY      ; Let's slow things down
                    ; a little
    ld hl,XCOORD    ; Get the current position
                    ; and store into HL
    dec (hl)        ; Decrease HL by one
    ld a,(XCOORD)   ; Store new co-ordinate
    cp 255          ; Is it at the top line
                    ; of the screen yet?
    jr nz,LOOP      ; If not, then jump
                    ; back to LOOP
    ret
DELAY
    ld b,12         ; Delay length
DELAYLOOP
    halt            ; Stop everything
                    ; temporarily
    djnz DELAYLOOP  ; Decrease B by one
                    ; and repeat until B
                    ; is zero
    ret
INITXY
    ld a,22
    rst 16          ; Calls the Sinclair
                    ; PRINT AT routine
    ld a,(XCOORD)   ; Gets the X co-ordinate
    rst 16
    ld a,(YCOORD)   ; and the Y co-ordinate
    rst 16          ; So, essentially
                    ; PRINT AT X,Y; like in BASIC
    ret
XCOORD
    defb 0
YCOORD
    defb 15
GFX
    defb %00111100
    defb %01011010
    defb %01111110
    defb %01011010
    defb %01100110
    defb %00111100
    defb %00000000
  

Once it's compiled, run it and see what happens. As you will notice, the redefined character cell is represented in binary at the end of the program, with each '1' representing the bits that are 'on', and '0' [the bits that] are off. There is a deliberate fault in the program though, which you will realise soon enough. How can this be fixed? And can you make your UDG travel on the horizontal and vertical plane, or start from a different part of the screen? How about replacing the delay routine with a piece of code that will play a note, or perhaps wait for a key to be pressed on the keyboard before continuing with the main routine? The more you experiment, the more you will learn, so now is a good time to get your hands dirty by trying to sellotape together previous examples with this one. Don't worry about errors as it's all part of the process.

If you would like some theory as to what is happening and why, then pop along to the Micro Mart forums (the specific thread is at tinyurl.com/Speccy-Coding), or leave any questions that you have here.

Part XI: Chunky chars

Before I proceed, I'd just like to mention one of the most useful sources of reference for any budding Sinclair ZX Spectrum programmer, and that's the original 'Sinclair ZX Spectrum BASIC Programming' manual that was bundled with the original rubber-clad machines. This has also been archived online if you want to search for it, but with the sheer amount of Spectrum's sold during the early years of the 8-bit, a physical copy shouldn't be difficult to source second hand. I don't know about you, but I prefer printed books over electronic versions.

If you refer to chapters 23 and 24, you'll find a rather handy keyboard and memory map. As you're using assembly, and not BASIC, you have direct control over the memory usage, within the limitations of the available 41K or so available to you (which is about 9K if you're trying to squeeze something into 16K).

Something else I've found very useful for programming is a pencil and paper, to draw a scheme of the whole project. Breaking down the program into logical steps and solving problems is what it's really all about. To do this, you may use traditional methods, like flow charts or pseudo-code. The former is good for small routines and the latter for bigger chunks of code. There is a new kid on the block too, which is Test Driven Development, in which you write a test first that will check if your code works to the bare minimum that you need it to do. This might seem counter-intuitiave as the test will automatically fail until you've written the code that you want, but prevents unnecessary programming and Keeps It Simple and Stupid (KISS), which should be easy to maintain. The trick is to find what works for you, especially for personal development like programming 8-bits.

Last time, we looked at moving a simple redefined character cell. We did this by changing the Sinclair's two-byte system variable at location 23675 to point specifically at our character cell, which was a smiley face if you entered the bianry correctly. You could have easily redefined your own UDG (User Defined Graphic, sometimes referred to as MOBs or Movable Object Blocks) if you wanted to.

From BASIC, you have 21 UDGs (or 19 on 128K machines, at least in the 128's native mode). You may change the system variable at 23675 for each sets of UDGs you define but although it's quite easy to implement, things can become quite messy.

You will have noticed that the routine left it's trail as it moved up the screen. This was the deliberate bug that I left in for you to sort out. To stop this from happening (if you haven't worked it out), you would need to write the character 32 (space) after calling the DELAY routine and before moving one row up, or something like this. Try it yourself to see what produces the best results, trying to avoid any annoying flickering along the way. Here is a routine that will copy the ROM font into another part of RAM and manipulated it to make it look chunky:

    org $9000       ; Our program will
                    ; start at 9*4096 (36864)
    ld hl,15616     ; HL will point at
                    ; the ROM char set
    ld de,60000     ; Here is were in RAM
                    ; we will store it
    ld bc,96*8      ; We have 96 chars
                    ; times 8 rows
NEWFONT
    ld a,(hl)           
    rrca            ; Here's something new,
                    ; it 'rotates' each bit
                    ; in the accumulator
    or (hl)         ; Logical or, which will
                    ; combine the two sets of
                    ; bits (non-rotated and
                    ; rotated right)
    ld (de),a       ; Store the new bits
                    ; into RAM
    inc hl
    inc de
    dec bc          ; bc is used as a
                    ; counter here
    ld a,b          ; get the high byte in
                    ; the register pair (b)
                    ; in bc
    or c            ; and combine with the
                    ; low byte (c)
    jr nz,NEWFONT   ; Repeat until bc is zero
    ld hl,60000-256 ; font minus 32*8
    ld (23606),hl   ; Point to new font
    ret
  

Part XII: And finally...

They say all good things come to an end, though whether this series has been good or not is up to you. This will be the last of the tutorials for the old Sinclair ZX Spectrum for now. I plan to move onto the 6502-based processor next (this is now available through the pages of Commodore FREE magazine at https://archive.org/details/commodorefree). Let's get straight into the coding then, have a look at this routine.

    org 32768       ; This is where we'll
                    ; start our program
    call PRINTINIT
    ld bc,10010
    call DISPNUM
    ld a,13
    rst $10
    ret
PRINTINIT
    ld a,2
    call 5633       ; open channel
                    ; two (upper screen)
    ld a,22
    rst $10
    ld a,0
    rst $10
    rst $10         ; This is equivalent
                    ; to PRINT AT 0,0;
    ld de,STRING
    ld bc,EOS-STRING; Length of STRING
                    ; to print
    call 8252       ; Throws our STRING
                    ; out via channel two
    ret
DISPNUM
    call 11563      ; We'll push the BC
                    ; register onto the
                    ; calculator stack
    call 11747      ; and then output that
                    ; number to the screen
                    ; by calling this routine
    ret
STRING 
    defb 83,127,111,114,101,$3a
EOS
    defb 0
  

Run it (with RANDOMIZE USR 32768) and see what happens. Basically, as the comments within the DISPNUM routine suggests, it's pushing the BC register onto the Sinclair calculator stack, then displaying that next to the last character printed. This allows whole integer numbers between zero and 65535, which once you run the code, you'll see its' intended usage. There are some drawbacks to this method, but for now it will be good enough.

Here's another routine to mull over.

    org 32768       ; This is where our
                    ; program begins in RAM
INFINITE            ; Our main marker
    ld bc,63486     ; Listen for keys
                    ; 1 to 5, also Sinclair
                    ; Joystick port 2
    in a,(c)        ; What key has been
                    ; pressed?
    rra             ; Rotates the accumulator
    push af         ; Preserves AF register
                    ; (most importantly the A bit)
    call nc,LEFT    ; If left is being pressed,
                    ; call relevant routine
    pop af          ; restore accumulator.
    rra             ; The process repeats to
                    ; see what other bits
                    ; have been set
    push af
    call nc,RIGHT 
    pop af
    rra   
    push af
    call nc,DOWN 
    pop af     
    rra        
    call nc,UP  
    jp INFINITE     ; Unconditional jump
                    ; back to repeat process
LEFT
    ld a,0
    out (254),a     ; Okay, so what happens here?
    ret
RIGHT
    ld a,2
    out (254),a     ; And here?
    ret
DOWN
    ld a,4
    out (254),a     ; etc...
    ret
UP
    ld a,6
    out (254),a
    ret
  

Well, you can guess from the comments we're doing here. If you have a Sinclair joystick interface, or an Amstrad-made Spectrum, connect a compatible joystick (which can be emulated too) and see what happens. Checking for the fire button is missing, but I didn't want to make it too easy for you. Well, that's all for now, but it need not end here as the Micro Mart forums Spectrum Computing Development Forums are always open. Before I go, I think there's just enough space to thank Bob Smith and Jonathan Cauldwell for their help over the series, as well as everyone who endured it!

3 comments:

  1. Sorry about the probems with Blogger in displaying the binary representation of the smiley face.

    ReplyDelete
  2. excelent man! it was very useful for me, thanks a lot!

    ReplyDelete
    Replies
    1. Glad it was useful to you. I should think about something a bit more advanced in Z80 soon.

      Delete