Я не буду делать днинное вступление. Один знакомый хакер однажды сказал, что 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 в Петербурге. И, пока еще есть время, я хочу успеть дописать свой долгострой.

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

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

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

Комментарии (15)


  1. fearan
    30.12.2021 04:23
    +12

    Наркомания, но забавная.

    Как на 35 лет назад вернулся.


  1. axe_chita
    30.12.2021 08:11
    +4

    Статья неплохая, но кое что в ней немножко режет глаз.

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

    В русскоязычных источниках, транслятор ассемблера обычно просто называют ассемблером. Но в данном случае, это кросс-ассемблер.
    wine sjasmplus.exe noise.asm --lst=noise.lst --sym=noise.sym

    Имеет смысл уточнить что запуск кросс-ассемблера sjasmplus, работающего под windows осуществляется из под linux, используя Wine.
    skoolkid.github.io/rom/index.html — список интересных адресов в прошивке, с комментариями.

    В отношение Спектрума обычно используется не выражение адреса в «прошивке», а говорят про функции/подпрограммы в ROM или BASIC.
    Включаем эмулятор спектрума, вставляем кассету с нашей программой

    Тут тоже стоит уточнить какой версией эмулятора вы пользуетесь, там могут быть нюансы в работе с ним.

    Не сочтите все сказанное мной за ворчание, это было просто уточнение и мысли в слух.

    Советую обратить внимание на 8bitworkshop где вы можете работать не только с ZX Spectrum, но и другой любимой old-school восьмибитной системой, и даже сможете поработать с Verilog-ом. Причем для работы вам будет достаточно браузера.

    Пишите еще;)


    1. tvoybro
      30.12.2021 09:53
      +1

      8bitworkshop тема для быстрого старта. Если привыкнуть к средствам отладки то вообще шикарный инструмент. Можно выгружать скомпилированные бинари и дебажить в любимых Mame, Mesen, Unreal (который UnrealSpeccy). Кстати заодно похвастаюсь, там в разделе Projects есть одна мини-игра которую кодировал не так давно, доступная для редактирования и компиляции в данной веб-среде.

      Видел также поддержку для кодирования спектрумов в Visual Studio, тоже впечатлило.


  1. Javian
    30.12.2021 08:25
    +1

    За давностью лет уже деталей не помню, но меня одно время увлекало работать с изображением на экране командами сдвигов битов. Какие-то интересные узоры генерировались.


    1. Javian
      30.12.2021 20:37
      +2

      Нашел старый блокнотик, где я записал какой-то код для ZX. Посчитал - ужаснулся. Порядка 25 лет назад.


      1. usa_habro_user
        30.12.2021 21:46
        +1

        25 лет назад (то бишь в 1997 году) ZX Spectrum уже был "стариной". Может, 35 лет назад?


        1. Javian
          31.12.2021 12:53

          Вверху страницы написано Celeron и Riva TNT. Это я подбирал конфигурацию ПК. А спектрум мне отдал друг и я нашел его интересной игрушкой как раз на теме ассемблера. Все очень просто и понятно.


      1. saboteur_kiev
        31.12.2021 03:30
        +1

        о, это же 205,86,5, load с кассеты в "биосе". я даже помню адресацию для загрузки экранной заставки - 221,33,0,64 и 17,0,27


  1. saboteur_kiev
    30.12.2021 11:38
    +1

    Автозагрузочный бейсик можно сделать проще, без дополнительных программ:

    10 SAVE ""
    20 LOAD "" CODE
    30 RANDOMIZE USR 32768

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


  1. dlinyj
    30.12.2021 13:40
    +1

    Буквально сегодня выпустил статью и там использовал транслятор zasm. Рекомендую его под Линукс.


  1. d_ilyich
    30.12.2021 22:02

    Эх, ностальгия. «Увлекательно» было работать, имея в распоряжении только магнитофон. Зато сколько восторга было, когда первая бегущая строка заработала :)

    Время от времени меня посещают мысли о покупке спектрума в железе, но боюсь, что прежних ощущений уже не испытать.


  1. DrGluck07
    30.12.2021 22:47

    Если сдвигать что-то раз в четыре кадра, оно будет выглядеть очень хреного по сравнению с эффектом сдвига на один пиксель, но каждый кадр. Эх, где мои 15-лет…


  1. djv57
    31.12.2021 00:32

    RANDOMIZE USR 0


    1. Chupaka
      01.01.2022 22:09

      Причём в строке с номером 0 :)


  1. saboteur_kiev
    31.12.2021 03:32
    +1

    Между прочим.

    Я вот сейчас думаю, насколько легко понимание ассемблера и его jmp/jnz укладывались на знания бейсика, где каждая команда в отдельной строке.

    И какой был первичный ступор, когда взялся за С++, а там номеров строк нет, одни скобки и какой-то main