root/usr/src/test/bhyve-tests/tests/kdev/payload_vrtc_ops.c
/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2023 Oxide Computer Company
 */

#include "payload_common.h"
#include "payload_utils.h"
#include "test_defs.h"

/* Convenience definitions for RTC offsets */
#define RTC_SEC         0x00
#define RTC_MIN         0x02
#define RTC_HOUR        0x04
#define RTC_DAY         0x07
#define RTC_MONTH       0x08
#define RTC_YEAR        0x09
#define RTC_CENTURY     0x32

#define RTC_REGA        0x0a
#define RTC_REGB        0x0b
#define RTC_REGC        0x0c
#define RTC_REGD        0x0d

#define REGA_DIVIDER_32K        0x20
#define REGA_DIVIDER_DIS        0x70
#define REGA_PERIOD_512HZ       0x07
#define REGA_PERIOD_128HZ       0x09

#define REGB_HALT               0x80
#define REGB_IE_PERIODIC        0x40
#define REGB_IE_ALARM           0x20
#define REGB_IE_UPDATE          0x10
#define REGB_DATA_BIN           0x04
#define REGB_24HR               0x02
#define REGB_DST                0x01

#define REGC_IRQ                0x80
#define REGC_PERIODIC           0x40
#define REGC_ALARM              0x20
#define REGC_UPDATE             0x10

#define PPM_THRESHOLD   500
#define ABS(x)  ((x) < 0 ? -(x) : (x))

static uint8_t rtc_last_off = 0xff;

static uint8_t
rtc_read(uint8_t off)
{
        if (off != rtc_last_off) {
                outb(IOP_RTC_ADDR, off);
                rtc_last_off = off;
        }

        return (inb(IOP_RTC_DATA));
}

static void
rtc_write(uint8_t off, uint8_t data)
{
        if (off != rtc_last_off) {
                outb(IOP_RTC_ADDR, off);
                rtc_last_off = off;
        }

        return (outb(IOP_RTC_DATA, data));
}

static uint8_t
wait_for_flag(uint8_t mask)
{
        uint8_t regc;

        do {
                regc = rtc_read(RTC_REGC);
        } while ((regc & mask) == 0);

        return (regc);
}

/* Prepare the subordinate PIC to process interrupts from RTC */
static void
atpic_init(void)
{
        /* ICW1: INIT | ICW4 */
        outb(IOP_ATPIC_SCMD, 0x11);
        /* ICW2: vector offset (useless in context) */
        outb(IOP_ATPIC_SDATA, 0x20);
        /* ICW3: cascade info (ignored) */
        outb(IOP_ATPIC_SDATA, 0x00);
        /* ICW3: 8086_MODE | AEOI */
        outb(IOP_ATPIC_SDATA, 0x03);
        /* No masked bits */
        outb(IOP_ATPIC_SDATA, 0x00);

}

/* Poll the subordinate PIC for an IRQ */
static uint8_t
atpit_poll_for_intr(void)
{
        uint8_t val = 0;

        do {
                /* OCW3: POLL */
                outb(IOP_ATPIC_SCMD, 0x0c);

                val = inb(IOP_ATPIC_SDATA);
        } while ((val & 0x80) == 0);

        return (val);
}

static void
test_periodic_polling(void)
{
        /* Halt the RTC to prep for test of periodic timer */
        rtc_write(RTC_REGA, REGA_DIVIDER_DIS);
        rtc_write(RTC_REGB, REGB_HALT);

        /* Clear any pending event flags */
        (void) rtc_read(RTC_REGC);

        test_msg("testing periodic (polling)");

        /* Release divider to run, configuring a 512Hz periodic timer */
        rtc_write(RTC_REGA, REGA_DIVIDER_32K | REGA_PERIOD_512HZ);
        rtc_write(RTC_REGB, 0);

        /* Count periodic firings until the next time update */
        uint_t periodic_fire = 0;
        uint8_t events = 0;
        do {
                events = wait_for_flag(REGC_UPDATE | REGC_PERIODIC);

                if ((events & REGC_PERIODIC) != 0) {
                        periodic_fire++;
                }
        } while ((events & REGC_UPDATE) == 0);

        /*
         * In the 500ms between releasing the divider and the first time update,
         * we expect 256 firings of the 512Hz periodic timer.
         */
        if (periodic_fire != 256) {
                TEST_ABORT("unexpected periodic firing count at 512Hz");
        }

        /* Change the periodic timer to 128Hz */
        rtc_write(RTC_REGA, REGA_DIVIDER_32K | REGA_PERIOD_128HZ);

        /* Count periodic firings until the next time update */
        periodic_fire = 0;
        do {
                events = wait_for_flag(REGC_UPDATE | REGC_PERIODIC);

                if ((events & REGC_PERIODIC) != 0) {
                        periodic_fire++;
                }
        } while ((events & REGC_UPDATE) == 0);

        /*
         * With 1s between time updates, we expect 128 firings for the
         * reconfigured 128Hz periodic timer.
         */
        if (periodic_fire != 128) {
                TEST_ABORT("unexpected periodic firing count at 128Hz");
        }
}

static void
test_periodic_interrupts(void)
{
        /* Halt the RTC to prep for test of periodic timer */
        rtc_write(RTC_REGA, REGA_DIVIDER_DIS);
        rtc_write(RTC_REGB, REGB_HALT);

        /* Clear any pending event flags */
        (void) rtc_read(RTC_REGC);

        test_msg("testing periodic (interrupts)");

        /*
         * The RTC IRQ is routed on line 8, which corresponds to pin 0 on the
         * subordinate PIC.  Initialize it now so we can poll for interrupts.
         */
        atpic_init();

        /* Release divider to run, configuring a 512Hz periodic timer */
        rtc_write(RTC_REGA, REGA_DIVIDER_32K | REGA_PERIOD_512HZ);
        /* Enable interrupts for periodic timer and 1Hz update */
        rtc_write(RTC_REGB, REGB_IE_PERIODIC | REGB_IE_UPDATE);

        /* Count periodic firings until the next time update */
        uint_t periodic_fire = 0;
        uint8_t events = 0;
        do {
                const uint8_t irq = atpit_poll_for_intr();
                if (irq != 0x80) {
                        /*
                         * RTC is pin 0 on the subordinate PIC chip, so we
                         * expect only the interrupt-present bit set
                         */
                        TEST_ABORT("spurious interrupt on PIC");
                }

                events = rtc_read(RTC_REGC);

                /* Since we waited for the interrupt, the flag should be here */
                if ((events & REGC_IRQ) == 0) {
                        TEST_ABORT("missing IRQ flag in regc");
                }

                if ((events & REGC_PERIODIC) != 0) {
                        periodic_fire++;
                }
        } while ((events & REGC_UPDATE) == 0);

        /*
         * Like the polling periodic test, we expect 256 firings of the 512Hz
         * timer between the release of the divider and the first update.
         */
        if (periodic_fire != 256) {
                TEST_ABORT("unexpected periodic firing count at 512Hz");
        }

        /* Disable periodic configuration from RTC */
        rtc_write(RTC_REGA, REGA_DIVIDER_DIS);
        rtc_write(RTC_REGB, REGB_HALT);
}

void
start(void)
{
        /*
         * Initialize RTC to known state:
         * - rega: divider and periodic timer disabled
         * - regb: updates halted, intr disabled, 24hr time, binary fmt, no DST
         * - regc: cleared (by read)
         */
        rtc_write(RTC_REGA, REGA_DIVIDER_DIS);
        rtc_write(RTC_REGB, REGB_HALT | REGB_DATA_BIN | REGB_24HR);
        (void) rtc_read(RTC_REGC);

        /* Start at 1970 epoch */
        rtc_write(RTC_DAY, 1);
        rtc_write(RTC_MONTH, 1);
        rtc_write(RTC_YEAR, 70);
        rtc_write(RTC_CENTURY, 19);
        rtc_write(RTC_HOUR, 0);
        rtc_write(RTC_MIN, 0);
        rtc_write(RTC_SEC, 0);

        uint64_t start, end;
        /*
         * After allowing the divider to run, and enabling time updates, we
         * expect a 500ms delay until the first update to the date/time data.
         * Measure this with the TSC, even though we do not have a calibration
         * for its frequency.
         */
        rtc_write(RTC_REGA, REGA_DIVIDER_32K);
        start = rdtsc();
        rtc_write(RTC_REGB, REGB_DATA_BIN | REGB_24HR);

        if (rtc_read(RTC_REGC) != 0) {
                TEST_ABORT("unexpected flags set in regC");
        }

        test_msg("waiting for first update");
        (void) wait_for_flag(REGC_UPDATE);
        end = rdtsc();

        const uint64_t tsc_500ms = end - start;
        start = end;

        /* Expect the clock to read 00:00:01 after the first update */
        if (rtc_read(RTC_SEC) != 1) {
                TEST_ABORT("did not find 01 in seconds field");
        }

        /* Wait for another update to pass by */
        test_msg("waiting for second update");
        (void) wait_for_flag(REGC_UPDATE);
        end = rdtsc();

        const uint64_t tsc_1s = end - start;

        /* Expect the clock to read 00:00:02 after the second update */
        if (rtc_read(RTC_SEC) != 2) {
                TEST_ABORT("did not find 02 in seconds field");
        }

        /*
         * Determine ratio between the intervals which should be 500ms and
         * 1000ms long, as measured by the TSC.
         */
        int64_t ppm_delta = (int64_t)(tsc_500ms * 2 * 1000000) / tsc_1s;
        ppm_delta = ABS(ppm_delta - 1000000);

        if (ppm_delta > PPM_THRESHOLD) {
                TEST_ABORT("clock update timing outside threshold");
        }

        /* Put RTC in 12-hr, BCD-formatted mode */
        rtc_write(RTC_REGA, REGA_DIVIDER_DIS);
        rtc_write(RTC_REGB, REGB_HALT);

        /* Set time to 11:59:59, prepping for roll-over into noon */
        rtc_write(RTC_HOUR, 0x11);
        rtc_write(RTC_MIN, 0x59);
        rtc_write(RTC_SEC, 0x59);

        /* Release the clock to run again */
        rtc_write(RTC_REGA, REGA_DIVIDER_32K);
        rtc_write(RTC_REGB, 0);

        /* Wait for it to tick over */
        test_msg("waiting for noon tick-over");
        (void) wait_for_flag(REGC_UPDATE);

        if (rtc_read(RTC_SEC) != 0) {
                TEST_ABORT("invalid RTC_SEC value");
        }
        if (rtc_read(RTC_MIN) != 0) {
                TEST_ABORT("invalid RTC_MIN value");
        }
        /* Hour field should now hold 0x12 (BCD noon) | 0x80 (PM flag) */
        if (rtc_read(RTC_HOUR) != 0x92) {
                TEST_ABORT("invalid RTC_HOUR value");
        }

        test_periodic_polling();

        test_periodic_interrupts();

        /*
         * TODO - Add additional tests:
         * - alarm interrupts
         */

        /* Happy for now */
        test_result_pass();
}