95c22be9bd
Скролл региона 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>
309 lines
18 KiB
Markdown
309 lines
18 KiB
Markdown
# Управление памятью в 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`.
|
||
```
|