Files
snark13 c71e249a4e Add full compiler toolchain, libc, examples and reference docs
First substantive commit: the entire Sprinter C compiler tree on top of
the bare README+gitignore initial commit.

What's in here:
  bin/sprinter-cc        — driver script invoking SDCC + linker + mkexe
  libc/                  — Sprinter-specific libc layer over ESTEX/BIOS
                           (conio, gfx, io, mem, stdio + headers)
  runtime/               — crt0 variants (default/small/banked/minimal)
                           + heap + bank trampolines
  toolchain/             — mkexe (SprintEXE packer, C + tests)
  examples/              — 30 demo programs (gfx, file I/O, env, time, …)
  lib/Makefile           — builds the libc archive (sprinter.lib)
  docs/                  — converted Sprinter manuals + asm reference samples
  third_party/           — solid-c reference compiler dump + sdcc setup script
  release_docs/          — packaging / release notes

gitignore overhaul:
  • Drop dangerous blanket patterns: *.asm (would hide docs/samples/*.asm)
    and *.exe (case-insensitive match was hiding third_party/solid-c/*.EXE
    on macOS APFS).  Replaced with examples/*/*.{asm,exe,…} and lib/*.lib.
  • Restore tracking of toolchain/mkexe/tests/{one,big}.bin — those are
    INPUT fixtures, not build outputs.
  • Collapse the duplicated SDCC/C/Sdcc sections into one section per
    concern (build outputs / vendored / OS-junk).
  • Add .sprinter-cc-*/, build/ (catches lib/build/ too), .claude/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 16:13:21 +03:00

375 lines
19 KiB
Markdown
Raw Permalink 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.
# Platform Reference — особенности разработки под Sprinter
Документ собирает все нетривиальные нюансы платформы Sprinter и нашего
C-toolchain'а, накопленные в процессе разработки. Если вы пишете
программу и что-то "молча не работает" — скорее всего ответ здесь.
---
## 1. Архитектура Sprinter (cheat-sheet)
* **CPU:** Z84C15 (Z80 совместимый), 21 МГц / 3.5 МГц.
* **Адресное пространство:** 4 окна по 16 КБ:
* **W0** (0x0000..0x3FFF) — ESTEX DSS система
* **W1** (0x4000..0x7FFF) — обычно HOME-программа
* **W2** (0x8000..0xBFFF) — обычно данные / стек
* **W3** (0xC000..0xFFFF) — обычно banked / видео
* **Порты page-select:** 0x82 / 0xA2 / 0xC2 / 0xE2 для W0..W3 соответственно.
Запись номера страницы переключает окно. **Чтение** — возвращает текущий
номер страницы (полезно для детектирования).
* **Системные вызовы:**
* `RST 10h` — ESTEX DSS, номер функции в C
* `RST 8` — BIOS, номер функции в C
* `RST 30h` — Mouse driver, номер функции в C
* **Видеорежимы:** 320×256×256, 640×256×16, 80×32 text, 40×32 text.
* **Формат EXE:** SprintEXE (512-байтный header + образ HOME + опциональные банки).
---
## 2. Подводные камни вызовов системы
### IX **обязательно** сохранять
ESTEX и BIOS клобберят IX без предупреждения. SDCC использует IX как
frame pointer. Каждая обёртка над `RST 10h` / `RST 8` / `RST 30h`
ДОЛЖНА оборачивать вызов в `push ix` / `pop ix`:
```asm
push ix
ld c, #0x47 ; ESTEX APPINFO
rst #0x10
pop ix
```
Забудешь — frame pointer уедет, локальные переменные станут мусором,
debug будет долгим.
### BIOS требует стек в W2
BIOS-вызовы (`RST 8` / `CALL 3D13h`) требуют SP в диапазоне 0x8000..0xBFFF
(W2). ESTEX-вызовы — нет (они используют свой стек).
Практическое следствие: в `crt0_small.s` нельзя выделять W2 через
`BIOS $C4 EMM_GETPAGE + OUT (0xC2)`, потому что на этот момент стек
ещё в W1. Делается через **ESTEX `$3A SETWIN2`** — он маппит страницу
сразу из ESTEX без BIOS.
### ESTEX `$46 ENV` — баг в документации
`DiskSyscalls.txt v1.6` пишет: `A=0 — FOUND, A=1 — NOT FOUND`. Реально
наоборот: **A=0 — NOT FOUND**. Все наши getenv/putenv учитывают это.
### ESTEX `$21 SYSTIME` — день недели 1-based
`dow` (day-of-week) возвращается как `1..7` где `1 = Sunday`, `7 = Saturday`.
Если код ждёт `0..6` или `1..7` начиная с Monday — будет смещение.
### ESTEX `$19 F_FIRST` и каталоги "." / ".."
Запись с именем `"."` или `".."` для родительского каталога **не возвращается**
функцией F_FIRST при поиске `"*.*"` в подкаталоге. Если они нужны —
итерироваться через явные `"."` и `".."` запросы.
### `puts()` после `PCHARS` иногда теряет следующий байт
ESTEX `$5C PCHARS` после себя оставляет cursor state в котором
**следующий** `$5B PUTCHAR` может быть проигнорирован. Решение:
если нужна новая строка после PCHARS — встроить `\r\n` ВНУТРИ строки
для PCHARS, не вызывать отдельный PUTCHAR. Наш `puts()` так и делает.
---
## 3. SDCC ABI — нетривиальные моменты
### `__sdcccall(1)` — смешанная схема передачи
* **1-й 16-битный аргумент** → HL
* **2-й 16-битный аргумент** → DE
* **3-й и далее** → стек
* **1-й uint8_t / char** → A (не L!)
* **Long-аргументы на стеке** → caller pops
* **Int-аргументы на стеке** → callee pops
### Возврат значений
* `int` / `uint16_t` / `pointer`**DE** (НЕ HL как в старых SDCC!)
* `char` / `uint8_t` → A (low byte of DE)
* `long` / `uint32_t` → DE:HL (DE=low word, HL=high word)
* `float` → DE:HL по тому же layout
Самая частая ошибка — `ld a, l` в обёртках для char-возврата. Нужно
`ld a, e` (потому что char идёт в low byte регистра возврата = E).
### `__asm` блок клобберит DE, но SDCC об этом не знает
SDCC иногда сохраняет указатель аргумента в DE между C-кодом и
inline asm. Если внутри `__asm` написать `ld a, d` или `ld a, e`
(например для извлечения возврата из RST), DE будет клобберн, и
post-asm код типа `c->field = ...` запишет в случайный адрес.
**Решение:** парковать указатель в static BSS перед `__asm`, после
загружать заново:
```c
static mouse_cursor_t *dest = 0;
void mouse_get_cursor(mouse_cursor_t *c) {
dest = c;
__asm
; ... clobbers DE ...
__endasm;
mouse_cursor_t *p = dest; // SDCC fetches fresh from BSS
p->width = mc_width; // writes to correct address
}
```
### "Static без инициализатора" грабли BSS
Несколько подряд `static uint8_t x;` БЕЗ `=0` могут сколлапсировать в
**один и тот же адрес** — записи в одну стомпают другие.
```c
static uint8_t a; // адрес 0x9100
static uint8_t b; // ТОЖЕ адрес 0x9100!
static uint8_t c; // ТОЖЕ 0x9100!
a = 0xAA; b = 0xBB; c = 0xCC;
// a == b == c == 0xCC
```
**Решение:** всегда инициализировать: `static uint8_t a = 0;` — SDCC
гарантированно резервирует разные адреса.
### z80.lib почти полная — НЕ переписывать
SDCC z80.lib содержит работающие реализации:
- `atoi / atol / atof / strtol / strtoul`
- `malloc / free / calloc / realloc` (мы только переопределили heap location)
- `qsort / bsearch / rand / srand / abs / div`
- Полный `<string.h>` (memcpy/memset/strlen/strcmp/strcpy/strchr/strstr/strtok/etc.)
- `<ctype.h>` (toupper/tolower/isalpha/isdigit/etc.)
- `<math.h>` (sinf/cosf/sqrtf/etc.)
Линкер автоматически тянет нужное из z80.lib когда есть unresolved
symbol. Не переписывать ради переписывания.
---
## 4. Banking — нюансы
### ABI banked-вызовов
SDCC эмитит для `void f(int x) __banked`:
- символ `b_f = N` (bank id = число из `--codeseg BANKn`)
- символ `_f` = адрес внутри банка (с `bank_id` в верхнем 8-битном байте)
Вызов:
```asm
ld hl, #arg_value
push hl
ld e, #b_f ; E = bank id
ld hl, #_f ; HL = target addr (low 16 bits)
call ___sdcc_bcall_ehl
pop af ; caller cleans up arg
```
### Стековый "spacer" в trampoline
Между ret-адресом callee'я и аргументами трамплин ОБЯЗАН вставить
**ровно 3 байта** (1 сохранённая страница + 2 байта внутреннего
bcall return). SDCC компилирует доступ к аргументам с offset'ом +5
от стека. Любая разница ломает все banked-вызовы.
### CRITICAL: `pop af; out (n), a` клобберит A
Старый trampoline восстанавливал W3-страницу через `pop af`
**клоббит A**, а SDCC возвращает uint8_t/char именно в A. Все
banked-функции с char-возвратом тихо теряли результат.
Текущая версия использует `pop bc; ld c, #port; out (c), b` — порт
через C, значение через B, A сохраняется нетронутым.
### Bank-local статические данные
Для модуля целиком в банке:
```sh
sdcc --codeseg BANK1 --constseg BANK1 --dataseg BANK1 -c bank1.c
```
Всё (код + const + BSS) живёт в банке. `mkexe -p 0` нужен чтобы BSS
загружался обнулённым (иначе будет FF из padding).
Heap через `malloc()` из banked-функции работает прозрачно — heap в
W2 (HOME), W2 trampoline никогда не свопит, указатель валиден из
любого контекста.
---
## 5. Видео-режимы и графика
### Mode 0x81 (320×256×256)
* Адресация: pixel `(x, y)` → CPU-адрес `0xC000 + x` с **Port_Y (0x89) = y**
* 320 байт на видимую строку
* Палитра: 256 цветов из 4 палитр (BIOS `$A4 PIC_SET_PAL`)
* Cleanup: 0x300..0x39F = mode-descriptors, 0x3E0..0x3FF = palette данные — НЕ трогать в обычной отрисовке
### Mode 0x82 (640×256×16)
* Та же row-addressing что и 320 mode (320 байт на строку)
* НО каждый байт = 2 пикселя по 4 бита
* **HIGH nibble (биты 7-4) = LEFT пиксель** (even x)
* **LOW nibble (биты 3-0) = RIGHT пиксель** (odd x)
* Доки пишут "первыми младшие 4 бита" — это про **временной** порядок
в FPGA-сериализаторе, **не пространственный** на экране
* Палитра: 16 нижних цветов из любой из 4 палитр
### Графический Accelerator
CPU-опкоды используются как control-сигналы (NOP-tricks):
- `LD D,D` (0x52) — "ждать LD A, imm для размера блока"
- `LD C,C` (0x49) — Fill mode (горизонталь): `LD (HL),A` заполняет N байт
- `LD E,E` (0x5B) — Fill mode (вертикаль): `LD (HL),A` заполняет N pix вертикально (авто-Y инкремент)
- `LD L,L` (0x6D) — Copy (horizontal)
- `LD A,A` (0x7F) — Copy (vertical)
- `LD B,B` (0x40) — выключить accel
**КРИТИЧНО:** размер блока должен быть **immediate операндом** `LD A, n` (опкод 0x3E nn) сразу после `LD D,D`. Accel snoop'ит этот байт. `LD A, (mem)` (опкод 0x3A) — другой 2-й байт, accel захватит мусор.
**КРИТИЧНО 2:** между `LD C,C` (Fill mode) и `LD (HL),A` (fire) **нельзя** ставить второй `LD A, #imm` — accel re-interpret'ит его как новый block-size. Color через `C`/`B` регистр + `LD A, C` (опкод 0x79, 1 байт).
**Скорость:** ~7 µs/byte vs ~14-20 µs/byte ручного цикла → ~2-3× быстрее.
**Прерывания:** DI/EI обязательны вокруг accel — он подменяет систему команд CPU, ISR в это время крашит.
Реализация в `libc/gfx/gfx_lines.c``gfx_hline`/`gfx_vline`/`gfx_rect`/`gfx_fill_rect`. `gfx_clear` использует accel-burst column-major (320 vfill'ов × 256 пикселей за burst, ~4× быстрее ручного цикла).
### Формат шрифта BIOS
Шрифт 2 КБ = 256 chars × 8 rows × 1 byte/row, **interleaved row-major**:
`offset = row * 256 + char_code`.
То есть row 0 всех 256 chars лежит в 0x000..0x0FF, row 1 в 0x100..0x1FF, ...
Не "char 0 в 0x000..0x007, char 1 в 0x008..0x00F"! Наивное `font[char*8+row]` даст нечитаемую кашу.
Bit order внутри byte: **MSB-first**. Bit 7 = крайний левый пиксель.
Получить системный шрифт: BIOS `$B8 WIN_GET_ZG`, DE = destination, читает 2 КБ.
---
## 6. Memory modes
DSS выделяет страницы RAM по размеру программы. **Программа ≤16 КБ получает только 1 страницу.** В остальные окна подключается "страница `0xFF`" (read = 0xFF, write игнорируется).
Из-за этого классическая раскладка "код в W1 (0x4100+), данные в W2 (0x8000+)" для маленькой программы **молча не работает** — write в W2 уходит в никуда.
Решение — наши 5 режимов:
| Mode | Layout | Trick |
|---|---|---|
| tiny | CODE + DATA в W2 | DSS гарантирует W2 (загружает в неё образ) |
| small | CODE в W1, DATA chained | crt0_small читает порт 0xC2 — если 0xFF, выделяет W2 через ESTEX `$3D`/`$3A` |
| big | tiny + банки в W1 | crt0_banked с BANK_W1=1, trampoline свопит порт 0xA2 |
| huge | small + банки в W3 | crt0_banked с default BANK_W1=0, trampoline на port 0xE2 |
### Детектирование W2 — порт 0xC2
`IN A, (0xC2)` возвращает текущую страницу в W2. `0xFF` = "не выделена". Используется в crt0_small для auto-detect.
**Грабли:** в первой версии стоял `IN A, (0xA2)` — это **W1**, не W2! Для small mode там всегда code-page (не 0xFF) → auto-detect не срабатывал, W2 не выделялась, программа крашилась.
---
## 7. Mouse driver
* Всё через `RST 30h`, номер функции в `C`.
* Координаты в **пикселях**. Для text mode 03h (80×32) делить x/8 и y/8.
* **Sensitivity = divider** (не коэффициент): меньше = быстрее курсор.
Документация ProgrammerManual пишет наоборот — это ошибка.
* MAME `$0E GET_SENSITIVE` возвращает 0 (stub). Workaround: всегда
ставить значение через `mouse_set_sensitivity()` при старте.
* MAME `$0B RETURN_CURSOR` пишет битмап в IX-буфер, но не обновляет H/L/D/E.
`mouse_get_cursor()` вернёт width/height/hot_x/hot_y как 0 — это известное ограничение эмулятора.
* **Cursor bitmap format**: 1 byte per pixel, row-major; `0xFF = transparent`.
Cursor живёт в отдельном видео-банке, не в 0x50 page.
### `$81 CHANGE VIDEO MODE`
При смене видеорежима — звать `mouse_video_mode_changed(new_mode)`.
**Важно:** аргумент `A = режим экрана` обязателен, в документации
указан но легко пропустить. Без него драйвер не пересинхронизирует
координаты, и в graphics mode может остаться text-mode XOR-курсор.
---
## 8. Linker warnings — что есть и почему
`sdldz80` пишет `?ASlink-Warning-Definition of public symbol '_X' found more than once` когда наша `sprinter.lib` override'ит функцию из SDCC's `z80.lib`.
Текущие overrides:
- `_puts` — наш через PCHARS+\r\n vs SDCC стандартный
- `___sdcc_heap` — наш heap в W2 vs стандартный
- `_asctime`, `_localtime` — наш `posix_time.c` vs SDCC's `time.rel` (требует _RtcRead)
Линкер берёт **первое найденное** определение — это наши. Warning только шум.
`sprinter-cc` отфильтровывает эти warning-блоки из вывода `sdcc` (3 строки: warning + 2 follow-up `Library:` строк). Через `-v` всё видно.
---
## 9. Текстовый вывод — Turbo-C convention
В Sprinter нет ESTEX-функции "set persistent attribute" — только WRCHAR пишет char+attr единоразово. Поэтому два набора функций:
| Группа | Header | Скорость | Цвет |
|---|---|---|---|
| stdio | `<stdio.h>` | Fast (~5 µs/char через PCHARS / PUTCHAR) | НЕТ — ambient |
| conio | `<conio.h>` | Slow (~50 µs/char через WRCHAR) | ДА — `g_text_attr` |
`puts` / `printf` / `putchar` — быстрые без цвета. Цвет = whatever shell оставил. Программа должна `clrscr_attr(attr)` если нужен конкретный default.
`cputs` / `cprintf` / `putch` — медленные с цветом. Применяют `g_text_attr` (`textcolor`/`textbackground`/`textattr`). При `g_text_attr == KEEP_EXIST_ATTR` (0xFFFF) — fallback на fast path.
**Cputs/putch НЕ делают `\n` → CR LF translation** (как в Turbo C). Caller должен явно писать `"\r\n"`. `puts` делает.
---
## 10. Прочее
### `dec/hex8/16/32` — мини-форматтеры
Solid-C-style минимальный вывод чисел без формата:
```c
hex8(0xAB); // печатает "AB"
hex16(0xCAFE); // "CAFE"
dec16(50000); // "50000"
```
Использовать когда не хочется тащить полный printf.
### `<sprinter_compat.h>`
Единый header который подтягивает все стандартные + добавляет Solid-C
shims: `BOOL`/`uint`/`WORD`/`f_point` types, `setmem`/`movmem`/`min`/`max`
макросы, `inp`/`outp`, `enable`/`disable`, `ms_*` mouse aliases.
Программы из Solid-C 2004 портируются с минимальными правками.
### `--debug` runtime flag
```sh
sprinter-cc --debug -o foo.exe foo.c
```
Prepend'ит `DEBUG_RT = 1` в crt0 + передаёт `-DDEBUG_RT` в SDCC. Открывает symbol `_w2_self_allocated` (uint8_t) — runtime diagnostic кто аллоцировал W2 (0 = DSS, 1 = crt0 сам). Полезно для troubleshooting'а в small mode.
### MAME testing workflow
```sh
make floppy # пакует все .exe + data в mame/v306/IMG/mc.img
cd mame/v306 && ./run_mame.sh
```
Имена файлов на флопе должны быть 8.3 (FAT12). Все примеры названы соответственно — `banked_big → bankedbg`, `seek_demo → seek`, `time_dir_test → timedir`, etc.
---
## История изменений
- 2026-06-01 — первый релиз v1.0