# Управление памятью в 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 `` даёт 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`. ```