push hl,de,bc,af


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

Пожалуй, я рисну рассмотреть просты примеры.

Начну с простой задачи: как очистить экран? Начинающий Спектрумист скажет: "Легко!" и напишет примерный код:

ld hl,$4000;начальный адрес

ld de,$4001

ld bc,6143;размер экрана

ld (hl),0

ldir


Здесь написано верно, но теперь понадобятся простые подсчеты:

инструкция z80 ldir занимает 21/16 тактов на байт, выходит 21*6143=129003 тактов, а это очень много. При постоянной анимации такой подход не пройдет.

Вернуь к статье в забытом журнале ZX ревю:

инструкция push r16,(r16 - hl,de,bc) выполняется 11 тактов, причем push заполнит два байта. получается 6144/2*11=33792 тактов. Ого, уже лучше.

Возникает вопрос: а как применить?

Да очень просто, вот исходный тект ассемблера sjsmplus (stack_cls.asm):

    ld hl,0
lp:
    ei
    halt
   
    ld (back_sp+1),sp;запомнить значение стека
    ld sp,$5800;новый указатель памяти на конец экранной памяти
 dup 3072;развернутый цмкл
 push hl
edup
back_sp:ld sp,0
    jr $


Теперь я внезу небольшие изменения в код:

    device zxspectrum128
        ORG #6000
begin

    ld hl,0
lp:
    ei
    halt
   
    ld (back_sp+1),sp;запомнить значение стека

    ld sp,$5800;новый указатель памяти на конец экранной памяти
 dup 3072;развернутый цмкл
 push hl
edup
back_sp:ld sp,0
    inc hl
    jp lp
    jr $
end
    display /d,end-begin
    savesna "!stack_cls.sna",begin



результат работы программы.

 Все выглядит четко, но на экране картинка немного искажается. В чем причина?

Следует описать процесс рисования:


Изображение с сайта http://oldmachinery.blogspot.ru/2014/04/zx-sprites.html

При выполнении команды halt потребуется некоторая "задержка", Затем начинается отрисовка экрана, в программе, которую я написал происходит
этакий "конфликт" - заполняется экран и луч попадает на область памяти - так называемое "сечение лучем"/"лучи секутся".

Как тут быть? Как один вариант - использовать для ZX Spectrum 128 двойную буферизацию с двумя экранами.
Или, зная структуру экрана, перехитрить железо.

Сначала нужно нарисовать узор вверх ногами:

Теперь рисуется картинка:


Исходный текст(stack_pt.asm):

    device zxspectrum128
        ORG #6000
begin


lp:
    ei
    halt
   
    ld (back_sp+1),sp;запомнить значение стека

    ld sp,pat
    ld a,0:out ($FE),a;рамка черная
    pop hl,de,bc,af
    exx
    exa
    pop hl,de,bc,af
    exx
    exa
    ld sp,$5000;новый указатель памяти на конец экранной памяти

; dup 8

 dup 128
 push hl
edup

 dup 128
 push de
edup

 dup 128
 push bc
edup

 dup 128
 push af
edup

 exx
 exa
 
 dup 128
 push hl
edup

 dup 128
 push de
edup

 dup 128
 push bc
edup

 dup 128
 push af
edup

 exx
 exa

;edup

back_sp:ld sp,0
    ld a,7:out ($FE),a;рамка белая - так по старинке выполняется измерение времени исполнения программы

    jp lp

pat:
    db %01111100,%01111100
    db %10000010,%10000010
    db %10111010,%10111010
    db %10000010,%10000010
    db %10101010,%10101010
    db %10000010,%10000010
    db %01111100,%01111100
    db %00000000,%00000000

end
    display /d,end-begin
    savesna "!stack_pat.sna",begin


Теперь немного модифицирую программу и перейду к пояснениям


исходный текст(stack_pt_fs):
    device zxspectrum128
        ORG #6000
begin

;full screen
lp:
    ei
    halt

    ld a,0:out ($FE),a
    ld hl,$4800:call draw
    ld a,1:out ($FE),a

    ld hl,$5000:call draw
    ld a,2:out ($FE),a

    ld hl,$5800:call draw
    ld a,3:out ($FE),a

    jp lp

draw:
    ld (back_sp+1),sp;запомнить значение стека
    ld (sp1+1),hl

    ld sp,pat
    pop hl,de,bc,af
    exx
    exa
    pop hl,de,bc,af
    exx
    exa
sp1:    ld sp,$5000

; dup 8

 dup 128
 push hl
edup

 dup 128
 push de
edup

 dup 128
 push bc
edup

 dup 128
 push af
edup

 exx
 exa
 
 dup 128
 push hl
edup

 dup 128
 push de
edup

 dup 128
 push bc
edup

 dup 128
 push af
edup

 exx
 exa

;edup

back_sp:ld sp,0
    ret

pat:
    db %01111100,%01111100
    db %10000010,%10000010
    db %10111010,%10111010
    db %10000010,%10000010
    db %10101010,%10101010
    db %10000010,%10000010
    db %01111100,%01111100
    db %00000000,%00000000

end
    display /d,end-begin
    savesna "!stack_pat_fs.sna",begin


и последняя модификация(stack_pt_fs_a):

    ld hl,$5800:call draw
    ld a,3:out ($FE),a
    call anim ;сдвиг узора
    ld a,6:out ($FE),a

    jp lp

anim:
    ld ix,pat
    ld b,8
a_lp:
    xor a
    rr (ix+0) ;циклический сдвиг одной линии узора
    rr (ix+1)
    jr nc,nob7
    set 7,(ix+0)
nob7:
    inc ix,ix
    djnz a_lp ; повторить 8 раз
    ret


Круто, все движется в полный экран! Ну чем не демомейкеры? Осталось только придать узору вертикальное движение, оставлю такую задачу читателям.

Как работает этот замысловатый код?
Экран Спектрума организован следующим образом:  адреса 16384-22527($4000-$57FF) хранят информацию о пикселях. При разрешении экрана 256х192 выходит 256/8=32 байта на строку. Следующие 32 байта отведены для линии на 8 линий ниже.
Получается:
$4000-$401F - верхняя линия
$4020-$403F - 8я линия  ниже
$4040-$405F - 16 линия
$4060-$407F - 24я линия
...
$40E0-$40FF - 56 линия.

Адреса  $4100-$41FF организованы таким же образом, только описывают данные линий ниже, чем у начального адреса.
8 линий начинаются с адресов $4000,$4100,$4200,$4300,$4400,$4500,$4600,$4700.

С такой организацией экран можно разбить на три части:
1я: адреса $4000-$47FF описывают линии 0-63,
2я: адреса $4800-$4FFF линии 64-127,
3я: адреса $5000-$57FF линии 128-191.

Выглядит непонятно, но такая адресация удобна для печати символов 8х8.
Наберите на Бейсике программу:

10 FOR n=0 TO 6911: POKE 16384+n,PEEK n: NEXT n

и увидите, как экран заполняется в соответствии с описанием.

Как работает инструкция PUSH HL ?

Регистр SP(указатель стека) уменьшается на 1, по адресу, хранящемуся в SP, помешается значение регистра H, затем SP уменьшается на 1, и по адресу, хранящемуся в SP, помещается значение регистра L. Пример(void.asm):

    device zxspectrum128
        ORG #6000
begin
    di
    jr $
    ld sp,$4002
    ld hl,$0103
    push hl
    jr $
end
    display /d,end-begin
    savesna "!void.sna",begin


посмотреть на работу удобнеее в отладчике.

Довольно теории, пора разобрать пример stack_pt.asm. Как было сказано, используется узор 8х8(смайлик). Для удобства узор продублирован дважды:
pat:
    db %01111100,%01111100
    db %10000010,%10000010
    db %10111010,%10111010
    db %10000010,%10000010
    db %10101010,%10101010
    db %10000010,%10000010
    db %01111100,%01111100
    db %00000000,%00000000


процессор z80 предоставляет 4 регистровых пары - AF,BC,DE,HL и 4 альтернативных - AF',BC',DE',HL'. Поэтому будет удобнее использовать все пары, их значения получаются с помощью стека:

    ld sp,pat
    pop hl,de,bc,af
    exx
    exa
    pop hl,de,bc,af
    exx
    exa


Данные узора уже есть в регистровых парах, Заполнение экрана используется так::

    ld sp,$5000;новый указатель памяти на конец экранной памяти

 dup 128
 push hl
edup



128 раз выполненная инструкция push hl заполнит 256 байтов участка памяти $4F00-$4FFF, инструкция push de заполнит еще участок выше. Так, с использованием 8 регистровых пар заполняется вторая треть экрана.
С другими примерами несложно разобраться, одинаковая процедура вызывается с разными параметрами(начало стека для заливки).

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


Пример (gr8z.asm) не содержит комментариев, оставлю изучению самостоятельно:

    device zxspectrum128
        ORG #6000
begin

zz:
    ei
    halt
   

    ld a,0
    out ($FE),a
    ld (back_sp+1),sp
n=0
    dup 8+8+8
    ld sp,$5800+n*32+15
    pop af,bc,de,hl
    exx:exa
    pop af,bc,de,hl
    inc sp
    push hl,de,bc,af
    exx:exa
    push hl,de,bc,af

    ld sp,$5800+n*32
    pop af,bc,de,hl
    exx:exa
    pop af,bc,de,hl
    inc sp
    push hl,de,bc,af
    exx:exa
    push hl,de,bc,af

n=n+1
edup

back_sp:ld sp,0

; нарисовать линию узора XOR
; один недостаток - при сдвиге эта линия заливается трешем

    ld hl,$5800
    ld b,24
    ld de,32

mm:
    ld a,0
    xor b

    and 7
    ld c,a
    add a,a
    add a,a
    add a,a
    or c
    ld (hl),a
    add hl,de
    djnz mm

    ld hl,mm+1
    inc (hl)

    ld a,7
    out ($FE),a
    jp zz

end

    display /d,end-begin
    savesna "gr8z.sna",begin



Один недостаток примера - это размер, код весит 1198 байт. В следующей статье я опишу, как сформировать подобные процедуры.
Надо заметить, что стек использовался в различных демо:

Multimatograf 9 ( https://zxaaa.net/view_demo.php?id=7591) - вертикальный скролл текста.
Zombie TV (https://zxaaa.net/view_demo.php?id=8469) -тот же

Другая задача - вывод на экрана спрайта, или анимация на фиксированной позиции.
HNY 2014 (https://zxaaa.net/view_demo.php?id=7835)

Для решения задачи понядобится справочник опкодов z80 и калькулятор.
Переброска спрайта не решается LDIR(21/16 тактов) или цепочкой LDI:LDI(16 тактов)
Решается следующей процедурой:
POP HL
LD ($4800),HL

POP HL
LD ($4802),HL
...
POP HL
LD ($4810),HL

выходит (16+10)/2=13 тактов на байт.

При использовании стека нужно учитывать один нюанс: выполнение процедуры должно выполнится за 1 фрейм, иначе произойдет прерывание, которое испортит данные, хранящееся на стеке.

Исключение:
Disco Bears(https://zxaaa.net/view_demo.php?id=8940)

Здесь, при выводе узора, данные поднимаются со стека, и те же данные формируются заново.

SineDots(https://zxaaa.net/view_demo.php?id=10055)

Для построения точек используются адреса, которые поднимаются со стека(И эти же адреса используются при очистке точек).

К сожалению, время не позволяет остановиться на рассмотрении всех вопросов, связанных с деталями. До встречи в следующем номере.