Files
86Box/src/sio/sio_pc87309.c

725 lines
21 KiB
C

/*
* 86Box A hypervisor and IBM PC system emulator that specializes in
* running old operating systems and software designed for IBM
* PC systems and compatibles from 1981 through fairly recent
* system designs based on the PCI bus.
*
* This file is part of the 86Box distribution.
*
* Emulation of the NatSemi PC87309 Super I/O chip.
*
* Authors: Miran Grca, <mgrca8@gmail.com>
*
* Copyright 2020-2025 Miran Grca.
*/
#include <stdarg.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#define HAVE_STDARG_H
#include <86box/86box.h>
#include <86box/io.h>
#include <86box/timer.h>
#include <86box/device.h>
#include <86box/lpt.h>
#include <86box/machine.h>
#include <86box/mem.h>
#include <86box/nvr.h>
#include <86box/pci.h>
#include <86box/rom.h>
#include <86box/serial.h>
#include <86box/fdd.h>
#include <86box/fdc.h>
#include <86box/keyboard.h>
#include <86box/sio.h>
#include <86box/plat_fallthrough.h>
#include "cpu.h"
typedef struct pc87309_t {
uint8_t id;
uint8_t baddr;
uint8_t pm_idx;
uint8_t regs[48];
uint8_t ld_regs[256][208];
uint8_t pm[8];
uint16_t superio_base;
uint16_t pm_base;
int cur_reg;
void *kbc;
fdc_t *fdc;
serial_t *uart[2];
} pc87309_t;
enum {
LD_FDC = 0,
LD_LPT,
LD_UART2,
LD_UART1,
LD_PM,
LD_MOUSE,
LD_KBD
} pc87309_ld_t;
#define LD_MIN LD_FDC
#define LD_MAX LD_MOUSE
static void fdc_handler(pc87309_t *dev);
static void lpt1_handler(pc87309_t *dev);
static void serial_handler(pc87309_t *dev, int uart);
static void kbc_handler(pc87309_t *dev);
static void pc87309_write(uint16_t port, uint8_t val, void *priv);
static uint8_t pc87309_read(uint16_t port, void *priv);
#ifdef ENABLE_PC87309_LOG
int pc87309_do_log = ENABLE_PC87309_LOG;
static void
pc87309_log(const char *fmt, ...)
{
va_list ap;
if (pc87309_do_log) {
va_start(ap, fmt);
pclog_ex(fmt, ap);
va_end(ap);
}
}
#else
# define pc87309_log(fmt, ...)
#endif
static void
pc87309_pm_write(uint16_t port, uint8_t val, void *priv)
{
pc87309_t *dev = (pc87309_t *) priv;
if (port & 1)
dev->pm[dev->pm_idx] = val;
else {
dev->pm_idx = val & 0x07;
switch (dev->pm_idx) {
case 0x00:
fdc_handler(dev);
lpt1_handler(dev);
serial_handler(dev, 1);
serial_handler(dev, 0);
break;
default:
break;
}
}
}
uint8_t
pc87309_pm_read(uint16_t port, void *priv)
{
const pc87309_t *dev = (pc87309_t *) priv;
if (port & 1)
return dev->pm[dev->pm_idx];
else
return dev->pm_idx;
}
static void
pc87309_pm_remove(pc87309_t *dev)
{
if (dev->pm_base != 0xffff) {
io_removehandler(dev->pm_base, 0x0008,
pc87309_pm_read, NULL, NULL, pc87309_pm_write, NULL, NULL, dev);
dev->pm_base = 0xffff;
}
}
static void
pc87309_pm_init(pc87309_t *dev, uint16_t addr)
{
dev->pm_base = addr;
io_sethandler(dev->pm_base, 0x0008,
pc87309_pm_read, NULL, NULL, pc87309_pm_write, NULL, NULL, dev);
}
static void
kbc_handler(pc87309_t *dev)
{
uint8_t active = (dev->ld_regs[LD_KBD][0x00] & 0x01) &&
(dev->pm[0x00] & 0x01);
uint8_t active_2 = dev->ld_regs[LD_MOUSE][0x00] & 0x01;
uint8_t irq = (dev->ld_regs[LD_KBD][0x40] & 0x0f);
uint8_t irq_2 = (dev->ld_regs[LD_MOUSE][0x40] & 0x0f);
uint16_t addr = (dev->ld_regs[LD_KBD][0x30] << 8) |
dev->ld_regs[LD_KBD][0x31];
uint16_t addr_2 = (dev->ld_regs[LD_KBD][0x32] << 8) |
dev->ld_regs[LD_KBD][0x33];
pc87309_log("%02X, %02X, %02X, %02X, %04X, %04X\n",
active, active_2, irq, irq_2, addr, addr_2);
if (addr <= 0xfff8) {
pc87309_log("Enabling KBC #1 on %04X...\n", addr);
kbc_at_port_handler(0, active, addr, dev->kbc);
}
if (addr_2 <= 0xfff8) {
pc87309_log("Enabling KBC #2 on %04X...\n", addr_2);
kbc_at_port_handler(1, active, addr_2, dev->kbc);
}
kbc_at_set_irq(0, active ? irq : 0xffff, dev->kbc);
kbc_at_set_irq(1, (active && active_2) ? irq_2 : 0xffff, dev->kbc);
}
static void
fdc_handler(pc87309_t *dev)
{
fdc_remove(dev->fdc);
uint8_t active = (dev->ld_regs[LD_FDC][0x00] & 0x01) &&
(dev->pm[0x00] & 0x08);
uint8_t irq = (dev->ld_regs[LD_FDC][0x40] & 0x0f);
uint16_t addr = ((dev->ld_regs[LD_FDC][0x30] << 8) |
dev->ld_regs[LD_FDC][0x31]) & 0xfff8;
if (active && (addr <= 0xfff8)) {
pc87309_log("Enabling FDC on %04X, IRQ %i...\n", addr, irq);
fdc_set_base(dev->fdc, addr);
fdc_set_irq(dev->fdc, irq);
}
}
static void
lpt1_handler(pc87309_t *dev)
{
uint8_t active = (dev->ld_regs[LD_LPT][0x00] & 0x01) &&
(dev->pm[0x00] & 0x10);
uint8_t irq = (dev->ld_regs[LD_LPT][0x40] & 0x0f);
uint16_t addr = (dev->ld_regs[LD_LPT][0x30] << 8) |
dev->ld_regs[LD_LPT][0x31];
if (active && (addr <= 0xfffc)) {
pc87309_log("Enabling LPT1 on %04X...\n", addr);
lpt1_setup(addr);
} else
lpt1_setup(0xffff);
lpt1_irq(irq);
}
static void
serial_handler(pc87309_t *dev, int uart)
{
serial_remove(dev->uart[uart]);
uint8_t active = (dev->ld_regs[LD_UART1 - uart][0x00] & 0x01) &&
(dev->pm[0x00] & (1 << (6 - uart)));
uint8_t irq = (dev->ld_regs[LD_UART1 - uart][0x40] & 0x0f);
uint16_t addr = (dev->ld_regs[LD_UART1 - uart][0x30] << 8) |
dev->ld_regs[LD_UART1 - uart][0x31];
if (active && (addr <= 0xfff8)) {
pc87309_log("Enabling COM%i on %04X...\n", uart + 1, addr);
serial_setup(dev->uart[uart], addr, irq);
} else
serial_setup(dev->uart[uart], 0x0000, irq);
}
static void
pm_handler(pc87309_t *dev)
{
pc87309_pm_remove(dev);
uint8_t active = (dev->ld_regs[LD_PM][0x00] & 0x01);
uint16_t addr = (dev->ld_regs[LD_PM][0x30] << 8) |
dev->ld_regs[LD_PM][0x31];
if (active) {
pc87309_log("Enabling power management on %04X...\n", addr);
pc87309_pm_init(dev, addr);
}
}
static void
superio_handler(pc87309_t *dev)
{
if (dev->superio_base != 0x0000)
io_removehandler(dev->superio_base, 0x0002,
pc87309_read, NULL, NULL,
pc87309_write, NULL, NULL, dev);
switch (dev->regs[0x21] & 0x0b) {
default:
dev->superio_base = 0x0000;
break;
case 0x02:
case 0x08: case 0x0a:
dev->superio_base = 0x015c;
break;
case 0x03:
case 0x09: case 0x0b:
dev->superio_base = 0x002e;
break;
}
if (dev->superio_base != 0x0000) {
pc87309_log("Enabling Super I/O on %04X...\n", dev->superio_base);
io_sethandler(dev->superio_base, 0x0002,
pc87309_read, NULL, NULL,
pc87309_write, NULL, NULL, dev);
}
}
static void
pc87309_write(uint16_t port, uint8_t val, void *priv)
{
pc87309_t *dev = (pc87309_t *) priv;
uint8_t ld = dev->regs[0x07];
uint8_t reg = dev->cur_reg - 0x30;
uint8_t index = (port & 1) ? 0 : 1;
uint8_t old = dev->regs[dev->cur_reg];
if (index) {
dev->cur_reg = val;
return;
} else {
#ifdef ENABLE_PC87309_LOG
if (dev->cur_reg >= 0x30)
pc87309_log("[%04X:%08X] [W] (%04X) %02X:%02X = %02X\n",
CS, cpu_state.pc, port, ld, dev->cur_reg, val);
else
pc87309_log("[%04X:%08X] [W] (%04X) %02X = %02X\n",
CS, cpu_state.pc, port, dev->cur_reg, val);
#endif
switch (dev->cur_reg) {
case 0x00:
case 0x02: case 0x03:
case 0x06: case 0x07:
dev->regs[dev->cur_reg] = val;
break;
case 0x21:
dev->regs[dev->cur_reg] = val;
fdc_toggle_flag(dev->fdc, FDC_FLAG_PS2_MCA, !(val & 0x04));
superio_handler(dev);
break;
case 0x22:
dev->regs[dev->cur_reg] = val;
break;
default:
if (dev->cur_reg >= 0x30)
old = dev->ld_regs[ld][reg];
break;
}
}
switch (dev->cur_reg) {
case 0x30:
switch (ld) {
default:
break;
case LD_KBD: case LD_MOUSE:
dev->ld_regs[ld][reg] = val;
kbc_handler(dev);
break;
case LD_FDC:
dev->ld_regs[ld][reg] = val;
fdc_handler(dev);
break;
case LD_LPT:
dev->ld_regs[ld][reg] = val;
lpt1_handler(dev);
break;
case LD_UART2:
dev->ld_regs[ld][reg] = val;
serial_handler(dev, 1);
break;
case LD_UART1:
dev->ld_regs[ld][reg] = val;
serial_handler(dev, 0);
break;
case LD_PM:
dev->ld_regs[ld][reg] = val;
pm_handler(dev);
break;
}
break;
/* I/O Range Check. */
case 0x31:
switch (ld) {
default:
break;
case LD_MIN ... LD_MAX:
if (ld != LD_MOUSE)
dev->ld_regs[ld][reg] = val;
break;
}
break;
/* Base Address 0 MSB. */
case 0x60:
switch (ld) {
default:
break;
case LD_KBD:
dev->ld_regs[ld][reg] = val;
kbc_handler(dev);
break;
case LD_FDC:
dev->ld_regs[ld][reg] = val;
fdc_handler(dev);
break;
case LD_LPT:
dev->ld_regs[ld][reg] = (old & 0xfc) | (val & 0x03);
lpt1_handler(dev);
break;
case LD_UART2:
dev->ld_regs[ld][reg] = val;
serial_handler(dev, 1);
break;
case LD_UART1:
dev->ld_regs[ld][reg] = val;
serial_handler(dev, 0);
break;
case LD_PM:
dev->ld_regs[ld][reg] = val;
pm_handler(dev);
break;
}
break;
/* Base Address 0 LSB. */
case 0x61:
switch (ld) {
default:
break;
case LD_KBD:
dev->ld_regs[ld][reg] = (old & 0x04) | (val & 0xfb);
kbc_handler(dev);
break;
case LD_FDC:
dev->ld_regs[ld][reg] = (old & 0x07) | (val & 0xf8);
fdc_handler(dev);
break;
case LD_LPT:
dev->ld_regs[ld][reg] = (old & 0x03) | (val & 0xfc);
lpt1_handler(dev);
break;
case LD_UART2:
dev->ld_regs[ld][reg] = (old & 0x07) | (val & 0xf8);
serial_handler(dev, 1);
break;
case LD_UART1:
dev->ld_regs[ld][reg] = (old & 0x07) | (val & 0xf8);
serial_handler(dev, 0);
break;
case LD_PM:
dev->ld_regs[ld][reg] = (old & 0x01) | (val & 0xfe);
pm_handler(dev);
break;
}
break;
/* Base Address 1 MSB (undocumented for Logical Device 7). */
case 0x62:
switch (ld) {
default:
break;
case LD_KBD:
dev->ld_regs[ld][reg] = val;
kbc_handler(dev);
break;
}
break;
/* Base Address 1 LSB (undocumented for Logical Device 7). */
case 0x63:
switch (ld) {
default:
break;
case LD_KBD:
dev->ld_regs[ld][reg] = (old & 0x04) | (val & 0xfb);
kbc_handler(dev);
break;
}
break;
/* Interrupt Select. */
case 0x70:
switch (ld) {
default:
break;
case LD_KBD: case LD_MOUSE:
dev->ld_regs[ld][reg] = val;
kbc_handler(dev);
break;
case LD_FDC:
dev->ld_regs[ld][reg] = val;
fdc_handler(dev);
break;
case LD_LPT:
dev->ld_regs[ld][reg] = val;
lpt1_handler(dev);
break;
case LD_UART2:
dev->ld_regs[ld][reg] = val;
serial_handler(dev, 1);
break;
case LD_UART1:
dev->ld_regs[ld][reg] = val;
serial_handler(dev, 0);
break;
}
break;
/* Interrupt Type. */
case 0x71:
switch (ld) {
default:
break;
case LD_MIN ... LD_MAX:
if ((ld == LD_KBD) || (ld == LD_MOUSE))
dev->ld_regs[ld][reg] = (old & 0xfc) | (val & 0x03);
else
dev->ld_regs[ld][reg] = (old & 0xfd) | (val & 0x02);
break;
}
break;
/* DMA Channel Select 0. */
case 0x74:
switch (ld) {
default:
break;
case LD_FDC:
dev->ld_regs[ld][reg] = val;
fdc_handler(dev);
break;
case LD_LPT:
dev->ld_regs[ld][reg] = val;
lpt1_handler(dev);
break;
case LD_UART2:
dev->ld_regs[ld][reg] = val;
break;
}
break;
/* DMA Channel Select 1. */
case 0x75:
switch (ld) {
default:
break;
case LD_UART2:
dev->ld_regs[ld][reg] = val;
break;
}
break;
/* Configuration Register 0. */
case 0xf0:
switch (ld) {
default:
break;
case LD_KBD:
dev->ld_regs[ld][reg] = val;
break;
case LD_FDC:
dev->ld_regs[ld][reg] = val;
fdc_update_densel_polarity(dev->fdc, (val & 0x20) ? 1 : 0);
fdc_update_enh_mode(dev->fdc, (val & 0x40) ? 1 : 0);
break;
case LD_LPT:
dev->ld_regs[ld][reg] = val;
lpt1_handler(dev);
break;
case LD_UART2: case LD_UART1:
dev->ld_regs[ld][reg] = val;
break;
}
break;
/* Configuration Register 1. */
case 0xf1:
switch (ld) {
default:
break;
case LD_FDC:
dev->ld_regs[ld][reg] = val;
break;
}
break;
default:
break;
}
}
static uint8_t
pc87309_read(uint16_t port, void *priv)
{
const pc87309_t *dev = (pc87309_t *) priv;
uint8_t ld = dev->regs[0x07];
uint8_t reg = dev->cur_reg - 0x30;
uint8_t index = (port & 1) ? 0 : 1;
uint8_t ret = 0xff;
if (index)
ret = dev->cur_reg;
else {
if (dev->cur_reg >= 0x30)
ret = dev->ld_regs[ld][reg];
/* Write-only registers. */
else if ((dev->cur_reg == 0x00) ||
(dev->cur_reg == 0x02) || (dev->cur_reg == 0x03))
ret = 0x00;
else
ret = dev->regs[dev->cur_reg];
#ifdef ENABLE_PC87309_LOG
if (dev->cur_reg >= 0x30)
pc87309_log("[%04X:%08X] [R] (%04X) %02X:%02X = %02X\n",
CS, cpu_state.pc, port, ld, dev->cur_reg, ret);
else
pc87309_log("[%04X:%08X] [R] (%04X) %02X = %02X\n",
CS, cpu_state.pc, port, dev->cur_reg, ret);
#endif
}
return ret;
}
void
pc87309_reset(void *priv)
{
pc87309_t *dev = (pc87309_t *) priv;
memset(dev->regs, 0x00, 0x30);
for (uint16_t i = 0; i < 256; i++)
memset(dev->ld_regs[i], 0x00, 0xd0);
memset(dev->pm, 0x00, 0x08);
dev->regs[0x20] = dev->id;
dev->regs[0x21] = 0x04 | dev->baddr;
dev->ld_regs[LD_KBD ][0x00] = 0x01;
dev->ld_regs[LD_KBD ][0x31] = 0x60;
dev->ld_regs[LD_KBD ][0x33] = 0x64;
dev->ld_regs[LD_KBD ][0x40] = 0x01;
dev->ld_regs[LD_KBD ][0x41] = 0x02;
dev->ld_regs[LD_KBD ][0x44] = 0x04;
dev->ld_regs[LD_KBD ][0x45] = 0x04;
dev->ld_regs[LD_KBD ][0xc0] = 0x40;
dev->ld_regs[LD_MOUSE][0x40] = 0x0c;
dev->ld_regs[LD_MOUSE][0x41] = 0x02;
dev->ld_regs[LD_MOUSE][0x44] = 0x04;
dev->ld_regs[LD_MOUSE][0x45] = 0x04;
dev->ld_regs[LD_FDC ][0x01] = 0x01;
dev->ld_regs[LD_FDC ][0x30] = 0x03;
dev->ld_regs[LD_FDC ][0x31] = 0xf0;
dev->ld_regs[LD_FDC ][0x32] = 0x03;
dev->ld_regs[LD_FDC ][0x33] = 0xf7;
dev->ld_regs[LD_FDC ][0x40] = 0x06;
dev->ld_regs[LD_FDC ][0x41] = 0x03;
dev->ld_regs[LD_FDC ][0x44] = 0x02;
dev->ld_regs[LD_FDC ][0x45] = 0x04;
dev->ld_regs[LD_FDC ][0xc0] = 0x02;
dev->ld_regs[LD_LPT ][0x30] = 0x02;
dev->ld_regs[LD_LPT ][0x31] = 0x78;
dev->ld_regs[LD_LPT ][0x40] = 0x07;
dev->ld_regs[LD_LPT ][0x44] = 0x04;
dev->ld_regs[LD_LPT ][0x45] = 0x04;
dev->ld_regs[LD_LPT ][0xc0] = 0xf2;
dev->ld_regs[LD_UART2][0x30] = 0x02;
dev->ld_regs[LD_UART2][0x31] = 0xf8;
dev->ld_regs[LD_UART2][0x40] = 0x03;
dev->ld_regs[LD_UART2][0x41] = 0x03;
dev->ld_regs[LD_UART2][0x44] = 0x04;
dev->ld_regs[LD_UART2][0x45] = 0x04;
dev->ld_regs[LD_UART2][0xc0] = 0x02;
dev->ld_regs[LD_UART1][0x30] = 0x03;
dev->ld_regs[LD_UART1][0x31] = 0xf8;
dev->ld_regs[LD_UART1][0x40] = 0x04;
dev->ld_regs[LD_UART1][0x41] = 0x03;
dev->ld_regs[LD_UART1][0x44] = 0x04;
dev->ld_regs[LD_UART1][0x45] = 0x04;
dev->ld_regs[LD_UART1][0xc0] = 0x02;
dev->ld_regs[LD_PM ][0x44] = 0x04;
dev->ld_regs[LD_PM ][0x45] = 0x04;
dev->pm[0] = 0x79;
dev->pm[4] = 0x0e;
dev->pm_base = 0xffff;
/*
0 = 360 rpm @ 500 kbps for 3.5"
1 = Default, 300 rpm @ 500, 300, 250, 1000 kbps for 3.5"
*/
fdc_toggle_flag(dev->fdc, FDC_FLAG_PS2_MCA, 0);
fdc_reset(dev->fdc);
kbc_handler(dev);
fdc_handler(dev);
lpt1_handler(dev);
serial_handler(dev, 0);
serial_handler(dev, 1);
pm_handler(dev);
superio_handler(dev);
}
static void
pc87309_close(void *priv)
{
pc87309_t *dev = (pc87309_t *) priv;
free(dev);
}
static void *
pc87309_init(const device_t *info)
{
pc87309_t *dev = (pc87309_t *) calloc(1, sizeof(pc87309_t));
dev->id = info->local & 0xff;
dev->fdc = device_add(&fdc_at_nsc_device);
dev->uart[0] = device_add_inst(&ns16550_device, 1);
dev->uart[1] = device_add_inst(&ns16550_device, 2);
switch (info->local & PCX730X_KBC) {
default:
case PCX730X_AMI:
dev->kbc = device_add(&keyboard_ps2_intel_ami_pci_device);
break;
/* Optiplex! */
case PCX730X_PHOENIX_42:
dev->kbc = device_add(&keyboard_ps2_phoenix_device);
break;
case PCX730X_PHOENIX_42I:
dev->kbc = device_add(&keyboard_ps2_phoenix_pci_device);
break;
}
if (info->local & PCX730X_15C)
dev->baddr = 0x0a;
else
dev->baddr = 0x0b;
pc87309_reset(dev);
return dev;
}
const device_t pc87309_device = {
.name = "National Semiconductor PC87309 Super I/O",
.internal_name = "pc87309",
.flags = 0,
.local = 0,
.init = pc87309_init,
.close = pc87309_close,
.reset = pc87309_reset,
.available = NULL,
.speed_changed = NULL,
.force_redraw = NULL,
.config = NULL
};