Zx spectrum формат файла tap
Как это ни странно, даже сейчас, спустя столько десятилетий, есть множество людей, которым интересен ZX-Spectrum. И дело не ограничивается программными эмуляторами, нет. У этих людей есть вполне себе настоящие, “железные” спектрумы. Подавляющее большинство этих компьютеров оснащено дисководами, но есть и экземпляры только с магнитофонным входом. Такой компьютер можно загрузить, например, с аудиоплейера. Но при таком способе загрузки неудобно переходить между блоками данных внутри аудиофайла, например, если игра требует загрузки уровней. Да и места аудиофайлы занимают порядочно… Есть, конечно, ещё разные программы для смартфонов, воспроизводящие форматы файлов данных для спектрума tap и tzx. Но можно для этих же целей собрать аппаратный эмулятор магнитофона, описанный в этой статье.
Описываемый эмулятор собирается на базе микроконтроллера atmega16 и способен воспроизводить tap-файлы, лежащие на SD-карте. Записывать на SD-карту файлы он не умеет (да мне это и не требовалось).
Внешний вид эмулятора магнитофона в моём исполнении.
Схема эмулятора представлена на рисунке ниже.
Схема эмулятора магнитофона.
В схеме использован дисплей 1602, микроконтроллер atmega16 и динамическое ОЗУ MB81C4256. Зачем нужно ОЗУ в таком эмуляторе, ведь можно последовательно считывать два блока (один читаем, другой выводим) с карты памяти? Да, можно. Но применение большого ОЗУ упрощает программу – все выводимые данные целиком находятся в ОЗУ, и достаточно просто последовательно их читать и выводить. Кроме того, наличие ОЗУ позволяет разогнать скорость вывода сигнала практически до максимальной для ZX-Spectrum. Это, правда, потребует существенной модификации программы загрузки в ПЗУ спектрума. В данном эмуляторе максимальная скорость вывода данных в четыре раза больше, чем стандартная скорость загрузки спектрума. То есть, требуется модифицированное ПЗУ. Прошивки такого модифицированного ПЗУ представлены в архиве.
Формат tap-файла очень прост: 2 байта – размер блока, за которыми следуют данные блока. И так до исчерпания всех блоков.
Магнитофонный сигнал с ZX-Spectrum представляет собой частотно-модулированный сигнал, при этом самой высокой частотой закодированы ноль и синхросигнал (частота синхросигнала чуть выше, чем у ноля). Частотой в 2 раза ниже частоты ноля закодирована единица. Частотой в 2.5 раза ниже частоты ноля закодирован пилот-тон (звуки пи-и-и-и-и в начале загрузки). На рисунке показан формат сигнала в тактах процессора Z80 (частота в ZX-Spectrum 3.5 МГц, если кто забыл). Сначала идёт длительный (несколько секунд) пилот-тон, затем следует синхросигнал, а после него уже выдаются данные.
Формат магнитофонного сигнала ZX-Spectrum.
Собственно, ничего сложного тут вовсе нет. Если такой сигнал выдать с микроконтроллера, то спектрум его с радостью примет и загрузится. Для генерации сигнала в программе использован обычный таймер, переключающий выход магнитофона через заданные промежутки времени.
Формат файла tap zx spectrum
The .TAP files contain blocks of tape-saved data. All blocks start with two bytes specifying how many bytes will follow (not counting the two length bytes). Then raw tape data follows, including the flag and checksum bytes. The checksum is the bitwise XOR of all bytes including the flag byte. For example, when you execute the line SAVE "ROM" CODE 0,2 this will result:
13 00 00 03 52 4f 4d 7x20 02 00 00 00 00 80 f1 04 00 ff f3 af a3
^^^^^. first block is 19 bytes (17 bytes+flag+checksum)
^^. flag byte (A reg, 00 for headers, ff for data blocks)
^^ first byte of header, indicating a code block
file name ..^^^^^^^^^^^^^
header info . ^^^^^^^^^^^^^^^^^
checksum of header . ^^
length of second block . ^^^^^
flag byte . ^^
first two bytes of rom . ^^^^^
checksum (checkbittoggle would be a better name!). ^^
Note that it is possible to join .TAP files by simply stringing them together; for example, in DOS / Windows: COPY /B FILE1.TAP + FILE2.TAP ALL.TAP ; or in Unix/Linux: cp file1.tap all.tap && cat file2.tap >> all.tap
For completeness, I'll include the structure of a tape header. A header always consists of 17 bytes:
Byte Length Description
0 1 Type (0,1,2 or 3)
1 10 Filename (padded with blanks)
11 2 Length of data block
13 2 Parameter 1
15 2 Parameter 2
The type is 0,1,2 or 3 for a Program, Number array, Character array or Code file. A SCREEN$ file is regarded as a Code file with start address 16384 and length 6912 decimal. If the file is a Program file, parameter 1 holds the autostart line number (or a number >=32768 if no LINE parameter was given) and parameter 2 holds the start of the variable area relative to the start of the program. If it's a Code file, parameter 1 holds the start of the code block when saved, and parameter 2 holds 32768. For data files finally, the byte at position 14 decimal holds the variable name.
Вопрос по блокам
length of second block . ^^^^^
flag byte . ^^
first two bytes of rom . ^^^^^
checksum (checkbittoggle would be a better name!). ^^
что это за блок и откуда берутся first two bytes of rom (в каждом файле они разные)
Нужно ли этот блок считывать.
Размещение рекламы на форуме способствует его дальнейшему развитию
На самом деле, формат файла .TAP очень простой. Формат предназначен для хранения данных, записанных на кассете магнитофона на стандартной скорости записи, без информации о паузах и т.п.
Каждый блок данных предваряется двумя байтами его длины, и дальше идут собственно данные. Это всё.
2 байта длины (младший вначале), затем данные блока последовательно, начиная с первого флагового байта, заканчивая последним байтом контрольной суммы.
Собственно, формат о них ничего не знает, и первый и последний байт могут быть произвольными, но для корректной загрузки стандартными загрузчиками обычно первый байт - флаговый, последний - байт контрольной суммы (XOR всех предыдущих байтов блока).
Формат стандартного заголовочного блока Бейсика такой:
1 байт - флаговый, для блока заголовка всегда равен 0 (для блока данных за ним равен 255)
1 байт - тип Бейсик блока, 0 - бейсик программа, 1 - числовой массив, 2 - символьный массив, 3 - кодовый блок
10 байт - имя блока
2 байта - длина блока данных, следующего за заголовком (без флагового байта и байта контрольной суммы)
2 байта - Параметр 1, для Бейсик-программы - номер стартовой строки Бейсик-программы, заданный параметром LINE (или число >=32768, если стартовая строка не была задана. Для кодового блока - начальный адрес блока в памяти. Для массивов данных - 14й-байт хранит односимвольное имя массива
2 байта - Параметр 2. Для Бейсик-программы - хранит размер собственно Бейсик-програмы, без инициализированных переменных, хранящихся в памяти на момент записи Бейсик-программы. Для остальных блоков содержимое этого параметра не значимо, и я почти уверен, что это не два байта ПЗУ. Скорее всего, они просто не инициализируются при записи.
1 байт - контрольная сумма заголовочного блока.
Считывать блок заголовка нужно, если программа не знает точных параметров блока данных за ним. Если знает, его можно пропустить.
Понял, откуда 2 байта ПЗУ. В примере описано содержимое файла .TAP, для сохраненного из Бейсика командой <SAVE "ROM" CODE 0,2>
блока кодов ПЗУ длиной 2 байта. Это актуально только для данного примера:
Как это ни странно, даже сейчас, спустя столько десятилетий, есть множество людей, которым интересен ZX-Spectrum. И дело не ограничивается программными эмуляторами, нет. У этих людей есть вполне себе настоящие, “железные” спектрумы. Подавляющее большинство этих компьютеров оснащено дисководами, но есть и экземпляры только с магнитофонным входом. Такой компьютер можно загрузить, например, с аудиоплейера. Но при таком способе загрузки неудобно переходить между блоками данных внутри аудиофайла, например, если игра требует загрузки уровней. Да и места аудиофайлы занимают порядочно… Есть, конечно, ещё разные программы для смартфонов, воспроизводящие форматы файлов данных для спектрума tap и tzx. Но можно для этих же целей собрать аппаратный эмулятор магнитофона, описанный в этой статье.
Описываемый эмулятор собирается на базе микроконтроллера atmega16 и способен воспроизводить tap-файлы, лежащие на SD-карте. Записывать на SD-карту файлы он не умеет (да мне это и не требовалось).
Внешний вид эмулятора магнитофона в моём исполнении.
Схема эмулятора представлена на рисунке ниже.
Схема эмулятора магнитофона.
В схеме использован дисплей 1602, микроконтроллер atmega16 и динамическое ОЗУ MB81C4256. Зачем нужно ОЗУ в таком эмуляторе, ведь можно последовательно считывать два блока (один читаем, другой выводим) с карты памяти? Да, можно. Но применение большого ОЗУ упрощает программу – все выводимые данные целиком находятся в ОЗУ, и достаточно просто последовательно их читать и выводить. Кроме того, наличие ОЗУ позволяет разогнать скорость вывода сигнала практически до максимальной для ZX-Spectrum. Это, правда, потребует существенной модификации программы загрузки в ПЗУ спектрума. В данном эмуляторе максимальная скорость вывода данных в четыре раза больше, чем стандартная скорость загрузки спектрума. То есть, требуется модифицированное ПЗУ. Прошивки такого модифицированного ПЗУ представлены в архиве.
Формат tap-файла очень прост: 2 байта – размер блока, за которыми следуют данные блока. И так до исчерпания всех блоков.
Магнитофонный сигнал с ZX-Spectrum представляет собой частотно-модулированный сигнал, при этом самой высокой частотой закодированы ноль и синхросигнал (частота синхросигнала чуть выше, чем у ноля). Частотой в 2 раза ниже частоты ноля закодирована единица. Частотой в 2.5 раза ниже частоты ноля закодирован пилот-тон (звуки пи-и-и-и-и в начале загрузки). На рисунке показан формат сигнала в тактах процессора Z80 (частота в ZX-Spectrum 3.5 МГц, если кто забыл). Сначала идёт длительный (несколько секунд) пилот-тон, затем следует синхросигнал, а после него уже выдаются данные.
Формат магнитофонного сигнала ZX-Spectrum.
Собственно, ничего сложного тут вовсе нет. Если такой сигнал выдать с микроконтроллера, то спектрум его с радостью примет и загрузится. Для генерации сигнала в программе использован обычный таймер, переключающий выход магнитофона через заданные промежутки времени.
В отличие от современных компьютеров, на спектрумах понятия файловой системы не было как такового. Это значит, что загрузка с каждого типа носителя требовала отдельной реализации и в большинстве случаев программу нельзя было просто так скопировать с кассеты на дискету. В случаях, когда загрузчик программы был написан на бейсике, его можно было адаптировать к TR-DOS довольно простой доработкой. Однако ситуация осложнялась тем, что во многих играх (как фирменных так и взломанных) загрузчики были написаны в машинных кодах и иногда содержали защиту от копирования.
Несмотря на наличие «волшебной кнопки», которая просто делала полный дамп памяти компьютера и позволяла хоть как-то сохранить программу на дискету, среди специалистов считалось хорошим тоном создавать дисковые версии игр с сохранением оригинальной загрузочной картинки и прочих атрибутов.
В этой статье я расскажу, как выполнить такую адаптация на примере игры Pac-Man, а именно, оригинального образа Pac-Man.tzx.
Инструменты
Несмотря на то, что в былые времена вся такая работа делалась непосредственно на ZX Spectrum (за отсутствием других вариантов), я буду адаптировать игру с использованием эмулятора и утилит командной строки. Основная причина в том, что особенно поначалу процесс адаптации состоит из большого количества проб и ошибок, и он проходит гораздо менее болезненно, если его автоматизировать. Всё то же самое можно проделать и непосредственно на спектруме.
В первой части мы будем использовать следующие инструменты:
- Эмулятор Fuse для отладки и тестирования. для дизассемблирования.
Отключение автозапуска в загрузчике
Поскольку файл картинки и данных загружается без заголовочного блока (17 байт с именем и типом файла), это означает, что загрузчик написан в машинных кодах. Нужно найти, где эти коды располагаются и с какого адреса запускаются.
Есть несколько способов посмотреть на код загрузчика:
Самый простой — начать загружать программу, дождаться, пока загрузчик запустится, и остановить его нажатием клавиши Space . Во многих случаях это работает, но в случае с Pacman, как и во многих других, это приводит к сбросу.
Следующий способ — загрузить программу с использованием MERGE "" вместо LOAD "" . В отличие от LOAD , MERGE игнорирует автозапуск программы. В случае с Pac-Man загрузка через MERGE приводит к зависанию компьютера с характерным сдвигом экрана влево. Это связано с тем, что вместо того, чтобы выполнять программу построчно, MERGE пытается разобрать её целиком и слить с уже загруженной программой. Однако, если в программе есть блок с машинными кодами, который нарушает синтаксис программы, это приводит к сбою.
Если не хочется ломать голову, можно преобразовать образ ленты из TZX в TAP и воспользоваться утилитой listbasic , которая поставляется вместе с Fuse:
Адрес 23635 ( $5C53 ) соответствует системной переменной PROG , которая содержит начальный адрес области бейсика. Таким образом, точка входа в загрузчик смещена на 91 байт относительно области бейсика.
Ещё один способ посмотреть на загрузчик описан в статье Desativando a autoexecução de um programa BASIC. В отладчике Fuse нужно поставить точку останова br 2053 , загрузить программу, а когда загрузка закончится и выполнение кода прервётся, выполнить записать set 23619 128 . Это предотвратит автозапуск программы и позволит выйти в бейсика.
Дизассемблирование загрузчика
Зная смещение точки входа относительно области бейсика, можно рассчитать её абсолютный адрес. В случае с ZX Spectrum 48К без загруженной TR-DOS, область бейсика начинается с адреса 23755 ( $5CCB ). Следовательно, загрузчик будет начинаться с адреса 23755 + 91 = 23846 ( $5D26 ).
Для начала достаточно поставить точку останова на начальном адресе и посмотреть на машинные коды. В Fuse можно сделать br 23846 и начать загружать программу. Как только загрузчик начнёт выполняться, эмулятор остановится:
В случае, когда загрузчик совсем простой, достаточно посмотреть на дизассемблированный код в средней панели и понять, что куда загружается. Обычно код загрузки беззаголовочного файла выглядит приблизительно так:
Если кратко, нужно сделать следующее:
- Сделать снапшот Pac-Man.z80 памяти компьютера, используя tap2sna.py или возможности эмулятора.
- Создать контрольный файл Pac-Man.ctl с начальным набором инструкций для дизассемблирования:
- Запустить дизассемблирование: sna2skool.py -H -c Pac-Man.ctl Pac-Man.z80 > Pac-Man.skool .
- В ходе изучения кода добавлять новые инструкции и комментарии в контрольный файл.
- Повторять до полного просветления.
В результате, после первого прохода получаем следующее (комментарии мои, адреса опущены):
Расшифровка загрузчика
Всё, что из этого действительно важно, это то, что расшифрованный загрузчик находится по адресу PROG + $35 . Это значит, что если мы поставим точку останова br 23808 , то к этот момент расшифровка уже выполнится мы увидим расшифрованный загрузчик:
Эта программа уже гораздо более похожа на типичный случай, упомянутый выше. В регистры IX и DE загружается значение $4000 ( 16384 ), делается что-то ещё и передаётся управление подпрограмме ПЗУ по адресу $055A (это на несколько байт ниже чем стандартная точка входа в LD-BYTES ). Похоже, такой подход реализует какую-то защиту от копирования, т.к. стандартной процедурой этот файл не загружается и некоторые копировщики его не понимают.
Точка входа в программу
Осталось разобраться, как же вызывается программа после загрузки. Вместо привычного CALL LD-BYTES и JP здесь используется LD SP, XXXX и JP LD-BYTES . Первый (обычныйы) вариант работает следующим образом:
- CALL кладёт на стек текущее значение программного счётчика ( PC ).
- Управление передаётся вызываемой подпрограмме.
- При возврате из подпрограммы ( RET ) значение со стека снимается и происходит переход в вызывающую программу.
Почему здесь сделано иначе? Дело в том, что Pac-Man совместим с ZX Spectrum 16K и занимает абсолютно всю оперативную память (см. размер файла выше). Таким образом, загружаясь, программа затирает собой и загрузчик, и стек, где бы они ни находились. Если бы мы хотели перейти из ПЗУ в загрузчик с использованием стека и далее вызывать загруженную программу через JP , на момент окончания загрузки ни адреса, по которому находится JP , ни самой инструкции в памяти уже не было бы.
Вместо этого указатель стека перемещается на область памяти, по которому после загрузки окажется адрес точки входа в программу, и процессор, не заметив подмены, снимет его со стека по новому указателю и перейдёт по указанному адресу.
Итого
В результате изучения загрузчика мы выяснили следующее:
- Беззаголовочный файл длиной 16384 байт загружается по адресу 16384 (в экранную область, что в общем-то очевидно в процессе загрузки).
- По окончании загрузки указатель стека находится по адресу $5D7C , куда и передаётся управление.
В следующих частях я расскажу, о том как подготовить файлы для записи на диск и написать загрузчик моноблочного файла на ассемблере.
Читайте также: