root/sys/amd64/vmm/io/vrtc.c
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2014, Neel Natu (neel@freebsd.org)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice unmodified, this list of conditions, and the following
 *    disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/cdefs.h>
#include "opt_bhyve_snapshot.h"

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/queue.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/lock.h>
#include <sys/mutex.h>
#include <sys/clock.h>
#include <sys/sysctl.h>

#include <machine/vmm.h>
#include <machine/vmm_snapshot.h>

#include <isa/rtc.h>

#include <dev/vmm/vmm_ktr.h>

#include "vatpic.h"
#include "vioapic.h"
#include "vrtc.h"

/* Register layout of the RTC */
struct rtcdev {
        uint8_t sec;
        uint8_t alarm_sec;
        uint8_t min;
        uint8_t alarm_min;
        uint8_t hour;
        uint8_t alarm_hour;
        uint8_t day_of_week;
        uint8_t day_of_month;
        uint8_t month;
        uint8_t year;
        uint8_t reg_a;
        uint8_t reg_b;
        uint8_t reg_c;
        uint8_t reg_d;
        uint8_t nvram[36];
        uint8_t century;
        uint8_t nvram2[128 - 51];
} __packed;
CTASSERT(sizeof(struct rtcdev) == 128);
CTASSERT(offsetof(struct rtcdev, century) == RTC_CENTURY);

struct vrtc {
        struct vm       *vm;
        struct mtx      mtx;
        struct callout  callout;
        u_int           addr;           /* RTC register to read or write */
        sbintime_t      base_uptime;
        time_t          base_rtctime;
        struct rtcdev   rtcdev;
};

#define VRTC_LOCK(vrtc)         mtx_lock(&((vrtc)->mtx))
#define VRTC_UNLOCK(vrtc)       mtx_unlock(&((vrtc)->mtx))
#define VRTC_LOCKED(vrtc)       mtx_owned(&((vrtc)->mtx))

/*
 * RTC time is considered "broken" if:
 * - RTC updates are halted by the guest
 * - RTC date/time fields have invalid values
 */
#define VRTC_BROKEN_TIME        ((time_t)-1)

#define RTC_IRQ                 8
#define RTCSB_BIN               0x04
#define RTCSB_ALL_INTRS         (RTCSB_UINTR | RTCSB_AINTR | RTCSB_PINTR)
#define rtc_halted(vrtc)        ((vrtc->rtcdev.reg_b & RTCSB_HALT) != 0)
#define aintr_enabled(vrtc)     (((vrtc)->rtcdev.reg_b & RTCSB_AINTR) != 0)
#define pintr_enabled(vrtc)     (((vrtc)->rtcdev.reg_b & RTCSB_PINTR) != 0)
#define uintr_enabled(vrtc)     (((vrtc)->rtcdev.reg_b & RTCSB_UINTR) != 0)

static void vrtc_callout_handler(void *arg);
static void vrtc_set_reg_c(struct vrtc *vrtc, uint8_t newval);

static MALLOC_DEFINE(M_VRTC, "vrtc", "bhyve virtual rtc");

SYSCTL_DECL(_hw_vmm);
SYSCTL_NODE(_hw_vmm, OID_AUTO, vrtc, CTLFLAG_RW | CTLFLAG_MPSAFE, NULL,
    NULL);

static int rtc_flag_broken_time = 1;
SYSCTL_INT(_hw_vmm_vrtc, OID_AUTO, flag_broken_time, CTLFLAG_RDTUN,
    &rtc_flag_broken_time, 0, "Stop guest when invalid RTC time is detected");

static __inline bool
divider_enabled(int reg_a)
{
        /*
         * The RTC is counting only when dividers are not held in reset.
         */
        return ((reg_a & 0x70) == 0x20);
}

static __inline bool
update_enabled(struct vrtc *vrtc)
{
        /*
         * RTC date/time can be updated only if:
         * - divider is not held in reset
         * - guest has not disabled updates
         * - the date/time fields have valid contents
         */
        if (!divider_enabled(vrtc->rtcdev.reg_a))
                return (false);

        if (rtc_halted(vrtc))
                return (false);

        if (vrtc->base_rtctime == VRTC_BROKEN_TIME)
                return (false);

        return (true);
}

static time_t
vrtc_curtime(struct vrtc *vrtc, sbintime_t *basetime)
{
        sbintime_t now, delta;
        time_t t, secs;

        KASSERT(VRTC_LOCKED(vrtc), ("%s: vrtc not locked", __func__));

        t = vrtc->base_rtctime;
        *basetime = vrtc->base_uptime;
        if (update_enabled(vrtc)) {
                now = sbinuptime();
                delta = now - vrtc->base_uptime;
                KASSERT(delta >= 0, ("vrtc_curtime: uptime went backwards: "
                    "%#lx to %#lx", vrtc->base_uptime, now));
                secs = delta / SBT_1S;
                t += secs;
                *basetime += secs * SBT_1S;
        }
        return (t);
}

static __inline uint8_t
rtcset(struct rtcdev *rtc, int val)
{

        KASSERT(val >= 0 && val < 100, ("%s: invalid bin2bcd index %d",
            __func__, val));

        return ((rtc->reg_b & RTCSB_BIN) ? val : bin2bcd_data[val]);
}

static void
secs_to_rtc(time_t rtctime, struct vrtc *vrtc, int force_update)
{
        struct clocktime ct;
        struct timespec ts;
        struct rtcdev *rtc;
        int hour;

        KASSERT(VRTC_LOCKED(vrtc), ("%s: vrtc not locked", __func__));

        if (rtctime < 0) {
                KASSERT(rtctime == VRTC_BROKEN_TIME,
                    ("%s: invalid vrtc time %#lx", __func__, rtctime));
                return;
        }

        /*
         * If the RTC is halted then the guest has "ownership" of the
         * date/time fields. Don't update the RTC date/time fields in
         * this case (unless forced).
         */
        if (rtc_halted(vrtc) && !force_update)
                return;

        ts.tv_sec = rtctime;
        ts.tv_nsec = 0;
        clock_ts_to_ct(&ts, &ct);

        KASSERT(ct.sec >= 0 && ct.sec <= 59, ("invalid clocktime sec %d",
            ct.sec));
        KASSERT(ct.min >= 0 && ct.min <= 59, ("invalid clocktime min %d",
            ct.min));
        KASSERT(ct.hour >= 0 && ct.hour <= 23, ("invalid clocktime hour %d",
            ct.hour));
        KASSERT(ct.dow >= 0 && ct.dow <= 6, ("invalid clocktime wday %d",
            ct.dow));
        KASSERT(ct.day >= 1 && ct.day <= 31, ("invalid clocktime mday %d",
            ct.day));
        KASSERT(ct.mon >= 1 && ct.mon <= 12, ("invalid clocktime month %d",
            ct.mon));
        KASSERT(ct.year >= POSIX_BASE_YEAR, ("invalid clocktime year %d",
            ct.year));

        rtc = &vrtc->rtcdev;
        rtc->sec = rtcset(rtc, ct.sec);
        rtc->min = rtcset(rtc, ct.min);

        if (rtc->reg_b & RTCSB_24HR) {
                hour = ct.hour;
        } else {
                /*
                 * Convert to the 12-hour format.
                 */
                switch (ct.hour) {
                case 0:                 /* 12 AM */
                case 12:                /* 12 PM */
                        hour = 12;
                        break;
                default:
                        /*
                         * The remaining 'ct.hour' values are interpreted as:
                         * [1  - 11] ->  1 - 11 AM
                         * [13 - 23] ->  1 - 11 PM
                         */
                        hour = ct.hour % 12;
                        break;
                }
        }

        rtc->hour = rtcset(rtc, hour);

        if ((rtc->reg_b & RTCSB_24HR) == 0 && ct.hour >= 12)
                rtc->hour |= 0x80;          /* set MSB to indicate PM */

        rtc->day_of_week = rtcset(rtc, ct.dow + 1);
        rtc->day_of_month = rtcset(rtc, ct.day);
        rtc->month = rtcset(rtc, ct.mon);
        rtc->year = rtcset(rtc, ct.year % 100);
        rtc->century = rtcset(rtc, ct.year / 100);
}

static int
rtcget(struct rtcdev *rtc, int val, int *retval)
{
        uint8_t upper, lower;

        if (rtc->reg_b & RTCSB_BIN) {
                *retval = val;
                return (0);
        }

        lower = val & 0xf;
        upper = (val >> 4) & 0xf;

        if (lower > 9 || upper > 9)
                return (-1);

        *retval = upper * 10 + lower;
        return (0);
}

static time_t
rtc_to_secs(struct vrtc *vrtc)
{
        struct clocktime ct;
        struct timespec ts;
        struct rtcdev *rtc;
#ifdef KTR
        struct vm *vm = vrtc->vm;
#endif
        int century, error, hour, pm, year;

        KASSERT(VRTC_LOCKED(vrtc), ("%s: vrtc not locked", __func__));

        rtc = &vrtc->rtcdev;

        bzero(&ct, sizeof(struct clocktime));

        error = rtcget(rtc, rtc->sec, &ct.sec);
        if (error || ct.sec < 0 || ct.sec > 59) {
                VM_CTR2(vm, "Invalid RTC sec %#x/%d", rtc->sec, ct.sec);
                goto fail;
        }

        error = rtcget(rtc, rtc->min, &ct.min);
        if (error || ct.min < 0 || ct.min > 59) {
                VM_CTR2(vm, "Invalid RTC min %#x/%d", rtc->min, ct.min);
                goto fail;
        }

        pm = 0;
        hour = rtc->hour;
        if ((rtc->reg_b & RTCSB_24HR) == 0) {
                if (hour & 0x80) {
                        hour &= ~0x80;
                        pm = 1;
                }
        }
        error = rtcget(rtc, hour, &ct.hour);
        if ((rtc->reg_b & RTCSB_24HR) == 0) {
                if (ct.hour >= 1 && ct.hour <= 12) {
                        /*
                         * Convert from 12-hour format to internal 24-hour
                         * representation as follows:
                         *
                         *    12-hour format            ct.hour
                         *      12      AM              0
                         *      1 - 11  AM              1 - 11
                         *      12      PM              12
                         *      1 - 11  PM              13 - 23
                         */
                        if (ct.hour == 12)
                                ct.hour = 0;
                        if (pm)
                                ct.hour += 12;
                } else {
                        VM_CTR2(vm, "Invalid RTC 12-hour format %#x/%d",
                            rtc->hour, ct.hour);
                        goto fail;
                }
        }

        if (error || ct.hour < 0 || ct.hour > 23) {
                VM_CTR2(vm, "Invalid RTC hour %#x/%d", rtc->hour, ct.hour);
                goto fail;
        }

        /*
         * Ignore 'rtc->dow' because some guests like Linux don't bother
         * setting it at all while others like OpenBSD/i386 set it incorrectly.
         *
         * clock_ct_to_ts() does not depend on 'ct.dow' anyways so ignore it.
         */
        ct.dow = -1;

        error = rtcget(rtc, rtc->day_of_month, &ct.day);
        if (error || ct.day < 1 || ct.day > 31) {
                VM_CTR2(vm, "Invalid RTC mday %#x/%d", rtc->day_of_month,
                    ct.day);
                goto fail;
        }

        error = rtcget(rtc, rtc->month, &ct.mon);
        if (error || ct.mon < 1 || ct.mon > 12) {
                VM_CTR2(vm, "Invalid RTC month %#x/%d", rtc->month, ct.mon);
                goto fail;
        }

        error = rtcget(rtc, rtc->year, &year);
        if (error || year < 0 || year > 99) {
                VM_CTR2(vm, "Invalid RTC year %#x/%d", rtc->year, year);
                goto fail;
        }

        error = rtcget(rtc, rtc->century, &century);
        ct.year = century * 100 + year;
        if (error || ct.year < POSIX_BASE_YEAR) {
                VM_CTR2(vm, "Invalid RTC century %#x/%d", rtc->century,
                    ct.year);
                goto fail;
        }

        error = clock_ct_to_ts(&ct, &ts);
        if (error || ts.tv_sec < 0) {
                VM_CTR3(vm, "Invalid RTC clocktime.date %04d-%02d-%02d",
                    ct.year, ct.mon, ct.day);
                VM_CTR3(vm, "Invalid RTC clocktime.time %02d:%02d:%02d",
                    ct.hour, ct.min, ct.sec);
                goto fail;
        }
        return (ts.tv_sec);             /* success */
fail:
        /*
         * Stop updating the RTC if the date/time fields programmed by
         * the guest are invalid.
         */
        VM_CTR0(vrtc->vm, "Invalid RTC date/time programming detected");
        return (VRTC_BROKEN_TIME);
}

static int
vrtc_time_update(struct vrtc *vrtc, time_t newtime, sbintime_t newbase)
{
        struct rtcdev *rtc;
        time_t oldtime;
        uint8_t alarm_sec, alarm_min, alarm_hour;

        KASSERT(VRTC_LOCKED(vrtc), ("%s: vrtc not locked", __func__));

        rtc = &vrtc->rtcdev;
        alarm_sec = rtc->alarm_sec;
        alarm_min = rtc->alarm_min;
        alarm_hour = rtc->alarm_hour;

        oldtime = vrtc->base_rtctime;
        VM_CTR2(vrtc->vm, "Updating RTC secs from %#lx to %#lx",
            oldtime, newtime);

        VM_CTR2(vrtc->vm, "Updating RTC base uptime from %#lx to %#lx",
            vrtc->base_uptime, newbase);
        vrtc->base_uptime = newbase;

        if (newtime == oldtime)
                return (0);

        /*
         * If 'newtime' indicates that RTC updates are disabled then just
         * record that and return. There is no need to do alarm interrupt
         * processing in this case.
         */
        if (newtime == VRTC_BROKEN_TIME) {
                vrtc->base_rtctime = VRTC_BROKEN_TIME;
                return (0);
        }

        /*
         * Return an error if RTC updates are halted by the guest.
         */
        if (rtc_halted(vrtc)) {
                VM_CTR0(vrtc->vm, "RTC update halted by guest");
                return (EBUSY);
        }

        do {
                /*
                 * If the alarm interrupt is enabled and 'oldtime' is valid
                 * then visit all the seconds between 'oldtime' and 'newtime'
                 * to check for the alarm condition.
                 *
                 * Otherwise move the RTC time forward directly to 'newtime'.
                 */
                if (aintr_enabled(vrtc) && oldtime != VRTC_BROKEN_TIME)
                        vrtc->base_rtctime++;
                else
                        vrtc->base_rtctime = newtime;

                if (aintr_enabled(vrtc)) {
                        /*
                         * Update the RTC date/time fields before checking
                         * if the alarm conditions are satisfied.
                         */
                        secs_to_rtc(vrtc->base_rtctime, vrtc, 0);

                        if ((alarm_sec >= 0xC0 || alarm_sec == rtc->sec) &&
                            (alarm_min >= 0xC0 || alarm_min == rtc->min) &&
                            (alarm_hour >= 0xC0 || alarm_hour == rtc->hour)) {
                                vrtc_set_reg_c(vrtc, rtc->reg_c | RTCIR_ALARM);
                        }
                }
        } while (vrtc->base_rtctime != newtime);

        if (uintr_enabled(vrtc))
                vrtc_set_reg_c(vrtc, rtc->reg_c | RTCIR_UPDATE);

        return (0);
}

static sbintime_t
vrtc_freq(struct vrtc *vrtc)
{
        int ratesel;

        static sbintime_t pf[16] = {
                0,
                SBT_1S / 256,
                SBT_1S / 128,
                SBT_1S / 8192,
                SBT_1S / 4096,
                SBT_1S / 2048,
                SBT_1S / 1024,
                SBT_1S / 512,
                SBT_1S / 256,
                SBT_1S / 128,
                SBT_1S / 64,
                SBT_1S / 32,
                SBT_1S / 16,
                SBT_1S / 8,
                SBT_1S / 4,
                SBT_1S / 2,
        };

        KASSERT(VRTC_LOCKED(vrtc), ("%s: vrtc not locked", __func__));

        /*
         * If both periodic and alarm interrupts are enabled then use the
         * periodic frequency to drive the callout. The minimum periodic
         * frequency (2 Hz) is higher than the alarm frequency (1 Hz) so
         * piggyback the alarm on top of it. The same argument applies to
         * the update interrupt.
         */
        if (pintr_enabled(vrtc) && divider_enabled(vrtc->rtcdev.reg_a)) {
                ratesel = vrtc->rtcdev.reg_a & 0xf;
                return (pf[ratesel]);
        } else if (aintr_enabled(vrtc) && update_enabled(vrtc)) {
                return (SBT_1S);
        } else if (uintr_enabled(vrtc) && update_enabled(vrtc)) {
                return (SBT_1S);
        } else {
                return (0);
        }
}

static void
vrtc_callout_reset(struct vrtc *vrtc, sbintime_t freqsbt)
{

        KASSERT(VRTC_LOCKED(vrtc), ("%s: vrtc not locked", __func__));

        if (freqsbt == 0) {
                if (callout_active(&vrtc->callout)) {
                        VM_CTR0(vrtc->vm, "RTC callout stopped");
                        callout_stop(&vrtc->callout);
                }
                return;
        }
        VM_CTR1(vrtc->vm, "RTC callout frequency %d hz", SBT_1S / freqsbt);
        callout_reset_sbt(&vrtc->callout, freqsbt, 0, vrtc_callout_handler,
            vrtc, 0);
}

static void
vrtc_callout_handler(void *arg)
{
        struct vrtc *vrtc = arg;
        sbintime_t freqsbt, basetime;
        time_t rtctime;
        int error __diagused;

        VM_CTR0(vrtc->vm, "vrtc callout fired");

        VRTC_LOCK(vrtc);
        if (callout_pending(&vrtc->callout))    /* callout was reset */
                goto done;

        if (!callout_active(&vrtc->callout))    /* callout was stopped */
                goto done;

        callout_deactivate(&vrtc->callout);

        KASSERT((vrtc->rtcdev.reg_b & RTCSB_ALL_INTRS) != 0,
            ("gratuitous vrtc callout"));

        if (pintr_enabled(vrtc))
                vrtc_set_reg_c(vrtc, vrtc->rtcdev.reg_c | RTCIR_PERIOD);

        if (aintr_enabled(vrtc) || uintr_enabled(vrtc)) {
                rtctime = vrtc_curtime(vrtc, &basetime);
                error = vrtc_time_update(vrtc, rtctime, basetime);
                KASSERT(error == 0, ("%s: vrtc_time_update error %d",
                    __func__, error));
        }

        freqsbt = vrtc_freq(vrtc);
        KASSERT(freqsbt != 0, ("%s: vrtc frequency cannot be zero", __func__));
        vrtc_callout_reset(vrtc, freqsbt);
done:
        VRTC_UNLOCK(vrtc);
}

static __inline void
vrtc_callout_check(struct vrtc *vrtc, sbintime_t freq)
{
        int active __diagused;

        active = callout_active(&vrtc->callout) ? 1 : 0;
        KASSERT((freq == 0 && !active) || (freq != 0 && active),
            ("vrtc callout %s with frequency %#lx",
            active ? "active" : "inactive", freq));
}

static void
vrtc_set_reg_c(struct vrtc *vrtc, uint8_t newval)
{
        struct rtcdev *rtc;
        int oldirqf, newirqf;
        uint8_t oldval, changed;

        KASSERT(VRTC_LOCKED(vrtc), ("%s: vrtc not locked", __func__));

        rtc = &vrtc->rtcdev;
        newval &= RTCIR_ALARM | RTCIR_PERIOD | RTCIR_UPDATE;

        oldirqf = rtc->reg_c & RTCIR_INT;
        if ((aintr_enabled(vrtc) && (newval & RTCIR_ALARM) != 0) ||
            (pintr_enabled(vrtc) && (newval & RTCIR_PERIOD) != 0) ||
            (uintr_enabled(vrtc) && (newval & RTCIR_UPDATE) != 0)) {
                newirqf = RTCIR_INT;
        } else {
                newirqf = 0;
        }

        oldval = rtc->reg_c;
        rtc->reg_c = newirqf | newval;
        changed = oldval ^ rtc->reg_c;
        if (changed) {
                VM_CTR2(vrtc->vm, "RTC reg_c changed from %#x to %#x",
                    oldval, rtc->reg_c);
        }

        if (!oldirqf && newirqf) {
                VM_CTR1(vrtc->vm, "RTC irq %d asserted", RTC_IRQ);
                vatpic_pulse_irq(vrtc->vm, RTC_IRQ);
                vioapic_pulse_irq(vrtc->vm, RTC_IRQ);
        } else if (oldirqf && !newirqf) {
                VM_CTR1(vrtc->vm, "RTC irq %d deasserted", RTC_IRQ);
        }
}

static int
vrtc_set_reg_b(struct vrtc *vrtc, uint8_t newval)
{
        struct rtcdev *rtc;
        sbintime_t oldfreq, newfreq, basetime;
        time_t curtime, rtctime;
        int error __diagused;
        uint8_t oldval, changed;

        KASSERT(VRTC_LOCKED(vrtc), ("%s: vrtc not locked", __func__));

        rtc = &vrtc->rtcdev;
        oldval = rtc->reg_b;
        oldfreq = vrtc_freq(vrtc);

        rtc->reg_b = newval;
        changed = oldval ^ newval;
        if (changed) {
                VM_CTR2(vrtc->vm, "RTC reg_b changed from %#x to %#x",
                    oldval, newval);
        }

        if (changed & RTCSB_HALT) {
                if ((newval & RTCSB_HALT) == 0) {
                        rtctime = rtc_to_secs(vrtc);
                        basetime = sbinuptime();
                        if (rtctime == VRTC_BROKEN_TIME) {
                                if (rtc_flag_broken_time)
                                        return (-1);
                        }
                } else {
                        curtime = vrtc_curtime(vrtc, &basetime);
                        KASSERT(curtime == vrtc->base_rtctime, ("%s: mismatch "
                            "between vrtc basetime (%#lx) and curtime (%#lx)",
                            __func__, vrtc->base_rtctime, curtime));

                        /*
                         * Force a refresh of the RTC date/time fields so
                         * they reflect the time right before the guest set
                         * the HALT bit.
                         */
                        secs_to_rtc(curtime, vrtc, 1);

                        /*
                         * Updates are halted so mark 'base_rtctime' to denote
                         * that the RTC date/time is in flux.
                         */
                        rtctime = VRTC_BROKEN_TIME;
                        rtc->reg_b &= ~RTCSB_UINTR;
                }
                error = vrtc_time_update(vrtc, rtctime, basetime);
                KASSERT(error == 0, ("vrtc_time_update error %d", error));
        }

        /*
         * Side effect of changes to the interrupt enable bits.
         */
        if (changed & RTCSB_ALL_INTRS)
                vrtc_set_reg_c(vrtc, vrtc->rtcdev.reg_c);

        /*
         * Change the callout frequency if it has changed.
         */
        newfreq = vrtc_freq(vrtc);
        if (newfreq != oldfreq)
                vrtc_callout_reset(vrtc, newfreq);
        else
                vrtc_callout_check(vrtc, newfreq);

        /*
         * The side effect of bits that control the RTC date/time format
         * is handled lazily when those fields are actually read.
         */
        return (0);
}

static void
vrtc_set_reg_a(struct vrtc *vrtc, uint8_t newval)
{
        sbintime_t oldfreq, newfreq;
        uint8_t oldval, changed;

        KASSERT(VRTC_LOCKED(vrtc), ("%s: vrtc not locked", __func__));

        newval &= ~RTCSA_TUP;
        oldval = vrtc->rtcdev.reg_a;
        oldfreq = vrtc_freq(vrtc);

        if (divider_enabled(oldval) && !divider_enabled(newval)) {
                VM_CTR2(vrtc->vm, "RTC divider held in reset at %#lx/%#lx",
                    vrtc->base_rtctime, vrtc->base_uptime);
        } else if (!divider_enabled(oldval) && divider_enabled(newval)) {
                /*
                 * If the dividers are coming out of reset then update
                 * 'base_uptime' before this happens. This is done to
                 * maintain the illusion that the RTC date/time was frozen
                 * while the dividers were disabled.
                 */
                vrtc->base_uptime = sbinuptime();
                VM_CTR2(vrtc->vm, "RTC divider out of reset at %#lx/%#lx",
                    vrtc->base_rtctime, vrtc->base_uptime);
        } else {
                /* NOTHING */
        }

        vrtc->rtcdev.reg_a = newval;
        changed = oldval ^ newval;
        if (changed) {
                VM_CTR2(vrtc->vm, "RTC reg_a changed from %#x to %#x",
                    oldval, newval);
        }

        /*
         * Side effect of changes to rate select and divider enable bits.
         */
        newfreq = vrtc_freq(vrtc);
        if (newfreq != oldfreq)
                vrtc_callout_reset(vrtc, newfreq);
        else
                vrtc_callout_check(vrtc, newfreq);
}

int
vrtc_set_time(struct vm *vm, time_t secs)
{
        struct vrtc *vrtc;
        int error;

        vrtc = vm_rtc(vm);
        VRTC_LOCK(vrtc);
        error = vrtc_time_update(vrtc, secs, sbinuptime());
        VRTC_UNLOCK(vrtc);

        if (error) {
                VM_CTR2(vrtc->vm, "Error %d setting RTC time to %#lx", error,
                    secs);
        } else {
                VM_CTR1(vrtc->vm, "RTC time set to %#lx", secs);
        }

        return (error);
}

time_t
vrtc_get_time(struct vm *vm)
{
        struct vrtc *vrtc;
        sbintime_t basetime;
        time_t t;

        vrtc = vm_rtc(vm);
        VRTC_LOCK(vrtc);
        t = vrtc_curtime(vrtc, &basetime);
        VRTC_UNLOCK(vrtc);

        return (t);
}

int
vrtc_nvram_write(struct vm *vm, int offset, uint8_t value)
{
        struct vrtc *vrtc;
        uint8_t *ptr;

        vrtc = vm_rtc(vm);

        /*
         * Don't allow writes to RTC control registers or the date/time fields.
         */
        if (offset < offsetof(struct rtcdev, nvram[0]) ||
            offset == RTC_CENTURY || offset >= sizeof(struct rtcdev)) {
                VM_CTR1(vrtc->vm, "RTC nvram write to invalid offset %d",
                    offset);
                return (EINVAL);
        }

        VRTC_LOCK(vrtc);
        ptr = (uint8_t *)(&vrtc->rtcdev);
        ptr[offset] = value;
        VM_CTR2(vrtc->vm, "RTC nvram write %#x to offset %#x", value, offset);
        VRTC_UNLOCK(vrtc);

        return (0);
}

int
vrtc_nvram_read(struct vm *vm, int offset, uint8_t *retval)
{
        struct vrtc *vrtc;
        sbintime_t basetime;
        time_t curtime;
        uint8_t *ptr;

        /*
         * Allow all offsets in the RTC to be read.
         */
        if (offset < 0 || offset >= sizeof(struct rtcdev))
                return (EINVAL);

        vrtc = vm_rtc(vm);
        VRTC_LOCK(vrtc);

        /*
         * Update RTC date/time fields if necessary.
         */
        if (offset < 10 || offset == RTC_CENTURY) {
                curtime = vrtc_curtime(vrtc, &basetime);
                secs_to_rtc(curtime, vrtc, 0);
        }

        ptr = (uint8_t *)(&vrtc->rtcdev);
        *retval = ptr[offset];

        VRTC_UNLOCK(vrtc);
        return (0);
}

int
vrtc_addr_handler(struct vm *vm, bool in, int port, int bytes, uint32_t *val)
{
        struct vrtc *vrtc;

        vrtc = vm_rtc(vm);

        if (bytes != 1)
                return (-1);

        if (in) {
                *val = 0xff;
                return (0);
        }

        VRTC_LOCK(vrtc);
        vrtc->addr = *val & 0x7f;
        VRTC_UNLOCK(vrtc);

        return (0);
}

int
vrtc_data_handler(struct vm *vm, bool in, int port, int bytes, uint32_t *val)
{
        struct vrtc *vrtc;
        struct rtcdev *rtc;
        sbintime_t basetime;
        time_t curtime;
        int error, offset;

        vrtc = vm_rtc(vm);
        rtc = &vrtc->rtcdev;

        if (bytes != 1)
                return (-1);

        VRTC_LOCK(vrtc);
        offset = vrtc->addr;
        if (offset >= sizeof(struct rtcdev)) {
                VRTC_UNLOCK(vrtc);
                return (-1);
        }

        error = 0;
        curtime = vrtc_curtime(vrtc, &basetime);
        vrtc_time_update(vrtc, curtime, basetime);

        /*
         * Update RTC date/time fields if necessary.
         *
         * This is not just for reads of the RTC. The side-effect of writing
         * the century byte requires other RTC date/time fields (e.g. sec)
         * to be updated here.
         */
        if (offset < 10 || offset == RTC_CENTURY)
                secs_to_rtc(curtime, vrtc, 0);

        if (in) {
                if (offset == 12) {
                        /*
                         * XXX
                         * reg_c interrupt flags are updated only if the
                         * corresponding interrupt enable bit in reg_b is set.
                         */
                        *val = vrtc->rtcdev.reg_c;
                        vrtc_set_reg_c(vrtc, 0);
                } else {
                        *val = *((uint8_t *)rtc + offset);
                }
                VM_CTR2(vm, "Read value %#x from RTC offset %#x",
                    *val, offset);
        } else {
                switch (offset) {
                case 10:
                        VM_CTR1(vm, "RTC reg_a set to %#x", *val);
                        vrtc_set_reg_a(vrtc, *val);
                        break;
                case 11:
                        VM_CTR1(vm, "RTC reg_b set to %#x", *val);
                        error = vrtc_set_reg_b(vrtc, *val);
                        break;
                case 12:
                        VM_CTR1(vm, "RTC reg_c set to %#x (ignored)",
                            *val);
                        break;
                case 13:
                        VM_CTR1(vm, "RTC reg_d set to %#x (ignored)",
                            *val);
                        break;
                case 0:
                        /*
                         * High order bit of 'seconds' is readonly.
                         */
                        *val &= 0x7f;
                        /* FALLTHRU */
                default:
                        VM_CTR2(vm, "RTC offset %#x set to %#x",
                            offset, *val);
                        *((uint8_t *)rtc + offset) = *val;
                        break;
                }

                /*
                 * XXX some guests (e.g. OpenBSD) write the century byte
                 * outside of RTCSB_HALT so re-calculate the RTC date/time.
                 */
                if (offset == RTC_CENTURY && !rtc_halted(vrtc)) {
                        curtime = rtc_to_secs(vrtc);
                        error = vrtc_time_update(vrtc, curtime, sbinuptime());
                        KASSERT(!error, ("vrtc_time_update error %d", error));
                        if (curtime == VRTC_BROKEN_TIME && rtc_flag_broken_time)
                                error = -1;
                }
        }
        VRTC_UNLOCK(vrtc);
        return (error);
}

void
vrtc_reset(struct vrtc *vrtc)
{
        struct rtcdev *rtc;

        VRTC_LOCK(vrtc);

        rtc = &vrtc->rtcdev;
        vrtc_set_reg_b(vrtc, rtc->reg_b & ~(RTCSB_ALL_INTRS | RTCSB_SQWE));
        vrtc_set_reg_c(vrtc, 0);
        KASSERT(!callout_active(&vrtc->callout), ("rtc callout still active"));

        VRTC_UNLOCK(vrtc);
}

struct vrtc *
vrtc_init(struct vm *vm)
{
        struct vrtc *vrtc;
        struct rtcdev *rtc;
        time_t curtime;

        vrtc = malloc(sizeof(struct vrtc), M_VRTC, M_WAITOK | M_ZERO);
        vrtc->vm = vm;
        mtx_init(&vrtc->mtx, "vrtc lock", NULL, MTX_DEF);
        callout_init(&vrtc->callout, 1);

        /* Allow dividers to keep time but disable everything else */
        rtc = &vrtc->rtcdev;
        rtc->reg_a = 0x20;
        rtc->reg_b = RTCSB_24HR;
        rtc->reg_c = 0;
        rtc->reg_d = RTCSD_PWR;

        /* Reset the index register to a safe value. */
        vrtc->addr = RTC_STATUSD;

        /*
         * Initialize RTC time to 00:00:00 Jan 1, 1970.
         */
        curtime = 0;

        VRTC_LOCK(vrtc);
        vrtc->base_rtctime = VRTC_BROKEN_TIME;
        vrtc_time_update(vrtc, curtime, sbinuptime());
        secs_to_rtc(curtime, vrtc, 0);
        VRTC_UNLOCK(vrtc);

        return (vrtc);
}

void
vrtc_cleanup(struct vrtc *vrtc)
{

        callout_drain(&vrtc->callout);
        mtx_destroy(&vrtc->mtx);
        free(vrtc, M_VRTC);
}

#ifdef BHYVE_SNAPSHOT
int
vrtc_snapshot(struct vrtc *vrtc, struct vm_snapshot_meta *meta)
{
        int ret;

        VRTC_LOCK(vrtc);

        SNAPSHOT_VAR_OR_LEAVE(vrtc->addr, meta, ret, done);
        if (meta->op == VM_SNAPSHOT_RESTORE)
                vrtc->base_uptime = sbinuptime();
        SNAPSHOT_VAR_OR_LEAVE(vrtc->base_rtctime, meta, ret, done);

        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.sec, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.alarm_sec, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.min, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.alarm_min, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.hour, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.alarm_hour, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.day_of_week, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.day_of_month, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.month, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.year, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.reg_a, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.reg_b, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.reg_c, meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.reg_d, meta, ret, done);
        SNAPSHOT_BUF_OR_LEAVE(vrtc->rtcdev.nvram, sizeof(vrtc->rtcdev.nvram),
                              meta, ret, done);
        SNAPSHOT_VAR_OR_LEAVE(vrtc->rtcdev.century, meta, ret, done);
        SNAPSHOT_BUF_OR_LEAVE(vrtc->rtcdev.nvram2, sizeof(vrtc->rtcdev.nvram2),
                              meta, ret, done);

        vrtc_callout_reset(vrtc, vrtc_freq(vrtc));

        VRTC_UNLOCK(vrtc);

done:
        return (ret);
}
#endif