Files
Sprinter-SDCC/docs/new/04-memory.md
T
snark13 64ce6339eb docs: справочники по железу Sprinter + правка gfx_scroll_h
- docs/new/ — сводные справочники (архитектура, BIOS, DSS, память,
  графика, акселератор, IRQ, порты, ввод, звук, известные баги);
- docs/Original/ — первоисточники, из которых они собраны (BIOS, Estex
  DSS, мануалы, описание акселератора), + Форум.doc/.docx в reference;
- libbgi/common/gfx_scroll_h.c — обход бага скролла при ширине >256
  (правка автора: шаг банды 255 и продвижение указателей на cw; старый
  вариант с 256 оставлен закомментированным с TODO);
- удалён applications/PoP/roomtest/hang_variants.png — рабочая раскладка
  из разбора позы виса, в репозитории ей не место.

Большие архивы (docs/extra ~568 МБ, docs/sources с вложенными git-репо
~68 МБ) в коммит НЕ включены — см. обсуждение.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 18:26:31 +03:00

32 KiB
Raw Blame History

4. Управление памятью Sprinter Sp2000

4.1 Аппаратная модель памяти

Z80 видит 64 КБ, разбитые на четыре окна (window) по 16 КБ. Каждое окно независимо маппится на физическую страницу (page) ОЗУ или ПЗУ через порт-регистр страницы:

Окно Адреса Порт страницы Назначение
W0 #0000..#3FFF #82 (PAGE0, ROM.SLOT0) RST-обработчики, BIOS/DSS ядро
W1 #4000..#7FFF #A2 (SLOT1, PAGE1) Приложение
W2 #8000..#BFFF #C2 (SLOT2, PAGE2) Стек — обязательно здесь
W3 #C000..#FFFF #E2 (SLOT3, PAGE3) Разделяемые страницы DSS, графика, резидентный код
  • Запись в порт #82/#A2/#C2/#E2 меняет физическую страницу окна; чтение возвращает текущую.
  • Для совместимости со старыми моделями (ZX-Spectrum 128, Scorpion, Pentagon) работают также порты #7FFD, #1FFD, #DFFD, #EFF7:
Порт Описание
#7FFD Биты 0-2: страница W3; бит 3: выбор ПЗУ (0=DOS, 1=Basic/48)
#1FFD Биты 0-2: страница W0; бит 3: дополнительная память; бит 4: турбо
#DFFD Страница W1 (Pentagon, когда бит 5 порта #7FFD=0)
#EFF7 Страница W2 (Pentagon)

Страница #FF: если окно не выделено программе (образ меньше 16/32 КБ), DSS маппит его на специальную страницу #FF. Чтение даёт #FF, запись игнорируется. Это ключевая причина «молчаливой» порчи данных — см. §4.9.

Физические страницы: порты #82/#A2/#C2/#E2 адресуют физические 16-КБ страницы в адресном пространстве Sprinter. Диапазон страниц зависит от установленной памяти (обычно 0..127 для 2 МБ, 0..255 для 4 МБ). Старшие страницы заняты видео-ОЗУ и ПЗУ.


4.2 Окно W0 (#0000..#3FFF) — системное

W0 — критическое окно: в нём живут обработчики RST 08h (BIOS), RST 10h (DSS/ESTEX) и RST 18h (BIOS-EXP). Пока в W0 стоит системная страница, вызовы доступны.

ROM/RAM switching:

Действие Код
Включить ПЗУ в W0 OUT (#7C),A (SYS_PORT.ROM)
Выключить ПЗУ, вернуть ОЗУ OUT (#3C),A (SYS_PORT.RAM)

При включённом ПЗУ в W0 отображается системная страница BIOS. Для вызова RST-функций W0 должен быть в ПЗУ — BIOS сам переключает его при входе через RST 08h.

Требования к прерываниям:

Sprinter работает в режиме IM1 — вектор прерывания фиксирован на #0038 (всегда в ПЗУ/ОЗУ W0). Обработчик EXP.asm по адресу #0038:

  1. Переключает W3 на SYS_PAGE.
  2. Проверяет флаг INT_ID = #AA.
  3. Если есть пользовательский обработчик — вызывает его (адрес из SYS_PAGE).
  4. Восстанавливает W3, EI, RETI.

Регистр I инициализирован в #3F (наследие ZX-Spectrum), но при IM1 не используется для диспетчеризации. Если программа переключается в IM2I и расположение таблицы векторов определяются программой произвольно, стандартного расположения нет.

DSS и W0: при вызове RST 10h DSS переключает W0 на свою рабочую страницу (COREPAGE = 4), выполняет функцию и восстанавливает W0. Если в W0 до вызова была ОЗУ, DSS восстановит её; если ПЗУ — вернёт ПЗУ.

Практические рекомендации:

  • Для вызова системных функций держите W0 в ПЗУ (или не заботьтесь — RST 08h и RST 10h его переключат сами).
  • При IM1 (режим по умолчанию) прерывание идёт на #0038 — это ПЗУ, переключение W0 не требуется.
  • При IM2 программа сама управляет таблицей векторов — не забывайте про DI/EI при переключении W0.

Размещение своей RAM-страницы в W0:

Если программе нужно дополнительное окно (например, для атласов спрайтов или кода, не требующего системных вызовов), можно временно маппить свою страницу в W0. Для безопасной работы страница должна содержать корректную таблицу RST-векторов в первые #40 байт:

Адрес Инструкция Назначение
#0000 JP boot или RST 00h (обычно не используется)
#0008 JP rst08_stub RST 08h — BIOS
#0010 JP rst10_stub RST 10h — ESTEX/DSS
#0018 JP rst18_stub RST 18h — BIOS-EXP
#0020..#0030 JP _w0_unused_stub RST 20h-30h — не используются
#0038 JP isr_stub IM1 (RST 38h)
#0066 RETN или JP nmi_stub NMI

Каждый стаб (в этой же странице, выше #40) выполняет одну задачу: сохранить текущий номер страницы W0, переключить W0 обратно на ПЗУ (OUT (#7C),A — SYS_PORT.ROM), выполнить настоящий системный вызов, после чего восстановить страницу программы и вернуться к исходному caller'у. Типовой паттерн для ESTEX (RST 10h):

rst10_stub:
    IN    A, (#82)        ; сохранить свою страницу W0
    PUSH  AF
    OUT   (#7C), A        ; включить ПЗУ в W0 (SYS_PORT.ROM)
    ; теперь в W0 — системная страница, RST 10h будет обработан корректно
    RST   #10             ; вызвать настоящий ESTEX (DSS сам переключит W0
                          ; на COREPAGE и восстановит ПЗУ на выходе)
    POP   BC
    LD    A, B
    OUT   (#82), A        ; вернуть свою страницу в W0
    RET                   ; возврат к исходному caller'у

Такой же паттерн для RST 08h и RST 18h. Для IM1 (RST 38h) — аналогично, но прерывание может прийти в любой момент, поэтому критические секции (своя страница в W0) обязаны быть обёрнуты в DI/EI (или проверять вложенность через счётчик), иначе повторное прерывание при уже замапленной странице попадёт в стаб, а не в EXP.asm.

Важно: пока в W0 стоит ваша страница, системные вызовы (RST 08h/10h/18h) проходят через стабы и работают корректно, но с накладными расходами (~10+ тактов на каждый вызов). Код на странице не должен вызывать DSS/BIOS напрямую — только через RST. При этом сам стаб не должен использовать RST-инструкции, так как они адресуют W0, который в момент вызова стаба всё ещё содержит вашу страницу, а не ПЗУ — сначала переключите ПЗУ через OUT (#7C),A.

Перед первым маппингом своей страницы в W0 сохраните текущее значение порта #82 (IN A,(#82) — оно даёт номер системной/ПЗУ-страницы). Эта страница понадобится стабам для восстановления после вызова во время прерываний, когда ПЗУ уже может быть в W0, а стаб не успел его переключить. Подтверждённая реализация: tests/w0page (см. sprite-api-design.md §9в).


4.3 Окно W1 (#4000..#7FFF) — приложение

W1 — основное окно для кода и данных приложения. По умолчанию содержит страницу 5 (RAM, инициализируется BIOS).

Использование BIOS/DSS:

BIOS и DSS могут временно переключать W1 на свою рабочую страницу. Это происходит:

  • При файловых операциях DSS: чтении FAT, загрузке секторов — DSS маппит одну из страниц разделяемого пула в W1 (DIRPAGE, FATPAGE, TXTPAGE, DRVPAGE).

  • При графических операциях BIOS: PIC_FN2/PIC_FN3 (блоковые копии) могут переключать W1 на страницу видео-ОЗУ.

  • Важно: перед возвратом из вызова BIOS/DSS восстанавливает предыдущую страницу W1. Типовой паттерн в исходниках DSS:

    IN A,(SLOT1)    ; сохранить текущую страницу W1
    PUSH AF
    ... работа с разделяемым буфером ...
    POP BC
    LD C,SLOT1
    OUT (C),B       ; восстановить W1
    

    Для вызывающей программы переключение W1 полностью прозрачно — после возврата из любой функции DSS/BIOS содержимое W1 идентично тому, что было до вызова.

Практические рекомендации:

  • Размещайте код и данные в W1, рассчитывая, что он остаётся стабильным между вызовами.
  • При длительных файловых операциях (последовательный READ больших объёмов) W1 может быть затронут только в моменты внутреннего обращения DSS к FAT/буферам, но не во время собственно чтения данных в буфер программы.
  • Если нужно гарантированно сохранить данные через вызов DSS — используйте W2, который BIOS/DSS никогда не трогают.

4.4 Окно W2 (#8000..#BFFF) — стек (критично)

W2 — единственное окно, которое BIOS и DSS гарантированно не трогают. Поэтому стек ОБЯЗАН находиться здесь:

  • SP ≤ #BFFF при вызовах DSS (RST 10h).
  • SP ≥ #8000 при вызовах BIOS (RST 08h).
  • Пересечение → стек в #8000..#BFFF.
  • По умолчанию SP инициализируется в #BFFE (значение из EXE-заголовка, §4.7).

Почему W2 — единственное стабильное окно:

Окно Может быть переключено BIOS/DSS Почему
W0 Да DSS переключает на COREPAGE при RST 10h; BIOS переключает на ПЗУ
W1 Да, временно DSS маппит разделяемые буферы для FAT/директорий; BIOS при графических операциях — и восстанавливает перед возвратом
W2 Нет Никогда. Стек должен быть доступен всегда.
W3 Да, временно DSS маппит разделяемые буферы; BIOS при графике переключает на видео-ОЗУ; оба восстанавливают исходную страницу перед возвратом

EXEC и W2: при загрузке новой программы (EXEC, функция 40h) DSS сохраняет страницу W2 в EXSTACK (вместе с W1 и W3). После завершения программы (EXIT, функция 41h) W2 восстанавливается.

Практические рекомендации:

  • Размещайте стек строго в W2. Стандартное значение SP = #BFFE — начало стека с запасом ~14 КБ до #8000. Можно уменьшить, если программа неглубокая.
  • W1 и W3 при вызовах DSS и BIOS сохраняются и восстанавливаются (см. §4.3, §4.5), но W2 — единственное окно, которое никогда не трогается ни DSS, ни BIOS.
  • Если программе нужно больше памяти, чем даёт W2 (например, для хранения больших массивов), выделяйте блоки через DSS GETMEM (функция 3Dh) и мапьте их в W1 или W3.

4.5 Окно W3 (#C000..#FFFF) — разделяемые страницы

W3 — окно, в которое DSS временно маппит страницы своего разделяемого пула для дисковых буферов и служебных данных (адрес #C000):

Страница Назначение Адрес в W3
DIRPAGE (0) Буфер директории #C000
FATPAGE (1) Кеш FAT #C000
TXTPAGE / ENVPAGE (2) Буфер строки/окружения #C000.. (PATH_PNT_ARRAY #FC80, ENVTEMP #FE00)
DRVPAGE (3) Страница драйвера #C000
COREPAGE (4) Ядро DSS (маппится в W0 при RST 10h)

DSS СОХРАНЯЕТ и ВОССТАНАВЛИВАЕТ W3 после каждой операции. Типовой паттерн в исходниках:

IN A,(SLOT3)    ; сохранить текущую страницу W3
PUSH AF
... работа с разделяемым буфером ...
POP BC
LD C,SLOT3
OUT (C),B       ; восстановить W3

Это означает, что для вызывающей программы переключение W3 полностью прозрачно — после возврата из любой функции DSS содержимое W3 идентично тому, что было до вызова.

Какие функции трогают W3:

Функции, обращающиеся к файловой системе, временно маппят разделяемые страницы в W3: поиск файла (F_FIRST/F_NEXT), чтение FAT (OPEN, READ, WRITE), работа с каталогами (CHDIR, MKDIR), буферы драйвера диска. Функции без дисковой активности (VERSION, CURDISK, WAITKEY, LOCATE, SYSTIME, ENVIRON и т.п.) W3 не трогают вообще.

BIOS при графических операциях также временно переключает W3 на страницы видео-ОЗУ (например, PIC_SET_PAL, PIC_FN2) — и тоже восстанавливает.

Практические рекомендации:

  • W3 можно использовать для данных программы — после возврата из любой функции DSS содержимое W3 не меняется.
  • Единственное исключение — EXEC (функция 40h): она загружает новую программу, и W3 получает страницы загруженного образа. Это ожидаемое поведение.
  • Если программа не вызывает дисковых и графических функций — W3 полностью стабилен.
  • Стек нельзя размещать в W3 — не потому что W3 «теряется», а потому что переключение W3 происходит ВО ВРЕМЯ вызова. Если SP указывает на W3, то в момент, когда DSS временно маппит свой буфер, все push/pop внутри DSS будут обращаться к буферной странице, а не к данным программы. Кроме того, при прерывании IM1 обработчик EXP.asm сам переключает W3 на SYS_PAGE — что также сломает стек, если он в W3.

4.6 Физическая память и страницы

Физическая память Sprinter — не сплошной массив RAM. Разные области адресного пространства зарезервированы под видео-ОЗУ, ПЗУ и кеш. Номер страницы — это значение, которое записывается в порт окна (#82/#A2/#C2/#E2); 8-битный порт даёт 256 значений (#00..#FF).

Фиксированная карта страниц:

Диапазон Страниц Назначение
#00..#4F (0..79) 80 (1,25 МБ) RAM общего назначения
#50..#5F (80..95) 16 (256 КБ) Видео-ОЗУ (VRAM) — фиксировано в железе
#60..#DF (96..223) 128 (2 МБ) RAM общего назначения (верхний диапазон)
#E0..#EF (224..239) 16 (256 КБ) ПЗУ: BIOS + EXP + ZX ROMs + bitstream — фиксировано
#F0..#FF (240..255) 16 (256 КБ) Fast RAM (кеш) / системные страницы

Выделенные страницы:

Страница Назначение
4 COREPAGE — ядро DSS (маппится в W0 при RST 10h)
5 Стандартная страница приложения W1 (по умолчанию)
#41 (65) Spec_Page — сохранение состояния при soft-reset [EXP.asm:859,1019]
#FE (254) SYS_PAGE — системные данные BIOS (INT_ID, INT_ADRESS, буферы) [MAIN.asm:58]
#FF (255) Dummy-страница: чтение → #FF, запись игнорируется

Ключевые моменты:

  • VRAM (#50..#5F) фиксирована аппаратно, независимо от объёма RAM. Эти 16 страниц не являются частью RAM SIMM — это отдельная микросхема 256 КБ. Биты 2-3 номера страницы кодируют режим записи: #50 normal, #54 без ОЗУ-тени, #58 прозрачный (байт #FF не пишется), #5C оба режима.
  • ПЗУ (#E0..#EF) — 256 КБ флеш-памяти с BIOS, EXP, ZX Spectrum 48/128 ROM, TR-DOS, логотипом и битстримом для CPLD.
  • Fast RAM (#F0..#FF) — до 512 КБ быстрого SRAM (0 wait-states), доступна через порт #FB. #FE (SYS_PAGE) и #FF (dummy) — часть этого диапазона. При включённом кеше порт #FB конфликтует с CBL-звуком.
  • Для 2 МБ RAM: #60.. заполнены до #7F (32 стр. = 512 КБ); остаток #80..#DF — неиспользуемые страницы (отображаются как #FF).
  • Для 4 МБ RAM: #60..#DF целиком заполнены RAM (128 стр. = 2 МБ).

Нумерация: порты #82/#A2/#C2/#E2 дают прямой доступ по физическому номеру (#00..#FF). Порты #7FFD/#1FFD (совместимость с ZX-Spectrum 128) используют сквозную нумерацию: номер в #7FFD (0..7) маппится на физические страницы видеоОЗУ #50..#5F младшими 3 битами; для работы с произвольными страницами используйте порты #82/#A2/#C2/#E2.


4.7 EXE-файл: формат и загрузка

Загрузка исполняемых файлов выполняется функцией DSS EXEC (40h). Формат EXE-файла:

Заголовок (512 байт):

Смещение Размер Поле Описание
+0 3 EXE_EXT Сигнатура "EXE"
+3 1 VERSION Минимальная версия DSS (1 = v1.xx, 0 = без пути/аргументов)
+4 2 OFFCOD1 Младшее слово смещения образа в файле
+6 2 OFFCOD2 Старшее слово смещения (OFFCOD2 << 16 + OFFCOD1 = offset)
+8 2 LOADER Размер первичного загрузчика (0 = весь образ сразу)
+10 6 RESERVED Зарезервировано
+16 2 LD_ADDR Адрес загрузки (#4100..#FFFF, старший байт маскирован #3F)
+18 2 PC_REG Точка входа (entry point)
+20 2 SP_REG Начальный SP (по умолчанию #BFFE)
+22 1 UnUsedPoint Не используется
+23 489 RESERVED2 Зарезервировано (обнуляется; используется как временный стек EXEC)

Процесс загрузки (EXEC, функция 40h):

  1. Открыть файл на чтение.
  2. Считать 512-байтный заголовок в буфер ядра.
  3. Проверить сигнатуру "EXE" и версию (≥ минимальной 1).
  4. Если LOADER=0: seek на смещение OFFCOD2:OFFCOD1, считать весь образ по адресу LD_ADDR.
  5. Если LOADER>0: считать только первичный загрузчик по адресу LD_ADDR (он дочитает остальные сегменты).
  6. Выделить блок памяти через GETMEM (функция 3Dh), зарегистрировать в таблице RAM.
  7. Сохранить текущие страницы окон (SLOT1-3) в EXSTACK.
  8. Определить, сколько страниц нужно, и замаппить их через SETWIN1/SETWIN2/SETWIN3:
    • Если LD_ADDR ≥ #C000: маппится только W3.
    • Если LD_ADDR ≥ #8000: маппятся W2 и W3.
    • Иначе: маппятся W1, W2 и W3.
  9. Построить PSP (Program Segment Prefix) — блок данных сразу ниже LD_ADDR:
    • LD_ADDR-3: файловый манипулятор (file handle).
    • LD_ADDR-2: идентификатор блока памяти.
    • LD_ADDR-1: номер задачи.
    • LD_ADDR+0: размер CLP (командной строки).
    • LD_ADDR+1..+128: CLP-буфер.
    • После CLP: строка пути "A:\DIR\FILE.EXE",0.
  10. Установить SP из заголовка (SP_REG), сохранить IX → адрес PSP.
  11. Если VERSION=0 — установить текущий каталог из пути PSP.
  12. Затолкать в стек адрес возврата (RETFAR к DSS), EI, JP на точку входа.

Multi-bank EXE:

При наличии нескольких банков (LOADER>0) файл устроен так:

[512-байт заголовок] [HOME-сегмент] [Банк 1 (16 КБ)] [Банк 2 (16 КБ)] ...
  • HOME-сегмент: образ, загружаемый DSS по LD_ADDR.
  • Банки: 16-КБ блоки, каждый на своей физической странице.
  • LOADER = размер HOME-сегмента (DSS загружает только его; первичный загрузчик сам дочитывает банки через READ из оставшегося открытым файла).

4.8 Прерывания и память

Обработчик IM1 (RST 38h):

Вектор фиксирован на #0038 (W0). Реализация в EXP.asm:

  1. Переключает W3 на SYS_PAGE (BIOS).
  2. Проверяет флаг INT_ID = #AA (установлен при инициализации BIOS).
  3. Если флаг совпадает — вызывает пользовательский обработчик, адрес которого хранится в SYS_PAGE (INT_ADDRESS, INT_PAGE).
  4. После возврата восстанавливает W3 и выполняет EI; RETI.

Это позволяет пользовательским программам устанавливать свои обработчики INT без полного перехвата вектора.

Обработчик IM1 (системный):

При возникновении прерывания Z80 исполняет RST 38h → вызов по адресу #0038. Реализация в EXP.asm (всегда доступна, т.к. #0038 в W0):

  1. Переключает W3 на SYS_PAGE (BIOS).
  2. Проверяет флаг INT_ID = #AA (установлен при инициализации BIOS).
  3. Если флаг совпадает — вызывает пользовательский обработчик, адрес и страница которого хранятся в SYS_PAGE (INT_ADDRESS, INT_PAGE). Это позволяет программам устанавливать свои ISR без полного перехвата вектора.
  4. После возврата восстанавливает W3 и выполняет EI; RETI.

Обработчик IM2 (опционально, для продвинутых программ):

Sprinter по умолчанию не использует IM2. Исходники содержат шаблон IM2_INT.asm для программ, желающих переключиться на IM2:

  1. Сохраняет AF, BC, DE, HL, IX, IY и теневые регистры.
  2. Включает ПЗУ (OUT (#7C),A) — KEYSCAN требует ПЗУ в W0.
  3. Вызывает KEYSCAN (опрос AT-клавиатуры).
  4. Восстанавливает все регистры, EI, RETI.

При переходе на IM2 программа сама выбирает значение I и размещает таблицу векторов в соответствующем диапазоне памяти.

Рекомендации:

  • Если программа не использует прерывания — держите W0 в ПЗУ.
  • Если программа использует IM1 — можно переопределить обработчик через структуру в SYS_PAGE (флаг INT_ID, поля INT_ADDRESS/INT_PAGE).
  • Если программа использует IM2 — переключение полностью на ответственности программы; ядро Sprinter не предоставляет стандартного расположения таблицы векторов.

4.9 Типовые грабли

  • Данные читаются как #FF / не сохраняются — код или данные попали в незамапленное окно (страница #FF). Причина: образ программы меньше 16 КБ, DSS выделил только одну страницу, а код/данные распределены линкером в незамапленные окна. Лечение: убедитесь, что код и данные укладываются в одно окно, или явно выделяйте и мапьте остальные окна через GETMEM/SETWIN1-SETWIN3.

  • Крэш при вызове RST при нестандартном W0 — если перед вызовом W0 содержал произвольную страницу ОЗУ, DSS при RST 10h переключит его, но может не восстановить корректно. Решение: перед вызовом RST включите ПЗУ (OUT (#7C),A), если не уверены в состоянии W0.

  • Стек в W3 (не-рабочий вариант) — если стек размещён в W3, при любом дисковом вызове W3 будет переключён на разделяемый буфер DSS, и стековые данные будут забиты. Результат: мгновенное зависание при возврате из функции. Единственное рабочее место для стека — W2.

  • CBL не работает при включённом кеше — порт #FB используется как кеш-память (Fast RAM) и как порт CBL-звука. Если кеш включён, запись в #FB уходит в кеш, а не в CBL. Детали: 11-ports.md, 12-bugs.md.

  • Прерывание уходит не туда при IM2 — если программа переключилась в IM2 с неинициализированной таблицей векторов, любое прерывание приведёт к исполнению мусора. Решение: используйте IM1 (режим по умолчанию) или инициализируйте таблицу перед переключением.

  • Не путать W3 с «теряется после вызова» — многие источники утверждают, что DSS сбрасывает W3. На самом деле DSS сохраняет и восстанавливает W3 после каждой операции. Стек всё равно нельзя размещать в W3 (см. §4.5), но для данных W3 полностью стабилен между вызовами. Если ваша программа полагается на старый миф — проверьте код.


4.10 Кросс-ссылки

  • Порты страниц и детали регистров: 11-ports.md
  • BIOS (графика, окна): 02-bios.md
  • DSS/ESTEX (EXEC, GETMEM, SETWIN): 03-dss.md
  • Прерывания: 07-irq.md
  • Баги (CBL vs кеш, порт #FB): 12-bugs.md
  • EXE-формат и PSР: memory/exe-format.md