Некоторое время назад копался, любопытствуя, в материалах по программированию под ZX Spectrum 128 и понравилось несколько вещей технического плана. Я этих вещей не знал, т.к. в детстве у меня был клон ZX Spectrum 48 с магнитофоном в качестве накопителя, а вот для некоторых многое здесь рассказанное будет не ново. Здесь я рассмотрю методы расширения базового функционала ZX Spectrum 48 использованные в: ZX Interface 1 ZX Spectrum 128 ZX Spectrum +3 (+3 DOS) Beta 128 Disk Interface (TR–DOS) Для начала напомню слегка архитектуру ZX Spectrum 48. Базовая архитектура — ZX Spectrum 16/48Процессором в ZX Spectrum 48 трудится 8–битный Zilog Z80 — самый наверное навороченный 8–битный микропроцессор из широко распространённых в своё время. В самом в начале памяти (с адреса 0) лежит 16Кб ПЗУ с бейсиком в качестве «ОС», а потому вместе со всякими сервисными процедурами. Далее с адреса 16384 (0x4000 в шестнадцатиричной системе исчисления) следуют 16 Кб памяти за которую конкурирует система видеовывода, ибо как раз в их начале лежат 6144 байт монохромного изображения 256x192 пикселей и 768 байт цветов 32x24 знакомест. Поэтому сразу за этим всем добром, ровнёхонько с адреса 23296 (0x5B00) располагаются системные переменные, которые используются ПЗУ/бейсиком и на которые оно нередко рассчитывает. Программисту на ассемблере полностью захватить всё ОЗУ до последнего байта в своё распоряжение становится возможным только перестав использовать процедуры из ПЗУ и перехватив и начав обрабатывать самостоятельно единственное маскируемое прерывание — VBlank — оно активируется 50 раз в секунду видеосистемой, когда она заканчивает формировать видеосигнал кадра телевизионного изображения. Дело тут в том, что штатный обработчик прерывания находится по адресу 0x38 в ПЗУ и обновляет некоторые системные переменные — таймер «тиков» со старта компьютера и последнюю нажатую на клавиатуре клавишу, например. Так что программист на ассемблере должен это учитывать и либо ставить собственный обработчик прерываний, либо полагаться на данное поведение ПЗУ с бейсиком как на «родное». Вообще в ПЗУ бейсика немало располагается полезных процедур. На будущее надо заметить, что 256 первых байт области системных переменных использовано под буфер печати принтера, который так редко использовался, что программисты нередко спихивали в него всякие штуки — особенно на первой самой модели ZX Spectrum 16, где, как понятно из названия, было всего 16Кб ОЗУ, то есть больше памяти и не было. Так что можно сказать, что самые важные системные переменные начинались с адреса 23552 (0x5C00). Для скорости работы штатных процедур ПЗУ предполагает всегда, что в одном из двух индексных регистров процессора Z80, а именно — IY, находится адрес примерно середины области системных переменных, так что к ним всегда можно обращаться косвенной индексацией, например LD A, (IX+VarX), поэтому нельзя вызывать процедуры ПЗУ не выполнив это правило — в результате этим индексным регистром в спектруме либо вообще не пользуются, либо запрещают прерывания, сохраняют и, попользовавшись, возвращают обратно. Прерывания надо запрещать как раз потому что штатный обработчик, как понятно, тоже пользуется IY. Поэтому есть еще третья альтернатива по высвобождению IY — не пользоваться ПЗУ и поставить свой обработчик прерываний. Но вернемся к нашим баранам — если запустить ZX 16/48 и дождаться пока ПЗУ приведет все свои переменные в порядок, то с учетом и системных и всяких прочих вспомогательных буферов свободная память в нём будет начинаться примерно (ориентировочно) с адреса 23840 (0x5D20), то есть пользователю ZX Spectrum 16 оставалось под программу на бейсике вместе с переменными и стеком ~8900 байт. Во время появления ZX Spectrum 16 покупали и такое, но очень быстро настоящую популярность завоевала модель ZX Spectrum 48, где было добавлено еще 32Кб ОЗУ в конце 64–килобайтного адресного пространства. Соответственно программисту в норме было доступно примерно 41 с лишним килобайт памяти. Для удобства дальнейшего изложения будем рассматривать память блоками по 16Кб (страницами) и приведем карту памяти ZX Spectrum 48 (адреса растут сверху–вниз): началоназначение0x0000ROM Basic 480x4000RAM base + video0x8000RAM ext 10xC000RAM ext 2ZX Interface 1Внешним накопителем на ZX Specturm 16/48 был обычный бытовой магнитофон — информация сохранялась и записывалась на обычные аудиокассеты как серия звуковых сигналов напоминающих писк dialup-модемов (а точнее — наоборот). Это было весьма медленно и неудобно — искать файлы приходилось банальной перемоткой кассеты почти наугад и можно было легко затереть один файл другим. Сперва создатели планировали выпустить для системы дисковод, но пытаясь сохранить спектрум дешевым и доступным пришли к микродрайву — накопителю являющемуся чем то средним между магнитофоном и дискетой — закольцованная в картридже магнитная плёнка повышенной ёмкости, которая описывала полный круг за восемь секунд. Соответственно время поиска по сравнению с магнитофоном резко снизилось, а удобство повысилось, но всё это не достигло уровня дискет. Микродрайвы не подключались к ПК напрямую, а требовали наличия так называемого модуля расширения ZX Interface 1 — это подставка куда втыкался спектрум своим слотом для расширений и она, кроме поддержки микродрайвов, позволяла еще объединять компьютеры в сеть и наделяла их последовательным портом. В сущности порт/слот расширения ZX Spectrum — это «обнажённая» системная шина компьютера. Сюда были выведены: 16 линий шины адреса, 8 линий шины данных и ряд управляющих проводов. Одной из таких управляющих линий была ROMCS (ROM Chip Select), с помощью управления которой внешнее устройство могло подавить активность ROM-чипа в кишках самого спектрума, а считывая линии адреса при запросах к нему подставлять на линии данных информацию из собственных микросхем. Именно так и делал ZX Interface 1 — он содержал 8Кб дополнительного ПЗУ и активировал его в нужные моменты, подавляя ПЗУ Бейсика, то есть выполняя переключение нижнего банка/страницы памяти. А вот как эти «нужные моменты» определялись — это я и хочу рассказать. В процессоре Z80 был ряд команд — RST XX — где XX менялось от 0 с шагом в 8 восемь раз, которая по сути была быстрым (однобайтовым) способом вызвать процедуру по одному из восьми адресов XX (альтернатива трёхбайтовой команде CALL, которая могла вызвать процедуру из любого места памяти). В силу краткости этих инструкций они нередко использовались для каких то особо частых операций в машине — например вывод символа. Одной из таких частых инструкций была RST 8 — вызов процедуры обработки ошибок Бейсика, которая, как понятно находилась по адресу 8 в ПЗУ. Модуль ZX Interface 1 постоянно отслеживал по шине адреса откуда процессор пытается считывать данные и при встрече адреса 8 выполнял переключение ПЗУ на своё теневое. Это позволяло сделать изящный трюк — включить в систему команд бейсика команды по работе с микродрайвом, которых в ПЗУ Basic 48 изначально никогда не было — когда сразу после таких команд как LOAD или SAVE ставился знак умножения. Например обычная команда загрузки файла с именем «file» с магнитофона LOAD «file» переписывалась для микродрайва следующим образом: LOAD *«m»;1;«file». ПЗУ оригинального бейсика встретив такую конструкцию порождала ошибку «Nonsense in Basic» для чего уходила в RST 8, где микродрайв бережно перехватывал управление, проверял вызвавшую ошибку строку бейсика и, если это была инструкция для микродрайва (или локальной сети), то подавлял ошибку и выполнял нужную команду. Забавно, что точкой входа в процедуры микродрайва из машинного кода была эта же самая RST 8, просто вместо кодов ошибки бейсика в неё загонялись другие коды, обозначающие уже непосредственные команды для микродрайва. Что код ошибки бейсика, что код команды из ассемблера должен был быть следующим байтом после инструкции RST 8. Так что данный механизм вызова процедур ZX Intefrace 1 очень похож на механизм вызова команд BIOS или DOS в IBM PC, за исключением того как именно передаётся номер функции и куда именно внедрен перехватчик. Как и в последнем случае здесь возник слой изоляции — разные версии ZX Intefrace 1 имели как минимум две разных прошивки в которых многие процедуры меняли своё местоположение в памяти, но это было неважно, т.к. код обработчика сам решал куда передавать управление анализируя переданный код функции. Это был шаг вперед по сравнению с самим ПЗУ Basic 48 в котором программисты использовали процедуры по фиксированным адресам и потому все последующие ревизии и версии компьютеров с крайней неохотой в нём что либо меняли и только очень точечно — чтобы не повредить обратной совместимости. Еще один важный момент заключается в том, что переключение банков ПЗУ отстаёт на один запрос — дело в том, что модуль расширения может определить попытку считывания с «интересного» адреса только в тот момент когда это считывание уже происходит — в результате чего первая инструкция обычного ПЗУ по данному адресу уже начинает исполняться — теневое ПЗУ микродрайва просто содержит ту же самую инструкцию в данном адресе, чтобы не глючить, отличия начинаются со следующей. Этот момент мы еще вспомним в дальнейшем. Так как ZX Interface 1 подменяет только ПЗУ, то все его системные переменные располагаются после классических системных переменных. Подобный метод программно-аппаратного расширения встретится нам и позднее, но немного под другим соусом. ZX Spectrum 128Когда 48Кб ОЗУ перестало хватать на рынок был выведен ZX Spectrum 128. Разумеется без обратной совместимости с ZX 48 он был бы мало кому нужен, поэтому в нём было сделано всё возможное для неё. Основные фишки — прибавка 80Кб ОЗУ и появление музыкального чипа AY-3-8912, а так же обновлённая версия редактора Basic с небольшим расширением системы команд последнего — это потребовало включения еще одной страницы ПЗУ на 16Кб. Таким образом вместо вместо одной страницы ПЗУ и трёх страниц ОЗУ как в базовой модели в ZX Spectrum 128 было две страницы ПЗУ и восемь страниц ОЗУ. Но так как все они сразу не могли влезть в 64Кб-е адресное пространство Z80, то с помощью порта ввода-вывода 0x7FFD эти страницы надо было переключать так, что раскладка памяти теперь выглядела следующим образом: началоназначение0x0000ROM 0 (128), 1 (48)0x4000RAM 50x8000RAM 20xC000RAM 0,1,2,3,4,5,6,7То есть в первой странице ПЗУ могло быть или ПЗУ Basic 128 или ПЗУ Basic 48. В последней же странице ОЗУ (и только в ней) можно было выбирать одну из восьми страниц ОЗУ, при этом конфигурацией ZX Spectrum 48 считалась ситуация когда в ПЗУ выбрано ROM 1, а в последней странице ОЗУ выбрано RAM 0. Существовала возможность зафиксировать ПК в этой конфигурации и перестать реагировать на запись в порты переключения страниц для максимальной имитации ZX Spectrum 48 — выйти из такого режима можно было только полным сбросом. Две средних страницы ОЗУ всегда были RAM 5 и RAM 2 без возможности выбора. Сразу после включения ZX Spectrum 128 выбирал ROM 0 в качестве ROM и RAM 7 в качестве последней страницы RAM. Стек при этом уводился куда то в область системных переменных, потому что если бы он был в конце памяти (как в ZX 48), то портился бы при переключении страниц. Новые страницы ОЗУ напрямую были недоступны даже обновлённому Basic 128 — в целом он вёл себя как будто в ПК было не более типовых 48Кб, однако Basic 128 расширен таким образом, что мог использовать их как RamDisk. Он был дополнен командами сохранения и загрузки программ и данных в RamDisk через добавление восклицательного знака. Например команду сохранения файла программы на кассету SAVE «file» можно было переделать под RamDisk как SAVE! «file». Так как в бейсике сохранять и загружать можно было и части программы и массивы данных, то мгновенно работающий RamDisk становился естественным способом расширять возможности программ даже в Basic. Программисты на ассемблере конечно уже могли использовать дополнительные страницы как угодно. Обновлённому бейсику потребовались новые системные переменные — часть таковых он размещал в 7-ом банке ОЗУ (например каталог файлов RamDisk и переменные редактора Basic 128), но самые важные были размещены в том самом буфере принтера — по адресу 23296 (0x5B00). Причём с самого начала — в 0x5B00 располагалась важнейшая процедура переключения банков памяти с помощью которой Basic 128 делегировал старому Basic 48 огромную часть работ. Вот как она выглядела: Код (ASM): 5B00: PUSH AF ; сохраняем в стек AF PUSH BC ; сохраняем в стек BC LD BC, 0x7FFD ; BC = адрес порта переключения банков LD A, (0x5B5C) ; взять последнее записанное в порт значение XOR 0x10 ; инвертировать в нём бит ответственный за страницу ПЗУ DI ; отключить маскируемые прерывания LD (0x5B5C) ; обновить последнее записанное в порт значение OUT (C), A ; записать в порт ввода-вывода (из BC) новое значение EI ; включить прерывания POP BC ; восстанавливаем BC POP AF ; восстанавливаем AF RET ; выходим из процедуры Процедура довольно проста и прямолинейна — т.к. порт 0x7FFD доступен только на запись, то в системной переменной по адресу 0x5B5C должно храниться последнее записанное в него значение. Оно берется, в нём инвертируется бит ответственный за активную страницу ПЗУ и всё обновляется, после чего, без порчи регистров, процедура возвращается к вызвавшему его коду. Так как эта процедура располагается во второй странице ОЗУ, то она всегда доступна какие бы банки не были выбраны в ПЗУ или верхней странице ОЗУ. Помните я говорил, что в ZX Spectrum 48 50 раз в секунду по VBlank генерируется прерывание исполняющее код по адресу 0x38? ПЗУ Basic 128 умудряется даже эту работу делегировать в ПЗУ Basic 48! Давайте посмотрим как это делается. Вот что располагается в ПЗУ 128 по адресу 0x0038: Код (ASM): 0038: PUSH HL LD HL, 0x0048 PUSH HL LD HL, 0x5B00 PUSH HL LD HL, 0x0038 PUSH HL JP 5B00 0048: POP HL RET Тут происходит маленькая магия — сперва код прерывания сохраняет регистровую пару HL, т.к. портит её. Потом сохраняет на стек три адреса и просто переходит (а не вызывает) на процедуру в 5B00. Запомним как выглядит вершина стека сразу после того как код прыгнет на 5B00 (адреса растут снизу-вверх): Код (ASM): . . . старый HL 0x0048 0x5B00 0x0038 Итак, возможно вы уже начинаете догадываться что тут происходит — код в 5B00 сменит банк ПЗУ и вернётся по адресу на вершине стека, то есть перейдёт на выполнение кода по адресу 0x0038 — а это получится код обработки прерывания в ПЗУ 48! Когда код в ПЗУ 48 сделает свои дела и выполнит инструкцию RET он в свою очередь вернётся в следующий адрес на вершине стека — 0x5B00, который является… опять процедурой смены банков ПЗУ, которая восстановит ПЗУ 128 и вернется уже в адрес 0x0048 — то есть в конец процедуры обработки прерывания в ПЗУ 128, где уже восстанавливается регистр HL и происходит завершение обработки прерывания. Вот так ПЗУ Basic 128 делегирует в старое почти неизменённое ПЗУ Basic 48 огромное число работ. Замечу, что даже в режиме Basic 128 когда мы запускаем программу Basic на выполнение командой RUN, то конфигурация памяти чаще всего находится в состоянии эквивалентном ZX Spectrum 48. ZX Spectrum +3 (+3 DOS)В следующем году после выпуска ZX 128 Sinclair Research Ltd выпускает модель ZX Spectrum +3 главной фичей которой является наличие дисковода гибких дисков. Это потребовало удвоения числа страниц ПЗУ — понадобилась страница под код для работы с дисководом и еще одна страница для расширения Basic 128, чтобы он смог использовать эти возможности. В связи с этим добавился еще один порт ввода-вывода 0x1ffd посредством которого можно было выбирать две новые страницы ПЗУ (помимо прочего), а так же активировать несколько режимов, когда ПЗУ вообще исчезало из памяти и вся она отдавалась под ОЗУ (главным образом для поддержки операционной системы CP/M). За счёт расширения Basic 128 до так называемого Basic +3 еще одной страницей ПЗУ дисковые операции были внедрены в него совершенно бесшовным образом. Например чтобы загрузить файл достаточно было написать LOAD «file» — в точности так как раньше грузились файлы с кассеты. Но ZX Spectrum +3 при этом попытается загрузить файл с первого дисковода «a:». Можно было подключить второй дисковод «b:» и грузить файлы указывая дисковод явно: LOAD «b:file». Был даже устранён знак "!" для работы с RamDisk — он просто стал рассматриваться как дисковод «m:», то есть, например: SAVE «m:file». Любой дисковод можно было сделать текущим командами SAVE/LOAD не указывая имя файла, например подряд выполненные команды LOAD «m:» и SAVE «file» сохранят файл в RamDisk. Более того — чтобы команды сохранения/загрузки в Basic +3 начали работать с кассетным магнитофоном (обеспечивая совместимость с Basic 128) нужно было просто сделать текущим дисководом виртуальный дисковод «t:» (tape). Таким образом с точки зрения пользователя Basic +3 становился самим +3 DOS, а +3 DOS был бесшовно вшит в Basic +3 — еще бесшовнее, чем расширял синтаксис бейсика ZX Interface 1 для работы с микродрайвом. С точки зрения программиста на ассемблере работа с +3 DOS была чуть сложнее — необходимо было активировать ПЗУ с +3 DOS и в верхней странице ОЗУ выбрать RAM 7. После этого по адресу 256 (0x100) в ПЗУ окажется таблица переходов на пронумерованные функции DOS. Переход в эту конфигурацию памяти и возврат из неё программист должен был делать сам. Beta 128 Disk Interface (TR-DOS)Плат расширения для подключения дисководов к ZX Spectrum (как 48 так и 128) появилось несколько от разных фирм и в России получил широчайшее распространение далеко не самый популярный у себя на родине (и вообще на западе) вариант. Английская компания Technology Research Ltd в 1985 году выпустила на рынок интерфейс подключения дисководов к ZX Spectrum 48 — Beta Disk Interface. А в 1987 году она доработала его до подключения к ZX Spectrum 128 и этот вариант получил название Beta 128 Disk Interface — именно он и покорил сердца российских создателей клонов спектрума. Данная плата содержала в себе страницу ПЗУ с операционной системой TR-DOS (от Technology Research) и подключала её в ПЗУ спектрума по той же технике как это делал ZX Interface 1 — перехватом адреса выполнения процессора. Но этот адрес был уже не 8, а целое семейство адресов в ПЗУ ПК — от 15616 до 15871. Если взглянуть на этот диапазон в 16-ричной системе исчисления — от 0x3D00 до 0x3DFF, то становится понятно, что активация теневого ПЗУ TR-DOS происходит когда верхний байт адреса становится равен 0x3D (в версии для ZX Spectrum 48 это был 0x3C). Такой выбор не случаен — в оригинальном ПЗУ спектрума по этим адресам лежат таблицы шрифтов. Вы можете спросить — а что произойдёт когда ПК будет рисовать текст и для этого читать эти ячейки памяти? А ничего — дело в том, что процессор Z80 имеет выходные линии на своих ножках по которым он сообщает внешнему миру что именно он читает или записывает по шине данных выставляя адреса на шине адреса. Возможны три варианта: чтение/запись данных из памяти, чтение/запись данных из портов ввода–вывода и, наконец, чтение инструкций кода. Таким образом гипотетически с этим процессором можно реализовать систему с двумя независимыми адресными пространствами — для кода и данных, но на практике это всё было единое пространство. Так или иначе данные сигналы были выведены и на шину подключения внешних устройств, поэтому контроллер Beta Disk Interface мог отделить попытку исполнить инструкции по адресам 0x3Dnn от попытки считать оттуда данные, чем он и пользовался для активации ПЗУ TR-DOS только в нужные моменты (вообще то так поступает и ZX Interface 1, но для него это непринципиальный момент). Пришло время вспомнить еще одну деталь — как я говорил выше, активация теневого ПЗУ могла немного отставать и порождать некие коллизии во время подмены ПЗУ и процессор мог успеть считать один байт инструкции из обычного ПЗУ. Как с этим справлялся TR–DOS? А довольно просто — символы шрифта почти всегда содержат пустые полоски пикселей, а иногда и только их (символ пробела), а с точки зрения процессора Z80 такая полоска является инструкцией с кодом 0 — NOP — отсутствие операции, то есть процессор исполняя её ничего не делал. Точки входа в TR–DOS были выбраны таким образом чтобы попадать в эти пустые места и таким образом первая инструкция из нормального ПЗУ ничего не делала, а дальше уже управление перехватывало теневое ПЗУ. Следует отметить, что в российских клонах такая проблема судя по всему вообще не возникает и эти NOP-ы можно игнорировать прыгая на инструкцию дальше, но в целях совместимости они в ПЗУ TR-DOS до сих пор присутствуют. Возврат к обычному ПЗУ происходил автоматически при выходе счётчика команд за пределы адресов ROM, то есть код находящийся в ОЗУ компьютера в принципе не мог считывать данные из ПЗУ TR–DOS, хотя сделать последнее можно было косвенными путями. Точек входа было несколько. Оригинальный ZX Spectrum 128 будучи подключенным к Beta 128 Disk Interface при запуске вёл себя как обычно. Чтобы активировать TR-DOS надо было выполнить команду RANDOMIZE USR 15616 — это переключало нас в интерактивную командную строку TR-DOS, где пользователь мог набирать команды специфичные для TR-DOS. Создатели российских клонов для удобства переделали ПЗУ Basic 128 чтобы меню запуска содержало пункт входа в TR-DOS. Чтобы вернуться к программированию в бейсик из TR-DOS надо было в последнем набрать команду RETURN. Следует заметить, что основная масса ПЗУ бейсика оставалась нетронутой, поэтому его ядро ничего не знало ни про TR-DOS ни про диски. Чтобы активировать функции TR-DOS из бейсика необходимо было выполнять следующий трюк: в строке кода бейсика в конце написать: RANDOMIZE USR 15619:REM: команда TR-DOS Адрес 15619 является еще одной точкой входа в TR-DOS — с автоматическим возвратом в бейсик. То есть при выполнении этой команды в Basic он передаёт управление в TR-DOS, который анализирует системную переменную в которой хранится текущая исполняемая команда Basic и извлекает комментарий из конца строки чтобы исполнить его как команду TR-DOS, после чего управление передаётся обратно в Basic и он выполняет программу дальше. Например: RANDOMIZE USR 15619:REM:LOAD «program» Понятно, что такой механизм вызова функций TR-DOS из программ на Basic является довольно таки громоздким и неудобным по сравнению с тем же +3 DOS. Это была плата за дешевизну Beta 128 Disk Interface — и именно благодаря этой дешевизне данный способ подключения дисководов к спектрумам и стал таким популярным в России в таких клонах спектрума как Scorpion или Pentagon. Для программиста на ассемблере существовали другие точки входа в TR-DOS, например 15635 (0x3D13) — сюда в регистре процессора C передавался номер функции. Так или иначе, но числа 15616 и 15619 впечатаны в память многих русскоязычных пользователей ZX Spectrum-совместимых компьютеров с бахромой и золотым тиснением. Удивительно, но эта местечковая популярность TR-DOS в пост-СССР аукнулась и западному пользователю — из-за дешевизны в своё время сам спектрум стал столь популярен у нас, что для него было написано большое количество и игр и демо-сцены, причём в эпоху когда из спектрумовского железа давили уже все соки. Из-за этого TR-DOS обладает в принципе самой большой библиотекой софта и игр после формата кассетного накопителя. Поэтому, к примеру, создатели эмуляторов спекки на западе для поддержания марки своих продуктов на высоте вынуждены встраивать в них поддержку советских клонов Pentagon и Scorpion с TR-DOS.