Files
Sprinter-SDCC/docs/memory-management.md
T
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

309 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Управление памятью в 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`.
```