© Иван Рощин, Москва
| ZXNet | : 500:95/462.53 |
| : bestview@mtu-net.ru | |
| WWW | : http://www.ivr.da.ru |
(«Радиомир. Ваш
компьютер» 12/2001, 2/2002, 4/2002)
Дата последнего редактирования: 26.03.2003.
Немного теории
Подпрограммы в верхней памяти и особенности
их вызова
Менеджер вызова: что это такое и как
он работает
Листинг менеджера вызова
Возможности оптимизации
Адресное пространство процессора Z80 невелико всего 64 килобайта. Для доступа к большему количеству памяти в компьютере ZX Spectrum 128 используется страничная адресация. Оперативная память (а именно она будет нас интересовать) разбита на банки по 16 килобайтов (всего получается 8 банков с номерами от 0 до 7). На адреса #4000#7FFF и #8000#BFFF постоянно подключены банки 5 и 2 соответственно, а на адреса #C000#FFFF может быть подключён любой из банков (рис. 1).
![]() |
Банки 5 и 2 я буду в дальнейшем называть нижней памятью. Они всегда находятся в адресном пространстве процессора. Все остальные банки (их я буду называть верхней памятью) не обладают этим свойством. Только один из них может быть подключён.
Номер подключённого на адреса #C000#FFFF банка памяти задаётся битами 0, 1 и 2 числа, выводимого в порт #7FFD. Чтение из этого порта невозможно. Поэтому, чтобы можно было узнать, какой банк подключён, надо при каждом выводе в порт запоминать выводимое значение в специальной переменной.
При выводе в порт мы не можем обратиться только к трём младшим его
разрядам, не трогая остальные. Поэтому придётся рассказать и о назначении
других битов порта #7FFD. В третьем бите указывается номер видеостраницы:
0 стандартная, 1 расположенная в 7 банке памяти. В
четвёртом бите номер подключённого банка ПЗУ: 0
При написании программы может возникнуть необходимость размещения отдельных
её подпрограмм в различных банках верхней памяти. Причины этого могут быть
самыми разными. Может быть, программа получается настолько большой, что
Так вот, при обращении к размещённым в верхней памяти подпрограммам, если банк, в котором находится вызываемая подпрограмма, не подключён, возникают сложности. Как, например, вызвать из нижней памяти подпрограмму, находящуюся в не подключённом на данный момент банке верхней памяти (рис. 2, стрелка 1)? Или как вызвать из подключённого банка верхней памяти подпрограмму, находящуюся в другом банке верхней памяти (рис. 2, стрелка 2)?
![]() |
Очевидно, без переключения банков не обойтись. Поэтому начнём с рассмотрения используемой для этого процедуры.
Процедура SETPORT - установка банка памяти.
Вход: A - выводимое в порт #7FFD значение.
Выход: значение выведено в порт и записано в переменную BANK.
Значения регистров: не изменены.
SETPORT PUSH BC
LD BC,#7FFD
LD (BANK),A
OUT (C),A
POP BC
RET
BANK DS 1 ;Хранится текущее состояние порта #7FFD.
При вызове этой процедуры в аккумуляторе должно содержаться уже
подготовленное для вывода в порт значение, т.е. кроме того, что в битах
02 должен быть номер требуемого банка памяти, остальные биты также
должны быть установлены соответствующим образом (см. предыдущий раздел). Если,
например, при работе программы подключено ПЗУ
В дальнейшем под номером банка я буду иметь в виду значение, уже подготовленное для вывода в порт (если явно не подразумевается иное).
И ещё, обратите внимание: в процедуре сначала производится запись
выводимого значения в переменную BANK, а только потом вывод в порт
#7FFD. Почему нужна именно такая последовательность действий? Предположим, что
между командами записи и вывода в порт произошло прерывание, а процедура
обработки прерывания устроена так: сначала она запоминает, какой банк памяти
был подключён, по содержимому переменной BANK, после этого устанавливает
нужный ей банк, выполняет
С подключением банков, кажется, разобрались. Вернёмся теперь к нашим подпрограммам. Как же быть с их вызовом? Рассмотрим сначала вызов из нижней памяти подпрограммы, находящейся в не подключённом на данный момент банке верхней памяти. Очень часто при этом ещё требуется, чтобы после вызова был подключён тот же банк памяти, который был до вызова.
Последовательность необходимых для этого действий такова:
Тогда вызов подпрограммы можно оформить следующим образом:
LD A,(BANK) ;Запоминаем номер
PUSH AF ;текущего банка.
LD A,N ;Устанавливаем банк, в котором
CALL SETPORT ;находится вызываемая подпрограмма.
CALL subrout ;Вызываем её.
POP AF ;Устанавливаем банк,
CALL SETPORT ;который был до вызова.
Как видим, по сравнению с обычным CALLом тратятся лишних 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 байтов.
Итак, при вызове расположенных в верхней памяти подпрограмм приходится использовать громоздкие конструкции, причём находящиеся в нижней памяти, а её и так не хватает. И если таких подпрограмм много (а если программа большая, то их много!), то расход нижней памяти также будет значительным.
Как уменьшить расход памяти на вызовы подпрограмм? При написании своей
программы 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), мы получим номер использованной точки входа, т.е. номер вызванной подпрограммы. Легко и просто, не правда ли? Номер не требуется явно указывать ни в регистрах, ни в памяти, и на это не тратятся лишние байты!
![]() |
Тем не менее, у этого способа есть свои особенности, которые надо учитывать.
Если имена самих подпрограмм записывать прописными буквами, а имена точек входа строчными, то в тексте программы сразу будет видно, какая подпрограмма как вызывается: с использованием менеджера или нет. Видим в тексте, например, CALL PRINT это обычный вызов. Видим CALL cls это вызов с использованием менеджера. Так что сразу становится понятно, где можно выполнять оптимизацию, а где нельзя.
Вопрос 2. Как определять номер банка, в котором находится вызываемая подпрограмма?
Наиболее естественный ответ по таблице. Можно в каждом байте таблицы хранить номер банка соответствующей подпрограммы, уже подготовленный для вывода в порт. Можно использовать тот факт, что на хранение номера банка требуется только три бита, и хранить данные более экономно: в одном байте два номера, или (что более сложно) в трёх байтах восемь номеров. Памяти будет расходоваться меньше, но придётся затратить больше времени на извлечение номера банка из таблицы и подготовку к выводу в порт.
Но обратите внимание: с одной стороны, таблица неизбежно займёт дополнительное место в памяти, а с другой мы имеем столько же занятых командами NOP ячеек памяти, сколько имеется подпрограмм. Нельзя ли их совместить?
Оказывается, можно! Команды NOP в этих ячейках нужны лишь для того, чтобы,
не выполняя
| Команда | Код | Три младших бита кода |
|---|---|---|
| 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 |
Всего, вместе с NOPом, получается восемь команд, и банков памяти тоже восемь! Значит, между ними можно установить однозначное соответствие. И если для каждой подпрограммы мы поставим по адресу точки входа для её вызова одну из восьми команд, соответствующую банку памяти, в котором находится эта подпрограмма, то лишнюю память на таблицу банков тратить не придётся!
Вот она, красота кода!!! Команды, которые казались совершенно бесполезными,
вдруг оказались единственно нужными! При этом ещё смотрите, как удачно
получается: у всех семи
Как можете видеть, в таблице нет команды, у которой три младших бита кода
равны 6. Поэтому шестому банку мы поставим в соответствие команду NOP. Код
этой команды 0. Было бы, конечно, гораздо удобнее, если бы три
младших бита кода NOP были бы равны 6 (или если бы среди семи
Вопрос 3. Как уже упоминалось выше, для определения номера вызываемой подпрограммы нужно снять со стека адрес возврата и произвести определённые действия. При этом, очевидно, будут использованы некоторые регистры. Поэтому первоначальные значения этих регистров нужно сохранить, а перед вызовом подпрограммы восстановить.
Но если в реентерабельном участке кода требуется
Но если мы сначала сохраним в стеке значения используемых регистров, то уже не сможем получить доступ к адресу возврата, ведь он теперь не будет на вершине стека! И это не единственная коллизия такого рода. Смотрите: перед запуском подпрограммы нам надо запомнить в стеке номер текущего банка памяти, чтобы потом, после запуска, восстановить его. Но если сначала мы запомним в стеке значения регистров, а потом номер банка, то как будем восстанавливать значения регистров перед вызовом подпрограммы? Они ведь уже не будут на вершине стека! И ещё: как будем извлекать из стека этот номер банка после окончания работы запущенной подпрограммы? Перед этим придётся сохранить в стеке значения используемых регистров, а значит, номер банка уже не будет на вершине стека. И как же быть???
Вопрос 4. А как, собственно, запускать подпрограмму? Пусть нам
известен её адрес, ну и что? Записать этот адрес в поле операнда команды CALL
и выполнить её? Ага, разбежались! :) Ещё раз повторю: если в
реентерабельном участке кода требуется
Ответы на эти два вопроса заключаются в нетрадиционном использовании стека и команд работы с ним. Думаю, лучший способ объяснить это подробно рассмотреть все манипуляции со стеком в менеджере.
На рисунках справа от каждого элемента стека указывается его длина в байтах. Вершина стека изображена сверху, но учитывайте, что в памяти стек хранится перевёрнутым: вершине соответствует самый низкий адрес, или, как говорят, стек растёт вниз. Адрес вершины стека содержится в регистре SP.
Исходное состояние при вызове менеджера:
| Адрес возврата | 2 |
| ................. |
Резервируем в стеке 5 байтов. Для этого достаточно уменьшить SP на 5 (не забываем: стек растёт вниз!). Уменьшение выполняется так: DEC SP: PUSH HL: PUSH HL. Что будет занесено в стек командами PUSH, в данном случае совершенно не важно: мы используем их лишь для уменьшения SP (потому что PUSH на байт короче, чем две команды DEC SP).
| Резерв | 5 |
| Адрес возврата | 2 |
| ................. |
Сохраняем в стеке HL, DE, AF.
| AF | 2 | 11 |
| DE | 2 | |
| HL | 2 | |
| Резерв | 5 | |
| Адрес возврата | 2 | |
| ................. |
Зная адрес вершины стека и смещение элемента в стеке, можно получить к нему доступ, даже если это не верхний элемент! А смещение адреса возврата мы знаем: как видно из рисунка, оно равно 11. По адресу возврата определяем номер вызываемой подпрограммы и номер банка памяти, в котором она находится.
Для описания следующих действий удобно изобразить содержимое стека так:
| AF | 2 | 10 |
| DE | 2 | |
| HL | 2 | |
| Резерв | 4 | |
| Резерв | 1 | |
| Адрес возврата | 2 | |
| ................. |
Определяем адрес вызываемой подпрограммы по её номеру. Сохраняем номер текущего (т.е. установленного при вызове менеджера) банка памяти в нижнем из ранее зарезервированных байтов стека. Как видно из вышеприведённого рисунка, его адрес равен SP+10.
| AF | 2 |
| DE | 2 |
| HL | 2 |
| Резерв | 4 |
| Номер банка, подключённого при вызове менеджера | 1 |
| Адрес возврата | 2 |
| ................. |
Записываем в оставшиеся четыре байта резерва адрес, по которому будет передано управление при выполнении команды 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 | |
| ................. |
По адресу SP+4 находится номер банка памяти, который был подключён при вызове менеджера. Устанавливаем этот банк. Снимаем ранее запомненные в стеке значения HL и AF. Теперь содержимое всех регистров такое же, какое было при выходе из подпрограммы.
| Номер банка, подключённого при вызове менеджера | 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
;В 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_EXIT\256 ;Младший байт.
;Адрес вызываемой подпрограммы:
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
Фрагмент, в котором по коду
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 ..........
может быть оптимизирован так:
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 такта быстрее исходного варианта.
Как видим, если с помощью менеджера вызываются подпрограммы в
шестом банке памяти, то менеджер оказывается длиннее и работает медленнее. А
всё потому, что среди
Но есть способ оптимизации, позволяющий обойти это неудобство! Правда, при
его использовании требуется, чтобы хотя бы в одном из восьми банков памяти не
было вызываемых с помощью менеджера подпрограмм (в реальных программах это
условие,
Смотрите: если определять выводимое в порт #7FFD число по коду
Две команды 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_0bank_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
|
Другие мои статьи о различных приёмах оптимизации при программировании на ассемблере Z80:
|