Историческая эволюция дисковых накопителей в мире Spectrum

Контекст эпохи магнитных лент

Оригинальный ZX Spectrum 48K использовал обычный кассетный магнитофон для загрузки и сохранения данных со скоростью примерно 1500 бит/сек. Среднее время загрузки игры составляло 3-5 минут, что создавало спрос на более быстрые носители.

Первые попытки внедрения дисководов

Первые дисковые интерфейсы для Spectrum появились в 1983-1984 годах:

  • Mikro+ (MGT) — первый коммерчески успешный интерфейс
  • Beta Disk (MGT) — усовершенствованная версия, ставшая стандартом
  • Disciple (Romantic Robot) — альтернативное решение
  • Plus D (MGT) — интерфейс для принтера и дисковода
  • Opus Discovery — британский интерфейс с двумя дисководами

TR-DOS: рождение стандарта

Файловая система TR-DOS (Tape Recording DOS) была создана в 1984 году компанией Technology Research Ltd (позже MGT) и стала де-факто стандартом для клонов Spectrum в Восточной Европе и СССР.

Аппаратная архитектура Beta Disk Interface

Технические характеристики

Код:
Процессор: WD1793 или совместимый (например, КР1818ВГ93 в СССР)
Тактовая частота: 4 МГц
Поддержка дисководов: до 4 (обычно 1-2)
Типы дисководов: 5.25" (реже 3.5")
Емкость дискеты: 640 КБ (80 дорожек × 16 секторов × 256 байт × 2 стороны)
Интерфейс с Z80: через порты ввода-вывода
Память: собственный ROM 16 КБ, иногда дополнительный RAM

Схема подключения и адресация

Assembler Z80:
; Основные порты Beta Disk
BETA_REG        equ $1FFD   ; Основной регистр управления
BETA_DATA       equ $3FFD   ; Данные/статус
BETA_TRACK      equ $5FFD   ; Регистр дорожки
BETA_SECTOR     equ $7FFD   ; Регистр сектора
BETA_DMA        equ $FFFD   ; DMA регистр (младший/старший)

; Альтернативные адреса для разных клонов
BETA_ALT1       equ $7FFD   ; Scorpion, Pentagon
BETA_ALT2       equ $DFFD   ; Некоторые советские клоны

Принцип работы WD1793

Контроллер WD1793 содержит четыре основных регистра:

  1. Командный регистр (запись) / Регистр статуса (чтение)
  2. Регистр дорожки
  3. Регистр сектора
  4. Регистр данных

Файловая система TR-DOS: полная анатомия

Структура дискеты

Код:
Дорожки: 0-79 (односторонняя) или 0-159 (двусторонняя)
Секторы: 1-16 на дорожке
Размер сектора: 256 байт
Общая емкость: 640 КБ (односторонняя) или 1.2 МБ (двусторонняя)
Зарезервированные дорожки: 0-2 (системная информация)

Сектор 0 дорожки 0: загрузочный сектор

Assembler Z80:
BOOT_SECTOR:
    .signature      db 0, 0, $22, $22, 0, 0    ; Сигнатура TR-DOS
    .format_info    db "80 track single side"  ; Информация о формате
    .unused         ds 9, 0
    .first_free_track db 16                    ; Первая свободная дорожка
    .disk_type      db 0                       ; 0 = 80 дорожек
    .files_count    db 128                     ; Макс. файлов в каталоге
    .free_sectors   dw 0                       ; Свободные секторы
    .trdos_id       db "TR-DOS"               ; Идентификатор
    .unused2        ds 240, 0

Каталог файлов (дорожки 0-2, сектора 1-8)

Каждая запись каталога — 16 байт:
Assembler Z80:
CATALOG_ENTRY:
    .filename       ds 8, ' '      ; Имя файла (дополняется пробелами)
    .extension      db 'C'         ; Тип: C=CODE, B=BASIC, #=числовой массив
    .start          dw 0           ; Адрес загрузки
    .length         dw 0           ; Длина в байтах
    .length_sectors db 0           ; Длина в секторах
    .sector         db 0           ; Начальный сектор
    .track          db 0           ; Начальная дорожка
    .reserved       db 0

Типы файлов в TR-DOS

Код:
'B' — Basic программа
'C' — Код (машинный код)
'D' — Массив чисел (Data array)
'#' — Числовой массив
'$' — Строковый массив
'A' — ASCII файл

Программирование контроллера на ассемблере Z80

Инициализация и базовые процедуры

Assembler Z80:
; Инициализация дисковода
INIT_DISK:
    ld a, %00000001      ; Включить дисковод, мотор выкл
    out (BETA_REG), a
    ; Ждем раскрутки мотора
    ld b, 100
.spin_wait:
    halt
    djnz .spin_wait
    ret

; Выбор дисковода
; A = номер дисковода (0-3)
SELECT_DRIVE:
    and %00000011
    or %00000100        ; Бит 2 = 1 (выбор дисковода)
    out (BETA_REG), a
    ret

Чтение сектора

Assembler Z80:
; Вход: B = дорожка (0-79), C = сектор (1-16)
; Выход: HL = буфер для данных (256 байт)
READ_SECTOR:
    ; Устанавливаем дорожку
    ld a, b
    out (BETA_TRACK), a
    ; Устанавливаем сектор
    ld a, c
    out (BETA_SECTOR), a
    ; Команда чтения сектора
    ld a, %10001000     ; Чтение сектора + spin-up
    out (BETA_DATA), a
    ; Ждем завершения
.wait:
    in a, (BETA_DATA)
    bit 0, a            ; Бит busy
    jr nz, .wait
    ; Проверяем ошибки
    and %00011100       ; Ошибки: CRC, track0, index
    jr nz, .error
    ; Читаем 256 байт в буфер
    ld b, 0
    ld c, BETA_DMA
.read_loop:
    ini                 ; (HL) <- in (C), HL++, B--
    jr nz, .read_loop
    ret
.error:
    scf                 ; Устанавливаем флаг переноса = ошибка
    ret

Запись сектора

Assembler Z80:
; Вход: B = дорожка, C = сектор, HL = данные
WRITE_SECTOR:
    ; Устанавливаем дорожку и сектор
    ld a, b
    out (BETA_TRACK), a
    ld a, c
    out (BETA_SECTOR), a
    ; Команда записи сектора
    ld a, %10101000     ; Запись сектора
    out (BETA_DATA), a
    ; Записываем 256 байт
    ld b, 0
    ld c, BETA_DMA
.write_loop:
    outi                ; out (C), (HL)++, B--
    jr nz, .write_loop
    ; Ждем завершения
.wait:
    in a, (BETA_DATA)
    bit 0, a
    jr nz, .wait
    ; Проверка ошибок
    and %00011100
    ret z
    scf                 ; Ошибка
    ret

Поиск дорожки 0

Assembler Z80:
SEEK_TRACK0:
    ld a, %00001000     ; Команда восстановления (seek track 0)
    out (BETA_DATA), a
.wait:
    in a, (BETA_DATA)
    bit 0, a
    jr nz, .wait
    ; Проверяем, что нашли track 0
    and %00000100       ; Флаг track 0
    jr z, .error
    ; Сбрасываем регистр дорожки
    xor a
    out (BETA_TRACK), a
    ret
.error:
    scf
    ret

Работа с файловой системой TR-DOS

Чтение каталога диска

Assembler Z80:
; Загрузка каталога в буфер
LOAD_CATALOG:
    ld hl, CATALOG_BUFFER
    ld b, 0            ; Дорожка 0
    ld c, 1            ; Сектор 1 (первый сектор каталога)
    ld d, 8            ; 8 секторов каталога
.load_loop:
    push bc
    push de
    push hl
    call READ_SECTOR
    jr c, .error
    pop hl
    ld de, 256
    add hl, de         ; Следующий буфер
    pop de
    pop bc
    inc c              ; Следующий сектор
    dec d
    jr nz, .load_loop
    xor a              ; Успех
    ret
.error:
    pop hl
    pop de
    pop bc
    scf
    ret

CATALOG_BUFFER:
    ds 2048, 0         ; 8 секторов × 256 байт

Поиск файла в каталоге

Assembler Z80:
; Вход: HL = имя файла (8 символов)
; Выход: A = номер файла (0-127), HL = адрес записи
FIND_FILE:
    ld de, CATALOG_BUFFER
    ld b, 128          ; Максимум 128 файлов
    ld c, 0            ; Счетчик файлов
.search_loop:
    push hl
    push bc
    push de
    ; Сравниваем имя файла
    ld b, 8
.compare:
    ld a, (de)
    cp (hl)
    jr nz, .next_file
    inc de
    inc hl
    djnz .compare
    ; Нашли файл
    pop de             ; DE = адрес записи
    pop bc
    pop hl
    ld a, c            ; A = номер файла
    ret
.next_file:
    pop de
    ld hl, 16
    add hl, de         ; Следующая запись
    ex de, hl
    pop bc
    pop hl
    inc c
    djnz .search_loop
    ; Файл не найден
    scf
    ret

Загрузка файла по имени

Assembler Z80:
; Вход: HL = имя файла, DE = адрес загрузки
LOAD_FILE:
    push de
    call FIND_FILE
    pop de
    ret c              ; Файл не найден
   
    ; Читаем параметры файла
    ld bc, 8
    add hl, bc         ; HL указывает на расширение
   
    ld a, (hl)         ; Тип файла
    inc hl
    ld c, (hl)         ; start (младший байт)
    inc hl
    ld b, (hl)         ; start (старший байт)
    inc hl
    push bc            ; Сохраняем start адрес
   
    ld c, (hl)         ; length (младший)
    inc hl
    ld b, (hl)         ; length (старший)
    inc hl
    push bc            ; Сохраняем длину
   
    ld c, (hl)         ; length в секторах
    inc hl
    ld b, (hl)         ; начальный сектор
    inc hl
    ld a, (hl)         ; начальная дорожка
   
    ; Восстанавливаем параметры
    pop hl             ; HL = длина
    pop bc             ; BC = start адрес
   
    ; Если адрес загрузки = 0, используем start из каталога
    ld a, d
    or e
    jr nz, .use_de
    ex de, hl          ; DE = длина
    ld h, b
    ld l, c            ; HL = start
    ex de, hl          ; HL = длина, DE = start
   
.use_de:
    ; Теперь загружаем файл
    ; DE = адрес загрузки, A = дорожка, B = сектор, C = секторов
    ; Реализация чтения нескольких секторов
    ; ... (продолжение следует)

Форматирование дискеты

Низкоуровневое форматирование

Assembler Z80:
FORMAT_DISK:
    ; Проверяем, что диск защищен от записи
    in a, (BETA_DATA)
    bit 6, a            ; Флаг write protect
    jr nz, .write_protected
   
    ; Форматируем каждую дорожку
    ld b, 80            ; 80 дорожек
    ld c, 0             ; Текущая дорожка
.track_loop:
    push bc
    ld a, c
    out (BETA_TRACK), a ; Устанавливаем дорожку
   
    ; Команда форматирования дорожки
    ld a, %11111000     ; Форматирование + spin-up
    out (BETA_DATA), a
   
    ; Формат секторов: C, H, R, N
    ; C=track, H=head, R=sector, N=sector size (256=1)
    ld hl, SECTOR_TABLE
    ld b, 16            ; 16 секторов
.format_loop:
    ld a, (hl)          ; Номер сектора
    out (BETA_DMA), a
    inc hl
    djnz .format_loop
   
.wait:
    in a, (BETA_DATA)
    bit 0, a
    jr nz, .wait
   
    pop bc
    inc c
    djnz .track_loop
    ret

.write_protected:
    scf
    ret

SECTOR_TABLE:
    db 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16

Оптимизированные процедуры для демо/игр

Быстрая загрузка без использования TR-DOS

Assembler Z80:
; Загрузка секторов напрямую (минуя файловую систему)
FAST_LOAD:
    ld hl, LOAD_TABLE   ; Таблица секторов для загрузки
.load_loop:
    ld a, (hl)          ; Флаг окончания (255)
    cp 255
    ret z
   
    ld b, (hl)          ; Дорожка
    inc hl
    ld c, (hl)          ; Сектор
    inc hl
    ld e, (hl)          ; Адрес буфера (младший)
    inc hl
    ld d, (hl)          ; Адрес буфера (старший)
    inc hl
   
    push hl
    ex de, hl
    call READ_SECTOR
    pop hl
    jr nc, .load_loop
    ; Ошибка загрузки
    ret

LOAD_TABLE:
    db 0, 1, $00, $C0   ; Дорожка 0, сектор 1 -> C000h
    db 0, 2, $00, $C1   ; Дорожка 0, сектор 2 -> C100h
    ; ...
    db 255             ; Конец таблицы

Мультизагрузка с прерываниями

Assembler Z80:
; Фоновая загрузка во время отрисовки
BACKGROUND_LOAD:
    di
    ld (save_sp), sp
    ld sp, $6000        ; Временный стек
   
    ; Сохраняем регистры
    push af
    push bc
    push de
    push hl
    push ix
    push iy
    exx
    push hl
    push de
    push bc
    exx
   
    ; Загружаем один сектор
    ld hl, (next_sector_ptr)
    ld b, (hl)          ; Дорожка
    inc hl
    ld c, (hl)          ; Сектор
    inc hl
    ld e, (hl)          ; Буфер младший
    inc hl
    ld d, (hl)          ; Буфер старший
    inc hl
    ld (next_sector_ptr), hl
   
    ex de, hl
    call READ_SECTOR
   
    ; Восстанавливаем регистры
    exx
    pop bc
    pop de
    pop hl
    exx
    pop iy
    pop ix
    pop hl
    pop de
    pop bc
    pop af
   
    ld sp, (save_sp)
    ei
    ret

save_sp: dw 0
next_sector_ptr: dw SECTOR_TABLE

Особенности разных клонов и интерфейсов

Pentagon 128K/512K

Порты: обычно $FF3F (контроллер) и $FF5F (картридж)
Часто два контроллера: Beta 128 и Beta 48
Автозагрузка с диска при включении
Возможность загрузки TR-DOS из ROM

Scorpion ZS 256

Расширенные команды через порт $DFFD
Поддержка больших дискет (до 1.6 МБ)
Собственная файловая система Scorpion DOS
Turbo-режимы загрузки

ATM Turbo

Советский клон с уникальным дисковым контроллером
Порты: $3F, $5F, $FF (через конфигурационный регистр)
Поддержка TR-DOS и собственного формата
Часто используется WD1793-совместимый КР1818ВГ93

Sinclair +3

Встроенный дисковод 3"
Собственный контроллер на основе 8272
Файловая система +3DOS
Несовместимость с TR-DOS на аппаратном уровне

Обслуживание и отладка

Диагностика дисковода

Assembler Z80:
CHECK_DRIVE:
    ; Проверка наличия диска
    in a, (BETA_DATA)
    bit 7, a            ; Флаг ready
    jr z, .no_disk
   
    ; Проверка защищенности от записи
    bit 6, a
    jr nz, .write_protected
   
    ; Проверка track 0
    call SEEK_TRACK0
    ret c
   
    ; Чтение первого сектора
    ld b, 0
    ld c, 1
    ld hl, BUFFER
    call READ_SECTOR
    ret

.no_disk:
    ; Обработка отсутствия диска
    ret

.write_protected:
    ; Обработка защиты от записи
    ret

Калибровка дисковода

Assembler Z80:
CALIBRATE_DRIVE:
    ; Поиск track 0
    call SEEK_TRACK0
    ret c
   
    ; Перемещение на дорожку 40
    ld a, 40
    out (BETA_TRACK), a
    ld a, %00011000     ; Seek команда
    out (BETA_DATA), a
.wait1:
    in a, (BETA_DATA)
    bit 0, a
    jr nz, .wait1
   
    ; Возврат на track 0
    call SEEK_TRACK0
    ret

Создание загрузчика игр

Интелектуальный загрузчик

Assembler Z80:
GAME_LOADER:
    ; Инициализация
    call INIT_DISK
   
    ; Загрузка заголовка
    ld hl, HEADER_BUFFER
    ld b, 0
    ld c, 1
    call READ_SECTOR
   
    ; Проверка сигнатуры
    ld hl, HEADER_BUFFER
    ld de, SIGNATURE
    ld b, 6
.check_sig:
    ld a, (de)
    cp (hl)
    jr nz, .not_valid
    inc hl
    inc de
    djnz .check_sig
   
    ; Загрузка основной программы
    ld hl, (HEADER_BUFFER.load_addr)
    ld bc, (HEADER_BUFFER.sector_count)
.load_main:
    push bc
    ld b, (ix)          ; Дорожка из таблицы
    ld c, (ix+1)        ; Сектор из таблицы
    call READ_SECTOR
    pop bc
    dec bc
    ld a, b
    or c
    jr z, .load_complete
    ; Обновляем буфер
    ld de, 256
    add hl, de
    inc ix
    inc ix
    jr .load_main
   
.load_complete:
    jp (hl)             ; Запуск игры

.not_valid:
    ; Обработка ошибки
    ret

SIGNATURE: db "GAMELD"

Компрессия и декомпрессия на лету

Assembler Z80:
LOAD_COMPRESSED:
    call READ_SECTOR    ; Загружаем сжатые данные
    ld hl, COMPRESSED_BUFFER
    ld de, OUTPUT_BUFFER
.decompress:
    ld a, (hl)
    inc hl
    cp $FF              ; Маркер конца
    ret z
    bit 7, a
    jr z, .raw_byte
    ; RLE распаковка
    and $7F             ; Длина
    ld b, a
    ld a, (hl)          ; Повторяемый байт
    inc hl
.fill:
    ld (de), a
    inc de
    djnz .fill
    jr .decompress
.raw_byte:
    ld (de), a          ; Обычный байт
    inc de
    jr .decompress

Современные эмуляторы и инструменты

Работа с образами дисков

Форматы образов:

  • .TRD — точный образ TR-DOS дискеты
  • .SCL — сжатый каталог файлов
  • .FDI — образ с информацией о формате
  • .UDI — универсальный образ

Инструменты разработки

  1. TRD Manager — создание и редактирование образов
  2. Disk Explorer — просмотр содержимого дискет
  3. EmuZWin — эмулятор с поддержкой дисководов
  4. FUSE — кроссплатформенный эмулятор
  5. bintap — конвертер между форматами

Заключение: наследие дисковых контроллеров Spectrum

Контроллеры дисководов для ZX Spectrum, особенно Beta Disk с TR-DOS, стали неотъемлемой частью культуры Spectrum. Они позволили:

  • Ускорить загрузку в 100+ раз (с 5 минут до 3 секунд)
  • Создать сложные многофайловые игры
  • Реализовать профессиональные приложения
  • Обеспечить удобное хранение данных
Ключевые уроки для современных разработчиков:

  1. Эффективность через простоту — WD1793 имел всего 4 регистра
  2. Важность стандартизации — TR-DOS стал общим языком для клонов
  3. Оптимизация под железо — прямое программирование контроллера
  4. Обратная совместимость — большинство решений сохраняли работу с кассетами
Дисковые системы Spectrum демонстрируют, как даже ограниченные ресурсы (8-битный процессор, 64 КБ памяти) при грамотном проектировании позволяли создавать полноценные файловые системы, сравнимые по удобству с современными.

Сегодня эти технологии живут в эмуляторах, ретро-сборках и сердцах фанатов, напоминая о времени, когда каждый байт имел значение, а программирование было настоящим искусством низкоуровневого взаимодействия с железом.
  • Angry
  • Like
Реакции: AndXor и Deniss Matjusevs