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