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

19 KiB
Raw Permalink Blame History

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:

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 / pointerDE (НЕ 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, после загружать заново:

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 могут сколлапсировать в один и тот же адрес — записи в одну стомпают другие.

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-битном байте)

Вызов:

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 статические данные

Для модуля целиком в банке:

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.cgfx_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 минимальный вывод чисел без формата:

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

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

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