gfx/time: vsync через polling бита кадра, sleep/delayms без halt-подсчёта

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 10:47:57 +03:00
parent 5086c47f0f
commit 46bd9ad0f1
13 changed files with 421 additions and 36 deletions
+96
View File
@@ -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 <irq.h>
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;
}
+31
View File
@@ -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 <stdint.h>
/* Итераций тесного цикла на ОДИН кадр (~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
+42
View File
@@ -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 <unistd.h>
#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);
}
}
+45 -15
View File
@@ -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 <unistd.h>
#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);
}
}
}