├── .github └── FUNDING.yml ├── .gitignore ├── Makefile ├── README.md ├── number-output.z80 ├── ram-increment.z80 ├── simple-monitor.z80 └── string-output.z80 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: skx 3 | custom: https://steve.fi/donate/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bin 2 | all 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Compile all files by default 4 | # 5 | default: $(patsubst %.z80,%.bin,$(wildcard *.z80)) 6 | touch all 7 | 8 | # 9 | # Cleanup 10 | # 11 | clean: 12 | rm -f *.bin all 13 | 14 | # 15 | # Rule for compiling a single file 16 | # 17 | %.bin: %.z80 18 | z80asm -o $@ $< 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Z80 Examples 2 | 3 | This repository contains example code for the Z80 processor, written in assembly language. This is where I'll post things as I experiment. 4 | 5 | The code should be standalone, but I assume that there is the ability to output a single byte to the serial-console, or STDOUT via: 6 | 7 | ld a, '3' 8 | out (1), a 9 | 10 | Similarly I assume reading a single character from a serial-console, or STDIN, is possible via: 11 | 12 | in a, (1) 13 | 14 | This is true of the [Z80 emulator](https://github.com/skx/z80emulater/) I'm using, as well as the [physical machine I intend to build](https://blog.steve.fi/tags/z80/). 15 | 16 | 17 | ## 00. See Also 18 | 19 | If you'd rather see something _complete_ then you should check out this repository: 20 | 21 | * [The lighthouse of doom](https://github.com/skx/lighthouse-of-doom) 22 | 23 | That contains a complete (simple!) text-based adventure-game written in Z80 assembly, which will work on any ZX Spectrum emulator, and can also run on CP/M systems (again under emulation if you don't have any retro-hardware). 24 | 25 | 26 | ## 01. RAM Increment 27 | 28 | The simplest example is the first, which increments a single byte of RAM endlessly. If you have an emulator that lets you dump RAM after every instruction, or physical hardware upon which you can do the same this should prove your code is working: 29 | 30 | * [ram-increment.z80](ram-increment.z80) 31 | 32 | 33 | ## 02. String Output 34 | 35 | Outputing a string, held inline. Simple test of loops: 36 | 37 | * [string-output.z80](string-output.z80) 38 | 39 | 40 | ## 03. Number Output 41 | 42 | This example is similar to the previous one, but instead outputs the contents of the HL register-pair, as a four-digit hexadecimal number. 43 | 44 | i.e. If you assume this `ld hl, 0x123F`, then you should see the output "0x123F" generated, which proves a number a has been converted to an ASCII-string, and output correctly. 45 | 46 | * [number-output.z80](number-output.z80) 47 | 48 | 49 | ## 04. Simple Monitor 50 | 51 | This is the most complex/complete program in the repository, and it is a "monitor program" which allows you to interactively use your Z80 processor. There are a couple of builtin commands for dumping ram, inputing data, and calling functions. 52 | 53 | Each command is invoked via a letter. For example to (D)ump the 16 bytes of RAM at 0x1000 run: 54 | 55 | ``` 56 | > D1000 57 | 0x1000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 58 | > D 59 | 0x1010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60 | ``` 61 | 62 | To (I)nput a simple routine: 63 | 64 | ``` 65 | > I1000 3E 68 D3 01 3E 65 D3 01 3E 6C D3 01 3E 6C D3 01 66 | > I 3E 6F D3 01 3E 21 D3 01 3E 0A D3 01 C9 67 | ``` 68 | 69 | (Here you see that the input-address is optional in the second line, bytes are just appended to the value previously set.) 70 | 71 | Finally you may (c)all the routine you've just loaded: 72 | 73 | ``` 74 | > C1000 75 | hello! 76 | ``` 77 | 78 | Steve 79 | -- 80 | -------------------------------------------------------------------------------- /number-output.z80: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; This program demonstrates converting the number in HL to 3 | ;; ASCII and outputing it. 4 | ;; 5 | 6 | 7 | ; Set HL to 0xffff 8 | ld hl, 0 9 | dec hl 10 | 11 | ; Display the value 12 | call DispHLhex 13 | halt 14 | 15 | ; 16 | ; Display a 16- or 8-bit number in hex. 17 | ; The value to be shown should be stored in HL 18 | ; 19 | DispHLhex: 20 | 21 | ; show "0x" as a prefix 22 | ld a, '0' 23 | out(1),a 24 | ld a, 'x' 25 | out(1),a 26 | 27 | ; Show the high-value 28 | ld c,h 29 | call OutHex8 30 | 31 | ; Show the low-value 32 | ld c,l 33 | call OutHex8 34 | 35 | ; Return to our caller 36 | ret 37 | 38 | ; 39 | ; Output the hex value of the 8-bit number stored in C 40 | ; 41 | OutHex8: 42 | ld a,c 43 | rra 44 | rra 45 | rra 46 | rra 47 | call Conv 48 | ld a,c 49 | Conv: 50 | and $0F 51 | add a,$90 52 | daa 53 | adc a,$40 54 | daa 55 | ; Show the value. 56 | out (1),a 57 | ret 58 | -------------------------------------------------------------------------------- /ram-increment.z80: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; This program operates in an endless loop, and increments the 3 | ;; value stored at a particular address. 4 | ;; 5 | ;; Since there is no output it is a tricky-program to test, but 6 | ;; if you're single-stepping through code, and can dump RAM then 7 | ;; it is a nice standalone example program. 8 | ;; 9 | 10 | org 0 11 | 12 | ;; Set HL to the address we're going to modify. 13 | ld hl, output 14 | 15 | ;; A is zero. 16 | xor a,a 17 | 18 | loop: 19 | 20 | ;; Increment the value of A 21 | inc a 22 | 23 | ;; Store it in the address 24 | ld (hl), a 25 | 26 | ;; Repeat. Forever. 27 | jp loop 28 | 29 | output: 30 | db 00 31 | -------------------------------------------------------------------------------- /simple-monitor.z80: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; Simple monitor program. 3 | ;; 4 | ;; Accept strings from STDIN, and execute them. 5 | ;; 6 | ;; Built-in commands 7 | ;; 8 | ;; [c]all xxxx -> Call the routine at XXXX 9 | ;; 10 | ;; [d]ump -> Dump 16 bytes of RAM at a given address. 11 | ;; 12 | ;; [i]nput -> Enter bytes 13 | ;; 14 | ;; Other input will generate an "ERR" message, and be ignored. 15 | ;; 16 | 17 | org 0 18 | 19 | ;; 20 | ;; Ensure we have a stack-pointer setup, with some room. 21 | ;; 22 | ld sp, stack_start 23 | 24 | ;; 25 | ;; Entry-point to the monitor. 26 | ;; 27 | ;; Read text into into `input_buffer`, appending to the buffer until a newline 28 | ;; is seen, then invoke `process` to handle the input. 29 | ;; 30 | monitor: 31 | ;; show the prompt. 32 | ld a, '>' 33 | out (1),a 34 | 35 | 36 | ld hl, input_buffer 37 | monitor_input_loop: 38 | ;; overwrite an previous input 39 | ld (hl), '\n' 40 | 41 | ;; read and store the new character 42 | in a,(1) 43 | ld (hl),a 44 | 45 | ;; was it a newline? If so process. 46 | cp '\n' 47 | jr z, process_input_line 48 | 49 | ;; Otherwise loop round for more input. 50 | inc hl 51 | jr monitor_input_loop 52 | 53 | 54 | 55 | ;; 56 | ;; process_input_line is called when the monitor has received a complete 57 | ;; newline-terminated line of text. 58 | ;; 59 | ;; We process the contents by looking for commands we understand, if we see 60 | ;; input we don't recognize we show a message and return, otherwise we invoke 61 | ;; the appropriate handler. 62 | ;; 63 | ;; C => CALL 64 | ;; D => DUMP 65 | ;; I => INPUT 66 | ;; 67 | process_input_line: 68 | 69 | ld hl, input_buffer 70 | ld a, (hl) 71 | 72 | ;; C == CALL 73 | cp 'c' 74 | jr z, call_handler 75 | cp 'C' 76 | jr z, call_handler 77 | 78 | ;; D == DUMP 79 | cp 'd' 80 | jr z, dump_handler 81 | cp 'D' 82 | jr z, dump_handler 83 | 84 | ;; I == INPUT 85 | cp 'i' 86 | jr z, input_handler 87 | cp 'I' 88 | jr z, input_handler 89 | 90 | ;; 91 | ;; Unknown command: show a message and restart our monitor 92 | ;; 93 | ;; We just show "ERR" which is simple, and saves bytes compared to 94 | ;; outputting a longer message and using a print-string routine. 95 | ;; 96 | ld a, 'E' 97 | out (1),a 98 | ld a, 'R' 99 | out (1), a 100 | out (1), a 101 | ld a, '\n' 102 | out (1),a 103 | jr monitor 104 | 105 | 106 | 107 | 108 | ;; 109 | ;; Call is invoked with the address to call 110 | ;; 111 | ;; For example "C0003" will call the routine at 0x0003 112 | ;; 113 | call_handler: 114 | 115 | ;; Our input-buffer will start with [cC], so we start looking at the 116 | ;; next character. 117 | ld hl, input_buffer+1 118 | 119 | ;; Read the address to call into BC 120 | call read_16_bit_ascii_number 121 | 122 | ;; We'll be making a call, so we need to have the return 123 | ;; address on the stack so that when the call'd routine ends 124 | ;; execution goes somewhere sane. 125 | ;; 126 | ;; We'll want to re-load the monitor, so we'll store the 127 | ;; entry point on the stack 128 | ;; 129 | ld hl,monitor 130 | push hl 131 | 132 | ;; Now we jump, indirectly, to the address in the BC register. 133 | push bc 134 | ret 135 | 136 | 137 | 138 | ;; 139 | ;; Dump 16 bytes from the current dump_address 140 | ;; 141 | ;; We're called with either "D" to keep going where we left off or 142 | ;; "D1234" if we should start at the given offset. 143 | ;; 144 | dump_handler: 145 | 146 | ;; Our input-buffer will start with [dD], so we start looking at the 147 | ;; next character. 148 | ld hl, input_buffer+1 149 | 150 | ;; Look at the next input-byte. If empty then no address. 151 | ld a, (hl) 152 | cp '\n' 153 | jr z, dump_handler_no_number 154 | 155 | ;; OK we expect an (ASCII) address following HL - read it into BC. 156 | call read_16_bit_ascii_number 157 | ld (dump_address), bc 158 | 159 | dump_handler_no_number: 160 | ;; The address we start from 161 | ld hl, (dump_address) 162 | ;; show the address 163 | call output_16_bit_number 164 | 165 | ;; Loop to print the next 16 bytes at that address. 166 | ld b, 16 167 | dump_byte: 168 | ;; show a space 169 | ld a, ' ' 170 | out (1), a 171 | 172 | ;; show the memory-contents. 173 | ld c, (hl) 174 | call output_8_bit_number 175 | inc hl 176 | djnz dump_byte 177 | 178 | ;; all done 179 | ld a, '\n' 180 | out (1),a 181 | 182 | ;; store our updated/final address. 183 | ld (dump_address), hl 184 | jmp_monitor: 185 | jr monitor 186 | 187 | 188 | 189 | ;; 190 | ;; Input handler allows code to be assembled at a given address 191 | ;; 192 | ;; Usage is: 193 | ;; 194 | ;; I01234 01 02 03 04 0f 195 | ;; 196 | ;; i.e. "I
byte1 byte2 .. byteN" 197 | ;; 198 | ;; If there is no address keep going from the last time, which means this 199 | ;; works as you expect: 200 | ;; 201 | ;; I1000 01 03 202 | ;; I 03 04 0F 203 | ;; 204 | input_handler: 205 | ;; Our input-buffer will start with [iI], so we start looking at the 206 | ;; next character. 207 | ld hl, input_buffer+1 208 | 209 | ;; Look at the next input-byte. If it is a space then no address was 210 | ;; given, so we keep appending bytes to the address set previously. 211 | ld a, (hl) 212 | cp ' ' 213 | jr z, input_handler_no_address 214 | 215 | ;; OK we expect an (ASCII) address following HL - Read it into BC. 216 | call read_16_bit_ascii_number 217 | ld (input_address), bc 218 | 219 | input_handler_no_address: 220 | 221 | ;; HL contains the a string. Get the next byte 222 | ld a,(hl) 223 | inc hl 224 | 225 | ;; space? skip 226 | cp ' ' 227 | jr z, input_handler_no_address 228 | 229 | ;; newline? If so we're done 230 | cp '\n' 231 | jr z, jmp_monitor 232 | 233 | ;; OK then we have a two-digit number 234 | dec hl 235 | call read_8_bit_ascii_number 236 | 237 | ;; store the byte in RAM 238 | ld bc, (input_address) 239 | ld (bc), a 240 | 241 | ;; bump to the next address 242 | inc bc 243 | ld (input_address), bc 244 | 245 | ;; continue 246 | jr input_handler_no_address 247 | 248 | 249 | 250 | 251 | ;; 252 | ;; Convert a 4-digit ASCII number, pointed to by HL to a number. 253 | ;; Return that number in BC. 254 | ;; 255 | read_16_bit_ascii_number: 256 | ;; HL is a pointer to a four-char string 257 | ;; This is read as a 16 bit hex number 258 | ;; The number is stored in BC 259 | call read_8_bit_ascii_number 260 | ld b, a 261 | call read_8_bit_ascii_number 262 | ld c, a 263 | ret 264 | 265 | 266 | 267 | 268 | 269 | ;; 270 | ;; Read the two-digit HEX number from HL, and convert to an integer 271 | ;; stored in the A-register. 272 | ;; 273 | ;; HL will be incremented twice. 274 | ;; 275 | read_8_bit_ascii_number: 276 | ld a, (hl) 277 | ;; is it lower-case? If so upper-case it. 278 | cp 'a' 279 | jr c, read_8_bit_ascii_number_uc 280 | cp 'z' 281 | jr nc, read_8_bit_ascii_number_uc 282 | sub a, 32 283 | read_8_bit_ascii_number_uc: 284 | call read_8_bit_ascii_number_hex 285 | add a, a 286 | add a, a 287 | add a, a 288 | add a, a 289 | ld d, a 290 | inc hl 291 | ld a, (hl) 292 | 293 | ;; upper-case the second character too, if necessary. 294 | cp 'a' 295 | jr c, conty 296 | cp 'z' 297 | jr nc, conty 298 | sub a, 32 299 | conty: 300 | 301 | call read_8_bit_ascii_number_hex 302 | or d 303 | inc hl 304 | ret 305 | read_8_bit_ascii_number_hex: 306 | sub a, '0' 307 | cp 10 308 | ret c 309 | sub a,'A'-'0'-10 310 | ret 311 | 312 | 313 | ;; 314 | ;; Display the 16-bit number stored in HL in hex. 315 | ;; 316 | output_16_bit_number: 317 | 318 | ld c,h 319 | call output_8_bit_number 320 | ld c,l 321 | call output_8_bit_number 322 | ret 323 | 324 | ;; 325 | ;; Display the 8-bit number stored in C in hex. 326 | ;; 327 | output_8_bit_number: 328 | ld a,c 329 | rra 330 | rra 331 | rra 332 | rra 333 | call Conv 334 | ld a,c 335 | Conv: 336 | and $0F 337 | add a,$90 338 | daa 339 | adc a,$40 340 | daa 341 | out (1),a 342 | ret 343 | 344 | 345 | 346 | ;;;;;;;; 347 | ;;;;;;;; RAM stuff 348 | ;;;;;;;; 349 | 350 | ;; 351 | ;; Here we store some values. 352 | ;; 353 | 354 | ;; DUMP: We track of the address from which we're dumping. 355 | dump_address: 356 | db 0,0 357 | ;; INPUT: Keep track of the address to which we next write. 358 | input_address: 359 | db 0,0 360 | 361 | ;; We don't nest calls too deeply .. 362 | stack_end: 363 | db 0, 0 364 | db 0, 0 365 | db 0, 0 366 | db 0, 0 367 | db 0, 0 368 | db 0, 0 369 | db 0, 0 370 | db 0, 0 371 | db 0, 0 372 | db 0, 0 373 | stack_start: 374 | 375 | ;; Command-line input buffer. 376 | input_buffer: 377 | -------------------------------------------------------------------------------- /string-output.z80: -------------------------------------------------------------------------------- 1 | ; 2 | ; This is a simple program which is designed to print "inline" 3 | ; strings. 4 | ; 5 | ; Of course such a thing is a little crazy, as it really messes 6 | ; with disassemblers, but it's still a cute hack. 7 | ; 8 | org 0 9 | 10 | call print 11 | db "This is a test!\n$" 12 | 13 | call print 14 | db "As you can see we're printing INLINE strings!\n$" 15 | 16 | halt 17 | 18 | 19 | ; 20 | ; This routine is designed to be CALLed, when it is invoked 21 | ; the address of the next instruction will be placed on the 22 | ; stack, which means we can find the address of the string to 23 | ; print from there. 24 | ; 25 | ; We print each character until we find a '$' character, then 26 | ; jmp back to the location after that. 27 | ; 28 | print: 29 | ; 30 | ; The return address, i.e. the instruction after our call, will be 31 | ; on the stack. In our case we know that points to the string to be 32 | ; printed. 33 | ; 34 | print_loop: 35 | pop hl 36 | 37 | ; 38 | ; Load the character in the hl-register into A. 39 | ; 40 | ld a,(hl) 41 | 42 | ; 43 | ; Bump to the next. 44 | ; 45 | inc hl 46 | 47 | ; 48 | ; Since we've incremented the HL register we're either 49 | ; going to get the next character - or the address to 50 | ; which we should return. Push it onto the stack either way. 51 | ; 52 | push hl 53 | 54 | ; 55 | ; Is the character '$'? If so we return - because we've 56 | ; just stored the next address on the stack we'll go to 57 | ; the correct location. 58 | ; 59 | cp '$' 60 | 61 | ; 62 | ; Return if we're done. 63 | ; 64 | ret z 65 | 66 | ; 67 | ; Otherwise output the single character, and start again. 68 | ; 69 | out (1),a 70 | jp print_loop 71 | --------------------------------------------------------------------------------