🎯 Введение: Зачем сегодня погружаться в ассемблер?

Привет, спектрумист! 👋 Ты наверняка знаешь, что все культовые игры для ZX Spectrum — «Saboteur!», «Jet Set Willy», «Chuckie Egg» — были созданы на ассемблере Z80. Это не причуда прошлого, а священный грааль скорости и контроля. 🏎️💨
  • BASIC — это удобный диалог с машиной.
  • Ассемблер — это прямое слияние с ее душой. Ты начинаешь мыслить как процессор: регистры, флаги, адреса памяти. Это программирование на уровне битов и тактовых циклов, где каждая сохраненная микросекунда — победа!
Эта статья — не справочник команд (их найдешь в мануалах), а путеводитель по философии и ключевым приемам, который превратит тебя из новичка, дрожащего перед LD A, (HL), в мага, рисующего спрайты в середине кадра ретрейса. 🧙‍♂️✨

🛠️ Часть 1: Подготовка поля боя — Инструменты и среда

Не пытайся писать код прямо на «Спектруме» на бумажной ленте! 😄 Мы живем в прекрасную эпоху кросс-разработки.

🔧 Выбор ассемблера (Твой волшебный посох):
  • SjASMPlus — наш король! 🏆 Мощный, гибкий, с поддержкой директив .MODULE, макросов, виртуальных устройств (DEVICE ZXSPECTRUM128). Идеален для современных ПК.
  • Pasmo — простой и надежный, отлично интегрируется в скрипты.
  • Бонус-раунд: z88dk или ZX Basic — для тех, кто хочет микшировать высокоуровневый код с ассемблерными вставками.
🎮 Эмулятор (Твое окно в мир):
  • FUSE / Unreal Speccy / Speccy — запускай, отлаживай, используй встроенные снапшоты (.SNA) для моментального сохранения состояния.
🐛 Отладчик (Твой рентгеновский аппарат):
  • Дебаггер в Unreal Speccy — позволяет прогонять код пошагово, смотреть содержимое памяти, регистров. БЕСЦЕНЕН.
  • SPIN — целая интегрированная среда разработки (IDE) для Spectrum. (Возможно про него обзор в виде статьи создам поздее)
📁 Организация проекта:
MyDemo/
├── src/
│ ├── main.asm # точка входа
│ ├── graphics.asm # работа с экраном
│ └── music.asm # драйвер бейджера
├── build/ # сюда собирается бинарник
├── tools/ # ассемблер, конвертеры
└── make.bat # скрипт сборки

⚙️ Часть 2: Язык машины — Регистры и ключевые парадигмы

Z80 — 8-битный процессор с 16-битной шиной адреса. Это значит: 64 КБ памяти — твое полотно. 🎨

📦 Регистры — твои карманы:
Основные (8-бит): A, B, C, D, E, H, L
Парные (16-бит): BC, DE, HL, AF (A + Флаги)
Специальные: IX, IY (индексные), SP (указатель стека), PC (счетчик команд)
HL — твой главный герой! 🦸 90% операций с памятью идут через него.

💡 Священные заповеди Z80:
  1. Все, что делается часто, должно быть БЫСТРО. Цени каждый такт.
  2. Память иерархична. Самое быстрое — регистры, потом стек, потом видеобуфер.
  3. Используй прерывания (IM 1) для синхронизации с кадром. EI и HALT — твои лучшие друзья для плавной анимации.
  4. Документируй ВСЕ. Что делает эта черная магия с CPL и ADC A, 0? Комментарий — твоя память через месяц.

🚀 Часть 3: Черная магия оптимизации (Практикум)

Вот где начинается настоящая алхимия! 🧪

🎯 Пример 1: Умножение на 10. Медленно vs Быстро.
  • Наивно (в BASIC-стиле): Сложить число 10 раз. Кошмар! ❌
  • На ассемблере (умно):
Assembler Z80:
; Умножить HL на 10 -> HL = HL * 10
add hl, hl      ; HL * 2
ld d, h
ld e, l         ; DE = HL * 2
add hl, hl      ; HL * 4
add hl, hl      ; HL * 8
add hl, de      ; HL * 8 + HL * 2 = HL * 10!
  • Никаких циклов, только сложения и сдвиги (через ADD HL, HL). Быстрее в десятки раз! ✅
🎯 Пример 2: Быстрое копирование буфера (заливка экрана).
Assembler Z80:
    ld hl, BUFFER   ; Источник
    ld de, SCREEN   ; Приемник (видеобуфер)
    ld bc, 6912     ; Размер экрана (256*192)/8 * 2 (атрибуты) = 6912 байт
    ldir            ; Магия! Копирует (HL) -> (DE), увеличивает HL, DE, уменьшает BC. Работает пока BC != 0
LDIR — одна команда, которая делает цикл. Это суперсила Z80! 💥

🎨 Пример 3: Алхимия графики — вывод спрайта 8x8.
Assembler Z80:
; Вход: DE = адрес на экране, HL = адрес данных спрайта
    ld b, 8         ; 8 строк
LoopSprite:
    ld a, (hl)     
    ld (de), a     
    inc hl         
    inc d           
    djnz LoopSprite
Понимание адресации экрана Spectrum — это отдельная магия, без которой никуда!

📚 Часть 4: Ресурсы для прокачки — Библиотека мага

  • 📖 Книги-легенды:
    • «Программирование на Z80» Родан С. (Библия! Ищи сканы).
    • «Машинный код для начинающих» на Spectrum-Info.
    • Оригинальные мануалы Zilog Z80.
  • 🌐 Сайты-архивы:
    • World of Spectrum — исходники игр!
    • CPCWiki / MSX Resource Center — архитектурно близкие миры.
  • 💾 Исходники великих: Качай дизассемблированные .ASM файлы игр. Читать код «Arkanoid» или «Knight Lore» — лучшее обучение.

🚀 Часть 5: Твой первый проект — ритуал инициации

Не пиши игру сразу! Начни с демо-эффектов, они учат самому важному:
  1. «Бегущая строка» → учишь LDIR, работу с экраном.
  2. «Мерцающий статичный спрайт» → учишь XOR-графику для быстрой перерисовки.
  3. «Плавный скроллинг» → погружаешься в прерывания и синхронизацию.
  4. «Генератор простой музыки» → постигаешь порты #FE и тонкую работу с таймингами.
🎬 Шаблон минимальной демо-программы:
Assembler Z80:
        ORG 32768           ; Стартовый адрес
        di                  ; Выключить прерывания на время инициализации
        ld hl, 0            ; Очистка экрана (упрощенно)
        ld de, 1
        ld bc, 6144
        ld (hl), 0
        ldir
StartLoop:
        halt                ; Ждем начало кадрового прерывания
        ei                  ; Разрешаем прерывания
        ; === ТВОЙ КОД ДЛЯ КАДРА ЗДЕСЬ ===
        ; Например, инкремент бордюра
        ld a, (23624)       ; Системная переменная BORDCR
        inc a
        and 7
        out (254), a        ; Меняем цвет бордюра!
        ; ================================
        jr StartLoop        ; Бесконечный цикл

🌀 Часть 6: Черная магия и продвинутые трюки Z80 — За гранью учебников

Здесь живут приемы, которые превращают обычный код в произведение искусства. Запоминайте, эти трюки выжимали последние такты из процессора в гонке за плавность в играх 80-х!

🔮 Трюк 1: Быстрый обмен значений между регистрами без стека

Задача: Поменять местами HL и DE. Обычно делают через стек (PUSH/POP), но это медленно (11+10 тактов).

Магия через стек:
Assembler Z80:
    push hl  ; 11 тактов
    push de  ; 11 тактов
    pop hl   ; 10 тактов
    pop de   ; 10 тактов
    ; Итого: 42 такта

Черная магия (через XOR): 💥
Assembler Z80:
    ld a, h
    xor d
    ld d, a
    xor h
    ld h, a
    xor d
    ld d, a
    
    ld a, l
    xor e
    ld e, a
    xor l
    ld l, a
    xor e
    ld e, a
    ; Итого: 56 тактов? Медленнее! Но...

А вот настоящая магия (через сложение): 🚀
Assembler Z80:
    add hl, de  ; HL = HL + DE
    ld a, h     ;
    sub d       ;
    ld h, a     ;
    ld a, l     ;
    sub e       ;
    ld l, a     ;
    sbc hl, de  ; HL = (HL - DE - перенос)
    ; Итого: ~35 тактов + сохраняются флаги!

Но король обменов — это EXX (всего 4 такта!), если работаешь в альтернативном наборе регистров!

🔮 Трюк 2: Супербыстрое умножение на 5, 9, 10, 12, 17...

Мы видели умножение на 10. А вот умножение HL на 5 за рекордные 11 тактов:
Assembler Z80:
    ; HL * 5 = HL * 4 + HL
    ld b, h
    ld c, l     ; BC = HL (сохраняем оригинал)
    add hl, hl  ; HL * 2
    add hl, hl  ; HL * 4
    add hl, bc  ; + оригинал = HL * 5!
    ; 6 команд, 11 тактов вместо десятков в цикле!

Умножение на 15 (HL * 16 - HL):
Assembler Z80:
    ld b, h
    ld c, l     ; BC = HL
    add hl, hl  ; *2
    add hl, hl  ; *4
    add hl, hl  ; *8
    add hl, hl  ; *16
    sbc hl, bc  ; *16 - *1 = *15, и все за 16 тактов!

🔮 Трюк 3: Быстрая проверка на -1 (или 255)

Вместо:
Assembler Z80:
    ld a, (hl)
    cp 255
    jr z, is_minus_one

Используй инкремент/декремент с проверкой флагов:
Assembler Z80:
    ld a, (hl)
    inc a       ; Если A был 255, то после INC A будет 0 и Z=1!
    jr z, is_minus_one
    dec a       ; Восстанавливаем значение, если нужно

Это экономит команду и часто быстрее!

🔮 Трюк 4: Волшебный DAA для не-десятичной арифметики

DAA (Decimal Adjust Accumulator) предназначен для BCD-арифметики, но его можно использовать для... быстрой коррекции при сложении шестнадцатеричных чисел!

Пример: нужно сложить два шестнадцатеричных числа и получить корректный результат как для отображения:
Assembler Z80:
    ld a, $9A   ; 154 в десятичной
    add a, $0F  ; +15
    ; A = $A9 (169), но нам нужно как будто это BCD
    daa         ; Корректирует! A = $09, флаг C=1 (перенос)
    ; Теперь можно правильно вывести "169" через дальнейшую обработку

🔮 Трюк 5: Использование RLD/RRD для работы с 4-битными значениями

Эти команды — настоящая алхимия! Они циклически сдвигают 4 бита между аккумулятором и памятью.

Пример: Распаковка упакованных данных (два 4-битных значения в одном байте):
Assembler Z80:
    ld hl, PACKED_DATA
    ld de, BUFFER
    
    ld a, (hl)      ; Берем байт вида %xxxxyyyy
    rld             ; A = %xxxx0000, (HL) = %yyyyxxxx
    and $F0         ; Оставляем старшие 4 бита
    ld (de), a      ; Сохраняем первое значение
    inc de
    
    ld a, (hl)      ; Берем снова (уже %yyyyxxxx)
    rld             ; A = %yyyy0000, (HL) = %xxxxyyyy (восстановлено!)
    and $F0         ; Оставляем старшие 4 бита
    ld (de), a      ; Сохраняем второе значение
    
    inc hl
    ; Мы распаковали 2 значения из 1 байта!

🔮 Трюк 6: Секретный "быстрый" INC/DEC для парных регистров

Все знают, что INC HL быстрее INC H + INC L. Но знаете ли вы разницу между:
Assembler Z80:
    inc hl  ; 6 тактов
    inc de  ; 6 тактов
    inc bc  ; 6 тактов

А вот INC IX или INC IY — уже 10 тактов! Поэтому в критичных по скорости местах избегайте индексных регистров.

🔮 Трюк 7: BIT-тестирование с действием в одной команде

Вместо:
Assembler Z80:
    bit 7, (hl)
    jr z, not_set
    ; бит установлен...

🔮 Трюк 8: Сверхбыстрое копирование с самоизменяющимся кодом

Это опасно, но невероятно быстро! Идея в том, чтобы модифицировать команды LD прямо в коде:
Assembler Z80:
CopyBlock:
    ld hl, Source
    ld de, Destination
    ld bc, 512
    
    ; Обычный способ:
    ldir           ; 21 тактов на байт
    
    ; Самоизменяющийся способ (для фиксированного размера):
    ld a, (hl)
    ld (de), a
    inc hl
    inc de
    ; ... повторять 512 раз вручную? Нет!
    
    ; Вместо этого создадим цикл в памяти:
    ld hl, CopyCodeTemplate
    ld de, CopyCode
    ld bc, CopyCodeEnd - CopyCodeTemplate
    ldir           ; Копируем шаблон
    
    ld hl, Source
    ld de, Destination
    jp CopyCode    ; Прыгаем в сгенерированный код!
    
CopyCodeTemplate:
    .rept 512
    ldi            ; LD (DE), (HL); INC HL; INC DE; DEC BC
    .endr
    ret
CopyCodeTemplateEnd:

CopyCode:          ; Сюда копируется шаблон
    ; Этот код выполняется БЕЗ ПРОВЕРКИ BC!
    ; 512 команд LDI подряд = 16 тактов на байт вместо 21!

🔮 Трюк 9: Использование регистра R для генерации псевдослучайных чисел

Регистр R (Refresh) увеличивается на 1 с каждым командным тактом. Его можно использовать как плохой, но очень быстрый ГПСЧ:
Assembler Z80:
RandomByte:
    ld a, r        ; Берем значение refresh-регистра
    xor l          ; XOR с младшим байтом HL
    ld l, a        ; Обновляем L для следующего вызова
    ret            ; A содержит "случайное" число

Для лучшего качества смешивают с другими регистрами и используют сложные формулы.

🔮 Трюк 10: Бит-реверс байта за 29 тактов!

Assembler Z80:
BitReverse:
    ; Вход: A = байт для реверса
    ; Выход: A = реверснутый байт
    ld b, 8
    ld c, a
ReverseLoop:
    rl c        ; Бит из C в перенос
    rra         ; Перенос в старший бит A
    djnz ReverseLoop
    ; Готово! Всего 8*4 + 7 = ~39 тактов
    
    ; Еще более быстрая версия через таблицу (67 байт, но 29 тактов!):
    ld l, a
    ld h, HIGH(ReverseTable)  ; Таблица в старшем байте адреса
    ld a, (hl)
    ret
    
ReverseTable:
    .db $00, $80, $40, $C0, $20, $A0, $60, $E0
    .db $10, $90, $50, $D0, $30, $B0, $70, $F0
    .db $08, $88, $48, $C8, $28, $A8, $68, $E8
    .db $18, $98, $58, $D8, $38, $B8, $78, $F8
    .db $04, $84, $44, $C4, $24, $A4, $64, $E4
    .db $14, $94, $54, $D4, $34, $B4, $74, $F4
    .db $0C, $8C, $4C, $CC, $2C, $AC, $6C, $EC
    .db $1C, $9C, $5C, $DC, $3C, $BC, $7C, $FC
    .db $02, $82, $42, $C2, $22, $A2, $62, $E2
    .db $12, $92, $52, $D2, $32, $B2, $72, $F2
    .db $0A, $8A, $4A, $CA, $2A, $AA, $6A, $EA
    .db $1A, $9A, $5A, $DA, $3A, $BA, $7A, $FA
    .db $06, $86, $46, $C6, $26, $A6, $66, $E6
    .db $16, $96, $56, $D6, $36, $B6, $76, $F6
    .db $0E, $8E, $4E, $CE, $2E, $AE, $6E, $EE
    .db $1E, $9E, $5E, $DE, $3E, $BE, $7E, $FE
    .db $01, $81, $41, $C1, $21, $A1, $61, $E1
    .db $11, $91, $51, $D1, $31, $B1, $71, $F1
    .db $09, $89, $49, $C9, $29, $A9, $69, $E9
    .db $19, $99, $59, $D9, $39, $B9, $79, $F9
    .db $05, $85, $45, $C5, $25, $A5, $65, $E5
    .db $15, $95, $55, $D5, $35, $B5, $75, $F5
    .db $0D, $8D, $4D, $CD, $2D, $AD, $6D, $ED
    .db $1D, $9D, $5D, $DD, $3D, $BD, $7D, $FD
    .db $03, $83, $43, $C3, $23, $A3, $63, $E3
    .db $13, $93, $53, $D3, $33, $B3, $73, $F3
    .db $0B, $8B, $4B, $CB, $2B, $AB, $6B, $EB
    .db $1B, $9B, $5B, $DB, $3B, $BB, $7B, $FB
    .db $07, $87, $47, $C7, $27, $A7, $67, $E7
    .db $17, $97, $57, $D7, $37, $B7, $77, $F7
    .db $0F, $8F, $4F, $CF, $2F, $AF, $6F, $EF
    .db $1F, $9F, $5F, $DF, $3F, $BF, $7F, $FF

🔮 Трюк 11: Счетчик циклов в регистре B без DJNZ

Иногда DJNZ не подходит (нужно делать что-то еще кроме декремента). Вместо:
Assembler Z80:
    ld b, 10
Loop:
    ; ... делаем что-то
    djnz Loop

Можно использовать флаг Z от DEC B:
Assembler Z80:
    ld b, 10
Loop:
    ; ... делаем что-то
    dec b
    jr nz, Loop
    ; Плюс в том, что между DEC B и JR NZ можно вставить другой код!

🔮 Трюк 12: Быстрое определение знака числа (sgn(x))

Assembler Z80:
    ; Вход: A = число (-128..127)
    ; Выход: A = -1 (если отрицательное), 0 (если 0), 1 (если положительное)
    or a          ; Устанавливаем флаги, не меняя A
    ret z         ; Если ноль, возвращаем 0 (A=0)
    ld a, 1       ; Готовим +1
    ret p         ; Если был положительный, возвращаем +1
    ld a, -1      ; Иначе -1
    ret

⚡ Золотое правило:

Каждый сэкономленный такт в цикле, выполняемом тысячи раз, дает выигрыш в тысячи тактов!
Именно такие трюки позволяли играм вроде «Saboteur» или «Knight Lore» работать плавно на 3.5 МГц. Это не просто код — это поэзия ограничений, где каждая сохраненная микросекунда приближает тебя к гениям прошлого.

Эти трюки — лишь вершина айсберга. Настоящая магия начинается, когда ты:
  1. Анализируешь дизассемблированный код великих игр — находишь их секретные оптимизации
  2. Экспериментируешь — пробуешь переписать свой код пятью разными способами
  3. Делишься находками на форуме, как это делаем мы сейчас!
Помни: в мире Z80 размер и скорость — две стороны одной медали. Иногда 10 лишних байт кода экономят 1000 тактов выполнения. Выбор за тобой, волшебник! 🧙‍♂️💾

Удачи в оптимизации! Пусть твой код будет быстрым, а спрайты — плавными! 🚀✨

💎 Заключение: Путь самурая

Изучение ассемблера Z80 в 21 веке — это не стремление к практической пользе, а форма медитации и уважения к истокам. Это диалог с инженерами 80-х, чьи ограничения рождали гениальные решения. Каждая написанная тобой строка кода — это шаг по тонкому мосту между прошлым и будущим, где ты одновременно и ученик, и хранитель. 🏛️

Не бойся сложностей. Первые успехи — когда твой код впервые меняет бордюр по таймеру, когда спрайт наконец-то двигается без мигания — даруют ни с чем не сравнимое чувство прямого творения.

Коди, тестируй, оптимизируй, делись на форуме! Наш ретро-форум — именно то место, где такие статьи и проекты обретают жизнь и поддержку. Удачи в кодинге! Пусть палитра Z80 всегда будет с тобой! 🎨🐉

P.S. Коллеги, если Вам моя статья кажется чересчур "красочной" и преувеличенной, не обессудте, это у меня просто такой стиль. Так же если есть ошибки - пишите, я их исправлю.