Files
Sprinter-SDCC/docs/memory-management.md
snark13 95c22be9bd libbgi: скролл-примитивы + --w3 + отчёт раскладки памяти
Скролл региона video->video (неактивная страница -> активная, банк 0x50:
копия = скролл + heal цели):
- gfx_scroll_h / _bgi_scroll_rows_raw — горизонтальный, построчно без
  страйдов (~54Т/строку), DI/EI бандами по 16 строк, h=0=>256;
- gfx_scroll_v / _bgi_scroll_cols_raw — верт. И/ИЛИ гориз. за один проход
  без буфера (колонка = accel-burst LD A,A, STOP между read/write делает
  промежуточный OUT Port_Y безопасным), банды по 16 колонок;
- _gfx_addr_shadow_base (адрес неактивной страницы) + gfx_rect_t.
Пример examples/scroll.

check_banks.py + sprinter-cc: отчёт раскладки памяти для ЛЮБОЙ модели
(W1/W2 код/данные, остаток кучи/стека, W3 при --w3, банки при --bank),
не только при --bank.  Док docs/memory-management.md §10.

--w3 (резидентный код окна W3) + сопутствующее: crt0_banked W3_RESIDENT,
mkexe -W, tests/w3probe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:38:00 +03:00

18 KiB
Raw Permalink Blame History

Управление памятью в sprinter-cc

Документ описывает модель памяти Sprinter (Sp2000), режимы памяти обёртки sprinter-cc и способы размещения кода/данных: single-page, split, банки (трамплины) и резидентный код окна W3 (--w3).

Всё в этом документе подтверждено сборкой и прогоном в MAME v3.06 / DSS 1.71.57 (см. tests/w3probe, tests/banktest, tests/banklocl).


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

Z80 видит 64 КБ, разбитые на четыре окна по 16 КБ. Каждое окно независимо маппится на физическую страницу через порт-регистр страницы:

Окно Адреса Порт страницы Назначение по умолчанию
W0 0x0000-0x3FFF 0x82 DSS / BIOS (RST-ы, системные вызовы)
W1 0x4000-0x7FFF 0xA2 приложение
W2 0x8000-0xBFFF 0xC2 приложение
W3 0xC000-0xFFFF 0xE2 приложение / графика / банки
  • Запись в порт 0x{8/A/C/E}2 меняет физ-страницу окна; чтение возвращает текущую страницу.
  • W0 занят DSS/BIOS — там живут RST-обработчики (RST #08 BIOS, RST #10 ESTEX). Пока в W0 стоит системная страница, вызовы доступны; подменять W0 нельзя без потери RST-ов.
  • «Неиспользуемое» окно маппится на специальную страницу #FF: чтение даёт 0xFF, запись игнорируется. Это ключевая причина «молчаливой» порчи данных — см. §7.

Порты в C

<sprinter.h> даёт SFR-обёртки: _io_page_w0..w3 (чтение/запись порта), sprinter_page_w0..w3(page).


2. Как DSS загружает EXE

Из документации DSS, последовательность EXEC:

  1. Открыть exe-файл на чтение.
  2. Считать префикс exe в рабочую область.
  3. Выделить блок памяти размером под весь файл (если loader==0) или под первичный загрузчик (loader>0).
  4. Сохранить стек.
  5. Подключить страницы выделенного блока в окна (последовательно от окна адреса загрузки: W1→W2→W3…).
  6. Построить префикс запуска → регистр IX.
  7. Считать файл по адресу загрузки (смещение 16 в заголовке).
  8. Закрыть exe, если это не первичный загрузчик (loader==0).
  9. Установить SP = значение из смещения 20 (адрес стека).
  10. Передать управление по адресу из смещения 18 (entry).

Следствия, на которых стоит вся схема памяти:

  • DSS выделяет число страниц по размеру образа: <16 КБ → 1 страница, 16..32 КБ → 2, 32..48 КБ → 3. Лишние окна = страница #FF.
  • Страницы маппятся подряд начиная с окна адреса загрузки. Образ, тянущийся 0x4100..0xFFFF (3 страницы), даёт W1+W2+W3 замапленными автоматически — это и есть база для резидентного кода W3 (§6, подход A).
  • loader>0 (multi-bank .exe) → DSS грузит только HOME-часть и оставляет файл открытым (handle в IX-3), а crt0 дочитывает банки сам (§5).

Упаковкой в этот формат занимается toolchain/mkexe.


3. Правила стека (критично)

  • SP ≤ 0xBFFF при вызовах DSS (ESTEX, RST #10).
  • SP ≥ 0x8000 при вызовах некоторых функций BIOS (RST #08).
  • Пересечение этих требований → стек обязан жить в W2 (0x8000-0xBFFF). По умолчанию SP инициализируется в 0xBFFE.

Проверено (tests/w3probe): во всех режимах на входе main SP ≈ 0xBFFC, т.е. в W2.

Chicken-and-egg для split-режимов (small/huge): DSS ставит SP=0xBFFE из заголовка, но для программ <16 КБ окно W2 ещё не выделено (там #FF). Пуши туда теряются, первый call возвращается в мусор. Поэтому crt0_small/ crt0_banked сначала работают на загрузочном стеке в W1 (реальное ОЗУ), маппят W2 и только потом переставляют SP=0xBFFE. Маппинг W2 делается через ESTEX $3A SETWIN2 (не BIOS $C4+OUT — тому нужен стек уже в W2).


4. Режимы памяти

Выбираются флагом sprinter-cc --memory MODE (по умолчанию tiny). Режим задаёт адрес кода, размещение данных, crt0 и наличие банков.

Режим CODE DATA/BSS Стек Банки crt0 Первичный загрузчик
tiny W2 0x8100 за кодом (W2) W2 crt0.s нет (1 страница)
small W1 0x4100 за кодом (W1→W2) W2 crt0_small.s да (сам маппит W2)
big W2 0x8100 за кодом (W2) W2 W1 (трамплины) crt0_banked.s (BANK_W1) да
huge W1 0x4100 за кодом (W1→W2) W2 W3 (трамплины) crt0_banked.s да
manual явно явно W2 crt0.s зависит

Во всех режимах DATA цепляется линкером сразу за кодом (--data-loc 0), а не кладётся по фиксированному адресу. Раньше huge использовал фиксированный DATA=0x8000, что ломалось при коде >16 КБ (код перетекал в W2 и накрывал DATA); сейчас DATA динамически идёт за концом кода.

4.1 tiny — всё в одной странице

0x8000..0x80FF  зарезервировано (startup-prefix)
0x8100          _start / _CODE … _DATA … _BSS … _HEAP
0xBB00          heap top (по умолчанию)
0xBFFE          стек ↓
  • Один блок 16 КБ, DSS маппит его в W2. W1 и W3 = #FF.
  • Ничего выделять/маппить не надо; crt0.s предполагает, что W2 уже дан DSS.
  • Практический потолок кода+данных+кучи+стека ≈ 14 КБ.
  • --code-loc 0x8100 --data-loc 0; mkexe -L 0x8100 -E 0x8100 -S 0xBFFE.

4.2 small — CODE в W1, данные перетекают в W2

0x4100          _CODE …            (W1)
  … за кодом →  _DATA _BSS _HEAP   (W1, перетекает в W2)
0xBB00          heap top
0xBFFE          стек ↓             (W2)
  • Покрывает ~0..30 КБ (код+данные вместе).
  • crt0_small.s авто-определяет W2: читает порт 0xC2. Если ≠0xFF — DSS уже дал W2 (образ >16 КБ), маппить не надо. Если =0xFF — сам выделяет страницу (ESTEX $3D GETMEM) и маппит (ESTEX $3A SETWIN2).
  • --code-loc 0x4100 --data-loc 0.

4.3 big — tiny + банки в W1

  • База как tiny (CODE+DATA+стек в W2, 0x8100).
  • Свапаемые банки в W1 (0x4000-0x7FFF, порт 0xA2), вызываются через трамплины (§5). crt0_banked.s собирается с BANK_W1=1.
  • mkexe получает -B 0x4000 (банки живут в W1). Виртуальный адрес банка N = 0x{N}4000.
  • W3 свободно — доступно под графику или резидентный код (--w3).

4.4 huge — small + банки в W3

  • База как small (CODE 0x4100, DATA за кодом, авто-детект W2).
  • Свапаемые банки в W3 (0xC000-0xFFFF, порт 0xE2), через трамплины. Виртуальный адрес банка N = 0x{N}C000.
  • crt0_banked.s (без BANK_W1) грузит банки из .exe после старта.

4.5 manual — явное размещение

--memory manual --memory-manual SPEC, где SPEC = список KEY=VAL: CODE=W1|W2, DATA=W1|W2|SAME, BANKED=W1|W3. Плюс прямые --code-loc / --data-loc / -L / -E / -S перекрывают дефолты любого режима.


5. Банки и трамплины (--bank)

Для big/huge. Модуль-банк собирается в отдельную область и линкуется по виртуальному 24-битному адресу (bank_id в старшем байте):

sprinter-cc --memory huge --bank 1=engine.c --bank 2=audio.c -o app.exe main.c
  • Банк N компилируется --codeseg/--constseg/--dataseg BANKN, линкуется -Wl-b_BANKN=0x{N}C000 (huge) или 0x{N}4000 (big).
  • main.c обязан объявить const uint8_t n_banks = N;crt0_banked читает это до gsinit, поэтому только const (инициализатор ещё не скопирован).
  • Функции банка помечаются __banked — SDCC генерирует вызов через трамплин ___sdcc_bcall_ehl_CODE/W1, всегда замаплен): он сохраняет текущую страницу окна, маппит нужный банк (_bank_pages[id]), jp в функцию, по возврату восстанавливает страницу.
  • Загрузка: mkexe пакует header + HOME + bank1(16К) + bank2(16К)…, loader=размер HOME; crt0_banked выделяет страницы (GETMEM), маппит и дочитывает каждый банк ESTEX READ из открытого файла.
  • Проверка размеров банков — toolchain/check_banks.py (часть отчёта раскладки, см. §10).

Writable bank-local данные возможны, но с оговорками — см. memory/bank_local_data_pattern.


6. Резидентный код окна W3 (--w3)

Альтернатива банкам без трамплинов. Модуль размещается резидентно в W3 (0xC000) и вызывается прямым call — как обычная функция. Работает во всех режимах (tiny|small|big|huge); по умолчанию подразумевает small.

sprinter-cc --w3 render.c -o app.exe main.c            # → small
sprinter-cc --memory huge --w3 render.c --bank 1=lvl.c -o app.exe main.c

Как работает (подход A): образ с областью W3CODE@0xC000 тянется до 0xC0xx (≥3 страницы), и DSS сам маппит W3 при загрузке (§2) — загрузчик в crt0 не нужен (кроме huge). Прямые вызовы резолвятся линкером в реальные 0xC0xx.

Механизм сборки:

  • W3-модуль компилируется --codeseg W3CODE --constseg W3CODE — код и rodata в W3. --dataseg НЕ переопределяется: писучие статики уходят в обычный _DATA (W2). Отсюда правило «в W3 только код + rodata».
  • Линк -Wl-b_W3CODE=0xC000.

Раскладка страниц по режимам (проверено MAME):

Режим W1 W2 W3
tiny #FF (не исп.) код+данные резидент
small код данные резидент
big банк (трамплин) код+данные резидент
huge код данные резидент делит окно с трамплин-банками

huge — особый случай (резидент + банки в одном окне W3):

  • crt0_banked под .ifdef W3_RESIDENT захватывает физ-страницу резидента (in a,(0xE2)) сразу после загрузки DSS и возвращает её дефолтом после цикла загрузки банков (иначе в W3 остался бы последний банк).
  • Трамплин на каждый __banked-вызов сам сохраняет/восстанавливает страницу W3 — поэтому дефолтная страница обязана быть резидентной.
  • mkexe получает флаг -W (разрешить HOME тянуться в W3 при наличии W3-банков — намеренное совмещение).

Правила разработчика:

  • Код в W3 не переключает страницу W3.
  • Код в W1/W2, свапающий W3 на другую страницу (скретч, графика), обязан вернуть исходную (in a,(0xE2) → работа → out (0xE2),a); оборачивать в DI/EI, если есть ISR, дёргающий W3.
  • В W3 — только код + rodata, писучих переменных там быть не должно.
  • Из __banked-контекста (пока в W3 замаплен банк) резидентный W3-код недостижим транзитивно. Обратное — резидент → __banked через трамплин W1 — работает (трамплин вернёт резидентную страницу перед ret).

Подробности и артефакты — memory/w3_resident_code, тест tests/w3probe.


7. Куча и стек

  • Стек: init SP=0xBFFE, растёт вниз. Меняется через -S 0xADDR.
  • Куча: от конца _BSS вверх до ___sdcc_heap_end (по умолчанию 0xBB00 → ~1278 байт под стек). malloc берёт &___sdcc_heap_end как потолок.
  • runtime/heap.s — динамическая куча (авто-размер = зазор BSS…heap_top), НЕ фиксированный .ds.
  • --stack-size N регенерирует heap_top как HEAP_TOP = 0xBFFF - N, зажимая рост кучи ради стека.

8. Семейство crt0

Выбирается режимом; --crt0=TYPE перекрывает.

crt0 Файл Для чего
default runtime/crt0.s tiny/manual; парсит argv, argv[0] через APPINFO
minimal runtime/crt0_minimal.s tiny без argv (меньше размер)
small runtime/crt0_small.s small; авто-детект/выделение W2
banked runtime/crt0_banked.s big/huge; авто-детект W2 + загрузка банков

crt0/bank.s собираются пер-сборка внутри sprinter-cc (с префиксами BANK_W1 / W3_RESIDENT / DEBUG_RT), а не бандлятся в библиотеку.


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

  • static-переменные читаются как 0xFF / не меняются — DATA попала в невыделенное окно (#FF). Причина: код и данные в разных окнах при образе <16 КБ, где DSS дал только одну страницу. Лечится правильным режимом (small/huge) или единым окном (tiny). См. memory/sprinter_memory_modes.
  • Крэш после первого call в split-режиме — стек ещё в невыделенном W2. Решает загрузочный стек в W1 (уже в crt0).
  • Банк «прыгает в мусор» — таблица _bank_pages[] в _DATA занулилась gsinit’ом; она обязана жить в _CODE. Уже исправлено в bank.s.
  • --w3: резидент недоступен из банка — ожидаемо (см. §6); держи вход в резидент только из W1/W2 или из самого W3-кода.

10. Отчёт по раскладке памяти (после линковки)

sprinter-cc печатает отчёт toolchain/check_banks.py для любой модели памяти (по .map). Показывает, где легли код/данные и сколько свободно в каждом окне и банке:

memory: huge — CODE в W1 (0x4100), DATA→W2, банки в W3
  _CODE         @ 0x4100  size   3767  (W1)  → 0x4FB7
  данные        @ 0x4FB7  size    336  (W1)  → 0x5107   [_DATA/_BSS/_INITIALIZED/…]
  статика до 0x5107  —  куча 0x5107..0xBB00 (27129 Б), стек 0xBB00..0xBFFE (1279 Б)
  _W3CODE       @ 0xC000  size    249  (резидент W3)  → 0xC0F9, 16135 Б свободно до 0x10000  OK
  _BANK1        @ 0x0001C000  size    219 / 16384 ( 1.3%)  → 16165 Б свободно  OK
  • _CODE / данные — окно (W1/W2), размер, конец; строка статика … куча … стек … = свободное место в W1/W2 (под кучу malloc до ___sdcc_heap_end и под стек).
  • _W3CODE — только при --w3: остаток окна W3.
  • _BANKn — только при --bank: занятость/остаток каждого 16 КБ-банка.
  • Ненулевой выход (проглатывается || true), если банк > 16 КБ или статика заехала за heap_top/0xC000.