/* * _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; }