/*
 * timer.c
 * 
 * Deadline-based timer callbacks.
 * 
 * Written & released by Keir Fraser <keir.xen@gmail.com>
 * 
 * This is free and unencumbered software released into the public domain.
 * See the file COPYING for more details, or visit <http://unlicense.org>.
 */

#if STM32F == 1
void IRQ_25(void) __attribute__((alias("IRQ_timer")));
#define TIMER_IRQ 25
#define tim tim1
#define tim_bits 16
#elif STM32F == 7
void IRQ_50(void) __attribute__((alias("IRQ_timer")));
#define TIMER_IRQ 50
#define tim tim5 /* 32-bit timer */
#define tim_bits 32
#endif

/* IRQ only on counter overflow, one-time enable. */
#define TIM_CR1 (TIM_CR1_URS | TIM_CR1_OPM)

/* Empirically-determined offset applied to timer deadlines to counteract the
 * latency incurred by reprogram_timer() and IRQ_timer(). */
#define SLACK_TICKS 12

#define TIMER_INACTIVE ((struct timer *)1ul)

static struct timer *head;

static void reprogram_timer(int32_t delta)
{
    tim->cr1 = TIM_CR1;
    if ((tim_bits == 32) || (delta < 0x10000)) {
        /* Fine-grained deadline (sub-microsecond accurate) */
        tim->psc = SYSCLK_MHZ/TIME_MHZ-1;
        tim->arr = (delta <= SLACK_TICKS) ? 1 : delta-SLACK_TICKS;
    } else {
        /* Coarse-grained deadline, fires in time to set a shorter,
         * fine-grained deadline. */
        tim->psc = sysclk_us(100)-1;
        tim->arr = min_t(uint32_t, 0xffffu,
                         delta/time_us(100)-50); /* 5ms early */
    }
    tim->egr = TIM_EGR_UG; /* update CNT, PSC, ARR */
    tim->sr = 0; /* dummy write, gives hardware time to process EGR.UG=1 */
    tim->cr1 = TIM_CR1 | TIM_CR1_CEN;
}

void timer_init(struct timer *timer, void (*cb_fn)(void *), void *cb_dat)
{
    timer->cb_fn = cb_fn;
    timer->cb_dat = cb_dat;
    timer->next = TIMER_INACTIVE;
}

static bool_t timer_is_active(struct timer *timer)
{
    return timer->next != TIMER_INACTIVE;
}

static void _timer_cancel(struct timer *timer)
{
    struct timer *t, **pprev;

    if (!timer_is_active(timer))
        return;

    for (pprev = &head; (t = *pprev) != timer; pprev = &t->next)
        continue;

    *pprev = t->next;
    t->next = TIMER_INACTIVE;
}

void timer_set(struct timer *timer, time_t deadline)
{
    struct timer *t, **pprev;
    time_t now;
    int32_t delta;
    uint32_t oldpri;

    oldpri = IRQ_save(TIMER_IRQ_PRI);

    _timer_cancel(timer);

    timer->deadline = deadline;

    now = time_now();
    delta = time_diff(now, deadline);
    for (pprev = &head; (t = *pprev) != NULL; pprev = &t->next)
        if (delta <= time_diff(now, t->deadline))
            break;
    timer->next = *pprev;
    *pprev = timer;

    if (head == timer)
        reprogram_timer(delta);

    IRQ_restore(oldpri);
}

void timer_cancel(struct timer *timer)
{
    uint32_t oldpri;
    oldpri = IRQ_save(TIMER_IRQ_PRI);
    _timer_cancel(timer);
    IRQ_restore(oldpri);
}

void timers_init(void)
{
#if STM32F == 7
    rcc->apb1enr |= RCC_APB1ENR_TIM5EN;
    peripheral_clock_delay();
#endif
    tim->cr2 = 0;
    tim->dier = TIM_DIER_UIE;
    IRQx_set_prio(TIMER_IRQ, TIMER_IRQ_PRI);
    IRQx_enable(TIMER_IRQ);
}

static void IRQ_timer(void)
{
    struct timer *t;
    int32_t delta;

    tim->sr = 0;

    while ((t = head) != NULL) {
        if ((delta = time_diff(time_now(), t->deadline)) > SLACK_TICKS) {
            reprogram_timer(delta);
            break;
        }
        head = t->next;
        t->next = TIMER_INACTIVE;
        (*t->cb_fn)(t->cb_dat);
    }
}

/*
 * Local variables:
 * mode: C
 * c-file-style: "Linux"
 * c-basic-offset: 4
 * tab-width: 4
 * indent-tabs-mode: nil
 * End:
 */