#include <sys/cdefs.h>
#include <sys/param.h>
#include <sys/mutex.h>
#include <sys/kernel.h>
#include <sys/kmem.h>
#include <sys/systm.h>
#include <dev/acpica/acpi_hpet.h>
#include <machine/vmm.h>
#include <machine/vmm_dev.h>
#include "vmm_lapic.h"
#include "vatpic.h"
#include "vioapic.h"
#include "vhpet.h"
#define HPET_FREQ 16777216
#define FS_PER_S 1000000000000000ul
#define HPET_TCAP_RO_MASK (HPET_TCAP_INT_ROUTE | \
HPET_TCAP_FSB_INT_DEL | \
HPET_TCAP_SIZE | \
HPET_TCAP_PER_INT)
#define VHPET_NUM_TIMERS 8
CTASSERT(VHPET_NUM_TIMERS >= 3 && VHPET_NUM_TIMERS <= 32);
struct vhpet_callout_arg {
struct vhpet *vhpet;
int timer_num;
};
struct vhpet_timer {
uint64_t cap_config;
uint64_t msireg;
uint32_t compval;
uint32_t comprate;
struct callout callout;
hrtime_t callout_expire;
struct vhpet_callout_arg arg;
};
struct vhpet {
struct vm *vm;
kmutex_t lock;
uint64_t config;
uint64_t isr;
uint32_t base_count;
hrtime_t base_time;
struct vhpet_timer timer[VHPET_NUM_TIMERS];
};
#define VHPET_LOCK(vhp) mutex_enter(&((vhp)->lock))
#define VHPET_UNLOCK(vhp) mutex_exit(&((vhp)->lock))
#define VHPET_LOCKED(vhp) MUTEX_HELD(&((vhp)->lock))
static void vhpet_start_timer(struct vhpet *vhpet, int n, uint32_t counter,
hrtime_t now);
static uint64_t
vhpet_capabilities(void)
{
uint64_t cap = 0;
cap |= 0x8086 << 16;
cap |= (VHPET_NUM_TIMERS - 1) << 8;
cap |= 1;
cap &= ~HPET_CAP_COUNT_SIZE;
cap &= 0xffffffff;
cap |= (FS_PER_S / HPET_FREQ) << 32;
return (cap);
}
static __inline bool
vhpet_counter_enabled(struct vhpet *vhpet)
{
return ((vhpet->config & HPET_CNF_ENABLE) ? true : false);
}
static __inline bool
vhpet_timer_msi_enabled(struct vhpet *vhpet, int n)
{
const uint64_t msi_enable = HPET_TCAP_FSB_INT_DEL | HPET_TCNF_FSB_EN;
if ((vhpet->timer[n].cap_config & msi_enable) == msi_enable)
return (true);
else
return (false);
}
static __inline int
vhpet_timer_ioapic_pin(struct vhpet *vhpet, int n)
{
if (vhpet_timer_msi_enabled(vhpet, n))
return (0);
return ((vhpet->timer[n].cap_config & HPET_TCNF_INT_ROUTE) >> 9);
}
static uint32_t
vhpet_counter(struct vhpet *vhpet, hrtime_t *nowptr)
{
const hrtime_t now = gethrtime();
uint32_t val = vhpet->base_count;
if (vhpet_counter_enabled(vhpet)) {
const hrtime_t delta = now - vhpet->base_time;
ASSERT3S(delta, >=, 0);
val += hrt_freq_count(delta, HPET_FREQ);
} else {
}
if (nowptr != NULL) {
*nowptr = now;
}
return (val);
}
static void
vhpet_timer_clear_isr(struct vhpet *vhpet, int n)
{
int pin;
if (vhpet->isr & (1 << n)) {
pin = vhpet_timer_ioapic_pin(vhpet, n);
KASSERT(pin != 0, ("vhpet timer %d irq incorrectly routed", n));
(void) vioapic_deassert_irq(vhpet->vm, pin);
vhpet->isr &= ~(1 << n);
}
}
static __inline bool
vhpet_periodic_timer(struct vhpet *vhpet, int n)
{
return ((vhpet->timer[n].cap_config & HPET_TCNF_TYPE) != 0);
}
static __inline bool
vhpet_timer_interrupt_enabled(struct vhpet *vhpet, int n)
{
return ((vhpet->timer[n].cap_config & HPET_TCNF_INT_ENB) != 0);
}
static __inline bool
vhpet_timer_edge_trig(struct vhpet *vhpet, int n)
{
KASSERT(!vhpet_timer_msi_enabled(vhpet, n), ("vhpet_timer_edge_trig: "
"timer %d is using MSI", n));
if ((vhpet->timer[n].cap_config & HPET_TCNF_INT_TYPE) == 0)
return (true);
else
return (false);
}
static void
vhpet_timer_interrupt(struct vhpet *vhpet, int n)
{
int pin;
if (!vhpet_timer_interrupt_enabled(vhpet, n))
return;
if ((vhpet->isr & (1 << n)) != 0) {
return;
}
if (vhpet_timer_msi_enabled(vhpet, n)) {
(void) lapic_intr_msi(vhpet->vm, vhpet->timer[n].msireg >> 32,
vhpet->timer[n].msireg & 0xffffffff);
return;
}
pin = vhpet_timer_ioapic_pin(vhpet, n);
if (pin == 0) {
return;
}
if (vhpet_timer_edge_trig(vhpet, n)) {
(void) vioapic_pulse_irq(vhpet->vm, pin);
} else {
vhpet->isr |= 1 << n;
(void) vioapic_assert_irq(vhpet->vm, pin);
}
}
static void
vhpet_adjust_compval(struct vhpet *vhpet, int n, uint32_t counter)
{
uint32_t compval, comprate, compnext;
KASSERT(vhpet->timer[n].comprate != 0, ("hpet t%d is not periodic", n));
compval = vhpet->timer[n].compval;
comprate = vhpet->timer[n].comprate;
compnext = compval + ((counter - compval) / comprate + 1) * comprate;
vhpet->timer[n].compval = compnext;
}
static void
vhpet_handler(void *arg)
{
const struct vhpet_callout_arg *vca = arg;
struct vhpet *vhpet = vca->vhpet;
const int n = vca->timer_num;
struct callout *callout = &vhpet->timer[n].callout;
VHPET_LOCK(vhpet);
if (callout_pending(callout) || !callout_active(callout)) {
VHPET_UNLOCK(vhpet);
return;
}
callout_deactivate(callout);
ASSERT(vhpet_counter_enabled(vhpet));
if (vhpet_periodic_timer(vhpet, n)) {
hrtime_t now;
uint32_t counter = vhpet_counter(vhpet, &now);
vhpet_start_timer(vhpet, n, counter, now);
} else {
vhpet->timer[n].callout_expire = 0;
}
vhpet_timer_interrupt(vhpet, n);
VHPET_UNLOCK(vhpet);
}
static void
vhpet_stop_timer(struct vhpet *vhpet, int n, hrtime_t now)
{
ASSERT(VHPET_LOCKED(vhpet));
callout_stop(&vhpet->timer[n].callout);
if (vhpet->timer[n].callout_expire < now) {
vhpet_timer_interrupt(vhpet, n);
}
vhpet->timer[n].callout_expire = 0;
}
static void
vhpet_start_timer(struct vhpet *vhpet, int n, uint32_t counter, hrtime_t now)
{
struct vhpet_timer *timer = &vhpet->timer[n];
ASSERT(VHPET_LOCKED(vhpet));
if (timer->comprate != 0)
vhpet_adjust_compval(vhpet, n, counter);
else {
}
const hrtime_t delta = hrt_freq_interval(HPET_FREQ,
timer->compval - counter);
timer->callout_expire = now + delta;
callout_reset_hrtime(&timer->callout, timer->callout_expire,
vhpet_handler, &timer->arg, C_ABSOLUTE);
}
static void
vhpet_start_counting(struct vhpet *vhpet)
{
int i;
vhpet->base_time = gethrtime();
for (i = 0; i < VHPET_NUM_TIMERS; i++) {
vhpet_start_timer(vhpet, i, vhpet->base_count,
vhpet->base_time);
}
}
static void
vhpet_stop_counting(struct vhpet *vhpet, uint32_t counter, hrtime_t now)
{
int i;
vhpet->base_count = counter;
for (i = 0; i < VHPET_NUM_TIMERS; i++)
vhpet_stop_timer(vhpet, i, now);
}
static __inline void
update_register(uint64_t *regptr, uint64_t data, uint64_t mask)
{
*regptr &= ~mask;
*regptr |= (data & mask);
}
static void
vhpet_timer_update_config(struct vhpet *vhpet, int n, uint64_t data,
uint64_t mask)
{
bool clear_isr;
int old_pin, new_pin;
uint32_t allowed_irqs;
uint64_t oldval, newval;
if (vhpet_timer_msi_enabled(vhpet, n) ||
vhpet_timer_edge_trig(vhpet, n)) {
if (vhpet->isr & (1 << n))
panic("vhpet timer %d isr should not be asserted", n);
}
old_pin = vhpet_timer_ioapic_pin(vhpet, n);
oldval = vhpet->timer[n].cap_config;
newval = oldval;
update_register(&newval, data, mask);
newval &= ~(HPET_TCAP_RO_MASK | HPET_TCNF_32MODE);
newval |= oldval & HPET_TCAP_RO_MASK;
if (newval == oldval)
return;
vhpet->timer[n].cap_config = newval;
allowed_irqs = vhpet->timer[n].cap_config >> 32;
new_pin = vhpet_timer_ioapic_pin(vhpet, n);
if (new_pin != 0 && (allowed_irqs & (1 << new_pin)) == 0) {
new_pin = 0;
vhpet->timer[n].cap_config &= ~HPET_TCNF_INT_ROUTE;
}
if (!vhpet_periodic_timer(vhpet, n))
vhpet->timer[n].comprate = 0;
if (vhpet->isr & (1 << n)) {
KASSERT(old_pin != 0, ("timer %d isr asserted to ioapic pin %d",
n, old_pin));
if (!vhpet_timer_interrupt_enabled(vhpet, n))
clear_isr = true;
else if (vhpet_timer_msi_enabled(vhpet, n))
clear_isr = true;
else if (vhpet_timer_edge_trig(vhpet, n))
clear_isr = true;
else if (vhpet_timer_ioapic_pin(vhpet, n) != old_pin)
clear_isr = true;
else
clear_isr = false;
if (clear_isr) {
(void) vioapic_deassert_irq(vhpet->vm, old_pin);
vhpet->isr &= ~(1 << n);
}
}
}
int
vhpet_mmio_write(struct vm *vm, int vcpuid, uint64_t gpa, uint64_t val,
int size)
{
struct vhpet *vhpet;
uint64_t data, mask, oldval, val64;
uint32_t isr_clear_mask, old_compval, old_comprate, counter;
hrtime_t now;
int i, offset;
vhpet = vm_hpet(vm);
offset = gpa - VHPET_BASE;
VHPET_LOCK(vhpet);
switch (size) {
case 8:
mask = 0xffffffffffffffff;
data = val;
break;
case 4:
mask = 0xffffffff;
data = val;
if ((offset & 0x4) != 0) {
mask <<= 32;
data <<= 32;
}
break;
default:
goto done;
}
if (offset & (size - 1)) {
goto done;
}
if (offset == HPET_CONFIG || offset == HPET_CONFIG + 4) {
counter = vhpet_counter(vhpet, &now);
oldval = vhpet->config;
update_register(&vhpet->config, data, mask);
vhpet->config &= ~HPET_CNF_LEG_RT;
if ((oldval ^ vhpet->config) & HPET_CNF_ENABLE) {
if (vhpet_counter_enabled(vhpet)) {
vhpet_start_counting(vhpet);
} else {
vhpet_stop_counting(vhpet, counter, now);
}
}
goto done;
}
if (offset == HPET_ISR || offset == HPET_ISR + 4) {
isr_clear_mask = vhpet->isr & data;
for (i = 0; i < VHPET_NUM_TIMERS; i++) {
if ((isr_clear_mask & (1 << i)) != 0) {
vhpet_timer_clear_isr(vhpet, i);
}
}
goto done;
}
if (offset == HPET_MAIN_COUNTER || offset == HPET_MAIN_COUNTER + 4) {
val64 = vhpet_counter(vhpet, NULL);
update_register(&val64, data, mask);
vhpet->base_count = val64;
if (vhpet_counter_enabled(vhpet))
vhpet_start_counting(vhpet);
goto done;
}
for (i = 0; i < VHPET_NUM_TIMERS; i++) {
if (offset == HPET_TIMER_CAP_CNF(i) ||
offset == HPET_TIMER_CAP_CNF(i) + 4) {
vhpet_timer_update_config(vhpet, i, data, mask);
break;
}
if (offset == HPET_TIMER_COMPARATOR(i) ||
offset == HPET_TIMER_COMPARATOR(i) + 4) {
old_compval = vhpet->timer[i].compval;
old_comprate = vhpet->timer[i].comprate;
if (vhpet_periodic_timer(vhpet, i)) {
val64 = vhpet->timer[i].comprate;
update_register(&val64, data, mask);
vhpet->timer[i].comprate = val64;
if ((vhpet->timer[i].cap_config &
HPET_TCNF_VAL_SET) != 0) {
vhpet->timer[i].compval = val64;
}
} else {
KASSERT(vhpet->timer[i].comprate == 0,
("vhpet one-shot timer %d has invalid "
"rate %u", i, vhpet->timer[i].comprate));
val64 = vhpet->timer[i].compval;
update_register(&val64, data, mask);
vhpet->timer[i].compval = val64;
}
vhpet->timer[i].cap_config &= ~HPET_TCNF_VAL_SET;
if (vhpet->timer[i].compval != old_compval ||
vhpet->timer[i].comprate != old_comprate) {
if (vhpet_counter_enabled(vhpet)) {
counter = vhpet_counter(vhpet, &now);
vhpet_start_timer(vhpet, i, counter,
now);
}
}
break;
}
if (offset == HPET_TIMER_FSB_VAL(i) ||
offset == HPET_TIMER_FSB_ADDR(i)) {
update_register(&vhpet->timer[i].msireg, data, mask);
break;
}
}
done:
VHPET_UNLOCK(vhpet);
return (0);
}
int
vhpet_mmio_read(struct vm *vm, int vcpuid, uint64_t gpa, uint64_t *rval,
int size)
{
int i, offset;
struct vhpet *vhpet;
uint64_t data;
vhpet = vm_hpet(vm);
offset = gpa - VHPET_BASE;
VHPET_LOCK(vhpet);
if (size != 4 && size != 8) {
data = 0;
goto done;
}
if (offset & (size - 1)) {
data = 0;
goto done;
}
if (offset == HPET_CAPABILITIES || offset == HPET_CAPABILITIES + 4) {
data = vhpet_capabilities();
goto done;
}
if (offset == HPET_CONFIG || offset == HPET_CONFIG + 4) {
data = vhpet->config;
goto done;
}
if (offset == HPET_ISR || offset == HPET_ISR + 4) {
data = vhpet->isr;
goto done;
}
if (offset == HPET_MAIN_COUNTER || offset == HPET_MAIN_COUNTER + 4) {
data = vhpet_counter(vhpet, NULL);
goto done;
}
for (i = 0; i < VHPET_NUM_TIMERS; i++) {
if (offset == HPET_TIMER_CAP_CNF(i) ||
offset == HPET_TIMER_CAP_CNF(i) + 4) {
data = vhpet->timer[i].cap_config;
break;
}
if (offset == HPET_TIMER_COMPARATOR(i) ||
offset == HPET_TIMER_COMPARATOR(i) + 4) {
data = vhpet->timer[i].compval;
break;
}
if (offset == HPET_TIMER_FSB_VAL(i) ||
offset == HPET_TIMER_FSB_ADDR(i)) {
data = vhpet->timer[i].msireg;
break;
}
}
if (i >= VHPET_NUM_TIMERS)
data = 0;
done:
VHPET_UNLOCK(vhpet);
if (size == 4) {
if (offset & 0x4)
data >>= 32;
}
*rval = data;
return (0);
}
struct vhpet *
vhpet_init(struct vm *vm)
{
int i, pincount;
struct vhpet *vhpet;
uint64_t allowed_irqs;
struct vhpet_callout_arg *arg;
vhpet = kmem_zalloc(sizeof (struct vhpet), KM_SLEEP);
vhpet->vm = vm;
mutex_init(&vhpet->lock, NULL, MUTEX_ADAPTIVE, NULL);
pincount = vioapic_pincount(vm);
if (pincount >= 32)
allowed_irqs = 0xff000000;
else if (pincount >= 20)
allowed_irqs = 0xf << (pincount - 4);
else
allowed_irqs = 0;
for (i = 0; i < VHPET_NUM_TIMERS; i++) {
vhpet->timer[i].cap_config = allowed_irqs << 32;
vhpet->timer[i].cap_config |= HPET_TCAP_PER_INT;
vhpet->timer[i].cap_config |= HPET_TCAP_FSB_INT_DEL;
vhpet->timer[i].compval = 0xffffffff;
callout_init(&vhpet->timer[i].callout, 1);
arg = &vhpet->timer[i].arg;
arg->vhpet = vhpet;
arg->timer_num = i;
}
return (vhpet);
}
void
vhpet_cleanup(struct vhpet *vhpet)
{
int i;
for (i = 0; i < VHPET_NUM_TIMERS; i++)
callout_drain(&vhpet->timer[i].callout);
mutex_destroy(&vhpet->lock);
kmem_free(vhpet, sizeof (*vhpet));
}
int
vhpet_getcap(struct vm_hpet_cap *cap)
{
cap->capabilities = vhpet_capabilities();
return (0);
}
void
vhpet_localize_resources(struct vhpet *vhpet)
{
for (uint_t i = 0; i < VHPET_NUM_TIMERS; i++) {
vmm_glue_callout_localize(&vhpet->timer[i].callout);
}
}
void
vhpet_pause(struct vhpet *vhpet)
{
VHPET_LOCK(vhpet);
for (uint_t i = 0; i < VHPET_NUM_TIMERS; i++) {
struct vhpet_timer *timer = &vhpet->timer[i];
callout_stop(&timer->callout);
}
VHPET_UNLOCK(vhpet);
}
void
vhpet_resume(struct vhpet *vhpet)
{
VHPET_LOCK(vhpet);
for (uint_t i = 0; i < VHPET_NUM_TIMERS; i++) {
struct vhpet_timer *timer = &vhpet->timer[i];
if (timer->callout_expire != 0) {
callout_reset_hrtime(&timer->callout,
timer->callout_expire, vhpet_handler,
&timer->arg, C_ABSOLUTE);
}
}
VHPET_UNLOCK(vhpet);
}
static int
vhpet_data_read(void *datap, const vmm_data_req_t *req)
{
VERIFY3U(req->vdr_class, ==, VDC_HPET);
VERIFY3U(req->vdr_version, ==, 1);
VERIFY3U(req->vdr_len, >=, sizeof (struct vdi_hpet_v1));
struct vhpet *vhpet = datap;
struct vdi_hpet_v1 *out = req->vdr_data;
VHPET_LOCK(vhpet);
out->vh_config = vhpet->config;
out->vh_isr = vhpet->isr;
out->vh_count_base = vhpet->base_count;
out->vh_time_base = vm_normalize_hrtime(vhpet->vm, vhpet->base_time);
for (uint_t i = 0; i < 8; i++) {
const struct vhpet_timer *timer = &vhpet->timer[i];
struct vdi_hpet_timer_v1 *timer_out = &out->vh_timers[i];
timer_out->vht_config = timer->cap_config;
timer_out->vht_msi = timer->msireg;
timer_out->vht_comp_val = timer->compval;
timer_out->vht_comp_rate = timer->comprate;
if (timer->callout_expire != 0) {
timer_out->vht_time_target =
vm_normalize_hrtime(vhpet->vm,
timer->callout_expire);
} else {
timer_out->vht_time_target = 0;
}
}
VHPET_UNLOCK(vhpet);
return (0);
}
enum vhpet_validation_error {
VVE_OK,
VVE_BAD_CONFIG,
VVE_BAD_BASE_TIME,
VVE_BAD_ISR,
VVE_BAD_TIMER_CONFIG,
VVE_BAD_TIMER_ISR,
VVE_BAD_TIMER_TIME,
};
static enum vhpet_validation_error
vhpet_data_validate(const vmm_data_req_t *req, struct vm *vm)
{
ASSERT(req->vdr_version == 1 &&
req->vdr_len >= sizeof (struct vdi_hpet_v1));
const struct vdi_hpet_v1 *src = req->vdr_data;
if ((src->vh_config & HPET_CNF_LEG_RT) != 0) {
return (VVE_BAD_CONFIG);
}
const hrtime_t base_time = vm_denormalize_hrtime(vm, src->vh_time_base);
if (base_time > gethrtime()) {
return (VVE_BAD_BASE_TIME);
}
if ((src->vh_isr & ~(uint64_t)((1 << VHPET_NUM_TIMERS) - 1)) != 0) {
return (VVE_BAD_ISR);
}
for (uint_t i = 0; i < 8; i++) {
const struct vdi_hpet_timer_v1 *timer = &src->vh_timers[i];
const bool msi_enabled =
(timer->vht_config & HPET_TCNF_FSB_EN) != 0;
const bool level_triggered =
(timer->vht_config & HPET_TCNF_INT_TYPE) != 0;
const bool irq_asserted = (src->vh_isr & (1 << i)) != 0;
const uint32_t allowed_irqs = (timer->vht_config >> 32);
const uint32_t irq_pin =
(timer->vht_config & HPET_TCNF_INT_ROUTE) >> 9;
if (msi_enabled) {
if (level_triggered) {
return (VVE_BAD_TIMER_CONFIG);
}
} else {
if (irq_pin != 0 &&
(allowed_irqs & (1 << irq_pin)) == 0) {
return (VVE_BAD_TIMER_CONFIG);
}
}
if (irq_asserted && !level_triggered) {
return (VVE_BAD_TIMER_ISR);
}
if (timer->vht_time_target != 0) {
const uint64_t timer_target =
vm_denormalize_hrtime(vm, timer->vht_time_target);
if (timer_target < base_time) {
return (VVE_BAD_TIMER_TIME);
}
}
}
return (VVE_OK);
}
static int
vhpet_data_write(void *datap, const vmm_data_req_t *req)
{
VERIFY3U(req->vdr_class, ==, VDC_HPET);
VERIFY3U(req->vdr_version, ==, 1);
VERIFY3U(req->vdr_len, >=, sizeof (struct vdi_hpet_v1));
struct vhpet *vhpet = datap;
if (vhpet_data_validate(req, vhpet->vm) != VVE_OK) {
return (EINVAL);
}
const struct vdi_hpet_v1 *src = req->vdr_data;
VHPET_LOCK(vhpet);
vhpet->config = src->vh_config;
vhpet->isr = src->vh_isr;
vhpet->base_count = src->vh_count_base;
vhpet->base_time = vm_denormalize_hrtime(vhpet->vm, src->vh_time_base);
for (uint_t i = 0; i < 8; i++) {
struct vhpet_timer *timer = &vhpet->timer[i];
const struct vdi_hpet_timer_v1 *timer_src = &src->vh_timers[i];
timer->cap_config = timer_src->vht_config;
timer->msireg = timer_src->vht_msi;
timer->compval = timer_src->vht_comp_val;
timer->comprate = timer_src->vht_comp_rate;
if (timer_src->vht_time_target != 0) {
timer->callout_expire = vm_denormalize_hrtime(vhpet->vm,
timer_src->vht_time_target);
if (!vm_is_paused(vhpet->vm)) {
callout_reset_hrtime(&timer->callout,
timer->callout_expire, vhpet_handler,
&timer->arg, C_ABSOLUTE);
}
} else {
timer->callout_expire = 0;
}
}
VHPET_UNLOCK(vhpet);
return (0);
}
static const vmm_data_version_entry_t hpet_v1 = {
.vdve_class = VDC_HPET,
.vdve_version = 1,
.vdve_len_expect = sizeof (struct vdi_hpet_v1),
.vdve_readf = vhpet_data_read,
.vdve_writef = vhpet_data_write,
};
VMM_DATA_VERSION(hpet_v1);