Zx spectrum формат файла tap
The Spectrum ROM routines save files to tape in two blocks, a header and a data block. Each of these blocks is encoded as a sequence of pulses.
There are also a wide variety of "custom loaders" which have been used for Spectrum software, for purposes such as copy protection or faster loading times. This article deals only with the format written by the Spectrum ROM.
Contents
Pulses
A 'pulse' here is either a mark or a space, so 2 pulses makes a complete square wave cycle.
Pilot tone: before each block is a sequence of 8063 (header) or 3223 (data) pulses, each of length 2168 T-states.
Sync pulses: the pilot tone is followed by two sync pulses of 667 and 735 T-states resp.
A '0' bit is encoded as 2 pulses of 855 T-states each.
A '1' bit is encoded as 2 pulses of 1710 T-states each (ie. twice the length of a '0')
The initial polarity of the signal does not matter - everything in the ROM loader is edge-triggered rather than level-triggered.
Speed
Due to the fact that the different bits take different time on tape, the Spectrum's tape routine does not have a well-defined speed. We can calculate some limits and typical values though; as noted above, a '0' bit takes 2 × 855 = 1710 T-states and a '1' bit takes 2 × 1710 = 3420 T-states. Assuming a 48K Spectrum which runs at 3.50 MHz (the 128K machines run at 3.54 MHz or about 1% faster; that difference is largely irrelevant here), this means:
- A stream of pure '0' bits loads at 3500000 ÷ 1710 ≈ 2046 baud
- A stream of pure '1' bits loads at 3500000 ÷ 3420 ≈ 1023 baud
- A mixed stream with equal numbers of '0' and '1' bits loads at 2 × 3500000 ÷ (3420 + 1710) ≈ 1364 baud
Real world Spectrum data tended to have more '0' bits than '1' bits, so the typical baud rate was actually a bit higher than 1364 baud.
Blocks
On the tape every block start with a marker byte (0x00 for header and 0xff for data blocks) and ends with a checksum byte.
The checksum byte is not really a sum, but a binary XOR of all header/data bytes.
Header block
The header, which is 17 bytes long, is as follows:
Byte (decimal) | 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:
Bits | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | Description |
---|---|---|---|---|---|---|---|---|---|
Numeric array | 1 | 0 | 0 | x | x | x | x | x | xxxxx = CODE(Name) - 0x60; e.g. DIM a() ⇒ 0x81, DIM y() ⇒ 0x99 |
String array | 1 | 1 | 0 | x | x | x | x | x | xxxxx = CODE(Name) - 0x60; e.g. DIM a$() ⇒ 0xc1, DIM y$() ⇒ 0xd9 |
Data block
Arrays
Number and string arrays are stored on tape in the same format as in RAM [1] except that the very first byte (the name of the array) is absent. The name of the array stored in the second byte of Parameter 1 in header block.
For example data block of a 2×3 numeric array (with small integer numbers):
Byte (decimal) | Length | Example value | Description |
---|---|---|---|
0 | 1 | 0x02 | Number of dimensions - DIM(2,3) |
1 | 2 | 0x02 0x00 | Dimension 1 - 2 |
3 | 2 | 0x03 0x00 | Dimension 2 (0x03 0x00 - 3) |
5 | 5 | 0x00 0x00 0x6f 0x00 0x00 | First number - 111 |
10 | 5 | 0x00 0x00 0x79 0x00 0x00 | Second number - 121 |
. | 5 | .. .. .. .. .. | . number |
30 | 5 | 0x00 0x00 0xd3 0x00 0x00 | Last number - 211 |
Note: although the array name is saved, you cannot just LOAD "" DATA, you have to explicitly say where to load: e.g. LOAD "" DATA w(), but you can use any letter regardless of the original array name.
BASIC programs
Program files are stored as a sequence of lines each stored the same way it is in RAM:
Note that numeric literals will be stored in both text and binary (text[] 0x0E[1] zxfloat[5]).
CODE, SCREEN$ - bytes
Code files are stored as a flat sequence of bytes, in the order they appear in RAM.
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, для сохраненного из Бейсика командой
блока кодов ПЗУ длиной 2 байта. Это актуально только для данного примера:
ZART
File Transfer Utility for ZX Spectrum / IBM PC Computers
(C) Rick Murray, 1994, Chelyabinsk
Структуры всяких штук.
typedef struct unsigned char type; тип файла
char name[10]; имя файла
unsigned int length; длина файла
unsigned int start; стартовый адрес
unsigned int blen; длина бейсика
> tHEADER;
typedef struct char name[8]; имя файла
char type; тип ('C','B', итд)
unsigned int start; стартовый адрес
unsigned int length; длина
unsigned char sex; количество секторов
unsigned char sector; стартовый сектор
unsigned char track; стартовая дорожка
> trHEADER;
Если это бейсиковский текст, то как правило start == length, а номер
стартовой строки вычисляется следующим способом:
- Берется буфер, куда считаны сектора, к-во которых берется из sex,
- От вершины буфера отсчитывается start байт,
- Следующими после этого должны идти 2 байта AA80h,
- Сразу за этими байтами - число int, обозначающее стартовую строчку.
typedef struct char name[8]; имя для TR-DOS
char type; тип ('C','B'. )
unsigned int start; стартовый адрес
unsigned int length; длина
unsigned char temp; всегда 0
unsigned char sex; количество секторов
unsigned int checksum; контрольная сумма заголовка
> dHEADER;
Сразу вслед за этим заголовком следуют данные собственно файла, имеющие
размер sex * 256 байт.
Контрольная сумма рассчитывается следующим образом:
int dhcheck()
unsigned int i,j,k;
Здесь Dhead - переменная типа dHEADER содержащая сформированный
заголовок.
Файл состоит из циклически повторяющихся блоков:
Pазмер Тип Hазначение
────────────────� �────────────────� ��───────
2 unsigned int Pазмер блока данных
1 unsigned char Тип блока (0-заголовок, FF-данные)
?? . Блок данных
1 unsigned char Контрольная сумма блока данных
Поле размера блока включает только размер самого блока данных.
Контрольная сумма считается как xor всех байт блока данных.
Аналогично файл состоит из повторяющихся кусков:
Pазмер Тип Hазначение
────────────────� �────────────────� ��───────
2 unsigned int Pазмер всего, что надо считать относящегося к файлу.
(иными словами - размер блока данных + 2, т.е. плюс
байт типа и байт crc)
1 unsigned char Тип блока (аналогично SPC)
?? . Блок данных
1 unsigned char Контрольная сумма блока данных вместе с байтом типа.
Поле размера включает размер блока данных + 2.
Контрольная сумма считается как xor фсех байт блока данных и байта типа.
TAP файл является точным отображением структуры записи на кассете за
исключением первых двух байт, вместо которых на пленке записан
лидер-сигнал и синхроимпульс.
Стандартный элемент какталога MS-DOS располагается в области таблицы
партиций и занимает по крайней мере первую половину диска MS-DOS. Для
того чтобы прочитать этот элемент необходимо по меньшей мере 12 минут,
для того чтобы направляющий ролик смог обнаружить межсекторное
пространство, а для того чтобы гарантировать правильное чтение данного
сектора, вы должны быть уверены, что сам какталог находится на диске, а
не под ним.
Спасибо за помощь.
Начал вникать, т.е. в приведенном примере
13 00 00 03 52 4f 4d 7x20 02 00 00 00 00 80 f1 04 00 ff f3 af a3
байты f3 af - два первых байта ПЗУ
и есть то что было записано командой
SAVE "ROM" CODE 0,2
в конце идет контр.сумма
согласно примеру заполню структуру
typedef struct unsigned char type; тип файла тут ставим 0 (программа)
char name[10]; имя файла ROM
unsigned int length; длина файла 0x02
unsigned int start; стартовый адрес 0x00
unsigned int blen; длина бейсика . что тут.
> tHEADER;
Формат файла 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, для сохраненного из Бейсика командой
блока кодов ПЗУ длиной 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.
Собственно, ничего сложного тут вовсе нет. Если такой сигнал выдать с микроконтроллера, то спектрум его с радостью примет и загрузится. Для генерации сигнала в программе использован обычный таймер, переключающий выход магнитофона через заданные промежутки времени.
Как мы выяснили в предыдущей части, машинные коды игры загрузить с дискеты непосредственно по адресу назначения нельзя. Мы загрузим их в другое место, а после загрузки переместим куда нужно. Кроме этого, мы хотим сделать моноблочный загрузчик, когда и загрузчик и загружаемые данные находятся в одном бейсик-файле. Такой загрузчик можно написать только в машинных кодах. При этом, поскольку файл у нас моноблочный, загрузчик в машинных кодах нужно будет поместить в комментарии к загрузчику на бейсике.
Итого, получается следующая многоходовка:
- Из бейсика передаём управление программе в машинных кодах.
- Программа в машинных кодах переносит загрузчик из области бейсика в другую область, которую не затронут машинные коды игры, и передаёт управление ему.
- Загружаем и распаковываем загрузочную картинку.
- Загружаем машинные коды игры в область, не перекрывающую область системных переменных.
- Переносим машинные коды по адресу назначения.
- Передаём управление программе.
Разработку придётся начать с середины (пункт 3). Дело в том, что для того, чтобы написать программу перемещения, нужно знать размер перемещаемой программы, а чтобы встроить машинные коды в бейсик, нужно знать размер программы перемещения.
Моноблочный загрузчик (часть в машинных кодах)
* — см. сжатие загрузочной картинки в предыдущей части;
** — распаковщик релоцируемый, так что загружать можно куда угодно.
Аналогичным образом загружаем машинные коды игры:
На этом этапе TR-DOS нам больше не нужен, можно перенести машинные коды по адресу назначения, используя инструкция процессора LDIR :
Ну и в конце концов передаём управление программе тем же образом, как и в оригинальном загрузчике — через перемещение указателя стека:
Теперь, когда код загрузчика готов, нужно скомпилировать его, чтобы знать его размер, который понадобится нам дальше.
Процедура перемещения загрузчика
Загрузчик занимает 44 байта. Теперь нужно написать процедуру перемещения загрузчика из комментариев в бейсике (пункт 2 списка в начале статьи). Заковыка состоит в том, что адрес, по которому располагается область бейсика, может меняться в зависимости от подключённой к компьютеру периферии, поэтому, чтобы определить, откуда нужно переносить данные, нужно ориентироваться или на системную переменную PROG (так же как в оригинальном загрузчике) или на программный счётчик (регистр процессора PC ).
К программному счётчику нельзя так просто доступиться — никаких инструкций процессора вроде LD HL, PC не существует. Решение я подсмотрел в Laser Compress и выглядит оно так (не особо целевое использование процедуры UNSTACK_Z ):
Далее компонуем процедуру перемещения с загрузчиком (см. готовый файл), который она будет перемещать и снова компилируем. Должно получиться 56 байт.
Здесь нужно заметить, что уже после того, как я написал этот кусок, я разобрался, что с вместо вычисления длины перемещаемой программы можно было использовать метки и дать ассемблеру самому во всём разобраться. Но для исторической справедливости оставим всё как есть.
Моноблочный загрузчик (часть на бейсике)
Теперь, когда мы знаем размер загрузчика в машинных кодах, можно написать загрузчик на бейсике и собрать всё в моноблочный файл.
Машинные коды в бейсик-файл встраивают или в комментарии, или в конец файла. Второе обычно затрудняет изучение файла и больше подходит для защиты, поэтому будем использовать первый вариант. Вариант с комментарием выглядит следующим образом:
Если бы для создания загрузчика мы не использовали никаких дополнительных утилит, нужно было бы после REM ввести произвольные символы в количестве не меньшем, чем длина программы в машинных кодах, которую мы хотим поместить на место комментария (в нашем случае 56 байт). После этого туда можно было бы загрузить программу через LOAD "" CODE PEEK 23635+256*PEEK 23636+5 и сохранить файл.
Однако, утилита bas2tap может значительно облегчить процесс, т.к. она может скомпилировать бейсик-файл и встроить в него двоичные данные, если каждый байт представлен в виде шестнадцатеричного числа в фигурных скобках. Для этого прогоним скомпилированный загрузчик через hexdump :
Вывод hexdump вставляем на место комментария в первой строке после REM и компилируем загрузчик на бейсике ( -sboot — имя файла на ленте, -a10 — номер строки автостарта):
Преобразуем загрузчик из формата tap в hobeta через промежуточный формат 0 :
Создание моноблочного файла
К этому моменту все необходимые файлы для создания образа дискеты у нас уже есть. Можно создать и образ и скопировать в него все необходимые файлы:
Принцип следующий: TR-DOS хранит избыточную информацию о размере файлов:
- Размер в секторах — используется для размещения файлов на дискете и копирования.
- Размер в байтах — используется для загрузки содержимого.
Обычно эти размеры соответствуют друг другу (256 байт на сектор), но это не обязательно. Этим мы и воспользуемся. Если изменить размер boot-файла в секторах на значение, равное суммарному размеру всех файлов, которые мы хотим загрузить, но не изменять размер в байтах, TR-DOS будет копировать все данные как один большой файл, но при этом при загрузке будет загружаться только бейсик-часть.
На настоящем спектруме или в эмуляторе нулевую дорожку можно редактировать программами типа Disk Doctor, например, Hex Disk Editor:
Но можно сделать и проще: trd-образ — это ничто иное как побайтовая копия всех данных на дискете, поэтому его можно редактировать в любом шестнадцатеричном редакторе:
Теперь у нас есть полноценный образ дискеты с моноблочным файлом. Он должен правильно загружаться и целиком копироваться с дискеты на дискету. Осталось только уменьшить размер образа. Поскольку trd-образ — это побайтовая копия, он всегда занимает 640КБ. На практике в большинстве случаев удобнее использовать формат scl, который больше похож на hobeta хранит непосредственно данные файлов:
Теперь точно всё. Процесс адаптации от начала до конце можно найти в репозитории проекта на гитхабе.
Читайте также: