Скролл региона 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>
18 KiB
Управление памятью в 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 #08BIOS,RST #10ESTEX). Пока в W0 стоит системная страница, вызовы доступны; подменять W0 нельзя без потери RST-ов. - «Неиспользуемое» окно маппится на специальную страницу
#FF: чтение даёт0xFF, запись игнорируется. Это ключевая причина «молчаливой» порчи данных — см. §7.
Порты в C
<sprinter.h> даёт SFR-обёртки: _io_page_w0..w3 (чтение/запись порта),
sprinter_page_w0..w3(page).
2. Как DSS загружает EXE
Из документации DSS, последовательность EXEC:
- Открыть exe-файл на чтение.
- Считать префикс exe в рабочую область.
- Выделить блок памяти размером под весь файл (если
loader==0) или под первичный загрузчик (loader>0). - Сохранить стек.
- Подключить страницы выделенного блока в окна (последовательно от окна адреса загрузки: W1→W2→W3…).
- Построить префикс запуска → регистр
IX. - Считать файл по адресу загрузки (смещение 16 в заголовке).
- Закрыть exe, если это не первичный загрузчик (
loader==0). - Установить
SP= значение из смещения 20 (адрес стека). - Передать управление по адресу из смещения 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(CODE0x4100, 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.