From 46bd9ad0f1699a8702df83eaec528ed5067c70ef Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 8 Jul 2026 10:47:57 +0300 Subject: [PATCH] =?UTF-8?q?gfx/time:=20vsync=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20polling=20=D0=B1=D0=B8=D1=82=D0=B0=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=B4=D1=80=D0=B0,=20sleep/delayms=20=D0=B1=D0=B5=D0=B7=20halt?= =?UTF-8?q?-=D0=BF=D0=BE=D0=B4=D1=81=D1=87=D1=91=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gfx_wait_vsync() и sleep() раньше предполагали, что КАЖДОЕ прерывание на векторе 0xFF — кадровый тик; с CBL/клавиатурой на том же векторе это уже не так. gfx_wait_vsync(): вместо halt — polling бита 5 порта 0xFE (реальная позиция луча, см. MAME kbd_fe_r), доступного пока включён CBL bit7 порта 0x004E. Разделяемое владение портом с CBL через _cbl_port_ref/unref (тот же ref-counting паттерн, что у IM2-таблицы) — cbl_close() возвращает "немой" режим вместо полного выключения, если gfx ещё держит ссылку. Fallback на halt при таймауте. sleep()/delayms(): калиброванный busy-wait по духу delayms.asm вместо подсчёта halt-пробуждений. Калибровка одна на кадр (не на секунду — не переполняет uint16_t и не требует умножения/32-бит арифметики), общий движок libc/time/_sleep_calib.c для обеих функций. Fallback на старое поведение при EBUSY (фрейм-хук занят другим irq_install()). Co-Authored-By: Claude Sonnet 5 --- docs/libc-reference.md | 65 +++++++++++++++++++++++++- docs/size_baseline.tsv | 10 ++-- libc/cbl/_cbl.h | 19 ++++++++ libc/cbl/_cbl_port.c | 34 ++++++++++++++ libc/cbl/_cbl_state.c | 4 +- libc/cbl/cbl_close.c | 14 ++++-- libc/gfx/gfx_wait_vsync.c | 69 +++++++++++++++++++++++++--- libc/include/unistd.h | 7 ++- libc/time/_sleep_calib.c | 96 +++++++++++++++++++++++++++++++++++++++ libc/time/_sleep_calib.h | 31 +++++++++++++ libc/time/delayms.c | 42 +++++++++++++++++ libc/time/sleep.c | 60 ++++++++++++++++++------ tests/rt_test/rt_test.c | 6 +++ 13 files changed, 421 insertions(+), 36 deletions(-) create mode 100644 libc/cbl/_cbl_port.c create mode 100644 libc/time/_sleep_calib.c create mode 100644 libc/time/_sleep_calib.h create mode 100644 libc/time/delayms.c diff --git a/docs/libc-reference.md b/docs/libc-reference.md index 99344e4..4c4b536 100644 --- a/docs/libc-reference.md +++ b/docs/libc-reference.md @@ -88,11 +88,50 @@ in-place регистр, латиница + кириллица CP866. | `int isatty(fd)` | fd <= 0 (файловые манипуляторы DSS с 1) | | `int mkdir/rmdir/chdir(path)` | ESTEX $1B/$1C/$1D | | `char *getcwd(buf, size)` | буфер 256 байт, size игнорируется | -| `void sleep(seconds)` | 50 halt/с по кадровому IRQ | +| `void sleep(seconds)` | калиброванный busy-wait (см. ниже) | +| `void delayms(uint16_t ms)` | то же, гранулярность — миллисекунды (см. ниже) | **Лимит: 8 одновременных манипуляторов**; 9-й OPEN вешает DSS — libc отказывает сама (EMFILE, предохранитель _fd_guard). +**`sleep()` (2026-07-07, редизайн)**: старая версия считала halt- +пробуждения (50 = 1 c), предполагая, что КАЖДОЕ прерывание — кадровый +тик; с CBL/клавиатурой на векторе 0xFF это уже не так (CBL прерывает +намного чаще кадра — sleep() возвращался бы раньше срока). Теперь: +лениво, один раз калибруется кратковременным `irq_install()` против +РЕАЛЬНОГО кадрового тика (трамплин зовёт хук только на настоящих +кадровых прерываниях — клавиатура/CBL уходят в свои ветки раньше), +считая, сколько итераций тесного цикла умещается в один КАДР (не в +секунду — калибровка на секунду переполняла `uint16_t`, был баг: +`sleep(5)` отрабатывал быстрее секунды, найден пользователем на +реальном прогоне и исправлен). `sleep(seconds)` — вложенный цикл: +внешний по секундам, внутренний ровно 50 раз калиброванный busy-wait — +без единого прерывания и БЕЗ умножения/32-битной арифметики (по духу +`docs/samples/delayms.asm`, который тоже калибрует на 1 мс, а не на +1 с). Погрешность ~5-10% (калибровочный и рабочий циклы не тактово- +идентичны). Fallback на старое "50 halt" поведение, если фрейм-хук уже +занят другим `irq_install()`-клиентом (EBUSY). +**Cтоит дороже по размеру** (~+580 Б) — тянет весь модуль `irq` +(install/remove/трамплин/IM2-таблицу), даже если программа больше +ничего из irq не использует. + +**`delayms(ms)`** — тот же движок (`libc/time/_sleep_calib.c`), общий с +`sleep()`: калибровка одна на двоих (первый вызов ЛЮБОЙ из функций +калибрует, вторая просто использует готовое). Производная величина +"итераций на 1 мс" — одно 16-битное деление (`__divuint`, НЕ +`__mullong`) на константу 20, вычисляется один раз при калибровке; +`delayms(ms)` — простой цикл `ms` раз, без умножения вообще. Fallback +при EBUSY — грубый (одно кадровое `halt`), точной альтернативы для +миллисекундной гранулярности без калибровки нет. + +**Квирк общий для sleep()/delayms()**: калибровка сама стоит ~20-40 мс +(синхронизация на границу кадра + сам замер) — ПЕРВЫЙ вызов ЛЮБОЙ из +двух функций в программе превысит запрошенное время на эту величину; +для `delayms()` с маленьким `ms` это заметно (`delayms(5)` на первом +вызове может растянуться на ~25-45 мс). Все последующие вызовы точны. +Оставлено как есть по решению пользователя — не стали усложнять API +отдельным calibrate()-примитивом. + ## `errno` (int), коды = коды DSS (EOK..EUNKERR) + POSIX-имена @@ -146,7 +185,20 @@ wrchar rdchar`; режимы `gettextmode/settextmode` (0x02=40×32, Setup: `gfx_init(mode,page)→prev`, `gfx_done(prev)`. Страницы/банк: `gfx_set/get_visible_page`, `gfx_set/get_draw_page` (double buffering), `gfx_set/get_bank` (0x50..0x5F, 0x58 = FF- -прозрачность), `gfx_wait_vsync()`. +прозрачность). + +`gfx_wait_vsync()` (2026-07-07, редизайн): ждёт переход бита 5 порта +0xFE из 1 в 0 — реальное аппаратное состояние луча (Y>256 → начало +кадра, см. MAME sprinter.cpp kbd_fe_r), а не прерывание — поэтому не +путается с клавиатурой/CBL/CTC, деляющими вектор 0xFF. Бит доступен +только пока включён `cbl_mode()` (bit7 порта 0x004E) — если приложение +уже играет через `cbl_open()`, бит достаётся бесплатно; иначе +`gfx_wait_vsync()` лениво занимает bit7 "немым" кодом частоты через +`_cbl_port_ref()`/`_cbl_port_unref()` (см. ``, `_cbl_port.c`) — +разделяемое владение портом 0x004E, безопасное при любом порядке +использования с реальным CBL-звуком. Фолбэк на одно кадровое +прерывание (`halt`), если бит не ведёт себя как ожидается за разумное +число попыток. Примитивы (суффикс _256 / _16): `clear putpixel hline vline line rect fill_rect` — через акселератор (hline/vline burst до 256 байт; в 0x82 vline через RMW). Координаты int, клиппинг по краям. @@ -224,6 +276,15 @@ banked-функции; только volatile-глобалы и быстрая р блока) читается только пока `cbl_open` не закрыт (при выключенном CBL бит всегда 1 — MAME-квирк). +**Разделяемое владение портом 0x004E** (2026-07-07): бит 5 порта 0xFE +(позиция луча — см. `` `gfx_wait_vsync()`) доступен только пока +включён bit7 порта 0x004E, независимо от того, играет ли реальный +звук. `_cbl_port_ref()`/`_cbl_port_unref()` (internal, `_cbl_port.c`) +дают gfx-модулю занять bit7 "немым" кодом частоты (не заводящим таймер +CBL — без звука/прерываний), не мешая реальной `cbl_open()`-сессии, +если она уже идёт (и наоборот — `cbl_close()` возвращает "немой" режим +вместо полного выключения порта, если gfx его ещё держит). + ## — низкий уровень (BIOS $A4/$A6) `pal_load pal_get pal_set_color pal_get_color` (страница 0..7, diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv index 8feb7ba..1db9216 100644 --- a/docs/size_baseline.tsv +++ b/docs/size_baseline.tsv @@ -8,9 +8,9 @@ banklocl 4832 banktest 3767 bios_text 4470 cat 915 -cblstream 6234 -cbltest 6374 -cblwav 6443 +cblstream 6277 +cbltest 6407 +cblwav 6486 conio 4634 conio2 3946 dec_test 860 @@ -20,7 +20,7 @@ fdmax 6337 filetest 11624 gets 509 gfx_d16 3319 -gfx_dbuf 2396 +gfx_dbuf 2544 gfx_demo 3219 gfx_mous 5542 gfx_text 2560 @@ -35,7 +35,7 @@ mem_test 4558 mouse 4394 openenv 6127 ptime 5896 -rt_test 4103 +rt_test 4906 seek 4177 simple 963 solidt 12381 diff --git a/libc/cbl/_cbl.h b/libc/cbl/_cbl.h index 431f6f8..a57b039 100644 --- a/libc/cbl/_cbl.h +++ b/libc/cbl/_cbl.h @@ -23,6 +23,15 @@ * был бы избыточен. _cbl_fill — callback приложения; _cbl_silence — * буфер тишины, malloc'ится в cbl_open() ТОЛЬКО при * CBL_UNDERRUN_SILENCE (иначе NULL). + * + * _cbl_port_ref/_cbl_port_unref (_cbl_port.c) — общий счётчик желающих + * держать bit7 порта 0x004E включённым НЕЗАВИСИМО от полноценной CBL- + * сессии. Нужно gfx_wait_vsync() (см. libc/gfx/gfx_wait_vsync.c): бит + * 5 порта 0xFE отражает позицию луча (см. MAME sprinter.cpp kbd_fe_r), + * но доступен ТОЛЬКО пока включён cbl_mode(). Если реальный CBL уже + * открыт — портом владеет он, ref/unref его не трогают; иначе включают + * bit7 "немым" кодом частоты _CBL_VSYNC_FREQ (таймер CBL не заводится + * — divs[2]=0 в MAME — ни звука, ни прерываний, только синхро-бит). */ #ifndef _CBL_INTERNAL_H #define _CBL_INTERNAL_H @@ -33,6 +42,7 @@ #define _CBL_CTRL_PORT 0x004E #define _CBL_DATA_PORT 0x4F #define _CBL_ACCEL_PAGE 0xFD /* спец-страница EMM: запись на W3@0xC000 идёт в CBL */ +#define _CBL_VSYNC_FREQ 0x02 /* "немой" код частоты — divs[2]=0 в MAME, таймер не идёт */ extern uint16_t _cbl_block; /* размер блока: 128 или 256 (по формату) */ extern uint16_t _cbl_reqs; /* блоков-запросов обслужено */ @@ -40,6 +50,7 @@ extern uint16_t _cbl_undr; /* из них недолив (fill в extern cbl_fill_fn _cbl_fill; /* callback приложения, может быть NULL */ extern uint8_t _cbl_underrun_mode; /* CBL_UNDERRUN_* */ extern uint8_t *_cbl_silence; /* буфер тишины или NULL */ +extern uint8_t _cbl_port_refs; /* держатели bit7 помимо реальной CBL-сессии */ /* Запись в 16-битный порт управления 0x004E. */ void _cbl_ctrl(uint8_t v); @@ -49,4 +60,12 @@ void _cbl_ctrl(uint8_t v); void _cbl_pump_otir(void); void _cbl_pump_accel(void); +/* Разделяемое владение портом 0x004E (см. описание выше). + * _cbl_port_sync пересчитывает нужное значение порта из текущего + * состояния (реальная сессия? держатели?) и пишет его — вызывается + * из ref/unref, а также из cbl_close() после снятия _irq_cbl_hook. */ +void _cbl_port_ref(void); +void _cbl_port_unref(void); +void _cbl_port_sync(void); + #endif diff --git a/libc/cbl/_cbl_port.c b/libc/cbl/_cbl_port.c new file mode 100644 index 0000000..01a3c82 --- /dev/null +++ b/libc/cbl/_cbl_port.c @@ -0,0 +1,34 @@ +/* + * _cbl_port — разделяемое владение bit7 порта управления 0x004E между + * реальной CBL-сессией (cbl_open/cbl_close) и другими держателями + * (сейчас — gfx_wait_vsync(), которому нужен только бит 5 порта 0xFE, + * доступный лишь пока включён cbl_mode(); см. libc/gfx/gfx_wait_vsync.c + * и MAME sprinter.cpp kbd_fe_r). + * + * Если реальный CBL уже открыт (_irq_cbl_hook != 0) — портом владеет + * он, ref/unref его не трогают вообще (это уже нужное состояние). + * Иначе включаем/выключаем bit7 "немым" кодом частоты _CBL_VSYNC_FREQ + * — таймер CBL при этом коде не заводится (divs[2]=0 в MAME), значит + * ни звука, ни прерываний, только доступ к синхро-биту. + */ + +#include "_cbl.h" +#include "../irq/_irq.h" + +void _cbl_port_sync(void) +{ + if (_irq_cbl_hook) return; /* реальный CBL сам владеет портом */ + _cbl_ctrl(_cbl_port_refs ? (uint8_t)(0x80 | _CBL_VSYNC_FREQ) : 0); +} + +void _cbl_port_ref(void) +{ + _cbl_port_refs++; + _cbl_port_sync(); +} + +void _cbl_port_unref(void) +{ + if (_cbl_port_refs) _cbl_port_refs--; + _cbl_port_sync(); +} diff --git a/libc/cbl/_cbl_state.c b/libc/cbl/_cbl_state.c index f9db8d1..24ba680 100644 --- a/libc/cbl/_cbl_state.c +++ b/libc/cbl/_cbl_state.c @@ -1,6 +1,7 @@ /* * _cbl_state — данные CBL-модуля: размер блока, счётчики, callback - * приложения, режим недолива, буфер тишины (если аллоцирован). + * приложения, режим недолива, буфер тишины (если аллоцирован), + * счётчик разделяемых держателей порта (см. _cbl_port.c). * Только данные. */ @@ -12,3 +13,4 @@ uint16_t _cbl_undr; cbl_fill_fn _cbl_fill; uint8_t _cbl_underrun_mode; uint8_t *_cbl_silence; +uint8_t _cbl_port_refs; diff --git a/libc/cbl/cbl_close.c b/libc/cbl/cbl_close.c index 3fbcca6..87dce09 100644 --- a/libc/cbl/cbl_close.c +++ b/libc/cbl/cbl_close.c @@ -1,8 +1,12 @@ /* - * cbl_close — выключить CBL (запись 0 в порт управления, как в - * сэмпле), снять хук насоса, освободить буфер тишины (если был - * аллоцирован под CBL_UNDERRUN_SILENCE) и отпустить IM2-таблицу. - * Идемпотентно; вызывается и из atexit. + * cbl_close — выключить CBL, снять хук насоса, освободить буфер + * тишины (если был аллоцирован под CBL_UNDERRUN_SILENCE) и отпустить + * IM2-таблицу. Идемпотентно; вызывается и из atexit. + * + * Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync() + * пересчитывает нужное значение: если gfx_wait_vsync() ещё держит + * ссылку (_cbl_port_refs > 0), останется "немой" режим ради бита + * синхронизации; иначе порт гасится полностью. */ #include @@ -13,9 +17,9 @@ void cbl_close(void) { if (!_irq_cbl_hook) return; IRQ_DISABLE(); - _cbl_ctrl(0); _irq_cbl_hook = 0; IRQ_ENABLE(); + _cbl_port_sync(); free(_cbl_silence); _cbl_silence = 0; _irq_table_unref(); diff --git a/libc/gfx/gfx_wait_vsync.c b/libc/gfx/gfx_wait_vsync.c index 97021fe..469d8df 100644 --- a/libc/gfx/gfx_wait_vsync.c +++ b/libc/gfx/gfx_wait_vsync.c @@ -1,18 +1,73 @@ /* - * gfx_wait_vsync — ждать следующий кадровый IM2-прерыв (50 Гц, DSS). + * gfx_wait_vsync — ждать переход бита 5 порта 0xFE из 1 в 0: выход из + * нижнего blank/border (Y > 256 — см. MAME sprinter.cpp kbd_fe_r: + * `vpos() >= BORDER_TOP+SCREEN_YSIZE`) — начало нового кадра, идеальный + * момент для свопа страниц без разрывов. Это чтение РЕАЛЬНОГО + * состояния луча (не событие/прерывание), поэтому не подвержено + * путанице с другими источниками вектора 0xFF (клавиатура, CBL, CTC) — + * подробнее см. memory/gfx_wait_vsync_redesign. * - * HALT усыпляет Z80 до ближайшего IRQ; DSS обрабатывает его и - * возвращается на инструкцию после HALT — это начало обратного хода - * луча, идеальный момент для свопа страниц без разрывов. + * Бит доступен ТОЛЬКО пока включён cbl_mode() (bit7 порта 0x004E) — + * см. libc/cbl/_cbl_port.c: если приложение уже играет через + * cbl_open(), берём бит бесплатно (порт не трогаем); иначе + * лениво занимаем bit7 "немым" кодом частоты через _cbl_port_ref() + * при первом вызове, освобождаем в atexit. + * + * Fallback: если ни разу не увидели ожидаемое состояние за разумное + * число попыток (иное железо, бит ведёт себя не как в MAME) — + * откатываемся на одно кадровое прерывание (halt), чтобы не + * зависнуть намертво. */ #include "_gfx.h" +#include "../cbl/_cbl.h" +#include -void gfx_wait_vsync(void) __naked +static uint8_t vsync_ready; + +static void vsync_cleanup(void) { + _cbl_port_unref(); +} + +void gfx_wait_vsync(void) +{ + if (!vsync_ready) { + vsync_ready = 1; + _cbl_port_ref(); + atexit(vsync_cleanup); + } + __asm + ;; --- фаза 1: дождаться bit5==1 (гарантированно в blank) --- + ld bc, #0 ; ~65536 попыток на фазу — таймаут + _gwv_wait1: + in a, (#0xFE) + bit 5, a + jr nz, _gwv_have1 + dec bc + ld a, b + or c + jr nz, _gwv_wait1 + jr _gwv_fallback ; bit5 ни разу не встал за таймаут + _gwv_have1: + + ;; --- фаза 2: дождаться bit5==0 (сам переход — начало кадра) --- + ld bc, #0 + _gwv_wait0: + in a, (#0xFE) + bit 5, a + jr z, _gwv_done + dec bc + ld a, b + or c + jr nz, _gwv_wait0 + ;; таймаут и здесь — переходим к тому же фолбэку + + _gwv_fallback: ei - halt - ret + halt ; аварийный путь: одно кадровое прерывание + + _gwv_done: __endasm; } diff --git a/libc/include/unistd.h b/libc/include/unistd.h index 1677390..e3ca245 100644 --- a/libc/include/unistd.h +++ b/libc/include/unistd.h @@ -13,6 +13,7 @@ #define UNISTD_H #include /* size_t */ +#include /* uint16_t */ /* lseek whence values (POSIX). */ #define SEEK_SET 0 @@ -25,9 +26,13 @@ int close(int fd); int unlink(const char *path); long lseek(int fd, long offset, int whence); -/* Block the calling task for `seconds` seconds (50 Hz IRQ-based timer). */ +/* Block the calling task for `seconds` seconds (calibrated busy-wait). */ void sleep(unsigned int seconds); +/* Block the calling task for `ms` milliseconds (calibrated busy-wait, + * same engine as sleep() — see libc/time/_sleep_calib.c). */ +void delayms(uint16_t ms); + /* 1, если fd — консоль (наши псевдо-fd 0/-1/-2), 0 — файл (любой * положительный манипулятор DSS; из командной строки они идут с 1). */ int isatty(int fd); diff --git a/libc/time/_sleep_calib.c b/libc/time/_sleep_calib.c new file mode 100644 index 0000000..1d1ec8f --- /dev/null +++ b/libc/time/_sleep_calib.c @@ -0,0 +1,96 @@ +/* + * _sleep_calib — калиброванный busy-wait, общий движок для sleep() и + * delayms(). Один раз, лениво при первом вызове ЛЮБОЙ из них, ставим + * временный irq_install()-хендлер и считаем, сколько итераций тесного + * asm-цикла умещается между двумя кадровыми тиками (трамплин зовёт + * user-handler ТОЛЬКО на настоящих кадровых прерываниях — клавиатура/ + * CBL уходят в свои ветки раньше, см. docs/im2_isr_design.md). Дальше + * — чистый busy-wait (_sleep_spin) БЕЗ единого обращения к прерываниям. + * + * Единица калибровки — "итераций НА ОДИН КАДР" (не на секунду и не + * заранее домноженная) — по духу docs/samples/delayms.asm (там тоже + * калибруют на 1 мс, а не на 1 с): масштабирование делает вызывающий + * код через вложенный цикл повторных вызовов _sleep_spin(), а не через + * предварительное умножение константы. _sleep_units_per_ms — простое + * деление на константу 20 (один раз при калибровке), НЕ умножение и НЕ + * 32-битная арифметика. + * + * Точность: калибровочный (_sleep_count_frame) и рабочий (_sleep_spin) + * циклы не тактово-идентичны — ожидаемая систематическая погрешность + * в пределах нескольких процентов, того же порядка, что и у + * delayms.asm. Достаточно для семантики "примерно N". + * + * Квирк: калибровка сама стоит ~20-40 мс (синхронизация на границу + * кадра + сам замер до следующего тика) — ПЕРВЫЙ вызов sleep()/ + * delayms() в программе превысит запрошенное время на эту величину; + * все последующие вызовы точны (калибровка кэшируется). Для delayms() + * с маленьким ms это заметно (напр. delayms(5) на первом вызове может + * растянуться на ~25-45 мс) — если это критично, program может явно + * "прогреть" калибровку заранее (delayms(1) в начале main(), подальше + * от time-критичного кода) — отдельного calibrate()-примитива нет, + * решили не усложнять API. + */ + +#include "_sleep_calib.h" +#include + +uint16_t _sleep_units_per_frame; +uint16_t _sleep_units_per_ms; +static volatile uint8_t _sleep_tick_flag; + +static void _sleep_tick_handler(void) +{ + _sleep_tick_flag = 1; +} + +/* Считать итерации тесного цикла, пока *flag (HL — arg1) не станет + * ненулевым; вернуть счётчик (DE — возврат uint16_t по ABI). */ +static uint16_t _sleep_count_frame(volatile uint8_t *flag) __naked +{ + (void)flag; + __asm + ld de, #0 + _scf_loop: + inc de + ld a, (hl) + or a, a + jr z, _scf_loop + ret + __endasm; +} + +/* Прогнать units (HL — arg1) итераций тесного цикла — чистый busy-wait, + * ни одного обращения к чему-либо, связанному с прерываниями. */ +void _sleep_spin(uint16_t units) __naked +{ + (void)units; + __asm + _ssp_loop: + ld a, h + or l + ret Z + dec hl + jr _ssp_loop + __endasm; +} + +void _sleep_calibrate(void) +{ + if (irq_install(_sleep_tick_handler) != 0) { + _sleep_units_per_frame = 0; /* останется 0 — вызывающий возьмёт fallback */ + _sleep_units_per_ms = 0; + return; + } + + _sleep_tick_flag = 0; + while (!_sleep_tick_flag) ; /* синхронизация на границу кадра */ + _sleep_tick_flag = 0; + + uint16_t count = _sleep_count_frame(&_sleep_tick_flag); + + irq_remove(); + + _sleep_units_per_frame = count ? count : 1; + _sleep_units_per_ms = (uint16_t)(_sleep_units_per_frame / 20u); + if (!_sleep_units_per_ms) _sleep_units_per_ms = 1; +} diff --git a/libc/time/_sleep_calib.h b/libc/time/_sleep_calib.h new file mode 100644 index 0000000..1780cfa --- /dev/null +++ b/libc/time/_sleep_calib.h @@ -0,0 +1,31 @@ +/* + * _sleep_calib.h — внутренности калиброванного busy-wait, общие для + * sleep() и delayms() (НЕ публичный заголовок). + * + * Калибровка лениво запускается ПЕРВЫМ вызовом sleep() ИЛИ delayms() — + * если программа не зовёт ни одну из них, весь этот код даже не + * линкуется (DCE по модулям). См. sleep_redesign / delayms в памяти. + */ +#ifndef _SLEEP_CALIB_INTERNAL_H +#define _SLEEP_CALIB_INTERNAL_H + +#include + +/* Итераций тесного цикла на ОДИН кадр (~20 мс @ 50 Гц); 0 = ещё не + * откалибровано (или калибровка не удалась — irq_install занят). */ +extern uint16_t _sleep_units_per_frame; + +/* Итераций на 1 мс — производное от _sleep_units_per_frame/20, + * вычисляется один раз при калибровке (одно 16-битное деление на + * константу, НЕ умножение и НЕ 32 бита). */ +extern uint16_t _sleep_units_per_ms; + +/* Один раз откалиброваться против реального кадрового тика; при + * неудаче (EBUSY — фрейм-хук занят другим irq_install()-клиентом) + * оставляет оба поля в 0 — вызывающий берёт свой fallback. */ +void _sleep_calibrate(void); + +/* Прогнать units итераций тесного цикла — чистый busy-wait. */ +void _sleep_spin(uint16_t units); + +#endif diff --git a/libc/time/delayms.c b/libc/time/delayms.c new file mode 100644 index 0000000..89884e4 --- /dev/null +++ b/libc/time/delayms.c @@ -0,0 +1,42 @@ +/* + * delayms — заблокироваться на N миллисекунд калиброванным busy- + * wait'ом (см. _sleep_calib.c — общий движок с sleep(), по духу + * docs/samples/delayms.asm: DelayMS(HL)). + * + * delayms(ms) — просто ms раз калиброванный _sleep_spin() на 1 мс: + * умножения/32-бит арифметики нет вообще (масштабирование — через + * вложенный цикл вызовов, не через домножение константы). + * + * Fallback: если фрейм-хук уже занят другим irq_install()-клиентом + * (EBUSY) — калибровку сделать некорректно; точной альтернативы для + * миллисекундной гранулярности без калибровки нет, поэтому просто + * ждём один кадр (ei;halt) — грубо, зато не виснет. + */ + +#include +#include "_sleep_calib.h" + +static void _delayms_legacy(void) __naked +{ + __asm + ei + halt + ret + __endasm; +} + +void delayms(uint16_t ms) +{ + if (ms == 0) return; + + if (!_sleep_units_per_frame) _sleep_calibrate(); + + if (!_sleep_units_per_frame) { + _delayms_legacy(); + return; + } + + while (ms--) { + _sleep_spin(_sleep_units_per_ms); + } +} diff --git a/libc/time/sleep.c b/libc/time/sleep.c index 628c3ea..955079d 100644 --- a/libc/time/sleep.c +++ b/libc/time/sleep.c @@ -1,30 +1,60 @@ /* - * sleep — заблокироваться на N секунд по кадровому прерыванию 50 Гц. + * sleep — заблокироваться на N секунд калиброванным busy-wait'ом (см. + * _sleep_calib.c — общий движок с delayms()). * - * ISR Sprinter срабатывает 50 раз в секунду. `halt` паркует CPU до - * следующего IRQ, так что 50 halt'ов = ~1 секунда стенного времени. - * Тот же трюк, что у solid-c в IO.ASM:285. + * Старая версия (50 halt = 1 c) предполагала, что КАЖДОЕ прерывание — + * кадровый тик 50 Гц; с CBL/клавиатурой на векторе 0xFF это неверно + * (CBL может прерывать в разы/десятки раз чаще кадра — sleep() вернулся + * бы намного раньше срока). * - * Замечание: требует включённых прерываний (по умолчанию включены — - * ESTEX настраивает IM 1 с кадровым ISR до запуска нашей программы). + * sleep(seconds) — вложенный цикл: внешний по секундам, внутренний + * ровно 50 раз калиброванный _sleep_spin() — та же структура, что и в + * старой версии (внешний секунды, внутренний 50×halt), только вместо + * halt — калиброванный спин. Umножения/32-бит арифметики нет. + * + * Fallback: если фрейм-хук уже занят другим irq_install()-клиентом + * (EBUSY) — калибровку сделать некорректно, откатываемся к старому + * "50 halt ~ 1 c" поведению для этого конкретного вызова. */ #include +#include "_sleep_calib.h" -void sleep(unsigned int seconds) __naked +/* Старое поведение — фолбэк, если calibrate() не может безопасно + * получить кадровый хук. */ +static void _sleep_legacy(unsigned int seconds) __naked { (void)seconds; __asm - inter: - ;; HL = seconds на входе (единственный int-аргумент SDCC). + _slg_outer: ld a, h - or a, l - ret Z ; sleep(0) — вернуться сразу - ld b, #50 ; 50 halt-ов на секунду (50 Гц) - inner: + or l + ret Z + ld b, #50 ; 50 halt-ов ~ 1 c (при ЕДИНСТВЕННОМ + ; источнике прерываний на векторе 0xFF) + _slg_inner: halt - djnz inner + djnz _slg_inner dec hl - jr inter + jr _slg_outer __endasm; } + +void sleep(unsigned int seconds) +{ + if (seconds == 0) return; + + if (!_sleep_units_per_frame) _sleep_calibrate(); + + if (!_sleep_units_per_frame) { + _sleep_legacy(seconds); + return; + } + + while (seconds--) { + uint8_t frame = 50; /* 50 кадров ~ 1 с при 50 Гц */ + while (frame--) { + _sleep_spin(_sleep_units_per_frame); + } + } +} diff --git a/tests/rt_test/rt_test.c b/tests/rt_test/rt_test.c index c070f2b..9d90c2a 100644 --- a/tests/rt_test/rt_test.c +++ b/tests/rt_test/rt_test.c @@ -52,6 +52,12 @@ int main(void) sleep(5); puts(" done."); + /* --- delayms --- */ + puts(""); + puts("delayms(500) x 6: pausing ~3 seconds..."); + for (int i = 0; i < 6; i++) delayms(500); + puts(" done."); + puts(""); puts("Press any key, atexit chain will run after."); (void)getchar();