Pull to refresh

Примеры ассемблерного кода для ZX Spectrum

Reading time6 min
Views22K

Я не буду делать длинное вступление. Один знакомый хакер однажды сказал, что 10 строк кода могут быть понятнее и интереснее, чем 1000 слов объяснений. Все эти примеры написаны на ассемблере для архитектуры Z80 и запускаются на ретро-компьютере ZX Spectrum 48k.

Книги, ссылки, разные полезности и все такое прочее

http://zxnext.narod.ru/manuals/Basic_Programming.pdf - книга по бейсику для спектрума
http://www.retro8bitcomputers.co.uk/Content/downloads/books/SpectrumMachineLanguageForTheAbsoluteBeginner.pdf - и по ассемблеру
http://zxpress.ru/book.php?id=18 - Издательство Инфорком, Программирование в машинных кодах и на языке ассемблера
https://spectrumforeveryone.com/technical/zx-spectrum-pcb-schematics-layout/ - схемотехника. Что и куда подключается
http://datasheets.chipdb.org/Mostek/3880.pdf http://www.zilog.com/docs/z80/um0080.pdf - даташиты на процессор z80
https://clrhome.org/table/ - удобная и практичная таблица доступных опкодов
https://www.chibiakumas.com/z80/ - самоучитель

Выводим текст на экран. Сами символы будут рисоваться с помощью кода, который уже есть в ROM памяти.

  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 - список интересных адресов в прошивке, с комментариями.
https://skoolkid.github.io/rom/asm/1FFC.html - вот тут спрятана $203c процедура.

Сохраняем программу как текстовый файл noise.asm. Запускаем компилятор:

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

Смотрим листинг:

А вот так выгдядит список меток. Теперь каждая метка получила свой адрес:

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

Включаем эмулятор спектрума, вставляем кассету с нашей программой, отдаем команду LOAD "" CODE, включаем кассету на воспроизведение. Программа засасывается в память по адресу, который указан в заголовке блока, то есть, 32768 ($8000).

Запускаем то, что лежит в памяти: RANDOMIZE USR 32768

Добавим пробелы по сторонам и цветовые коды. Пусть надпись будет в центре.

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

Байты можно указывать в 16-ричном виде ($00 - $ff), а можно в десятичном (0 - 255), а можно в виде строкового литерала.

Байт $16 - это команда 'AT', и после нее должны идти еще два байта, задающие новые координаты курсора.

Байты $10, $11 - это команды 'INK' и 'PAPER', после каждой из них должен идти один байт, задающий цвет ($00 - $07).

Надо добавить бейсиковый загрузчик, чтобы программа могла сама запускаться после загрузки с кассеты.

Формат .tap описан здесь: https://documentation.help/BASin/format_tape.html https://sinclair.wiki.zxnet.co.uk/wiki/TAP_format

Спектрум хранит бейсиковые программы в особом формате (https://habr.com/ru/post/103127/). И если набрать программу на эмуляторе спектрума, потом сделать дамп памяти, то можно найти то место, где все это хранится:

Затем эти байты можно вставить прямо в асм и попросить sjasmplus собрать нам tap файл, который содержит загрузчик. Это немного дурацкий метод, но он работает.

  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

Чтобы загрузить такую кассету, нужна команда LOAD ""

Получается, на кассете будет записан сначала заголовок BASIC блока, потом сама программа на бейсике, которая сразу же самозапустится (номер строки, с которой начнется выполнение, хранится в заголовке). А она уже загрузит наш код и запустит его.

Добавим движения. Пусть надпись будет ползать по экрану по диагонали, менять цвет и оставлять следы. Еще сделаем клавишу для выхода.

Длиннотекст и анимированная гифка
  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

К чему это все? Кому это может понадобиться? Дело в том, что у ассемблерщиков сформировалась традиция проводить фестивали демосцены. Демосцена возникла не на пустом месте, у нее есть свои традиции и своя история. И вы читаете эту статью не случайно. Вам ведь надо срочно научиться кодить на Z80, чтобы успеть отправить свою первую работу на competition. Верно?

Готовые работы других демосценеров доступны для всех желающих (можно запустить на эмуляторе самому или посмотреть видео в YouTube)

На момент написания статьи (29 декабря 2021) ближайшие demoparty сначала DI/HALT в Нижнем Новгороде, а потом Chaos Constructions в Петербурге (upd: перенесен на Август). И, пока еще есть время, я хочу успеть дописать свой долгострой.

Я пересел на Z80 недавно, раньше я писал прошивки для микроконтроллеров на AVR архитектуре (все началось после этой статьи и этой книжки). Я и сейчас думаю, что AVR - самый адекватный ассемблер, подходящий для новичков. Но правила игры там совсем другие.

Если хотите, я могу написать еще примеры кода для звука, цветной графики, портов, клавиатуры. В эту статью они не поместились.

Если я не упомянул вашу любимую книжку про спектрум, то дайте ссылку. Мне интересно.

Tags:
Hubs:
Total votes 48: ↑48 and ↓0+48
Comments29

Articles