aboutsummaryrefslogtreecommitdiff
path: root/hw/misc/mac_via.c
diff options
context:
space:
mode:
Diffstat (limited to 'hw/misc/mac_via.c')
-rw-r--r--hw/misc/mac_via.c234
1 files changed, 233 insertions, 1 deletions
diff --git a/hw/misc/mac_via.c b/hw/misc/mac_via.c
index f84cc68..b6206ef 100644
--- a/hw/misc/mac_via.c
+++ b/hw/misc/mac_via.c
@@ -16,6 +16,7 @@
*/
#include "qemu/osdep.h"
+#include "exec/address-spaces.h"
#include "migration/vmstate.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
@@ -114,6 +115,9 @@
#define VIA1A_CPUID1 0x04 /* CPU id bit 0 on RBV, others */
#define VIA1A_CPUID2 0x10 /* CPU id bit 0 on RBV, others */
#define VIA1A_CPUID3 0x40 /* CPU id bit 0 on RBV, others */
+#define VIA1A_CPUID_MASK (VIA1A_CPUID0 | VIA1A_CPUID1 | \
+ VIA1A_CPUID2 | VIA1A_CPUID3)
+#define VIA1A_CPUID_Q800 (VIA1A_CPUID0 | VIA1A_CPUID2)
/*
* Info on VIA1B is from Macintosh Family Hardware & MkLinux.
@@ -698,6 +702,12 @@ static void adb_via_send(MOS6522Q800VIA1State *v1s, int state, uint8_t data)
break;
case ADB_STATE_IDLE:
+ ms->b |= VIA1B_vADBInt;
+ adb_autopoll_unblock(adb_bus);
+
+ trace_via1_adb_send("IDLE", data,
+ (ms->b & VIA1B_vADBInt) ? "+" : "-");
+
return;
}
@@ -865,6 +875,159 @@ static void via1_auxmode_update(MOS6522Q800VIA1State *v1s)
if (irq != oldirq) {
trace_via1_auxmode(irq);
qemu_set_irq(v1s->auxmode_irq, irq);
+
+ /*
+ * Clear the ADB interrupt. MacOS can leave VIA1B_vADBInt asserted
+ * (low) if a poll sequence doesn't complete before NetBSD disables
+ * interrupts upon boot. Fortunately NetBSD switches to the so-called
+ * "A/UX" interrupt mode after it initialises, so we can use this as
+ * a convenient place to clear the ADB interrupt for now.
+ */
+ s->b |= VIA1B_vADBInt;
+ }
+}
+
+/*
+ * Addresses and real values for TimeDBRA/TimeSCCB to allow timer calibration
+ * to succeed (NOTE: both values have been multiplied by 3 to cope with the
+ * speed of QEMU execution on a modern host
+ */
+#define MACOS_TIMEDBRA 0xd00
+#define MACOS_TIMESCCB 0xd02
+
+#define MACOS_TIMEDBRA_VALUE (0x2a00 * 3)
+#define MACOS_TIMESCCB_VALUE (0x079d * 3)
+
+static bool via1_is_toolbox_timer_calibrated(void)
+{
+ /*
+ * Indicate whether the MacOS toolbox has been calibrated by checking
+ * for the value of our magic constants
+ */
+ uint16_t timedbra = lduw_be_phys(&address_space_memory, MACOS_TIMEDBRA);
+ uint16_t timesccdb = lduw_be_phys(&address_space_memory, MACOS_TIMESCCB);
+
+ return (timedbra == MACOS_TIMEDBRA_VALUE &&
+ timesccdb == MACOS_TIMESCCB_VALUE);
+}
+
+static void via1_timer_calibration_hack(MOS6522Q800VIA1State *v1s, int addr,
+ uint64_t val, int size)
+{
+ /*
+ * Work around timer calibration to ensure we that we have non-zero and
+ * known good values for TIMEDRBA and TIMESCCDB.
+ *
+ * This works by attempting to detect the reset and calibration sequence
+ * of writes to VIA1
+ */
+ int old_timer_hack_state = v1s->timer_hack_state;
+
+ switch (v1s->timer_hack_state) {
+ case 0:
+ if (addr == VIA_REG_PCR && val == 0x22) {
+ /* VIA_REG_PCR: configure VIA1 edge triggering */
+ v1s->timer_hack_state = 1;
+ }
+ break;
+ case 1:
+ if (addr == VIA_REG_T2CL && val == 0xc) {
+ /* VIA_REG_T2CL: low byte of 1ms counter */
+ if (!via1_is_toolbox_timer_calibrated()) {
+ v1s->timer_hack_state = 2;
+ } else {
+ v1s->timer_hack_state = 0;
+ }
+ }
+ break;
+ case 2:
+ if (addr == VIA_REG_T2CH && val == 0x3) {
+ /*
+ * VIA_REG_T2CH: high byte of 1ms counter (very likely at the
+ * start of SETUPTIMEK)
+ */
+ if (!via1_is_toolbox_timer_calibrated()) {
+ v1s->timer_hack_state = 3;
+ } else {
+ v1s->timer_hack_state = 0;
+ }
+ }
+ break;
+ case 3:
+ if (addr == VIA_REG_IER && val == 0x20) {
+ /*
+ * VIA_REG_IER: update at end of SETUPTIMEK
+ *
+ * Timer calibration has finished: unfortunately the values in
+ * TIMEDBRA (0xd00) and TIMESCCDB (0xd02) are so far out they
+ * cause divide by zero errors.
+ *
+ * Update them with values obtained from a real Q800 but with
+ * a x3 scaling factor which seems to work well
+ */
+ stw_be_phys(&address_space_memory, MACOS_TIMEDBRA,
+ MACOS_TIMEDBRA_VALUE);
+ stw_be_phys(&address_space_memory, MACOS_TIMESCCB,
+ MACOS_TIMESCCB_VALUE);
+
+ v1s->timer_hack_state = 4;
+ }
+ break;
+ case 4:
+ /*
+ * This is the normal post-calibration timer state: we should
+ * generally remain here unless we detect the A/UX calibration
+ * loop, or a write to VIA_REG_PCR suggesting a reset
+ */
+ if (addr == VIA_REG_PCR && val == 0x22) {
+ /* Looks like there has been a reset? */
+ v1s->timer_hack_state = 1;
+ }
+
+ if (addr == VIA_REG_T2CL && val == 0xf0) {
+ /* VIA_REG_T2CL: low byte of counter (A/UX) */
+ v1s->timer_hack_state = 5;
+ }
+ break;
+ case 5:
+ if (addr == VIA_REG_T2CH && val == 0x3c) {
+ /*
+ * VIA_REG_T2CH: high byte of counter (A/UX). We are now extremely
+ * likely to be in the A/UX timer calibration routine, so move to
+ * the next state where we enable the calibration hack.
+ */
+ v1s->timer_hack_state = 6;
+ } else if ((addr == VIA_REG_IER && val == 0x20) ||
+ addr == VIA_REG_T2CH) {
+ /* We're doing something else with the timer, not calibration */
+ v1s->timer_hack_state = 0;
+ }
+ break;
+ case 6:
+ if ((addr == VIA_REG_IER && val == 0x20) || addr == VIA_REG_T2CH) {
+ /* End of A/UX timer calibration routine, or another write */
+ v1s->timer_hack_state = 7;
+ } else {
+ v1s->timer_hack_state = 0;
+ }
+ break;
+ case 7:
+ /*
+ * This is the normal post-calibration timer state once both the
+ * MacOS toolbox and A/UX have been calibrated, until we see a write
+ * to VIA_REG_PCR to suggest a reset
+ */
+ if (addr == VIA_REG_PCR && val == 0x22) {
+ /* Looks like there has been a reset? */
+ v1s->timer_hack_state = 1;
+ }
+ break;
+ default:
+ g_assert_not_reached();
+ }
+
+ if (old_timer_hack_state != v1s->timer_hack_state) {
+ trace_via1_timer_hack_state(v1s->timer_hack_state);
}
}
@@ -872,9 +1035,36 @@ static uint64_t mos6522_q800_via1_read(void *opaque, hwaddr addr, unsigned size)
{
MOS6522Q800VIA1State *s = MOS6522_Q800_VIA1(opaque);
MOS6522State *ms = MOS6522(s);
+ uint64_t ret;
+ int64_t now;
addr = (addr >> 9) & 0xf;
- return mos6522_read(ms, addr, size);
+ ret = mos6522_read(ms, addr, size);
+ switch (addr) {
+ case VIA_REG_A:
+ case VIA_REG_ANH:
+ /* Quadra 800 Id */
+ ret = (ret & ~VIA1A_CPUID_MASK) | VIA1A_CPUID_Q800;
+ break;
+ case VIA_REG_T2CH:
+ if (s->timer_hack_state == 6) {
+ /*
+ * The A/UX timer calibration loop runs continuously until 2
+ * consecutive iterations differ by at least 0x492 timer ticks.
+ * Modern hosts execute the timer calibration loop so fast that
+ * this situation never occurs causing a hang on boot. Use a
+ * similar method to Shoebill which is to randomly add 0x500 to
+ * the T2 counter value during calibration to enable it to
+ * eventually succeed.
+ */
+ now = qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL);
+ if (now & 1) {
+ ret += 0x5;
+ }
+ }
+ break;
+ }
+ return ret;
}
static void mos6522_q800_via1_write(void *opaque, hwaddr addr, uint64_t val,
@@ -882,8 +1072,13 @@ static void mos6522_q800_via1_write(void *opaque, hwaddr addr, uint64_t val,
{
MOS6522Q800VIA1State *v1s = MOS6522_Q800_VIA1(opaque);
MOS6522State *ms = MOS6522(v1s);
+ int oldstate, state;
+ int oldsr = ms->sr;
addr = (addr >> 9) & 0xf;
+
+ via1_timer_calibration_hack(v1s, addr, val, size);
+
mos6522_write(ms, addr, val, size);
switch (addr) {
@@ -894,6 +1089,38 @@ static void mos6522_q800_via1_write(void *opaque, hwaddr addr, uint64_t val,
v1s->last_b = ms->b;
break;
+
+ case VIA_REG_SR:
+ {
+ /*
+ * NetBSD assumes it can send its first ADB command after sending
+ * the ADB_BUSRESET command in ADB_STATE_NEW without changing the
+ * state back to ADB_STATE_IDLE first as detailed in the ADB
+ * protocol.
+ *
+ * Add a workaround to detect this condition at the start of ADB
+ * enumeration and send the next command written to SR after a
+ * ADB_BUSRESET onto the bus regardless, even if we don't detect a
+ * state transition to ADB_STATE_NEW.
+ *
+ * Note that in my tests the NetBSD state machine takes one ADB
+ * operation to recover which means the probe for an ADB device at
+ * address 1 always fails. However since the first device is at
+ * address 2 then this will work fine, without having to come up
+ * with a more complicated and invasive solution.
+ */
+ oldstate = (v1s->last_b & VIA1B_vADB_StateMask) >>
+ VIA1B_vADB_StateShift;
+ state = (ms->b & VIA1B_vADB_StateMask) >> VIA1B_vADB_StateShift;
+
+ if (oldstate == ADB_STATE_NEW && state == ADB_STATE_NEW &&
+ (ms->acr & VIA1ACR_vShiftOut) &&
+ oldsr == 0 /* ADB_BUSRESET */) {
+ trace_via1_adb_netbsd_enum_hack();
+ adb_via_send(v1s, state, ms->sr);
+ }
+ }
+ break;
}
}
@@ -996,6 +1223,9 @@ static void mos6522_q800_via1_reset_hold(Object *obj)
adb_set_autopoll_enabled(adb_bus, true);
v1s->cmd = REG_EMPTY;
v1s->alt = REG_EMPTY;
+
+ /* Timer calibration hack */
+ v1s->timer_hack_state = 0;
}
static void mos6522_q800_via1_realize(DeviceState *dev, Error **errp)
@@ -1088,6 +1318,8 @@ static const VMStateDescription vmstate_q800_via1 = {
VMSTATE_INT64(next_second, MOS6522Q800VIA1State),
VMSTATE_TIMER_PTR(sixty_hz_timer, MOS6522Q800VIA1State),
VMSTATE_INT64(next_sixty_hz, MOS6522Q800VIA1State),
+ /* Timer hack */
+ VMSTATE_INT32(timer_hack_state, MOS6522Q800VIA1State),
VMSTATE_END_OF_LIST()
}
};