ZXNet эхоконференция «code.zx»


тема: Менеджер вызова подпрограмм из различных банков памяти



от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .1 ══════════════════

(c) Иван Рощин, Москва

Fido : 2:5020/689.53
ZXNet : 500:95/462.53
E-mail: asder_ffc@softhome.net
WWW : http://www.ivr.da.ru

Менеджер вызова подпрограмм из различных банков памяти
══════════════════════════════════════════════════════

("Радиомир. Ваш компьютер" 12/2001)
(Дополненная версия)

Hемного теории
──────────────

Адресное пространство процессора Z80 невелико - всего 64
килобайта. Для доступа к большему количеству памяти в компьютере
ZX Spectrum 128 используется страничная адресация. Оперативная
память (а именно она будет нас интересовать) разбита на банки по
16 килобайт (всего получается 8 банков с номерами от 0 до 7). Hа
адреса #4000-#7FFF и #8000-#BFFF постоянно подключены банки 5 и
2 соответственно, а на адреса #C000-#FFFF может быть подключен
любой из банков (см. рис. 1).

╔═════════════════╗
║ ║
#C000-#FFFF ║ Любой банк RAM ║
║ ║
╚═════════════════╝
│ │
#8000-#BFFF │ RAM 2 │
│ │
├─────────────────┤
│ │
#4000-#7FFF │ RAM 5 │
│ │
├─────────────────┤
│ │
#0000-#3FFF │ ROM │
│ │
└─────────────────┘

Рис. 1

Банки 5 и 2 я буду в дальнейшем называть нижней памятью. Они
всегда находятся в адресном пространстве процессора. Все
остальные банки (их я буду называть верхней памятью) не обладают
этим свойством. Только один из них может быть подключен.
Hомер подключенного на адреса #C000-#FFFF банка памяти
задается битами 0, 1 и 2 числа, выводимого в порт #7FFD. Чтение
из этого порта невозможно. Поэтому, чтобы можно было узнать,
какой банк подключен, надо при каждом выводе в порт запоминать
выводимое значение в специальной переменной.
При выводе в порт мы не можем обратиться только к трем
младшим его разрядам, не трогая остальные. Поэтому придется
рассказать и о назначении других битов порта #7FFD. В третьем
бите указывается номер видеостраницы: 0 - стандартная, 1 -
расположенная в 7 банке памяти. В четвертом бите - номер
подключенного банка ПЗУ: 0 - BASIC 128, 1 - BASIC 48. И,
наконец, вывод единицы в пятый бит приведет к отключению
дополнительной памяти до аппаратного "сброса" компьютера.
Остальные (6 и 7) биты в ZX Spectrum 128 не используются.


Подпрограммы в верхней памяти и особенности их вызова
─────────────────────────────────────────────────────

При написании программы может возникнуть необходимость
размещения отдельных ее подпрограмм в различных банках верхней
памяти. Причины этого могут быть самыми разными. Может быть,
программа получается настолько большой, что по-другому просто не
умещается в памяти. Может быть, требуется выделить как можно
больше нижней памяти под данные, которые должны быть всегда
доступны из любого банка памяти, и из-за этого приходится
уменьшать место, занимаемое в нижней памяти кодом программы,
перенося большую его часть в верхнюю память. Может быть,
программа пишется с учетом неодинаковой скорости доступа к
различным банкам памяти (такой особенностью обладает как
фирменный ZX Spectrum 128, так и некоторые совместимые модели),
и исполняемый код требуется размещать только в "быстрых" банках.
А может быть, для обработки данных нужен непрерывный участок
памяти как можно большей длины.
Так вот, при обращении к размещенным в верхней памяти
подпрограммам, если банк, в котором находится вызываемая
подпрограмма, не подключен, возникают сложности. Как, например,
вызвать из нижней памяти подпрограмму, находящуюся в не
подключенном на данный момент банке верхней памяти (см. рис. 2,
стрелка 1)? Или как вызвать из подключенного банка верхней
памяти подпрограмму, находящуюся в другом банке верхней памяти
(см. рис. 2, стрелка 2)?

подключенный банк неподключенные банки верхней памяти
верхней памяти ----------------^----------------
╔══════════════│═══════╗ / \n
║ #FFFF┌───────│──────┐║ ┌──────────────┐ ┌──────────────┐
║ │подпрограмма a│║ 2 │..............│ │..............│
║ │подпрограмма b ──────>подпрограмма i│...│..............│
║ │..............│║ │..............│ │подпрограмма x│
║ #C000└──────────────┘║ └──────────────┘ └──────^───────┘
║ #BFFF┌──────────────┐║ │
║ │ │║ │
║ │ нижняя │║ │
║ │ память │║ │
║ │ │║ 1 │
║ │ ───────────────────────────────────────┘
║ │ │║
║ │ │║
║ │ │║
║ #4000└──────────────┘║
║ #3FFF┌──────────────┐║
║ │ │║
║ │ ROM │║
║ │ │║
║ #0000└──────────────┘║
╚══════════════════════╝

Рис. 2

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .2 ══════════════════

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

Процедура SETPORT - установка банка памяти.
Вход: A - выводимое в порт #7FFD значение.
Выход: значение выведено в порт и записано в переменную BANK.
Значения регистров: не изменены.

SETPORT PUSH BC
LD BC,#7FFD
LD (BANK),A
OUT (C),A
POP BC
RET

BANK DS 1 ;хранится текущее состояние порта #7FFD

При вызове этой процедуры в аккумуляторе должно содержаться
уже подготовленное для вывода в порт значение, т.е. кроме того,
что в битах 0-2 должен быть номер требуемого банка памяти,
остальные биты также должны быть установлены соответствующим
образом (см. предыдущий раздел). Если, например, при работе
программы подключено ПЗУ BASIC-48, для вывода изображения
используется стандартная видеостраница, и надо подключить 3-й
банк ОЗУ, то для этого нужно вывести в порт #7FFD число #13.
В дальнейшем, когда я буду упоминать "номер банка", я буду
иметь в виду значение, уже подготовленное для вывода в порт
(если явно не подразумевается иное).
И еще, обратите внимание: в процедуре сначала производится
запись выводимого значения в переменную BANK, а только потом -
вывод в порт #7FFD. Почему нужна именно такая последовательность
действий? Предположим, что между командами записи и вывода в
порт произошло прерывание, а процедура обработки прерывания
устроена так: сначала она запоминает, какой банк памяти был
подключен, по содержимому переменной BANK, после этого
устанавливает нужный ей банк, выполняет какие-то действия, а
затем восстанавливает "старый" банк памяти. Так вот, если бы
процедура SETPORT сначала выводила значение в порт, а потом
записывала в переменную BANK, то в этом случае после окончания
обработки прерывания оказался бы установлен банк памяти,
соответствующий старому значению переменной BANK. А это нам
совершенно ни к чему.

С подключением банков, кажется, разобрались. Вернемся теперь
к нашим подпрограммам. Как же быть с их вызовом? Рассмотрим
сначала вызов из нижней памяти подпрограммы, находящейся в не
подключенном на данный момент банке верхней памяти. Очень часто
при этом еще требуется, чтобы после вызова был подключен тот же
банк памяти, который был до вызова.
Последовательность необходимых для этого действий такова:

- запомнить номер текущего банка памяти;
- установить банк, в котором находится вызываемая
подпрограмма;
- вызвать ее;
- установить тот банк памяти, который был до вызова (т.е.
с ранее запомненным номером).

Тогда вызов подпрограммы можно оформить следующим образом:

LD A,(BANK) ;Запоминаем номер
PUSH AF ;текущего банка.
LD A,N ;Устанавливаем банк, в котором
CALL SETPORT ;находится вызываемая подпрограмма.
CALL subrout ;Вызываем ее.
POP AF ;Устанавливаем банк,
CALL SETPORT ;который был до вызова.

Как видим, по сравнению с обычным CALL'ом тратятся лишних 13
байт. И это еще не все. Если для передачи данных в вызываемую
подпрограмму используется аккумулятор, то может потребоваться
сохранение его значения (например, в каком-либо свободном
регистре) перед установкой нужного банка памяти и восстановление
перед вызовом подпрограммы, а на это также потратятся лишние
байты. Точно так же, если аккумулятор (или регистр флагов!)
используется для возвращения результата работы подпрограммы,
возможно, потребуется где-то сохранять его значение (я говорю
"возможно", потому что в некоторых случаях можно сразу же
обработать возвращенный подпрограммой результат и уже потом,
когда он больше не нужен, выполнить команды POP AF: CALL
SETPORT). В итоге, как видим, дополнительные 13 байт на вызов
подпрограммы - это минимум, а в действительности может быть и
больше.
Если одну и ту же подпрограмму требуется вызывать в
нескольких местах программы, то можно, конечно, сам ее вызов
оформить как подпрограмму.

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

;В банке памяти, откуда происходит вызов:

CALL l_subrout

;В нижней памяти:

l_subrout LD A,(BANK) ;Запоминаем номер банка верхней памяти,
PUSH AF ;откуда был вызов.
LD A,N ;Устанавливаем банк, в котором
CALL SETPORT ;находится вызываемая подпрограмма.
CALL subrout ;Вызываем ее.
POP AF ;Устанавливаем банк, откуда был вызов,
JP SETPORT ;и возвращаем туда управление (JP здесь
;используется вместо CALL: RET, чтобы
;сэкономить байт).

Как видите, на каждую вызываемую таким образом подпрограмму
тратится, не считая CALL'а, еще не менее 16 байт.

Итак, при вызове расположенных в верхней памяти подпрограмм
приходится использовать громоздкие конструкции, причем
находящиеся в нижней памяти, а ее и так не хватает. И если таких
подпрограмм много (а если программа большая, то их много!), то
расход нижней памяти также будет значительным.

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .3 ══════════════════

Менеджер вызова: что это такое и как он работает
────────────────────────────────────────────────

Как уменьшить расход памяти на вызовы подпрограмм? При
написании своей программы BestView я решил сделать так: пусть
в нижней памяти будет расположена специальная процедура, так
называемый менеджер вызова. При необходимости вызова какой-либо
подпрограммы достаточно обратиться к менеджеру, передав ему
номер этой подпрограммы, а он уже осуществит все необходимые
действия по ее вызову: запомнит, какой банк памяти подключен,
установит банк, в котором находится вызываемая подпрограмма,
вызовет ее, после чего подключит банк памяти, бывший перед
вызовом, и вернет управление.
Адрес вызываемой подпрограммы и номер банка памяти, в
котором она расположена, менеджер извлекает из соответствующих
таблиц.
Естественно, значения всех регистров должны сохраняться: с
какими значениями был вызван менеджер - с такими же он должен
вызвать требуемую подпрограмму; какие значения оказались в
регистрах после вызова подпрограммы - такие же должны остаться и
после окончания работы менеджера.
И еще одно требование - реентерабельность. То есть нужно
учитывать, что в любой момент работы менеджера, от обращения к
нему и до выхода из него, может произойти прерывание, и в
процедуре его обработки также может быть обращение к менеджеру
(и, разумеется, когда менеджер запускает подпрограмму, она, в
свою очередь, также может содержать обращения к менеджеру).

После того, как требования к менеджеру были сформулированы,
дело было за малым - написать его. И тут возникли вопросы.

Вопрос 1. Как передавать менеджеру номер вызываемой
подпрограммы?
Передавать номер в каком-либо регистре? Hо, во-первых, тогда
потребуется дополнительная память на команду его загрузки в
каждой точке вызова, а во-вторых, быть может, именно этот
регистр используется для передачи параметров в вызываемую
подпрограмму.
Указывать номер в байте, непосредственно следующем за
командой вызова менеджера? Тогда в каждой точке вызова будет
тратиться лишний байт (а если подпрограмм больше 256, то даже
два байта). Казалось бы, один байт - пустяки, но если подсчитать
общее количество вызовов... К тому же затруднится отладка
программы из-за того, что отладчик будет считать байт номера
байтом начала следующей команды.
К счастью, есть оригинальный выход, лишенный перечисленных
выше недостатков! Сделаем для вызова каждой подпрограммы свою
точку входа так, чтобы в итоге управление передавалось на один
и тот же адрес:

;Точки входа:

subr_1 NOP ;для вызова подпрограммы SUBR_1
subr_2 NOP ;для вызова подпрограммы SUBR_2

subr_n NOP ;для вызова подпрограммы SUBR_N

;Hачалась обработка...

Тогда можно установить, какая точка входа была использована.
Пусть n - адрес команды обращения к менеджеру (см. рис. 3). При
ее выполнении в стек будет помещен адрес первого байта следующей
команды - n+3. Взяв этот адрес и уменьшив его на два, мы получим
адрес n+1, с которого размещается адрес точки входа. Взяв адрес
точки входа и отняв от него адрес первой точки входа (subr_1),
мы получим номер использованной точки входа, т.е. номер
вызванной подпрограммы. Легко и просто, не правда ли? Hомер не
требуется явно указывать ни в регистрах, ни в памяти, и на это
не тратятся лишние байты!

n n+1 n+2 n+3
───┬────────────┬──────────────┬──────────────┬─────────────┬───
. │ код │ младший байт │ старший байт │ первый байт │ .
. │ команды │ адреса │ адреса │ следующей │ .
. │ CALL │ точки входа │ точки входа │ команды │ .
───┴────────────┴──────────────┴──────────────┴─────────────┴───

Рис. 3

Тем не менее, у этого способа есть свои особенности, которые
надо учитывать.
Во-первых, при вызове подпрограммы какое-то время будет
затрачено на выполнение цепочки NOP'ов (4 такта на каждый). Чем
меньше порядковый номер точки входа, тем больше будет длина этой
цепочки и, соответственно, задержка. Так что, если для каких-то
подпрограмм задержка при вызове нежелательна, лучше располагать
их точки входа последними.
Во-вторых, часто используют такой прием оптимизации:
последовательность CALL subrout: RET заменяют просто на JP (или
JR) subrout, тем самым экономя память и повышая быстродействие
(выигрыш составляет 1 байт/17 тактов для JP и 2 байта/15 тактов
для JR). Hо обращение к менеджеру должно происходить _только_ с
помощью команды CALL! Иначе в стек не будет занесен адрес
следующей после CALL команды и, соответственно, нельзя будет
определить номер вызванной подпрограммы. Так что данный способ
оптимизации в случае, когда подпрограмма вызывается с помощью
менеджера, использовать нельзя.
Если имена самих подпрограмм записывать прописными буквами,
а имена точек входа - строчными, то в тексте программы сразу
будет видно, какая подпрограмма как вызывается: с использованием
менеджера или нет. Видим в тексте, например, CALL PRINT - это
обычный вызов. Видим CALL cls - это вызов с использованием
менеджера. Так что сразу становится понятно, где можно выполнять
оптимизацию, а где нельзя.

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .4 ══════════════════

Вопрос 2. Как определять номер банка, в котором находится
вызываемая подпрограмма?
Hаиболее естественный ответ - по таблице. Можно в каждом
байте таблицы хранить номер банка соответствующей подпрограммы,
уже подготовленный для вывода в порт. Можно использовать тот
факт, что на хранение номера банка требуется только три бита, и
хранить данные более экономно: в одном байте - два номера, или
(что более сложно) в трех байтах - восемь номеров. Памяти будет
расходоваться меньше, но придется затратить больше времени на
извлечение номера банка из таблицы и подготовку к выводу в порт.
Hо обратите внимание: с одной стороны, таблица неизбежно
займет дополнительное место в памяти, а с другой - мы имеем
столько же занятых командами NOP ячеек памяти, сколько имеется
подпрограмм. Hельзя ли их совместить?
Оказывается, можно! Команды NOP в этих ячейках нужны лишь
для того, чтобы, не выполняя каких-либо действий, перейти к
началу обработки. А теперь вспомним, что в системе команд Z80
есть и другие команды, также не выполняющие каких-либо действий:

┌─────────┬───────────────┬───────────────────────┐
│ команда │ код │ три младших бита кода │
├─────────┼───────────────┼───────────────────────┤
│ LD A,A │ #7F=%01111111 │ 7 │
│ LD B,B │ #40=%01000000 │ 0 │
│ LD C,C │ #49=%01001001 │ 1 │
│ LD D,D │ #52=%01010010 │ 2 │
│ LD E,E │ #5B=%01011011 │ 3 │
│ LD H,H │ #64=%01100100 │ 4 │
│ LD L,L │ #6D=%01101101 │ 5 │
└─────────┴───────────────┴───────────────────────┘

Табл. 1

Всего, вместе с NOP'ом, получается восемь команд, и банков
памяти тоже восемь! Значит, между ними можно установить
однозначное соответствие. И если для каждой подпрограммы мы
поставим по адресу точки входа для ее вызова одну из восьми
команд, соответствующую банку памяти, в котором находится эта
подпрограмма, то лишнюю память на таблицу банков тратить не
придется!
Вот она, красота кода!!! Команды, которые казались
совершенно бесполезными, вдруг оказались единственно нужными!
При этом еще смотрите, как удачно получается: у всех семи
NOP-подобных команд разные три младших бита кода (см. табл. 1).
Так что удобно сопоставить каждой из этих команд банк памяти,
номер которого - три младших бита кода этой команды. Тогда для
определения номера банка по коду команды достаточно будет
обнулить пять старших битов командой AND %111.
Как можете видеть, в таблице нет команды, у которой три
младших бита кода равны 6. Поэтому шестому банку мы поставим в
соответствие команду NOP. Код этой команды - 0. Было бы,
конечно, гораздо удобнее, если бы три младших бита кода NOP
были бы равны 6 (или если бы среди семи NOP-подобных команд не
оказалось команды с тремя младшими битами кода, равными 0):
тогда при определении номера банка по коду команды не
потребовалось бы дополнительных проверок. Hо чего нет, того
нет...

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .5 ══════════════════

Вопрос 3. Как уже упоминалось выше, для определения номера
вызываемой подпрограммы нужно снять со стека адрес возврата и
произвести определенные действия. При этом, очевидно, будут
использованы некоторые регистры. Поэтому первоначальные значения
этих регистров нужно сохранить, а перед вызовом подпрограммы -
восстановить.
Hо если в реентерабельном участке кода требуется что-то
сохранить, а затем восстановить, то использовать для этого можно
только стек! В самом деле, смотрите, что будет, если сохранять
значения в фиксированных ячейках памяти: если между сохранением
и восстановлением произойдет прерывание, и процедура обработки
прерывания обратится к этому же участку кода, то сохранение
опять будет выполнено в те же ячейки памяти, и их первоначальное
значение будет потеряно!
Hо если мы сначала сохраним в стеке значения используемых
регистров, то уже не сможем получить доступ к адресу возврата,
ведь он теперь не будет на вершине стека! И это не единственная
коллизия такого рода. Смотрите: перед запуском подпрограммы нам
надо запомнить в стеке номер текущего банка памяти, чтобы потом,
после запуска, восстановить его. Hо если сначала мы запомним в
стеке значения регистров, а потом - номер банка, то как будем
восстанавливать значения регистров перед вызовом подпрограммы?
Они ведь уже не будут на вершине стека! И еще: как будем
извлекать из стека этот номер банка после окончания работы
запущенной подпрограммы? Перед этим придется сохранить в стеке
значения используемых регистров, а значит, номер банка уже не
будет на вершине стека. И как же быть???

Вопрос 4. А как, собственно, запускать подпрограмму? Пусть
нам известен ее адрес, ну и что? Записать этот адрес в поле
операнда команды CALL и выполнить ее? Ага, разбежались! Еще раз
повторю: если в реентерабельном участке кода требуется что-то
сохранить (в данном случае адрес запуска), чтобы потом
использовать (в данном случае при выполнении команды CALL), то
фиксированные ячейки памяти (в данном случае поле операнда
команды CALL) использовать для этого нельзя!

Ответы на эти два вопроса заключаются в нетрадиционном
использовании стека и команд работы с ним. Думаю, лучший способ
объяснить это - подробно рассмотреть все манипуляции со стеком в
менеджере.
Hа рисунках справа от каждого элемента стека указывается его
длина в байтах. Вершина стека изображена сверху, но учитывайте,
что в памяти стек хранится перевернутым: вершине соответствует
самый низкий адрес, или, как говорят, стек растет вниз. Адрес
вершины стека содержится в регистре SP.

Исходное состояние при вызове менеджера:

┌─────────────────┐
│ адрес возврата │ 2
├─────────────────┤
│.................│

Резервируем в стеке 5 байт. Для этого достаточно уменьшить
SP на 5 (не забываем: стек растет вниз!). Уменьшение выполняется
так: DEC SP: PUSH HL: PUSH HL. Что будет занесено в стек
командами PUSH, в данном случае совершенно не важно: мы
используем их лишь для уменьшения SP (потому что PUSH на байт
короче, чем две команды DEC SP).

┌─────────────────┐
│ резерв │ 5
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

Сохраняем в стеке HL, DE, AF.

┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ │
│ DE │ 2 │
├─────────────────┤ ├ 11
│ HL │ 2 │
├─────────────────┤ │
│ резерв │ 5 │
├─────────────────┤ ┘
│ адрес возврата │ 2
├─────────────────┤
│.................│

Зная адрес вершины стека и смещение элемента в стеке, можно
получить к нему доступ, даже если это не верхний элемент! А
смещение адреса возврата мы знаем: как видно из рисунка, оно
равно 11. По адресу возврата определяем номер вызываемой
подпрограммы и номер банка памяти, в котором она находится.

┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ │
│ DE │ 2 │
├─────────────────┤ ├ 10
│ HL │ 2 │
├─────────────────┤ │
│ резерв │ 4 │
├─────────────────┤ ┘
│ резерв │ 1
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .6 ══════════════════

Определяем адрес вызываемой подпрограммы по ее номеру.
Сохраняем номер текущего (т.е. установленного при вызове
менеджера) банка памяти в одном из ранее зарезервированных
байтов стека, по адресу SP+10.

┌─────────────────┐
│ AF │ 2
├─────────────────┤
│ DE │ 2
├─────────────────┤
│ HL │ 2
├─────────────────┤
│ резерв │ 4
├─────────────────┤
│ номер банка, │
│подключенного при│ 1
│вызове менеджера │
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

Записываем в оставшиеся 4 байта резерва адрес, по которому
будет передано управление при выполнении команды RET в конце
вызываемой подпрограммы (это адрес SEL_EXIT, относящийся к
менеджеру), и адрес вызываемой подпрограммы.

┌─────────────────┐
│ AF │ 2
├─────────────────┤
│ DE │ 2
├─────────────────┤
│ HL │ 2
├─────────────────┤
│ адрес п/п │ 2
├─────────────────┤
│ адрес SEL_EXIT │ 2
├─────────────────┤
│ номер банка, │
│подключенного при│ 1
│вызове менеджера │
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

Подключаем банк подпрограммы. Снимаем ранее запомненные в
стеке значения HL, DE, AF. Теперь значения всех регистров такие
же, какими они были при вызове менеджера.

┌─────────────────┐
│ адрес п/п │ 2
├─────────────────┤
│ адрес SEL_EXIT │ 2
├─────────────────┤
│ номер банка, │
│подключенного при│ 1
│вызове менеджера │
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

Выполняем команду RET, при этом процессор снимет со стека
адрес подпрограммы и передаст по нему управление. Как видите,
RET здесь используется для вызова подпрограммы, хотя обычное
назначение этой команды - напротив, выход из подпрограммы. Вот
такой нестандартный прием. :-)

┌─────────────────┐
│ адрес SEL_EXIT │ 2
├─────────────────┤
│ номер банка, │
│подключенного при│ 1
│вызове менеджера │
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

После того, как подпрограмма выполнится, при выходе из нее
по команде RET управление будет передано на следующую команду
менеджера (с адресом SEL_EXIT).

┌─────────────────┐
│ номер банка, │
│подключенного при│ 1
│вызове менеджера │
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

Запоминаем в стеке HL и AF.

┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ ├ 4
│ HL │ 2 │
├─────────────────┤ ┘
│ номер банка, │
│подключенного при│ 1
│вызове менеджера │
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .7 ══════════════════

По адресу SP+4 находится номер банка памяти, который был
подключен при вызове менеджера. Устанавливаем этот банк. Снимаем
со стека AF и HL. Теперь содержимое всех регистров такое же,
какое было при выходе из подпрограммы.

┌─────────────────┐
│ номер банка, │
│подключенного при│ 1
│вызове менеджера │
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

Командой INC SP убираем из стека не нужный теперь номер
банка.

┌─────────────────┐
│ адрес возврата │ 2
├─────────────────┤
│.................│

Работа менеджера завершена! По команде RET возвращаем
управление.

И еще, обратите внимание: область применения менеджера
оказалась шире, чем задумывалось при его создании, т.е. его
можно использовать не только для вызова подпрограмм,
расположенных в верхней памяти. Пусть, например, нам надо
вызвать подпрограмму, находящуюся в нижней памяти, но
обрабатывающую данные, расположенные в определенном банке
верхней памяти. Hичего нет проще! Помещаем сведения о ней (адрес
и номер банка с данными) в менеджер, и можно будет ее вызывать,
причем из любого банка памяти. А если подпрограмму надо вызвать
сначала для обработки данных в одном банке памяти, потом - в
другом банке, то можно просто несколько раз описать ее в
менеджере, указывая каждый раз один и тот же адрес, но различные
номера банков памяти.

Hу что же, осталось лишь привести листинг менеджера. После
столь подробных объяснений, думаю, непонятных мест в нем не
будет!


Листинг менеджера вызова
────────────────────────

;Коды NOP-подобных команд, соответствующих каждому
;из 8 банков памяти:

bank_0 EQU #40 ;LD B,B
bank_1 EQU #49 ;LD C,C
bank_2 EQU #52 ;LD D,D
bank_3 EQU #5B ;LD E,E
bank_4 EQU #64 ;LD H,H
bank_5 EQU #6D ;LD L,L
bank_6 EQU #00 ;NOP
bank_7 EQU #7F ;LD A,A

;Таблица адресов подпрограмм:

SEL_TAB DW SUBR_1
DW SUBR_2
...........
DW SUBR_N

;В принципе, таблицу адресов можно разместить и в верхней
;памяти, тогда в нижней памяти затраты составят лишь 1 байт
;на каждую подпрограмму (без учета длины исполняемого кода
;менеджера).

;Точки входа:

SEL_BEG
subr_1 DB bank_3
subr_2 DB bank_6

subr_n DB bank_1

;Hачалась обработка:

DEC SP ;резервируем в стеке
PUSH HL ;5 байт
PUSH HL

PUSH HL ;сохраняем
PUSH DE ;используемые
PUSH AF ;регистры

LD HL,11
ADD HL,SP
LD E,(HL)
INC HL
LD D,(HL)

;DE - адрес возврата после вызова;
;перед этим адресом стоит команда
;CALL XXXX; вот этот адрес XXXX и берем:

EX DE,HL
DEC HL
LD D,(HL)
DEC HL
LD E,(HL)
EX DE,HL

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .8 ══════════════════

;В HL адрес XXXX; смотрим, какая команда по нему расположена, и
;по ее коду определяем номер банка памяти, в котором расположена
;вызываемая подпрограмма:

LD A,(HL)
AND A
JR Z,BANK_M_1 ;если NOP

AND 7 ;иначе - номер банка в младших трех
OR #10 ;битах кода команды; OR #10 - для
JR BANK_M_2 ;установки остальных разрядов порта.

BANK_M_1 LD A,#16 ;NOP соответствует 6 банку.

;Внимание! Если в программе используется не основной экран и/или
;не ПЗУ BASIC-48, то значения в командах OR #10 и LD A,#16
;должны быть скорректированы!

;Теперь в аккумуляторе число, которое нужно вывести в порт #7FFD
;для подключения банка памяти, где расположена подпрограмма.

;По адресу точки входа (заданному в HL) определяем адрес в
;таблице, по которому расположен адрес вызываемой подпрограммы.
;Искомый адрес в таблице равен (HL-SEL_BEG)*2+SEL_TAB, или, что
;то же самое (но проще для вычисления), 2*HL-2*SEL_BEG+SEL_TAB.

BANK_M_2 ADD HL,HL ;*2

;Скобки в загружаемом в DE выражении нужны, т.к. умножение
;отрицательных чисел при вычислении выражений в ассемблерe
;ZX ASM, которым я пользуюсь, выполняется неверно.

LD DE,-(2*SEL_BEG)+SEL_TAB
ADD HL,DE

;Читаем в DE адрес вызываемой подпрограммы:

LD E,(HL)
INC HL
LD D,(HL)

;Заносим в стек номер банка:

LD HL,10
ADD HL,SP
LD (HL),N ;N - начальное значение переменной BANK.
BANK EQU $-1

;Адрес возврата:

DEC HL
LD (HL),SEL_EXIT/256 ;старший байт
DEC HL
LD (HL),SEL_EXIT256 ;младший байт

;Адрес вызываемой подпрограммы:

DEC HL
LD (HL),D
DEC HL
LD (HL),E

;Устанавливаем банк вызываемой подпрограммы:

CALL SETPORT ;N банка

POP AF
POP DE
POP HL

;В стеке сейчас адрес вызываемой подпрограммы, а за ним - адрес
;команды SEL_EXIT.

RET ;запуск подпрограммы

;Восстанавливаем банк, номер которого был сохранен в стеке:

SEL_EXIT EX (SP),HL
PUSH AF
LD A,L
CALL SETPORT
POP AF
EX (SP),HL
INC SP

RET


Возможности оптимизации
───────────────────────

Фрагмент, в котором по коду NOP-подобной команды,
расположенной в точке входа, определяется число, которое нужно
вывести в порт #7FFD для установки банка памяти с вызываемой
подпрограммой:

LD A,(HL)
AND A
JR Z,BANK_M_1 ;если NOP

AND 7 ;иначе - номер банка в младших трех
OR #10 ;битах кода команды; OR #10 - для
JR BANK_M_2 ;установки остальных разрядов порта.

BANK_M_1 LD A,#16 ;NOP соответствует 6 банку.

BANK_M_2 ..........

═══════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .9 ══════════════════

может быть оптимизирован так:

LD A,(HL)
AND A
JR NZ,BANK_M
LD A,6
BANK_M AND 7
OR #10

Получается на два байта короче. При этом, если вызываемая
подпрограмма расположена не в шестом банке памяти, то выигрыш во
времени составит 7 тактов, а если в шестом банке - наоборот,
время работы будет на 9 тактов больше. Выгодна такая оптимизация
или нет, зависит от конкретного случая и выбранных критериев
выгодности. Hапример, если главное - сократить размер, или если
вызываемые подпрограммы расположены в основном не в шестом банке
(т.е. в среднем будет выигрыш во времени), то способ пригоден.
А если ставится целью как можно больше ускорить время запуска
подпрограмм именно в шестом банке памяти, то способ не подойдет
(можно, впрочем, попытаться разместить такие подпрограммы в
каком-либо другом банке).
Если в шестом банке вообще нет подпрограмм, можно записать
даже так:

LD A,(HL)
AND 7
OR #10

что будет уже на 7 байтов короче и на 23 такта быстрее исходного
варианта.
Как видим, если с помощью менеджера вызываются подпрограммы
в шестом банке памяти, то менеджер оказывается длиннее и
работает медленнее. А все потому, что среди NOP-подобных команд
нет такой, младшие три бита кода которой были бы равны шести.
Hо есть способ оптимизации, позволяющий обойти это
неудобство! Правда, при его использовании требуется, чтобы хотя
бы в одном из восьми банков памяти не было вызываемых с помощью
менеджера подпрограмм (в реальных программах это условие,
по-видимому, выполняется практически всегда).
Смотрите: если определять выводимое в порт #7FFD число по
коду NOP-подобной команды с помощью команд AND 7: OR #10, то
можно вызывать подпрограммы из всех банков памяти, кроме
шестого, т.е. из банков 0,1,2,3,4,5,7. А нам надо, скажем,
вызывать подпрограммы из всех банков, кроме третьего. Тогда
запишем так: AND 7: XOR 5: OR #10. Что получается? Команда XOR 5
превращает числа 0,1,2,3,4,5,7 соответственно в 5,4,7,6,1,0,2.
А это и есть все числа от 0 до 7, кроме 3.
Две команды XOR 5: OR #10 можно заменить на одну XOR #15,
так как после AND 7 старшие разряды аккумулятора обнулены. В
итоге определение выводимого в порт числа по коду команды будет
выглядеть так: AND 7: XOR #15. Выполнение этого фрагмента
занимает ровно столько же времени, как и AND 7: OR #10, и длина
у него такая же.
А вот неиспользуемый банк мы теперь можем выбирать сами.
Если номер этого банка - n, то операнд в команде XOR вычисляется
по формуле 6 XOR n. В данном случае он равен 6 XOR 3 (%110 XOR
%011), т.е. 5 (%101). Объединяя с операндом команды OR #10, в
итоге получаем #15.
И, разумеется, нужно еще переопределить значения констант
bank_0 - bank_7. Смотрим, какие числа в какие превращаются при
выполнении команды XOR, и переименовываем константы. В данном
случае bank_0 переименовываем в bank_5, bank_1 - в bank_4 и так
далее, в итоге получаем:

bank_5 EQU #40 ;LD B,B
bank_4 EQU #49 ;LD C,C
bank_7 EQU #52 ;LD D,D
bank_6 EQU #5B ;LD E,E
bank_1 EQU #64 ;LD H,H
bank_0 EQU #6D ;LD L,L
bank_2 EQU #7F ;LD A,A

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .1 ══════════════════

(c) Иван Рощин, Москва

Fido : 2:5020/689.53
ZXNet : 500:95/462.53
E-mail: asder_ffc@softhome.net
WWW : http://www.ivr.da.ru

Менеджер вызова подпрограмм из различных банков памяти
══════════════════════════════════════════════════════

("Радиомир. Ваш компьютер" 12/2001)
(Дополненная версия)

Немного теории
──────────────

Адресное пространство процессора Z80 невелико - всего 64
килобайта. Для доступа к большему количеству памяти в компьютере
ZX Spectrum 128 используется страничная адресация. Оперативная
память (а именно она будет нас интересовать) разбита на банки по
16 килобайт (всего получается 8 банков с номерами от 0 до 7). На
адреса #4000-#7FFF и #8000-#BFFF постоянно подключены банки 5 и
2 соответственно, а на адреса #C000-#FFFF может быть подключен
любой из банков (см. рис. 1).

╔═════════════════╗
║ ║
#C000-#FFFF ║ Любой банк RAM ║
║ ║
╚═════════════════╝
│ │
#8000-#BFFF │ RAM 2 │
│ │
├─────────────────┤
│ │
#4000-#7FFF │ RAM 5 │
│ │
├─────────────────┤
│ │
#0000-#3FFF │ ROM │
│ │
└─────────────────┘

Рис. 1

Банки 5 и 2 я буду в дальнейшем называть нижней памятью. Они
всегда находятся в адресном пространстве процессора. Все
остальные банки (их я буду называть верхней памятью) не обладают
этим свойством. Только один из них может быть подключен.
Номер подключенного на адреса #C000-#FFFF банка памяти
задается битами 0, 1 и 2 числа, выводимого в порт #7FFD. Чтение
из этого порта невозможно. Поэтому, чтобы можно было узнать,
какой банк подключен, надо при каждом выводе в порт запоминать
выводимое значение в специальной переменной.
При выводе в порт мы не можем обратиться только к трем
младшим его разрядам, не трогая остальные. Поэтому придется
рассказать и о назначении других битов порта #7FFD. В третьем
бите указывается номер видеостраницы: 0 - стандартная, 1 -
расположенная в 7 банке памяти. В четвертом бите - номер
подключенного банка ПЗУ: 0 - BASIC 128, 1 - BASIC 48. И,
наконец, вывод единицы в пятый бит приведет к отключению
дополнительной памяти до аппаратного "сброса" компьютера.
Остальные (6 и 7) биты в ZX Spectrum 128 не используются.


Подпрограммы в верхней памяти и особенности их вызова
─────────────────────────────────────────────────────

При написании программы может возникнуть необходимость
размещения отдельных ее подпрограмм в различных банках верхней
памяти. Причины этого могут быть самыми разными. Может быть,
программа получается настолько большой, что по-другому просто не
умещается в памяти. Может быть, требуется выделить как можно
больше нижней памяти под данные, которые должны быть всегда
доступны из любого банка памяти, и из-за этого приходится
уменьшать место, занимаемое в нижней памяти кодом программы,
перенося большую его часть в верхнюю память. Может быть,
программа пишется с учетом неодинаковой скорости доступа к
различным банкам памяти (такой особенностью обладает как
фирменный ZX Spectrum 128, так и некоторые совместимые модели),
и исполняемый код требуется размещать только в "быстрых" банках.
А может быть, для обработки данных нужен непрерывный участок
памяти как можно большей длины.
Так вот, при обращении к размещенным в верхней памяти
подпрограммам, если банк, в котором находится вызываемая
подпрограмма, не подключен, возникают сложности. Как, например,
вызвать из нижней памяти подпрограмму, находящуюся в не
подключенном на данный момент банке верхней памяти (см. рис. 2,
стрелка 1)? Или как вызвать из подключенного банка верхней
памяти подпрограмму, находящуюся в другом банке верхней памяти
(см. рис. 2, стрелка 2)?

подключенный банк неподключенные банки верхней памяти
верхней памяти ----------------^----------------
╔══════════════│═══════╗ / \n
║ #FFFF┌───────│──────┐║ ┌──────────────┐ ┌──────────────┐
║ │подпрограмма a│║ 2 │..............│ │..............│
║ │подпрограмма b ──────>подпрограмма i│...│..............│
║ │..............│║ │..............│ │подпрограмма x│
║ #C000└──────────────┘║ └──────────────┘ └──────^───────┘
║ #BFFF┌──────────────┐║ │
║ │ │║ │
║ │ нижняя │║ │
║ │ память │║ │
║ │ │║ 1 │
║ │ ───────────────────────────────────────┘
║ │ │║
║ │ │║
║ │ │║
║ #4000└──────────────┘║
║ #3FFF┌──────────────┐║
║ │ │║
║ │ ROM │║
║ │ │║
║ #0000└──────────────┘║
╚══════════════════════╝

Рис. 2

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .3 ══════════════════

Менеджер вызова: что это такое и как он работает
────────────────────────────────────────────────

Как уменьшить расход памяти на вызовы подпрограмм? При
написании своей программы BestView я решил сделать так: пусть
в нижней памяти будет расположена специальная процедура, так
называемый менеджер вызова. При необходимости вызова какой-либо
подпрограммы достаточно обратиться к менеджеру, передав ему
номер этой подпрограммы, а он уже осуществит все необходимые
действия по ее вызову: запомнит, какой банк памяти подключен,
установит банк, в котором находится вызываемая подпрограмма,
вызовет ее, после чего подключит банк памяти, бывший перед
вызовом, и вернет управление.
Адрес вызываемой подпрограммы и номер банка памяти, в
котором она расположена, менеджер извлекает из соответствующих
таблиц.
Естественно, значения всех регистров должны сохраняться: с
какими значениями был вызван менеджер - с такими же он должен
вызвать требуемую подпрограмму; какие значения оказались в
регистрах после вызова подпрограммы - такие же должны остаться и
после окончания работы менеджера.
И еще одно требование - реентерабельность. То есть нужно
учитывать, что в любой момент работы менеджера, от обращения к
нему и до выхода из него, может произойти прерывание, и в
процедуре его обработки также может быть обращение к менеджеру
(и, разумеется, когда менеджер запускает подпрограмму, она, в
свою очередь, также может содержать обращения к менеджеру).

После того, как требования к менеджеру были сформулированы,
дело было за малым - написать его. И тут возникли вопросы.

Вопрос 1. Как передавать менеджеру номер вызываемой
подпрограммы?
Передавать номер в каком-либо регистре? Но, во-первых, тогда
потребуется дополнительная память на команду его загрузки в
каждой точке вызова, а во-вторых, быть может, именно этот
регистр используется для передачи параметров в вызываемую
подпрограмму.
Указывать номер в байте, непосредственно следующем за
командой вызова менеджера? Тогда в каждой точке вызова будет
тратиться лишний байт (а если подпрограмм больше 256, то даже
два байта). Казалось бы, один байт - пустяки, но если подсчитать
общее количество вызовов... К тому же затруднится отладка
программы из-за того, что отладчик будет считать байт номера
байтом начала следующей команды.
К счастью, есть оригинальный выход, лишенный перечисленных
выше недостатков! Сделаем для вызова каждой подпрограммы свою
точку входа так, чтобы в итоге управление передавалось на один
и тот же адрес:

;Точки входа:

subr_1 NOP ;для вызова подпрограммы SUBR_1
subr_2 NOP ;для вызова подпрограммы SUBR_2

subr_n NOP ;для вызова подпрограммы SUBR_N

;Началась обработка...

Тогда можно установить, какая точка входа была использована.
Пусть n - адрес команды обращения к менеджеру (см. рис. 3). При
ее выполнении в стек будет помещен адрес первого байта следующей
команды - n+3. Взяв этот адрес и уменьшив его на два, мы получим
адрес n+1, с которого размещается адрес точки входа. Взяв адрес
точки входа и отняв от него адрес первой точки входа (subr_1),
мы получим номер использованной точки входа, т.е. номер
вызванной подпрограммы. Легко и просто, не правда ли? Номер не
требуется явно указывать ни в регистрах, ни в памяти, и на это
не тратятся лишние байты!

n n+1 n+2 n+3
───┬────────────┬──────────────┬──────────────┬─────────────┬───
. │ код │ младший байт │ старший байт │ первый байт │ .
. │ команды │ адреса │ адреса │ следующей │ .
. │ CALL │ точки входа │ точки входа │ команды │ .
───┴────────────┴──────────────┴──────────────┴─────────────┴───

Рис. 3

Тем не менее, у этого способа есть свои особенности, которые
надо учитывать.
Во-первых, при вызове подпрограммы какое-то время будет
затрачено на выполнение цепочки NOP'ов (4 такта на каждый). Чем
меньше порядковый номер точки входа, тем больше будет длина этой
цепочки и, соответственно, задержка. Так что, если для каких-то
подпрограмм задержка при вызове нежелательна, лучше располагать
их точки входа последними.
Во-вторых, часто используют такой прием оптимизации:
последовательность CALL subrout: RET заменяют просто на JP (или
JR) subrout, тем самым экономя память и повышая быстродействие
(выигрыш составляет 1 байт/17 тактов для JP и 2 байта/15 тактов
для JR). Но обращение к менеджеру должно происходить _только_ с
помощью команды CALL! Иначе в стек не будет занесен адрес
следующей после CALL команды и, соответственно, нельзя будет
определить номер вызванной подпрограммы. Так что данный способ
оптимизации в случае, когда подпрограмма вызывается с помощью
менеджера, использовать нельзя.
Если имена самих подпрограмм записывать прописными буквами,
а имена точек входа - строчными, то в тексте программы сразу
будет видно, какая подпрограмма как вызывается: с использованием
менеджера или нет. Видим в тексте, например, CALL PRINT - это
обычный вызов. Видим CALL cls - это вызов с использованием
менеджера. Так что сразу становится понятно, где можно выполнять
оптимизацию, а где нельзя.

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .4 ══════════════════

Вопрос 2. Как определять номер банка, в котором находится
вызываемая подпрограмма?
Наиболее естественный ответ - по таблице. Можно в каждом
байте таблицы хранить номер банка соответствующей подпрограммы,
уже подготовленный для вывода в порт. Можно использовать тот
факт, что на хранение номера банка требуется только три бита, и
хранить данные более экономно: в одном байте - два номера, или
(что более сложно) в трех байтах - восемь номеров. Памяти будет
расходоваться меньше, но придется затратить больше времени на
извлечение номера банка из таблицы и подготовку к выводу в порт.
Но обратите внимание: с одной стороны, таблица неизбежно
займет дополнительное место в памяти, а с другой - мы имеем
столько же занятых командами NOP ячеек памяти, сколько имеется
подпрограмм. Нельзя ли их совместить?
Оказывается, можно! Команды NOP в этих ячейках нужны лишь
для того, чтобы, не выполняя каких-либо действий, перейти к
началу обработки. А теперь вспомним, что в системе команд Z80
есть и другие команды, также не выполняющие каких-либо действий:

┌─────────┬───────────────┬───────────────────────┐
│ команда │ код │ три младших бита кода │
├─────────┼───────────────┼───────────────────────┤
│ LD A,A │ #7F=%01111111 │ 7 │
│ LD B,B │ #40=%01000000 │ 0 │
│ LD C,C │ #49=%01001001 │ 1 │
│ LD D,D │ #52=%01010010 │ 2 │
│ LD E,E │ #5B=%01011011 │ 3 │
│ LD H,H │ #64=%01100100 │ 4 │
│ LD L,L │ #6D=%01101101 │ 5 │
└─────────┴───────────────┴───────────────────────┘

Табл. 1

Всего, вместе с NOP'ом, получается восемь команд, и банков
памяти тоже восемь! Значит, между ними можно установить
однозначное соответствие. И если для каждой подпрограммы мы
поставим по адресу точки входа для ее вызова одну из восьми
команд, соответствующую банку памяти, в котором находится эта
подпрограмма, то лишнюю память на таблицу банков тратить не
придется!
Вот она, красота кода!!! Команды, которые казались
совершенно бесполезными, вдруг оказались единственно нужными!
При этом еще смотрите, как удачно получается: у всех семи
NOP-подобных команд разные три младших бита кода (см. табл. 1).
Так что удобно сопоставить каждой из этих команд банк памяти,
номер которого - три младших бита кода этой команды. Тогда для
определения номера банка по коду команды достаточно будет
обнулить пять старших битов командой AND %111.
Как можете видеть, в таблице нет команды, у которой три
младших бита кода равны 6. Поэтому шестому банку мы поставим в
соответствие команду NOP. Код этой команды - 0. Было бы,
конечно, гораздо удобнее, если бы три младших бита кода NOP
были бы равны 6 (или если бы среди семи NOP-подобных команд не
оказалось команды с тремя младшими битами кода, равными 0):
тогда при определении номера банка по коду команды не
потребовалось бы дополнительных проверок. Но чего нет, того
нет...

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .5 ══════════════════

Вопрос 3. Как уже упоминалось выше, для определения номера
вызываемой подпрограммы нужно снять со стека адрес возврата и
произвести определенные действия. При этом, очевидно, будут
использованы некоторые регистры. Поэтому первоначальные значения
этих регистров нужно сохранить, а перед вызовом подпрограммы -
восстановить.
Но если в реентерабельном участке кода требуется что-то
сохранить, а затем восстановить, то использовать для этого можно
только стек! В самом деле, смотрите, что будет, если сохранять
значения в фиксированных ячейках памяти: если между сохранением
и восстановлением произойдет прерывание, и процедура обработки
прерывания обратится к этому же участку кода, то сохранение
опять будет выполнено в те же ячейки памяти, и их первоначальное
значение будет потеряно!
Но если мы сначала сохраним в стеке значения используемых
регистров, то уже не сможем получить доступ к адресу возврата,
ведь он теперь не будет на вершине стека! И это не единственная
коллизия такого рода. Смотрите: перед запуском подпрограммы нам
надо запомнить в стеке номер текущего банка памяти, чтобы потом,
после запуска, восстановить его. Но если сначала мы запомним в
стеке значения регистров, а потом - номер банка, то как будем
восстанавливать значения регистров перед вызовом подпрограммы?
Они ведь уже не будут на вершине стека! И еще: как будем
извлекать из стека этот номер банка после окончания работы
запущенной подпрограммы? Перед этим придется сохранить в стеке
значения используемых регистров, а значит, номер банка уже не
будет на вершине стека. И как же быть???

Вопрос 4. А как, собственно, запускать подпрограмму? Пусть
нам известен ее адрес, ну и что? Записать этот адрес в поле
операнда команды CALL и выполнить ее? Ага, разбежались! Еще раз
повторю: если в реентерабельном участке кода требуется что-то
сохранить (в данном случае адрес запуска), чтобы потом
использовать (в данном случае при выполнении команды CALL), то
фиксированные ячейки памяти (в данном случае поле операнда
команды CALL) использовать для этого нельзя!

Ответы на эти два вопроса заключаются в нетрадиционном
использовании стека и команд работы с ним. Думаю, лучший способ
объяснить это - подробно рассмотреть все манипуляции со стеком в
менеджере.
На рисунках справа от каждого элемента стека указывается его
длина в байтах. Вершина стека изображена сверху, но учитывайте,
что в памяти стек хранится перевернутым: вершине соответствует
самый низкий адрес, или, как говорят, стек растет вниз. Адрес
вершины стека содержится в регистре SP.

Исходное состояние при вызове менеджера:

┌─────────────────┐
│ адрес возврата │ 2
├─────────────────┤
│.................│

Резервируем в стеке 5 байт. Для этого достаточно уменьшить
SP на 5 (не забываем: стек растет вниз!). Уменьшение выполняется
так: DEC SP: PUSH HL: PUSH HL. Что будет занесено в стек
командами PUSH, в данном случае совершенно не важно: мы
используем их лишь для уменьшения SP (потому что PUSH на байт
короче, чем две команды DEC SP).

┌─────────────────┐
│ резерв │ 5
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

Сохраняем в стеке HL, DE, AF.

┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ │
│ DE │ 2 │
├─────────────────┤ ├ 11
│ HL │ 2 │
├─────────────────┤ │
│ резерв │ 5 │
├─────────────────┤ ┘
│ адрес возврата │ 2
├─────────────────┤
│.................│

Зная адрес вершины стека и смещение элемента в стеке, можно
получить к нему доступ, даже если это не верхний элемент! А
смещение адреса возврата мы знаем: как видно из рисунка, оно
равно 11. По адресу возврата определяем номер вызываемой
подпрограммы и номер банка памяти, в котором она находится.

┌─────────────────┐ ┐
│ AF │ 2 │
├─────────────────┤ │
│ DE │ 2 │
├─────────────────┤ ├ 10
│ HL │ 2 │
├─────────────────┤ │
│ резерв │ 4 │
├─────────────────┤ ┘
│ резерв │ 1
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .7 ══════════════════

По адресу SP+4 находится номер банка памяти, который был
подключен при вызове менеджера. Устанавливаем этот банк. Снимаем
со стека AF и HL. Теперь содержимое всех регистров такое же,
какое было при выходе из подпрограммы.

┌─────────────────┐
│ номер банка, │
│подключенного при│ 1
│вызове менеджера │
├─────────────────┤
│ адрес возврата │ 2
├─────────────────┤
│.................│

Командой INC SP убираем из стека не нужный теперь номер
банка.

┌─────────────────┐
│ адрес возврата │ 2
├─────────────────┤
│.................│

Работа менеджера завершена! По команде RET возвращаем
управление.

И еще, обратите внимание: область применения менеджера
оказалась шире, чем задумывалось при его создании, т.е. его
можно использовать не только для вызова подпрограмм,
расположенных в верхней памяти. Пусть, например, нам надо
вызвать подпрограмму, находящуюся в нижней памяти, но
обрабатывающую данные, расположенные в определенном банке
верхней памяти. Ничего нет проще! Помещаем сведения о ней (адрес
и номер банка с данными) в менеджер, и можно будет ее вызывать,
причем из любого банка памяти. А если подпрограмму надо вызвать
сначала для обработки данных в одном банке памяти, потом - в
другом банке, то можно просто несколько раз описать ее в
менеджере, указывая каждый раз один и тот же адрес, но различные
номера банков памяти.

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


Листинг менеджера вызова
────────────────────────

;Коды NOP-подобных команд, соответствующих каждому
;из 8 банков памяти:

bank_0 EQU #40 ;LD B,B
bank_1 EQU #49 ;LD C,C
bank_2 EQU #52 ;LD D,D
bank_3 EQU #5B ;LD E,E
bank_4 EQU #64 ;LD H,H
bank_5 EQU #6D ;LD L,L
bank_6 EQU #00 ;NOP
bank_7 EQU #7F ;LD A,A

;Таблица адресов подпрограмм:

SEL_TAB DW SUBR_1
DW SUBR_2
...........
DW SUBR_N

;В принципе, таблицу адресов можно разместить и в верхней
;памяти, тогда в нижней памяти затраты составят лишь 1 байт
;на каждую подпрограмму (без учета длины исполняемого кода
;менеджера).

;Точки входа:

SEL_BEG
subr_1 DB bank_3
subr_2 DB bank_6

subr_n DB bank_1

;Началась обработка:

DEC SP ;резервируем в стеке
PUSH HL ;5 байт
PUSH HL

PUSH HL ;сохраняем
PUSH DE ;используемые
PUSH AF ;регистры

LD HL,11
ADD HL,SP
LD E,(HL)
INC HL
LD D,(HL)

;DE - адрес возврата после вызова;
;перед этим адресом стоит команда
;CALL XXXX; вот этот адрес XXXX и берем:

EX DE,HL
DEC HL
LD D,(HL)
DEC HL
LD E,(HL)
EX DE,HL

════════════════════════════════════════════════

С уважением, Иван Рощин.

от: Ivan Roshin
кому: All
дата: 08 Feb 2002
Hello, All!

═══════════════════ call .9 ══════════════════

может быть оптимизирован так:

LD A,(HL)
AND A
JR NZ,BANK_M
LD A,6
BANK_M AND 7
OR #10

Получается на два байта короче. При этом, если вызываемая
подпрограмма расположена не в шестом банке памяти, то выигрыш во
времени составит 7 тактов, а если в шестом банке - наоборот,
время работы будет на 9 тактов больше. Выгодна такая оптимизация
или нет, зависит от конкретного случая и выбранных критериев
выгодности. Например, если главное - сократить размер, или если
вызываемые подпрограммы расположены в основном не в шестом банке
(т.е. в среднем будет выигрыш во времени), то способ пригоден.
А если ставится целью как можно больше ускорить время запуска
подпрограмм именно в шестом банке памяти, то способ не подойдет
(можно, впрочем, попытаться разместить такие подпрограммы в
каком-либо другом банке).
Если в шестом банке вообще нет подпрограмм, можно записать
даже так:

LD A,(HL)
AND 7
OR #10

что будет уже на 7 байтов короче и на 23 такта быстрее исходного
варианта.
Как видим, если с помощью менеджера вызываются подпрограммы
в шестом банке памяти, то менеджер оказывается длиннее и
работает медленнее. А все потому, что среди NOP-подобных команд
нет такой, младшие три бита кода которой были бы равны шести.
Но есть способ оптимизации, позволяющий обойти это
неудобство! Правда, при его использовании требуется, чтобы хотя
бы в одном из восьми банков памяти не было вызываемых с помощью
менеджера подпрограмм (в реальных программах это условие,
по-видимому, выполняется практически всегда).
Смотрите: если определять выводимое в порт #7FFD число по
коду NOP-подобной команды с помощью команд AND 7: OR #10, то
можно вызывать подпрограммы из всех банков памяти, кроме
шестого, т.е. из банков 0,1,2,3,4,5,7. А нам надо, скажем,
вызывать подпрограммы из всех банков, кроме третьего. Тогда
запишем так: AND 7: XOR 5: OR #10. Что получается? Команда XOR 5
превращает числа 0,1,2,3,4,5,7 соответственно в 5,4,7,6,1,0,2.
А это и есть все числа от 0 до 7, кроме 3.
Две команды XOR 5: OR #10 можно заменить на одну XOR #15,
так как после AND 7 старшие разряды аккумулятора обнулены. В
итоге определение выводимого в порт числа по коду команды будет
выглядеть так: AND 7: XOR #15. Выполнение этого фрагмента
занимает ровно столько же времени, как и AND 7: OR #10, и длина
у него такая же.
А вот неиспользуемый банк мы теперь можем выбирать сами.
Если номер этого банка - n, то операнд в команде XOR вычисляется
по формуле 6 XOR n. В данном случае он равен 6 XOR 3 (%110 XOR
%011), т.е. 5 (%101). Объединяя с операндом команды OR #10, в
итоге получаем #15.
И, разумеется, нужно еще переопределить значения констант
bank_0 - bank_7. Смотрим, какие числа в какие превращаются при
выполнении команды XOR, и переименовываем константы. В данном
случае bank_0 переименовываем в bank_5, bank_1 - в bank_4 и так
далее, в итоге получаем:

bank_5 EQU #40 ;LD B,B
bank_4 EQU #49 ;LD C,C
bank_7 EQU #52 ;LD D,D
bank_6 EQU #5B ;LD E,E
bank_1 EQU #64 ;LD H,H
bank_0 EQU #6D ;LD L,L
bank_2 EQU #7F ;LD A,A

════════════════════════════════════════════════

С уважением, Иван Рощин.




Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Рассказы - 38 1/2 или биг-мак по-техасски.
Новости - Олимпиада'97 по информатике или размышления по окончании.
Анонс - В следующем номере ожидаются традиционные рубрики "Описания новинок из SOSG".
B.B.S. Новости - Ночь снятых паролей на Lime-BBS-2.
Обо всем понемногу - Подключение AY-Mouse.

В этот день...   29 марта