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);