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>
19 KiB
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, номер функции в CRST 8— BIOS, номер функции в CRST 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/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, после
загружать заново:
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 / strtoulmalloc / 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.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.cvs SDCC'stime.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