Введение: Зачем сегодня погружаться в ассемблер?
Привет, спектрумист! - BASIC — это удобный диалог с машиной.
- Ассемблер — это прямое слияние с ее душой. Ты начинаешь мыслить как процессор: регистры, флаги, адреса памяти. Это программирование на уровне битов и тактовых циклов, где каждая сохраненная микросекунда — победа!
Часть 1: Подготовка поля боя — Инструменты и среда
Не пытайся писать код прямо на «Спектруме» на бумажной ленте! - SjASMPlus — наш король!
Мощный, гибкий, с поддержкой директив .MODULE, макросов, виртуальных устройств (DEVICE ZXSPECTRUM128). Идеален для современных ПК. - Pasmo — простой и надежный, отлично интегрируется в скрипты.
- Бонус-раунд: z88dk или ZX Basic — для тех, кто хочет микшировать высокоуровневый код с ассемблерными вставками.
- FUSE / Unreal Speccy / Speccy — запускай, отлаживай, используй встроенные снапшоты (.SNA) для моментального сохранения состояния.
- Дебаггер в Unreal Speccy — позволяет прогонять код пошагово, смотреть содержимое памяти, регистров. БЕСЦЕНЕН.
- SPIN — целая интегрированная среда разработки (IDE) для Spectrum. (Возможно про него обзор в виде статьи создам поздее)
Часть 2: Язык машины — Регистры и ключевые парадигмы
Z80 — 8-битный процессор с 16-битной шиной адреса. Это значит: 64 КБ памяти — твое полотно. HL — твой главный герой!
- Все, что делается часто, должно быть БЫСТРО. Цени каждый такт.
- Память иерархична. Самое быстрое — регистры, потом стек, потом видеобуфер.
- Используй прерывания (IM 1) для синхронизации с кадром. EI и HALT — твои лучшие друзья для плавной анимации.
- Документируй ВСЕ. Что делает эта черная магия с CPL и ADC A, 0? Комментарий — твоя память через месяц.
Часть 3: Черная магия оптимизации (Практикум)
Вот где начинается настоящая алхимия! - Наивно (в 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). Быстрее в десятки раз!

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!
Assembler Z80:
; Вход: DE = адрес на экране, HL = адрес данных спрайта
ld b, 8 ; 8 строк
LoopSprite:
ld a, (hl)
ld (de), a
inc hl
inc d
djnz LoopSprite
Часть 4: Ресурсы для прокачки — Библиотека мага
Книги-легенды:
- «Программирование на Z80» Родан С. (Библия! Ищи сканы).
- «Машинный код для начинающих» на Spectrum-Info.
- Оригинальные мануалы Zilog Z80.
Сайты-архивы:
- World of Spectrum — исходники игр!
- CPCWiki / MSX Resource Center — архитектурно близкие миры.
Исходники великих: Качай дизассемблированные .ASM файлы игр. Читать код «Arkanoid» или «Knight Lore» — лучшее обучение.
Часть 5: Твой первый проект — ритуал инициации
Не пиши игру сразу! Начни с демо-эффектов, они учат самому важному:- «Бегущая строка» → учишь LDIR, работу с экраном.
- «Мерцающий статичный спрайт» → учишь XOR-графику для быстрой перерисовки.
- «Плавный скроллинг» → погружаешься в прерывания и синхронизацию.
- «Генератор простой музыки» → постигаешь порты #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 МГц. Это не просто код — это поэзия ограничений, где каждая сохраненная микросекунда приближает тебя к гениям прошлого.
Эти трюки — лишь вершина айсберга. Настоящая магия начинается, когда ты:
- Анализируешь дизассемблированный код великих игр — находишь их секретные оптимизации
- Экспериментируешь — пробуешь переписать свой код пятью разными способами
- Делишься находками на форуме, как это делаем мы сейчас!
Удачи в оптимизации! Пусть твой код будет быстрым, а спрайты — плавными!
Заключение: Путь самурая
Изучение ассемблера Z80 в 21 веке — это не стремление к практической пользе, а форма медитации и уважения к истокам. Это диалог с инженерами 80-х, чьи ограничения рождали гениальные решения. Каждая написанная тобой строка кода — это шаг по тонкому мосту между прошлым и будущим, где ты одновременно и ученик, и хранитель. Не бойся сложностей. Первые успехи — когда твой код впервые меняет бордюр по таймеру, когда спрайт наконец-то двигается без мигания — даруют ни с чем не сравнимое чувство прямого творения.
Коди, тестируй, оптимизируй, делись на форуме! Наш ретро-форум — именно то место, где такие статьи и проекты обретают жизнь и поддержку. Удачи в кодинге! Пусть палитра Z80 всегда будет с тобой!
P.S. Коллеги, если Вам моя статья кажется чересчур "красочной" и преувеличенной, не обессудте, это у меня просто такой стиль. Так же если есть ошибки - пишите, я их исправлю.