1 Like
Tutorial - learn machine code in 30mins
Message from the moderators - bigjon has requested that readers' attention be drawn to a better version of this tutorial, one which is easier to read: it can be found at
http://chuntey.wordpress.com/2010/01/12/tutorial-zx-spectrum-machine-code-game-30-minutes/
Original message of this post follows:
I've written a short tutorial to get you started writing machine code.
Anyone fancy giving it a go and reporting back on your progress here?
http://chuntey.wordpress.com/2010/01/12/tutorial-zx-spectrum-machine-code-game-30-minutes/
Original message of this post follows:
I've written a short tutorial to get you started writing machine code.
Anyone fancy giving it a go and reporting back on your progress here?
ZX SPECTRUM MACHINE CODE GAME 30 MINUTE TUTORIAL
By bigjon (Jon Kingsman)
Roadrace game by bigjon, incorporating suggestions from Dr Beep, skoolkid and Matt B
Hi folks, I'm a machine code novice who coded a very small roadrace game to help me learn.
I reckon you can learn the basics of machine code in half an hour by coding this game step by step.
This tutorial assumes you have a working knowledge of ZX Spectrum basic, and the ZX Spin emulator.
Make yourself a large cup of tea - by the time you've drunk it, you be able to program in machine code!
CHAPTER 1 - Create a machine code function that returns the score to BASIC
Machine code programs are a series of bytes in the Spectrum's memory.
In this chapter we will
- use the Spin assembler to write a few bytes into the memory generating a score for our game.
- write a BASIC program to run the machine code and print the score we have obtained from it.
Open ZX Spin. Select Tools -> Z80 Assembler.
To run our roadrace game, we need to execute the following steps
MAIN ;label for main section of program as opposed to graphics data etc
org 33000
;org 33000 = arrange to put our machine code at free, fast-running (over 32768) and memorable address in RAM
;initialise score
;initialise road, car
PRINCIPALLOOP ;label for the loop in the game that will execute over and over
;read keyboard
;set new carposition
;crash? if so, go to GAMEOVER.
;print car
;scroll road
;random road left or right
;jump back to PRINCIPALLOOP
GAMEOVER ; label for the cleaning up that needs to be done before returning to BASIC
;return score to BASIC
Copy and paste the paragraph above into the Spin Assembler.It will appear as 15 lines of mainly grey text.
The text is grey because text after a ; is a comment. The assembler ignores it but it's there for our benefit.
You can TAB comments over towards the right-hand side of the assembler page to make your code more readable.
The labels are in pink.The assembler won't put anything in RAM for them but will use them as entry points to jump to.
In the assembler, do File -> Save as and type something like mc30mintut.asm into the save box.
We'll do the first and last of these steps in this chapter, starting with the last one.
The assembly language instruction for 'return to calling program' (in our case a BASIC routine) is 'ret'.
Click on the end of line 15, press enter to create line 16 and type ret
The word 'ret' appears in blue. This is Spin's colour code for an instruction.
When the Spin assembler gets to the instruction 'ret' it writes the byte 201 into the memory at an address we choose.
The computer knows that the first byte it meets will be an instruction byte.
It does something different for each byte from 0 to 256. There's a list in Appendix A of the Spectrum Manual.
Some instruction bytes, like 201 for 'ret', are complete as is - no further info is needed to complete the action.
Some instruction bytes need one or two bytes of data afterwards for the computer to know what to do.
Some instruction bytes need a further instruction byte to clarify the action required.
Now we'll initialise the score, ready for the mc program to report it back to BASIC at the end of the game.
The computer does most of its work using 7 temporary byte-sized addresses called 'registers'.
The first register deals with single bytes only (numbers 0 to 255), the other six are in pairs to deal with 0 to 65355.
The first (single-byte) register is called the A register, sometimes also referred to as the accumulator.
The other three register pairs are called BC, DE, and HL (H is for High byte, L is for Low byte)
Any machine code function called from basic will return the value from 0 to 65355 in the BC register.
We will write the value 0 into the BC register, ready to increase it by 1 each time the game goes round its principal loop.
At the beginning of line 4, type 'ld bc,0'. ld is the instruction for load a value into a register or register pair.
The instruction byte for ld is different for each register or register pair that is loaded.
The instruction byte for ld bc is 1. The computer then expects 2 data bytes to give it a number from 0 to 65355.
In our case the two data bytes will be 0,0. So the assembler will write 1,0,0,201 at address 33000 in RAM
We'll assemble this code now. Do File -> Save, then File -> Assemble. Type 33000 into the Start Address box and click OK.
At the bottom window of the assembler you should see a report that says "No errors in 16 lines. 4 bytes generated"
You can see the four bytes are now at memory address 33000 by clicking in the main Spin display window on Tools -> Debugger
To run these four bytes of machine code, enter this one-line program in the main Spin display window:
10 PRINT AT 0,0; "Your score was "; USR 33000
Now RUN the program. Did you get "Your score was 0"? Congratulations - you have coded your first machine code program!
Do File -> Save in the main Spin display window and save as something like mc30mintut.sna. Here ends Chapter 1!
CHAPTER 2 - Display material on the screen.
There are two areas of the Spectrum's memory which have a direct effect on the screen display.
The complicated way is the Display file, from addresses 16384 to 22527, which stores a dash of 8 pixels per byte.
Try POKE-ing 255 into bytes within this range to see the funny order in which this memory area is mapped onto the screen.
The simple way is the Attribute file, from 22528 to 23296, which affects an 8x8 pixel block per byte, in logical order.
In this chapter we will
- draw our 'car' by changing the paper colour of one character square to blue.
- draw our 'road' by using a loop to create two vertical stripes of black paper colour down the screen.
In the spin assembler line 5, delete the word 'road' in the comments.
At the beginning of line 5, type ld hl,23278. This points HL to the middle of the bottom row in the display file.
Insert line 6, ld a,8. This puts a blue PAPER colour into the A register. Why 8? See the BASIC manual chapter 16.
Insert line 7, ld (hl),a. The brackets round hl mean the load will be to the address in RAM that hl is pointing to.
Insert line 8, ld (32900),hl ;save car posn. We'll store the attribute file address of the 'car' in some free bytes in RAM.
Now for the road. Insert line 4, ld hl,22537 ;initialise road. This points to a third of the way along the top line.
To save the road position, which we'll need frequently, we'll let the computer choose where to store it, on its 'stack'
Chapter 24 of the manual has a diagram showing where the machine stack is in the RAM.
To write to the stack we use 'push'. To write from the stack we use 'pop'. What goes on the stack first will come off last.
Insert line 5, push hl ;save road posn. Insert line 21, pop hl ;empty stack
To print a black road we need 0 in the accumulator (ch16 of the BASIC manual).
We could do 'ld a, 0' but this takes 2 bytes whereas 'xor a' takes only one. Insert line 6, 'xor a'
xor compares the chosen register to the A register and puts a 1 in the A register for each bit that is different.
We'll print the top line of the road. Two double squares of black with a 9-square gap between them.
Insert line 7, then copy and paste the following code:
ld (hl),a
inc hl ;inc increases the register by one, dec decreases it by one.
ld (hl),a
ld de,9 ;for a 9-space gap in the road.
add hl,de ;add adds the registers together, so hl points to the right hand side of the road.
ld (hl),a
inc hl
ld (hl),a
To get hl to point to the left hand verge on the next line, we need to move 21 bytes further in the attribute file.
Insert line 15, 'ld de, 21 ;point to left verge on next line'
Insert line 16, 'add hl,de'
To fill the screen with the road we will use machine code's equivalent of a FOR-NEXT loop, djnz.
djnz stands for Decrement then Jump if Not Zero. We load the b register with the number of times we want to loop.
Insert line 7, 'ld b,24 ;print road verge on 24 lines'
Insert line 8 'fillscreen' - this is the label for our loop to jump back to.
Insert line 19 'djnz fillscreen'
Because our routine will continue from the loop when b=0, we no longer need to initialise b as well as c to 0 in the next line.
Change line 20 'ld bc, 0' to 'ld c,b'. This is one byte shorter.
Assemble and save. If you want to see the blue 'car', you'll need to add something like 20 PAUSE 0 to your basic program.
CHAPTER 3 - move the car, test for collision.
Time to start playing the game! First we need to erase the car ready to move it if the player wants to.
Insert line 26, then copy and paste the following code
ld hl,(32900) ;retrieve car posn
ld a,56 ;erase car
ld (hl),a
Before we read the keyboard we will lock the keyboard for most of the game, and unlock it only when we want to read the keys.
The instruction to lock the keyboard is 'di' = 'disable interrupts. Its opposite is 'ei' = 'enable interrupts'
Replace line 3 with 'di'. Insert line 29, ei. Insert line 31, di. Insert line 41, ei
To read the keys we use the IN ports - see ch23 of the BASIC manual - to read the left and right half of the bottom row.
We load bc with the port number and use the instruction cp (= compare) to see if the number has dropped to show a keypress
Delete line30 and replace with the following code
ld bc,65278 ;read keyboard caps to v
in a,(c)
cp 191
jr nz, moveright
inc l
moveright
ld bc,32766 ;read keyboard space to b
in a,(c)
cp 191
jr nz, dontmove
dec l
dontmove
'jr nz' stands for jump relative if not zero. It skips over the instruction to increment / decrement the car position.
Replace line 43 with the following to see if we bump into the oncoming road 32 bytes (1 screen) down the attribute file.
ld (32900),hl ;store car posn
ld de, 32 ;new carposn
xor a ;set carry flag to 0
sbc hl,de
ld a,(hl) ;crash?
or a
jr z,gameover
ld a,8 ;print car
ld (hl),a
We'd like to 'sub hl,de' but there's no such instruction so we use sbc, subtract with carry, and set the carry flag to zero.
'or' compares the register to the a register bit by bit and leaves a 1 in the a register for each bit that is 1 in either.
If all the digits are zero, then the zero flag will be set, so we can use 'or a' to test for a black paper colour.
Delete line 53. Delete line 53 again!
To clean up the score at GAMEOVER insert line21, 'push bc; save score'. Replace line 57 with 'pop bc;retrieve score'
To cycle round the game before GAMEOVER change line 55 to 'jp PRINCIPALLOOP'.
Assemble, save, and run. You'll need to deliberately crash to get out!
CHAPTER 4 - scroll and move the road, keep score, adjust speed.
To scroll the road down the screen we copy the screen attribute bytes to the line beneath 736 times.
We use the instruction lddr, which stand for LoaD ((hl) to (de)),Decrement (hl and de) and Repeat (until bc is zero).
Replace line 53 with the following
ld hl,23263 ;scroll road
ld de,23295
ld bc,736
lddr
pop bc ;retrieve score
To add 1 to the score and save it ready for GAMEOVER, insert the following into line 59
inc bc ;add 1 to score
push bc ;save score
To move the road randomly left or right on the top line we use the following algorithm -
Choose a location in ROM where the are 256 random looking bytes and add the low byte of the score in bc to it.
If it is odd, lower the road position in hl by one. If it is even, increase by one.
(To test the last bit for odd and even we use 'and 1' which "masks" the last bit and sets the zero flag if it is 0)
Check to see if the road has reached the edge of the screen and bump it away if it has.
Print the new road top line like we did in chapter 2.
Replace line 58 with the following hefty chunk of code
pop hl ;retrieve road posn
push hl ;save road posn
ld a,56 ;delete old road
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a
;random road left or right
ld hl,14000 ;source of random bytes in ROM
ld d,0
ld e,c
add hl, de
ld a,(hl)
pop hl ;retrieve road posn
dec hl ;move road posn 1 left
and 1
jr z, roadleft
inc hl
inc hl
roadleft
ld a,l ;check left
cp 255
jr nz, checkright
inc hl
inc hl
checkright
ld a,l
cp 21
jr nz, newroadposn
dec hl
dec hl
newroadposn
push hl ;save road posn
xor a ;print new road
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a
The last thing we need to do to have a playable game is slow down our blindingly fast machine code.
Insert the following into line 106 (as extension material, you could adjust the figure in bc with a keypress to 'brake')
;wait routine
ld bc,$1fff ;max waiting time
wait
dec bc
ld a,b
or c
jr nz, wait
Save, assemble, and run - and that's it! Has your tea gone cold yet?
A full listing follows, with my email address at the end for your comments and suggestions.
main
org 33000
di
ld hl, 22537 ;initialise road
push hl ;save road posn
xor a
ld b,24
fillscreen
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a
ld de,21
add hl,de
djnz fillscreen
ld c,b ;initialise score
push bc ;save score
ld hl,23278 ;initialise car
ld a,8
ld (hl),a
ld (32900),hl ;save car posn
principalloop
ld hl,(32900) ;retrieve car posn
ld a,56 ;erase car
ld (hl),a
ei
ld bc,65278 ;read keyboard caps to v
in a,(c)
cp 191
jr nz, moveright
inc l
moveright
ld bc,32766 ;read keyboard space to b
in a,(c)
cp 191
jr nz, dontmove
dec l
dontmove
di
ld (32900),hl ;store car posn
ld de, 32 ;new carposn
xor a ;set carry flag to 0
sbc hl,de
ld a,(hl) ;crash?
or a
jr z,gameover
ld a,8 ;print car
ld (hl),a
ld hl,23263 ;scroll road
ld de,23295
ld bc,736
lddr
pop bc ;retrieve score
pop hl ;retrieve road posn
push hl ;save road posn
ld a,56 ;delete old road
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a
;random road left or right
ld hl,14000 ;source of random bytes in ROM
ld d,0
ld e,c
add hl, de
ld a,(hl)
pop hl ;retrieve road posn
dec hl ;move road posn 1 left
and 1
jr z, roadleft
inc hl
inc hl
roadleft
ld a,l ;check left
cp 255
jr nz, checkright
inc hl
inc hl
checkright
ld a,l
cp 21
jr nz, newroadposn
dec hl
dec hl
newroadposn
push hl ;save road posn
xor a ;print new road
ld (hl),a
inc hl
ld (hl),a
ld de,9
add hl,de
ld (hl),a
inc hl
ld (hl),a
inc bc ;add 1 to score
push bc ;save score
;wait routine
ld bc,$1fff ;max waiting time
wait
dec bc
ld a,b
or c
jr nz, wait
jp principalloop
gameover
pop bc ;retrieve score
pop hl ;empty stack
ei
ret; game and tutorial written by Jon Kingsman ('bigjon', 'bj'). electronic mail tesco.net - atsign - jon.kingsman (reversed)
Post edited by bigjon on
Comments
Just a couple of comments:
All the z80 registers are 8 bits: A, F (flags), B, C, D, E, H, L and, if we delve into the (sort of) undocumented, IXL, IXH, IYL, IYH, The z80 architecture allows these 8 bit registers to be paired up to act as 16 bit registers: AF, BC, DE, HL, IX, IY. This list excludes the specialized PC (program counter, the 16 bit register that holds the address of the next instruction), SP (the 16 bit register pointing at the address of the stack) and the second set of registers AFBCDEHL' available after an EXX or EX AF,AF'.
Secondly, the keyboard cannot be locked. It's a simple hardware peripheral that is always accessible via an i/o port. DI disables maskable interrupts and EI enables them. What happens in the spectrum is that the ULA generates an interrupt at the start of each video frame (ie 50 times per second). When maskable interrupts are enabled (EI), the z80 will stop what it is doing and respond by calling an interrupt service routine. Afterward it will resume where it left off. By disabling interrupts with DI you are preventing the ULA interrrupt from doing anything. ZX Basic arranges to run BASIC's interrupt routine so by default that is what will happen when your m/c runs with interrupts enabled. BASIC's ISR does things like scan the keyboard and update frames which is more or less useless from an m/c program's point of view.
Write games in C using Z88DK and SP1
It would be far most readable and easy to understand (at least for me) if it had some colors, bold fonts, spaces between paragraphs, maybe some pictures. So I would suggest putting in into document that allows such formatting (e.g. into Word).
Then I could evaluate the content :)
Also I couldn't follow what was going on in chapter 1 - perhaps chapter 1 would be better setting out the aim of the tutorial and how your going to acheive the aim by describing the game and it's coding sections, chapter 2 would then set out the development environment ( spin, basin, whatever ) and subsequent chapters leading through the code sections defined in chapter 1.
Still, nice to see someone putting their experiences on the record for others to learn from. :)
Nice try this tutorial, BTW.
You're answer suggests that I can ditch the EI before the keyboard read and the DI after it in my listing. I haven't got time to try it now but I will later.
However, I must agree that it quite hard to read at the moment. I will have to save it out to some other documant and re-format it, perhaps just by inserting some more line-breaks.
You can think of the z80 as having two 64k address spaces -- one where memory lies and one where peripherals (typically) lie. The only difference external to the z80 is that the mreq pin physically goes low when an address is in the memory space and the iorq pin physically goes low when an address is in i/o space. Memory watches the mreq pin to know it should respond and peripherals watch the iorq pin. z80 instructions that refer to memory cause the mreq pin to go low (eg, "LD HL,(16384)" -- read 16 bit value from addresses 16485/16385 ; "LD A,(HL)" -- read 8 bits from the memory address held in the HL register pair). z80 instructions that refer to i/o cause the iorq pin to go low (eg, "IN A,(C)" -- read 8 bits from the 16-bit i/o address held in BC). In the first sentence I said i/o space is "where peripherals typically lie". This is because peripherals can be wired to respond to memory addresses. Then they are known as memory-mapped peripherals. The reason one may want to do this is because the z80 instruction set is much, much richer in accessing memory than i/o. It is also faster to access memoiy than i/o. In the spectruim the ULA's display file can be thought of as a memory mapped display device. THe ULA actually owns the 16k RAM beginning at address 16384 and blocks the z80 from accessing it while it needs to access it (known as contention). Having the display file memory mapped allows the z80's full range of memory instructions to be used to manipulate display data. Consider an alternative found in the MSX machines -- they have a dedicated TMS99xx (?) VDU to generate the display that comes with sprite hardware. The drawback is that the full video memory owned by the VDU is not mapped into the z80's address space and writes to the VDU's display memory involves contorted and slow i/o instructions. This made the MSX fairly crap at scrolling, an operation involving a full update of the display data.
Some cpus such as the 65xx and 68xx have no separate i/o space. This means peripherals are always memory-mapped, the downside of which is that you never have access to a full 64k of memory since some of it must contain peripheral registers.
Sorry for getting a bit distracted there. I think your confusion comes from the fact that the BASIC system reads the keyboard in the interrupt service routine. If you disbale interrupts, the interrupt routine doesn't run and BASIC will not see any key presses. This does not mean you cannot read the state of the keyboard hardware directly. You can do that at any time.
Write games in C using Z88DK and SP1
I code 6502, not z80 so get an idea of some of it, just well thru most of the code you use decimal, I'd get people used to using Hex and also some small bits (address's) relate to a hex value.
Most people fall into one of the following camps >
* complete novice , don't know about hex, interupts, the op's / syntax etc. but maybe the next "mr smith" in the making..
* know basic , wanna progress further and gain the obvious speed advantage
* learned on another machine and want to see how it is different.
I do think showing people interupts is a bit too soon, getting them used to some of the op codes etc would be good.. and things like the stack.
even things like , when folks were in wh smith.. and they used to do "<insert name> .. is cool" print / goto program.. using that as a starting point and doing an asm version also..
although I'm sure there are books here folks would recommend to people starting out...
I only can comment on the c64 one, the definative programmers guide by Rae West if anyone ever wants to learn c64 stuff (ok.. don't send the lynch mob around :D)
things like the memory map etc help also, just a few thoughts anyway. I just think it's a bit well much for a noob to it. Everyone had to start of with the straight forward things first. yer it's boring ,but it's a good foundation.
I mean I intially started with the expert / action replay monitor on the c64...where there wasn't any helpers....e.g. you couldn't use labels etc you had to use specifics.. in a way that helps you with things though. It forces you to use precise things.. and even the fact that you can put in hex numbers instead of / also a command..e.g. 8d 20 d0 (would STA $D020) or is it 9d.. i forget. People might get into bad habits.
you yourself said you had read a boring book , BUT it was the foundations really. I have to think of the mindset of whats the equivelent c64 command I think,might make things easier
How does it work with looking at others code on the speccy though, with the c64 you'd just hit the action replay/expert button and you could get a good idea of the mechanics of how to do effects or what they were manipulating to do stuff.
the screen thing "io" on action replay was handy.. it'd show you the screen mode.. if sprites were turned on/off etc..Is there anything like that on speccy?
I'll have a look at that book though, funny his surname is Sinclair :) thanks
http://chuntey.wordpress.com/, third tutorial down at the time of writing.
Tutorials should really be written by people who have 2 qualities:
- Have a thorough understanding of the subject matter (=not in the case of a newbie).
- Know how to write in a manner that's accessible to newbies. This is a quality that doesn't depend on knowledge... a writer has it, or not.
That would help, but again: if above doesn't apply then there's probably X number of books that are better @ explaining things to newbies than your tutorial. Not to discourage your efforts, but really: your time might be better spent deepening your understanding of Z80 assembly (practice!), than trying to explain your limited knowledge to 'also-newbies'. Z80 assembly isn't hard to learn, and there's already plenty of good books out there. All it takes is time/perseverance, and perhaps a certain mindset....If that's true then thanks from me too, as I've already [strike]nicked[/strike] borrowed R-Tape's redefine key routine as I've been learning... :D
I only started learning it this year, and from a (personal) learner's point of view, I reckon the key motivation comes from having a clear game in mind that you want to create. Then taking it step by step - designing a sprite, printing it on screen, getting it moving etc.
I've found a lot of guides very useful (e.g. the JC document), but as soon as I see a game listing, or a routine I don't instantly want to use, I'm not particularly interested.
*********************************************************************************************
Message from the moderators - bigjon has requested that readers' attention be drawn to a better version of this tutorial, one which is easier to read: it can be found at
http://chuntey.wordpress.com/2010/01/12/tutorial-zx-spectrum-machine-code-game-30-minutes/
*********************************************************************************************
Many thanks!
thanks, much appreciated!
I was wondering, though, if you would consider expanding it to explain how certain things work exactly, like the collision detection, and what do the IN and CP instructions do.
Thanks in advance :)
Unfortunately it was edited out from the copy on WoS (the editor was a fool, eh Lee? :lol: )
Actually, if anyone could just point me to the magazines which ran an assembly tutorial, that'd be great. I really don't want to pour through the entire archive, checking each title individually.
For people in the first to second categories (though preferably with a decent knowledge of Sinclair Basic) - I did a ZX Basic Compiler tutorial some while ago when writing up a lil pac man game.
Several tutorials at http://www.boriel.com/wiki/en/index.php/ZX_BASIC:Tutorials - and in particular, the one I wrote at http://goo.gl/4jPd5
Since the whole game was completed (even if the tutorial wasn't quite) - you have a finished game and source code to look at [ http://www.worldofspectrum.org/infoseekid.cgi?id=0027713 ] after working through the whole tutorial. Hopefully I documented it enough :)
Why is this relevant? I maintain that moving from Sinclair basic to compiled basic - with a compiler that allows you to inline machine code (which I demonstrate in that tutorial) is a great way to step to M/C - start with routines, and replace speed critical code with an optimized routine. Then, if you want to, you can go machine code all the way if you want! But while learning, it lets you wrap the code in some basic much more easily to run it and get it started...
I am using the Zeus Assembler to build this example on a real speccy and I am getting an error 0 - "illegal character or incomplete statement" at the line
ld bc,$1fff; max waiting time
I am new to MC and also the Zeus assembler but thought I would ask to see if there was an incompatibility issue.
Thanks for the great effort applied here, I am definitely someone who learns by example.
Bytes:Chuntey - Spectrum tech blog.