hpet: add basic support for using timers The HPET is a disaster. This commit adds the basical support I needed for a watchdog timer. YMMV. Signed-off-by: Barret Rhoden <brho@cs.berkeley.edu>
diff --git a/kern/drivers/timers/hpet.c b/kern/drivers/timers/hpet.c index 0dd88e2..aeb4592 100644 --- a/kern/drivers/timers/hpet.c +++ b/kern/drivers/timers/hpet.c
@@ -1,12 +1,48 @@ +/* Copyright (c) 2020 Google Inc + * Copyright (c) 2014 The Regents of the University of California + * Barret Rhoden <brho@cs.berkeley.edu> + * See LICENSE for details. + * + * HPET nonsense */ + #include <string.h> #include <stdio.h> #include <assert.h> #include <endian.h> #include <pmap.h> #include <acpi.h> +#include <kmalloc.h> #include "hpet.h" +#define HPET_BLOCK_LEN 1024 + +#define HPET_CAP_ID 0x00 +#define HPET_CONFIG 0x10 +#define HPET_IRQ_STS 0x20 +#define HPET_MAIN_COUNTER 0xf0 + +#define HPET_CONF_LEG_RT_CNF (1 << 1) +#define HPET_CONF_ENABLE_CNF (1 << 0) + +#define HPET_TIMER_CONF 0x00 +#define HPET_TIMER_COMP 0x08 +#define HPET_TIMER_FSB 0x10 + +#define HPET_TN_INT_TYPE_CNF (1 << 1) +#define HPET_TN_INT_ENB_CNF (1 << 2) +#define HPET_TN_TYPE_CNF (1 << 3) +#define HPET_TN_PER_INT_CAP (1 << 4) +#define HPET_TN_SIZE_CAP (1 << 5) +#define HPET_TN_VAL_SET_CNF (1 << 6) +#define HPET_TN_32MODE_CNF (1 << 8) +#define HPET_TN_INT_ROUTE_CNF (0x1f << 9) +#define HPET_TN_FSB_EN_CNF (1 << 14) +#define HPET_TN_FSB_INT_DEL_CAP (1 << 15) +#define HPET_TN_INT_ROUTE_CAP (0xffffffffULL << 32) + +static struct hpet_block *gbl_hpet; + /* The HPET likes 64bit mmreg reads and writes. If the arch doesn't support * them, then things are a little trickier. Probably just replace these with * mm64 ops, and quit supporting 32 bit. */ @@ -20,45 +56,229 @@ return *((volatile uint64_t*)reg); } +void hpet_timer_enable(struct hpet_timer *ht) +{ + hpet_w64(ht->base + HPET_TIMER_CONF, ht->enable_cmd); +} + +void hpet_timer_disable(struct hpet_timer *ht) +{ + hpet_w64(ht->base + HPET_TIMER_CONF, 0); +} + +/* This only works on 64 bit counters, where we don't have to deal with + * wrap-around. */ +bool hpet_check_spurious_64(struct hpet_timer *ht) +{ + return hpet_r64(ht->hpb->base + HPET_MAIN_COUNTER) < + hpet_r64(ht->base + HPET_TIMER_COMP); +} + +/* There is no upper addr! But o/w, this is basically MSI. To be fair, we zero + * out the upper addr in msi.c too. */ +static uint64_t fsb_make_addr(uint8_t dest) +{ + return 0xfee00000 | (dest << 12); +} + +/* dmode: e.g. 000 = fixed, 100 = NMI. Assuming edge triggered */ +static uint64_t fsb_make_data(uint8_t vno, uint8_t dmode) +{ + return (dmode << 8) | vno; +} + +/* This is a very limited HPET timer, primarily used by the watchdog. + * + * It's a one-shot (non-periodic), FSB, 64-bit, edge-triggered timer for Core 0 + * with vector vno and delivery mode dmode. + * + * Why so specific? Okay, the HPET is a piece of shit, at least on my machine. + * If you disable the interrupt, and then the time comes where MAIN == COMP, the + * IRQ will be suppressed, but when you enable later on, the IRQ will fire. No + * way around it that I can find. + * + * One trick is to set the COMP to some time that won't fire, i.e. in the past. + * However, the 32 bit counters are only about a 5 minute reach. So this trick + * only works with the 64 bit counter. + * + * However, even with that, if the IRQ ever legitimately fires, any time you + * reenable the timer, it triggers an IRQ. + * + * This is all with FSB (which has to be edge triggered) interrupt styles. I + * tried various combos of "write to the IRQ_STS register", change certain + * fields in timer CONF, disable the global counter while making changes, etc. + * All things that aren't in the book, but that might clear whatever internal + * bit is set. + * + * Ultimately, I opted to handle the 'spurious' interrupt in SW, though with a 5 + * minute reach, you can't tell between an old value and a new one. Unless you + * use a 64 bit counter - then wraparound isn't a concern. If we wanted to do + * this for a 32 bit counter, we'd need to drastically limit the reach. */ +void hpet_magic_timer_setup(struct hpet_timer *ht, uint8_t vno, uint8_t dmode) +{ + /* Unlike the other reserved bits in the hpb's registers, the spec says + * that timer (ht) conf reserved bits should be set to 0. + * + * In lieu of screwing around too much, we just set the entire register + * in one shot. Disabled = 0, enabled = all bits needed. The + * disastrous behavior mentioned above occurs regardless of the + * technique used. */ + hpet_timer_disable(ht); + + /* Core 0, fixed, with vno */ + hpet_w64(ht->base + HPET_TIMER_FSB, + (fsb_make_addr(0) << 32) | fsb_make_data(vno, dmode)); + + ht->enable_cmd = HPET_TN_FSB_EN_CNF | HPET_TN_INT_ENB_CNF; +} + +/* Sets the time to fire X ns in the future. No guarantees or sanity checks. + * If we get delayed setting this, the main counter may pass by our ticks value + * before we write it, and you'll never get it. */ +void hpet_timer_increment_comparator(struct hpet_timer *ht, uint64_t nsec) +{ + uint64_t ticks; + + ticks = hpet_r64(ht->hpb->base + HPET_MAIN_COUNTER); + ticks += nsec / ht->hpb->nsec_per_tick; + hpet_w64(ht->base + HPET_TIMER_COMP, ticks); +} + +/* See above. Need 64 bit and FSB */ +struct hpet_timer *hpet_get_magic_timer(void) +{ + struct hpet_block *hpb = gbl_hpet; + struct hpet_timer *ret = NULL; + + if (!hpb) + return NULL; + spin_lock(&hpb->lock); + for (int i = 0; i < hpb->nr_timers; i++) { + struct hpet_timer *ht = &hpb->timers[i]; + + if (ht->in_use) + continue; + if (!ht->fsb) + continue; + if (!ht->bit64) + continue; + ht->in_use = true; + ret = ht; + break; + } + spin_unlock(&hpb->lock); + return ret; +} + +void hpet_put_timer(struct hpet_timer *ht) +{ + struct hpet_block *hpb = gbl_hpet; + + if (!hpb) + return; + spin_lock(&hpb->lock); + ht->in_use = false; + spin_unlock(&hpb->lock); +} + +static void print_hpb_stats(struct hpet_block *hpb) +{ + printk("HPET at %p:\n", hpb->base); + printk("\tVendor: 0x%x\n", (hpb->cap_id >> 16) & 0xffff); + printk("\tPeriod: 0x%08x\n", hpb->period); + printk("\t32 bit reach: %d sec\n", hpb->reach32); + printk("\tTimers: %d\n", hpb->nr_timers); + printk("\tMain counter size: %d\n", hpb->cap_id & (1 << 13) ? 64 : 32); + + printd("\tcap/id %p\n", hpb->cap_id); + printd("\tconfig %p\n", hpet_r64(hpb->base + HPET_CONFIG)); + printd("\tirqsts %p\n", hpet_r64(hpb->base + HPET_IRQ_STS)); + + for (int i = 0; i < hpb->nr_timers; i++) { + printk("\t\tTimer %d: conf %p comp %p, %d bit, %sFSB\n", i, + hpet_r64(hpb->timers[i].base + HPET_TIMER_CONF), + hpet_r64(hpb->timers[i].base + HPET_TIMER_COMP), + hpb->timers[i].bit64 ? 64 : 32, + hpb->timers[i].fsb ? "" : "no "); + } +} + struct Atable *parsehpet(struct Atable *parent, char *name, uint8_t *raw, size_t rawsize) { + struct hpet_block *hpb; + struct Atable *hpet; + uint32_t evt_blk_id; + + /* Only dealing with one block of these. */ + if (gbl_hpet) { + printk("Found another HPET, skipping!\n"); + return NULL; + } + /* Do we want to keep this table around? if so, we can use newtable, * which allocs an Atable and puts it on a global stailq. then we * return that pointer, not as an addr, but as a signal to parse code * about whether or not it is safe to unmap (which we don't do anymore). */ - struct Atable *hpet = mkatable(parent, HPET, "HPET", raw, rawsize, 0); - unsigned long hp_addr; - uint32_t evt_blk_id; - int nr_timers; - + hpet = mkatable(parent, HPET, "HPET", raw, rawsize, 0); assert(hpet); printk("HPET table detected at %p, for %d bytes\n", raw, rawsize); evt_blk_id = l32get(raw + 36); - printd("EV BID 0x%08x\n", evt_blk_id); - hp_addr = (unsigned long)KADDR_NOCHECK(l64get(raw + 44)); + hpb = kzmalloc(sizeof(*hpb), MEM_WAIT); + spinlock_init(&hpb->lock); - printd("cap/ip %p\n", hpet_r64(hp_addr + 0x00)); - printd("config %p\n", hpet_r64(hp_addr + 0x10)); - printd("irqsts %p\n", hpet_r64(hp_addr + 0x20)); + hpb->base = vmap_pmem_nocache(l64get(raw + 44), HPET_BLOCK_LEN); + if (!hpb->base) { + printk("HPET failed to get an iomapping, aborting\n"); + kfree(hpb); + kfree(hpet); + return NULL; + } - nr_timers = ((hpet_r64(hp_addr) >> 8) & 0xf) + 1; - for (int i = 0; i < nr_timers; i++) - printd("Timer %d, config reg %p\n", i, - hpet_r64(hp_addr + 0x100 + 0x20 * i)); - /* 0x10, general config register. bottom two bits are legacy mode and - * global enable. turning them both off. need to do read-modify-writes - * to HPET registers with reserved fields.*/ - hpet_w64(hp_addr + 0x10, hpet_r64(hp_addr + 0x10) & ~0x3); - printk("Disabled the HPET timer\n"); + hpb->cap_id = hpet_r64(hpb->base + HPET_CAP_ID); + if (evt_blk_id != (hpb->cap_id & 0xffffffff)) { + printk("HPET ACPI mismatch: ACPI: %p HPET: %p\n", evt_blk_id, + hpb->cap_id); + } + hpb->nr_timers = ((hpb->cap_id >> 8) & 0xf) + 1; + + /* femtoseconds (10E-15) per tick. + * e.g. 69 ns per tick. + * freq is just 10^15 / period: 14.318 MHz + * reach of 32 bit counter: + * period fs/tick * 1sec/10^15fs * 2^32tick/wrap ~= 299 seconds */ + hpb->period = hpb->cap_id >> 32; + hpb->nsec_per_tick = hpb->period / 1000000; + hpb->reach32 = hpb->period * (1ULL << 32) / 1000000000000000ULL; + + for (int i = 0; i < hpb->nr_timers; i++) { + struct hpet_timer *ht = &hpb->timers[i]; + uint64_t conf; + + ht->base = hpb->base + 0x100 + 0x20 * i; + ht->hpb = hpb; + conf = hpet_r64(ht->base + HPET_TIMER_CONF); + ht->bit64 = conf & HPET_TN_SIZE_CAP; + ht->fsb = conf & HPET_TN_FSB_INT_DEL_CAP; + hpet_timer_disable(ht); + } + + /* No interest in legacy mode. */ + hpet_w64(hpb->base + HPET_CONFIG, + hpet_r64(hpb->base + HPET_CONFIG) & HPET_CONF_LEG_RT_CNF); + /* All timers are off currently */ + hpet_w64(hpb->base + HPET_CONFIG, + hpet_r64(hpb->base + HPET_CONFIG) | HPET_CONF_ENABLE_CNF); + + gbl_hpet = hpb; return finatable_nochildren(hpet); } -void cmos_dumping_ground() +void cmos_dumping_ground(void) { uint8_t cmos_b;
diff --git a/kern/drivers/timers/hpet.h b/kern/drivers/timers/hpet.h index 70dd7f5..ddde2cf 100644 --- a/kern/drivers/timers/hpet.h +++ b/kern/drivers/timers/hpet.h
@@ -1,6 +1,42 @@ +/* Copyright (c) 2020 Google Inc + * Barret Rhoden <brho@cs.berkeley.edu> + * See LICENSE for details. + * + * HPET nonsense */ + #pragma once #include <acpi.h> +#include <atomic.h> + +struct hpet_timer { + uintptr_t base; + uint64_t enable_cmd; + bool bit64; + bool fsb; + bool in_use; + struct hpet_block *hpb; +}; + +struct hpet_block { + spinlock_t lock; + uintptr_t base; + uint64_t cap_id; + uint32_t period; + uint32_t nsec_per_tick; + uint32_t reach32; + unsigned int nr_timers; + struct hpet_timer timers[32]; +}; + +struct hpet_timer *hpet_get_magic_timer(void); +void hpet_put_timer(struct hpet_timer *ht); + +void hpet_timer_enable(struct hpet_timer *ht); +void hpet_timer_disable(struct hpet_timer *ht); +bool hpet_check_spurious_64(struct hpet_timer *ht); +void hpet_magic_timer_setup(struct hpet_timer *ht, uint8_t vno, uint8_t dmode); +void hpet_timer_increment_comparator(struct hpet_timer *ht, uint64_t nsec); struct Atable *parsehpet(struct Atable *parent, char *name, uint8_t *p, size_t rawsize);