Examples of assembly code for the ZX Spectrum

I will not do the day’s introduction. A hacker I know once said that 10 lines of code can be clearer and more interesting than 1000 words of explanation. All of these examples are written in assembly language for the Z80 architecture and run on a retro ZX Spectrum 48k computer.

Books, links, miscellaneous utilities and all that jazz

http://zxnext.narod.ru/manuals/Basic_Programming.pdf – BASIC book for Spectrum
http://www.retro8bitcomputers.co.uk/Content/downloads/books/SpectrumMachineLanguageForTheAbsoluteBeginner.pdf – and in assembler
http://zxpress.ru/book.php?id=18 – Publishing house Inforkom, Programming in machine codes and in assembly language
https://spectrumforeveryone.com/technical/zx-spectrum-pcb-schematics-layout/ – circuitry. What and where is connected
http://datasheets.chipdb.org/Mostek/3880.pdf http://www.zilog.com/docs/z80/um0080.pdf – datasheets for the z80 processor
https://clrhome.org/table/ – handy and practical table of available opcodes
https://www.chibiakumas.com/z80/ – self-instruction book

We display the text on the screen. The symbols themselves will be drawn using the code that is already in ROM memory.

  DEVICE ZXSPECTRUM48         ; даем компилятору знать, для какого компьютера мы пишем. Это влияет на экспорт .TAP файла
  org $8000                   ; пусть все наши байты попадут в память начиная с адреса 0x8000
  
  
  ld a, 2                     ; загружаем двойку в регистр A
  call $1601                  ; вызываем функцию, которая лежит где-то в прошивке (переключение потока печати)
  ld bc, string_end - string  ; просим компилятор вычесть одно из другого, это такой хитрый способ вычисления длины строки
  ld de, string               ; кладем адрес начала строки в регистровую пару de
  call $203c                  ; и вызываем что-то, что находится в прошивке (печать строки)
                              ; когда оно отработает, выполнение продолжится с этого же места
  
  ret                         ; программа закончилась, пора возвращаться в интерпретатор
  
string:
  db "zx spectrum rulez",$0d  ; строка текста будет лежать по этому адресу
string_end:
code_end:                     ; это просто метки, когда компилятор примется за дело, он расставит нужные адреса вместо меток

  SAVEBIN "noise.bin",$8000,code_end - $8000  ; встроенная в компилятор функция.
                                              ; можно экспортировать произвольный промежуток как бинарник

  EMPTYTAP "noise.tap"        ; а еще можно сохранить tap прямо из ассемблера, если ваш компилятор так умеет
  SAVETAP "noise.tap",CODE,"run32768",$8000,code_end - $8000,$8000

https://skoolkid.github.io/rom/index.html – a list of interesting addresses in the firmware, with comments.
https://skoolkid.github.io/rom/asm/1FFC.html – here is hidden $ 203c procedure.

Save the program as a text file noise.asm. Run the compiler:

wine sjasmplus.exe noise.asm --lst=noise.lst --sym=noise.sym

See listing:

And this is how the list of labels will look like. Now each label has got its own address:

code_end: EQU 0x00008021
string: EQU 0x0000800F
string_end: EQU 0x00008021

Turn on the spectrum emulator, insert the cassette with our program, issue the command LOAD "" CODE, turn on the cassette for playback. The program is sucked into memory at the address specified in the block header, that is, 32768 ($ 8000).

We launch what is in memory: RANDOMIZE USR 32768

Add spaces on the sides and color codes. Let the lettering be in the center.

  db $16,11,4,$11,$01,$10,$06,"   zx spectrum rulez   " ; немного поменяем эту строку

Bytes can be specified in hexadecimal ($ 00 – $ ff), or in decimal (0 – 255), or as a string literal.

The $ 16 byte is the command 'AT', and after it two more bytes must go, specifying the new coordinates of the cursor.

$ 10 bytes, $ 11 are commands 'INK' and 'PAPER', each of them must be followed by one byte specifying the color ($ 00 – $ 07).

It is necessary to add a basic bootloader so that the program can start itself after loading from a cassette.

The .tap format is described here: https://documentation.help/BASin/format_tape.html https://sinclair.wiki.zxnet.co.uk/wiki/TAP_format

Spectrum stores basic programs in a special format (https://habr.com/ru/post/103127/). And if you type the program on the Spectrum emulator, then make a memory dump, then you can find the place where all this is stored:

Then these bytes can be inserted directly into the ACM and asked sjasmplus to build us a tap file that contains the bootloader. This is a bit of a wacky method, but it works.

  org $4000 ; выбираем какой-нибудь адрес, и ничего, что он попадает в дисплей. Это временно
basic_loader:
  db $00,$0a,$0e,$00,$20,$fd,"32767",$0e,$00,$00,$ff,$7f,$00,$0d     ; 10 CLEAR 32767
  db $00,$14,$07,$00,$20,$ef,$22,$22,$20,$af,$0d                     ; 20 LOAD "" CODE
  db $00,$1e,$0f,$00,$20,$f9,$c0,"32768",$0e,$00,$00,$00,$80,$00,$0d ; 30 RANDOMIZE USR 32768
basic_loader_end:

  SAVEBIN "noise.bin",$8000,code_end - $8000
  EMPTYTAP "noise.tap"
  SAVETAP "noise.tap",BASIC,"loader",basic_loader,basic_loader_end - basic_loader, 10
  SAVETAP "noise.tap",CODE,"run32768",$8000,code_end - $8000,$8000

To load such a cassette, you need the command LOAD ""

It turns out that first the BASIC block header will be recorded on the cassette, then the BASIC program itself, which will immediately start itself (the line number from which the execution will start is stored in the header). And she will already download our code and run it.

Let’s add movement. Let the inscription crawl diagonally across the screen, change color and leave traces. We’ll also make a key to exit.

Long text and animated GIF
  DEVICE ZXSPECTRUM48
  org $8000
  
  ld a, 2
  call $1601                  ; select stream
  
  ld a, %00000001
  out ($fe), a                ; переключим цвет бордюра на синий
  
main_loop:

  halt ; после этой команды процессор ничего не может выполнять, программа виснет
  halt ; пока не придет прерывание (а оно срабатывает в начале кадра, 50 кадров в секунду)
  halt ; прерывания же включены, не так ли?
  halt ; наша строка будет делать один шаг каждые четыре кадра
  
  push bc                     ; у нас в регистре B хранится переменная. Надо ее сохранить в стеке
  ld bc, string_end - string
  ld de, string
  call $203c                  ; output string
  pop bc                      ; и не забыть вынуть ее обратно из стека
  
  ld hl, string + 1           ; Будем прямо на ходу менять байты в строке
  ld a, (hl)
  ld b, a                     ; сохраняем регистр A в регистр B, чтобы потом вернуть, если что
  inc a
  cp 22                       ; если цифра больше 21, то это уже много, нужно поставить ноль
  jr nz, vertical_check_ok
  xor a                       ; обнуляем регистр A
  
vertical_check_ok:
  ld (hl), a                  ; записываем получившийся байт обратно в строку
  inc hl
  
  ld a, (hl)
  or a                        ; сравниваем регистр A с самим собой. Если он нулевой, то прыгаем
  jr z, horisontal_need_to_jump
  dec a                       ; уменьшаем на единичку
  ld (hl), a
  jr horisontal_ok
  
horisontal_need_to_jump:
  ld a, 31                    ; ставим курсор на крайний правый символ
  ld (hl), a
  dec hl
  ld (hl), b                  ; тут мы снова лезем в вертикальный байт и меняем его обратно
  inc hl

horisontal_ok:
  
  inc hl
  inc hl
  ld a, (hl)                  ; меняем цвет фона
  inc a
  and %00000111               ; обнуляем биты, чтобы цифра была от 0 до 7
  ld (hl), a
  inc hl
  inc hl
  ld a, (hl)                  ; меняем цвет текста
  inc a
  and %00000111               ; то же самое
  ld (hl), a
  
  ld a, %01111111
  in a, ($fe)                 ; читаем клавиатуру
  and $00000001               ; если нажать пробел, то программа выходит обратно в бэйсик
  jr nz, main_loop            ; это такой цикл, он будет крутиться все время
  
  ret  ; return to basic
  
string:
  db $16,11,4,$11,$01,$10,$06,"   zx spectrum rulez   "
string_end:
code_end:

  org $4000
basic_loader:
  db $00,$0a,$0e,$00,$20,$fd,"32767",$0e,$00,$00,$ff,$7f,$00,$0d     ; 10 CLEAR 32767
  db $00,$14,$07,$00,$20,$ef,$22,$22,$20,$af,$0d                     ; 20 LOAD "" CODE
  db $00,$1e,$0f,$00,$20,$f9,$c0,"32768",$0e,$00,$00,$00,$80,$00,$0d ; 30 RANDOMIZE USR 32768
basic_loader_end:

  SAVEBIN "noise.bin",$8000,code_end - $8000
  EMPTYTAP "noise.tap"
  SAVETAP "noise.tap",BASIC,"loader",basic_loader,basic_loader_end - basic_loader, 10
  SAVETAP "noise.tap",CODE,"run32768",$8000,code_end - $8000,$8000

What is all this for? Who might need it? The fact is that assemblers have formed a tradition of holding demoscene festivals. The demoscene did not arise from scratch, it has its own traditions and its own history. And you are reading this article for a reason. After all, you urgently need to learn how to code on the Z80 in order to have time to send your first work to the competition. Right?

Ready-made works of other demoscenters are available for everyone (you can run it on the emulator yourself or watch the video on YouTube)

At the time of this writing (December 29, 2021), the nearest demoparty, first DI / HALT in Nizhny Novgorod, and then Chaos Constructions in St. Petersburg. And while there is still time, I want to have time to finish my long-term construction.

I recently switched to the Z80, I used to write firmware for microcontrollers on the AVR architecture (it all started after this article and this books). I still think that AVR is the most adequate assembler suitable for beginners. But the rules of the game are completely different there.

If you like, I can write some more code examples for sound, color graphics, ports, keyboard. They did not fit into this article.

If I have not mentioned your favorite book about the Spectrum, then give a link. I’m interested.

Similar Posts

Leave a Reply