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:
+63
-2
@@ -88,11 +88,50 @@ in-place регистр, латиница + кириллица CP866.
|
|||||||
| `int isatty(fd)` | fd <= 0 (файловые манипуляторы DSS с 1) |
|
| `int isatty(fd)` | fd <= 0 (файловые манипуляторы DSS с 1) |
|
||||||
| `int mkdir/rmdir/chdir(path)` | ESTEX $1B/$1C/$1D |
|
| `int mkdir/rmdir/chdir(path)` | ESTEX $1B/$1C/$1D |
|
||||||
| `char *getcwd(buf, size)` | буфер 256 байт, size игнорируется |
|
| `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 —
|
**Лимит: 8 одновременных манипуляторов**; 9-й OPEN вешает DSS —
|
||||||
libc отказывает сама (EMFILE, предохранитель _fd_guard).
|
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.h>
|
## <errno.h>
|
||||||
|
|
||||||
`errno` (int), коды = коды DSS (EOK..EUNKERR) + POSIX-имена
|
`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)`.
|
Setup: `gfx_init(mode,page)→prev`, `gfx_done(prev)`.
|
||||||
Страницы/банк: `gfx_set/get_visible_page`, `gfx_set/get_draw_page`
|
Страницы/банк: `gfx_set/get_visible_page`, `gfx_set/get_draw_page`
|
||||||
(double buffering), `gfx_set/get_bank` (0x50..0x5F, 0x58 = FF-
|
(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.h>`, `_cbl_port.c`) —
|
||||||
|
разделяемое владение портом 0x004E, безопасное при любом порядке
|
||||||
|
использования с реальным CBL-звуком. Фолбэк на одно кадровое
|
||||||
|
прерывание (`halt`), если бит не ведёт себя как ожидается за разумное
|
||||||
|
число попыток.
|
||||||
Примитивы (суффикс _256 / _16): `clear putpixel hline vline line
|
Примитивы (суффикс _256 / _16): `clear putpixel hline vline line
|
||||||
rect fill_rect` — через акселератор (hline/vline burst до 256 байт;
|
rect fill_rect` — через акселератор (hline/vline burst до 256 байт;
|
||||||
в 0x82 vline через RMW). Координаты int, клиппинг по краям.
|
в 0x82 vline через RMW). Координаты int, клиппинг по краям.
|
||||||
@@ -224,6 +276,15 @@ banked-функции; только volatile-глобалы и быстрая р
|
|||||||
блока) читается только пока `cbl_open` не закрыт (при выключенном CBL
|
блока) читается только пока `cbl_open` не закрыт (при выключенном CBL
|
||||||
бит всегда 1 — MAME-квирк).
|
бит всегда 1 — MAME-квирк).
|
||||||
|
|
||||||
|
**Разделяемое владение портом 0x004E** (2026-07-07): бит 5 порта 0xFE
|
||||||
|
(позиция луча — см. `<gfx.h>` `gfx_wait_vsync()`) доступен только пока
|
||||||
|
включён bit7 порта 0x004E, независимо от того, играет ли реальный
|
||||||
|
звук. `_cbl_port_ref()`/`_cbl_port_unref()` (internal, `_cbl_port.c`)
|
||||||
|
дают gfx-модулю занять bit7 "немым" кодом частоты (не заводящим таймер
|
||||||
|
CBL — без звука/прерываний), не мешая реальной `cbl_open()`-сессии,
|
||||||
|
если она уже идёт (и наоборот — `cbl_close()` возвращает "немой" режим
|
||||||
|
вместо полного выключения порта, если gfx его ещё держит).
|
||||||
|
|
||||||
## <palette.h> — низкий уровень (BIOS $A4/$A6)
|
## <palette.h> — низкий уровень (BIOS $A4/$A6)
|
||||||
|
|
||||||
`pal_load pal_get pal_set_color pal_get_color` (страница 0..7,
|
`pal_load pal_get pal_set_color pal_get_color` (страница 0..7,
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ banklocl 4832
|
|||||||
banktest 3767
|
banktest 3767
|
||||||
bios_text 4470
|
bios_text 4470
|
||||||
cat 915
|
cat 915
|
||||||
cblstream 6234
|
cblstream 6277
|
||||||
cbltest 6374
|
cbltest 6407
|
||||||
cblwav 6443
|
cblwav 6486
|
||||||
conio 4634
|
conio 4634
|
||||||
conio2 3946
|
conio2 3946
|
||||||
dec_test 860
|
dec_test 860
|
||||||
@@ -20,7 +20,7 @@ fdmax 6337
|
|||||||
filetest 11624
|
filetest 11624
|
||||||
gets 509
|
gets 509
|
||||||
gfx_d16 3319
|
gfx_d16 3319
|
||||||
gfx_dbuf 2396
|
gfx_dbuf 2544
|
||||||
gfx_demo 3219
|
gfx_demo 3219
|
||||||
gfx_mous 5542
|
gfx_mous 5542
|
||||||
gfx_text 2560
|
gfx_text 2560
|
||||||
@@ -35,7 +35,7 @@ mem_test 4558
|
|||||||
mouse 4394
|
mouse 4394
|
||||||
openenv 6127
|
openenv 6127
|
||||||
ptime 5896
|
ptime 5896
|
||||||
rt_test 4103
|
rt_test 4906
|
||||||
seek 4177
|
seek 4177
|
||||||
simple 963
|
simple 963
|
||||||
solidt 12381
|
solidt 12381
|
||||||
|
|||||||
|
@@ -23,6 +23,15 @@
|
|||||||
* был бы избыточен. _cbl_fill — callback приложения; _cbl_silence —
|
* был бы избыточен. _cbl_fill — callback приложения; _cbl_silence —
|
||||||
* буфер тишины, malloc'ится в cbl_open() ТОЛЬКО при
|
* буфер тишины, malloc'ится в cbl_open() ТОЛЬКО при
|
||||||
* CBL_UNDERRUN_SILENCE (иначе NULL).
|
* 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
|
#ifndef _CBL_INTERNAL_H
|
||||||
#define _CBL_INTERNAL_H
|
#define _CBL_INTERNAL_H
|
||||||
@@ -33,6 +42,7 @@
|
|||||||
#define _CBL_CTRL_PORT 0x004E
|
#define _CBL_CTRL_PORT 0x004E
|
||||||
#define _CBL_DATA_PORT 0x4F
|
#define _CBL_DATA_PORT 0x4F
|
||||||
#define _CBL_ACCEL_PAGE 0xFD /* спец-страница EMM: запись на W3@0xC000 идёт в CBL */
|
#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_block; /* размер блока: 128 или 256 (по формату) */
|
||||||
extern uint16_t _cbl_reqs; /* блоков-запросов обслужено */
|
extern uint16_t _cbl_reqs; /* блоков-запросов обслужено */
|
||||||
@@ -40,6 +50,7 @@ extern uint16_t _cbl_undr; /* из них недолив (fill в
|
|||||||
extern cbl_fill_fn _cbl_fill; /* callback приложения, может быть NULL */
|
extern cbl_fill_fn _cbl_fill; /* callback приложения, может быть NULL */
|
||||||
extern uint8_t _cbl_underrun_mode; /* CBL_UNDERRUN_* */
|
extern uint8_t _cbl_underrun_mode; /* CBL_UNDERRUN_* */
|
||||||
extern uint8_t *_cbl_silence; /* буфер тишины или NULL */
|
extern uint8_t *_cbl_silence; /* буфер тишины или NULL */
|
||||||
|
extern uint8_t _cbl_port_refs; /* держатели bit7 помимо реальной CBL-сессии */
|
||||||
|
|
||||||
/* Запись в 16-битный порт управления 0x004E. */
|
/* Запись в 16-битный порт управления 0x004E. */
|
||||||
void _cbl_ctrl(uint8_t v);
|
void _cbl_ctrl(uint8_t v);
|
||||||
@@ -49,4 +60,12 @@ void _cbl_ctrl(uint8_t v);
|
|||||||
void _cbl_pump_otir(void);
|
void _cbl_pump_otir(void);
|
||||||
void _cbl_pump_accel(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
|
#endif
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* _cbl_state — данные CBL-модуля: размер блока, счётчики, callback
|
* _cbl_state — данные CBL-модуля: размер блока, счётчики, callback
|
||||||
* приложения, режим недолива, буфер тишины (если аллоцирован).
|
* приложения, режим недолива, буфер тишины (если аллоцирован),
|
||||||
|
* счётчик разделяемых держателей порта (см. _cbl_port.c).
|
||||||
* Только данные.
|
* Только данные.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -12,3 +13,4 @@ uint16_t _cbl_undr;
|
|||||||
cbl_fill_fn _cbl_fill;
|
cbl_fill_fn _cbl_fill;
|
||||||
uint8_t _cbl_underrun_mode;
|
uint8_t _cbl_underrun_mode;
|
||||||
uint8_t *_cbl_silence;
|
uint8_t *_cbl_silence;
|
||||||
|
uint8_t _cbl_port_refs;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* cbl_close — выключить CBL (запись 0 в порт управления, как в
|
* cbl_close — выключить CBL, снять хук насоса, освободить буфер
|
||||||
* сэмпле), снять хук насоса, освободить буфер тишины (если был
|
* тишины (если был аллоцирован под CBL_UNDERRUN_SILENCE) и отпустить
|
||||||
* аллоцирован под CBL_UNDERRUN_SILENCE) и отпустить IM2-таблицу.
|
* IM2-таблицу. Идемпотентно; вызывается и из atexit.
|
||||||
* Идемпотентно; вызывается и из atexit.
|
*
|
||||||
|
* Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync()
|
||||||
|
* пересчитывает нужное значение: если gfx_wait_vsync() ещё держит
|
||||||
|
* ссылку (_cbl_port_refs > 0), останется "немой" режим ради бита
|
||||||
|
* синхронизации; иначе порт гасится полностью.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -13,9 +17,9 @@ void cbl_close(void)
|
|||||||
{
|
{
|
||||||
if (!_irq_cbl_hook) return;
|
if (!_irq_cbl_hook) return;
|
||||||
IRQ_DISABLE();
|
IRQ_DISABLE();
|
||||||
_cbl_ctrl(0);
|
|
||||||
_irq_cbl_hook = 0;
|
_irq_cbl_hook = 0;
|
||||||
IRQ_ENABLE();
|
IRQ_ENABLE();
|
||||||
|
_cbl_port_sync();
|
||||||
free(_cbl_silence);
|
free(_cbl_silence);
|
||||||
_cbl_silence = 0;
|
_cbl_silence = 0;
|
||||||
_irq_table_unref();
|
_irq_table_unref();
|
||||||
|
|||||||
@@ -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 обрабатывает его и
|
* Бит доступен ТОЛЬКО пока включён cbl_mode() (bit7 порта 0x004E) —
|
||||||
* возвращается на инструкцию после HALT — это начало обратного хода
|
* см. libc/cbl/_cbl_port.c: если приложение уже играет через
|
||||||
* луча, идеальный момент для свопа страниц без разрывов.
|
* cbl_open(), берём бит бесплатно (порт не трогаем); иначе
|
||||||
|
* лениво занимаем bit7 "немым" кодом частоты через _cbl_port_ref()
|
||||||
|
* при первом вызове, освобождаем в atexit.
|
||||||
|
*
|
||||||
|
* Fallback: если ни разу не увидели ожидаемое состояние за разумное
|
||||||
|
* число попыток (иное железо, бит ведёт себя не как в MAME) —
|
||||||
|
* откатываемся на одно кадровое прерывание (halt), чтобы не
|
||||||
|
* зависнуть намертво.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "_gfx.h"
|
#include "_gfx.h"
|
||||||
|
#include "../cbl/_cbl.h"
|
||||||
|
#include <sprinter_exit.h>
|
||||||
|
|
||||||
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
|
__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
|
ei
|
||||||
halt
|
halt ; аварийный путь: одно кадровое прерывание
|
||||||
ret
|
|
||||||
|
_gwv_done:
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#define UNISTD_H
|
#define UNISTD_H
|
||||||
|
|
||||||
#include <stddef.h> /* size_t */
|
#include <stddef.h> /* size_t */
|
||||||
|
#include <stdint.h> /* uint16_t */
|
||||||
|
|
||||||
/* lseek whence values (POSIX). */
|
/* lseek whence values (POSIX). */
|
||||||
#define SEEK_SET 0
|
#define SEEK_SET 0
|
||||||
@@ -25,9 +26,13 @@ int close(int fd);
|
|||||||
int unlink(const char *path);
|
int unlink(const char *path);
|
||||||
long lseek(int fd, long offset, int whence);
|
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);
|
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 — файл (любой
|
/* 1, если fd — консоль (наши псевдо-fd 0/-1/-2), 0 — файл (любой
|
||||||
* положительный манипулятор DSS; из командной строки они идут с 1). */
|
* положительный манипулятор DSS; из командной строки они идут с 1). */
|
||||||
int isatty(int fd);
|
int isatty(int fd);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
@@ -1,30 +1,60 @@
|
|||||||
/*
|
/*
|
||||||
* sleep — заблокироваться на N секунд по кадровому прерыванию 50 Гц.
|
* sleep — заблокироваться на N секунд калиброванным busy-wait'ом (см.
|
||||||
|
* _sleep_calib.c — общий движок с delayms()).
|
||||||
*
|
*
|
||||||
* ISR Sprinter срабатывает 50 раз в секунду. `halt` паркует CPU до
|
* Старая версия (50 halt = 1 c) предполагала, что КАЖДОЕ прерывание —
|
||||||
* следующего IRQ, так что 50 halt'ов = ~1 секунда стенного времени.
|
* кадровый тик 50 Гц; с CBL/клавиатурой на векторе 0xFF это неверно
|
||||||
* Тот же трюк, что у solid-c в IO.ASM:285.
|
* (CBL может прерывать в разы/десятки раз чаще кадра — sleep() вернулся
|
||||||
|
* бы намного раньше срока).
|
||||||
*
|
*
|
||||||
* Замечание: требует включённых прерываний (по умолчанию включены —
|
* sleep(seconds) — вложенный цикл: внешний по секундам, внутренний
|
||||||
* ESTEX настраивает IM 1 с кадровым ISR до запуска нашей программы).
|
* ровно 50 раз калиброванный _sleep_spin() — та же структура, что и в
|
||||||
|
* старой версии (внешний секунды, внутренний 50×halt), только вместо
|
||||||
|
* halt — калиброванный спин. Umножения/32-бит арифметики нет.
|
||||||
|
*
|
||||||
|
* Fallback: если фрейм-хук уже занят другим irq_install()-клиентом
|
||||||
|
* (EBUSY) — калибровку сделать некорректно, откатываемся к старому
|
||||||
|
* "50 halt ~ 1 c" поведению для этого конкретного вызова.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include "_sleep_calib.h"
|
||||||
|
|
||||||
void sleep(unsigned int seconds) __naked
|
/* Старое поведение — фолбэк, если calibrate() не может безопасно
|
||||||
|
* получить кадровый хук. */
|
||||||
|
static void _sleep_legacy(unsigned int seconds) __naked
|
||||||
{
|
{
|
||||||
(void)seconds;
|
(void)seconds;
|
||||||
__asm
|
__asm
|
||||||
inter:
|
_slg_outer:
|
||||||
;; HL = seconds на входе (единственный int-аргумент SDCC).
|
|
||||||
ld a, h
|
ld a, h
|
||||||
or a, l
|
or l
|
||||||
ret Z ; sleep(0) — вернуться сразу
|
ret Z
|
||||||
ld b, #50 ; 50 halt-ов на секунду (50 Гц)
|
ld b, #50 ; 50 halt-ов ~ 1 c (при ЕДИНСТВЕННОМ
|
||||||
inner:
|
; источнике прерываний на векторе 0xFF)
|
||||||
|
_slg_inner:
|
||||||
halt
|
halt
|
||||||
djnz inner
|
djnz _slg_inner
|
||||||
dec hl
|
dec hl
|
||||||
jr inter
|
jr _slg_outer
|
||||||
__endasm;
|
__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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ int main(void)
|
|||||||
sleep(5);
|
sleep(5);
|
||||||
puts(" done.");
|
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("");
|
||||||
puts("Press any key, atexit chain will run after.");
|
puts("Press any key, atexit chain will run after.");
|
||||||
(void)getchar();
|
(void)getchar();
|
||||||
|
|||||||
Reference in New Issue
Block a user