/*
 * 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.
 *
 *          Driver for the IBM PC-XT Fixed Disk controller.
 *
 *          The original controller shipped by IBM was made by Xebec, and
 *          several variations had been made:
 *
 *          #1 Original, single drive (ST412), 10MB, 2 heads.
 *          #2 Update, single drive (ST412) but with option for a
 *             switch block that can be used to 'set' the actual
 *             drive type. Four switches are defined, where switches
 *             1 and 2 define drive0, and switches 3 and 4 drive1.
 *
 *               0  ON  ON  306  2  0
 *               1  ON  OFF 375  8  0
 *               2  OFF ON  306  6  256
 *               3  OFF OFF 306  4  0
 *
 *             The latter option is the default, in use on boards
 *             without the switch block option.
 *
 *          #3 Another updated board, mostly to accomodate the new
 *             20MB disk now being shipped. The controller can have
 *             up to 2 drives, the type of which is set using the
 *             switch block:
 *
 *               SW1 SW2    CYLS HD SPT WPC
 *               0  ON  ON  306  4  17  0
 *               1  ON  OFF 612  4  17  0   (type 16)
 *               2  OFF ON  615  4  17  300 (Seagate ST-225, 2)
 *               3  OFF OFF 306  8  17  128 (IBM WD25, 13)
 *
 *             Examples of #3 are IBM/Xebec, WD10004A-WX1 and ST11R.
 *
 *          Since all controllers (including the ones made by DTC) use
 *          (mostly) the same API, we keep them all in this module.
 *
 * Authors: Fred N. van Kempen, <decwiz@yahoo.com>
 *          Sarah Walker, <https://pcem-emulator.co.uk/>
 *
 *          Copyright 2017-2019 Fred N. van Kempen.
 *          Copyright 2008-2019 Sarah Walker.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free  Software  Foundation; either  version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is  distributed in the hope that it will be useful, but
 * WITHOUT   ANY  WARRANTY;  without  even   the  implied  warranty  of
 * MERCHANTABILITY  or FITNESS  FOR A PARTICULAR  PURPOSE. See  the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the:
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place - Suite 330
 *   Boston, MA 02111-1307
 *   USA.
 */
#include <stdarg.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <wchar.h>
#define HAVE_STDARG_H
#include <86box/86box.h>
#include <86box/io.h>
#include <86box/mem.h>
#include <86box/rom.h>
#include <86box/timer.h>
#include <86box/device.h>
#include <86box/ui.h>
#include <86box/plat.h>
#include <86box/dma.h>
#include <86box/pic.h>
#include <86box/hdc.h>
#include <86box/hdd.h>
#include <86box/hdd_audio.h>

#define ST506_XT_TYPE_XEBEC              0
#define ST506_XT_TYPE_WDXT_GEN           1
#define ST506_XT_TYPE_DTC_5150X          2
#define ST506_XT_TYPE_ST11M              11
#define ST506_XT_TYPE_ST11R              12
#define ST506_XT_TYPE_WD1002A_WX1        21
#define ST506_XT_TYPE_WD1002A_WX1_NOBIOS 22
#define ST506_XT_TYPE_WD1002A_27X        23
#define ST506_XT_TYPE_WD1004A_WX1        24
#define ST506_XT_TYPE_WD1004_27X         25
#define ST506_XT_TYPE_WD1004A_27X        26
#define ST506_XT_TYPE_VICTOR_V86P        27
#define ST506_XT_TYPE_TOSHIBA_T1200      28

#define XEBEC_BIOS_FILE                  "roms/hdd/st506/ibm_xebec_62x0822_1985.bin"
#define WDXT_GEN_BIOS_FILE               "roms/hdd/st506/wdxt-gen/62-000128-000.bin"
#define DTC_BIOS_FILE                    "roms/hdd/st506/dtc_cxd21a.bin"
#define ST11_BIOS_FILE_OLD               "roms/hdd/st506/st11_bios_vers_1.7.bin"
#define ST11_BIOS_FILE_NEW               "roms/hdd/st506/st11_bios_vers_2.0.bin"
#define WD1002A_WX1_BIOS_FILE            "roms/hdd/st506/wd1002a_wx1-62-000094-032.bin"
#define WD1004A_WX1_BIOS_FILE            "roms/hdd/st506/western_digital_WD1004A-27X.bin"
/* SuperBIOS was for both the WX1 and 27X, users jumpers readout to determine
   if to use 26 sectors per track, 26 -> 17 sectors per track translation, or
   17 sectors per track. */
#define WD1002A_27X_BIOS_FILE "roms/hdd/st506/wd1002a_27x-62-000094-032.bin"
#define WD1004_27X_BIOS_FILE  "roms/hdd/st506/western_digital_WD1004A-27X.bin"
#define WD1004A_27X_BIOS_FILE "roms/hdd/st506/western_digital_WD1004A-27X.bin"
#define VICTOR_V86P_BIOS_FILE "roms/machines/v86p/2793VG.10010688.rom"

#define ST506_TIME            (250 * TIMER_USEC)
#define ST506_TIME_MS         (1000 * TIMER_USEC)

/* MFM and RLL use different sectors/track. */
#define SECTOR_SIZE 512
#define MFM_SECTORS 17
#define RLL_SECTORS 26

/* Status register. */
#define STAT_REQ 0x01 /* controller ready */
#define STAT_IO  0x02 /* input, data to host */
#define STAT_CD  0x04 /* command mode (else data) */
#define STAT_BSY 0x08 /* controller is busy */
#define STAT_DRQ 0x10 /* controller needs DMA */
#define STAT_IRQ 0x20 /* interrupt, we have info */

/* DMA/IRQ enable register. */
#define DMA_ENA 0x01 /* DMA operation enabled */
#define IRQ_ENA 0x02 /* IRQ operation enabled */

/* Error codes in sense report. */
#define ERR_BV              0x80
#define ERR_TYPE_MASK       0x30
#define ERR_TYPE_SHIFT      4
#define ERR_TYPE_DRIVE      0x00
#define ERR_TYPE_CONTROLLER 0x01
#define ERR_TYPE_COMMAND    0x02
#define ERR_TYPE_MISC       0x03

/* No, um, errors.. */
#define ERR_NONE 0x00

/* Group 0: drive errors. */
#define ERR_NO_SEEK       0x02 /* no seek_complete */
#define ERR_WR_FAULT      0x03 /* write fault */
#define ERR_NOT_READY     0x04 /* drive not ready */
#define ERR_NO_TRACK0     0x06 /* track 0 not found */
#define ERR_STILL_SEEKING 0x08 /* drive is still seeking */
#define ERR_NOT_AVAILABLE 0x09 /* drive not available */

/* Group 1: controller errors. */
#define ERR_ID_FAULT          0x10 /* could not read ID field */
#define ERR_UNC_ERR           0x11 /* uncorrectable data */
#define ERR_SECTOR_ADDR       0x12 /* sector address */
#define ERR_DATA_ADDR         0x13 /* data mark not found */
#define ERR_TARGET_SECTOR     0x14 /* target sector not found */
#define ERR_SEEK_ERROR        0x15 /* seek error- cyl not found */
#define ERR_CORR_ERR          0x18 /* correctable data */
#define ERR_BAD_TRACK         0x19 /* track is flagged as bad */
#define ERR_ALT_TRACK_FLAGGED 0x1c /* alt trk not flagged as alt */
#define ERR_ALT_TRACK_ACCESS  0x1e /* illegal access to alt trk */
#define ERR_NO_RECOVERY       0x1f /* recovery mode not avail */

/* Group 2: command errors. */
#define ERR_BAD_COMMAND   0x20 /* invalid command */
#define ERR_ILLEGAL_ADDR  0x21 /* address beyond disk size */
#define ERR_BAD_PARAMETER 0x22 /* invalid command parameter */

/* Group 3: misc errors. */
#define ERR_BAD_RAM  0x30 /* controller has bad RAM */
#define ERR_BAD_ROM  0x31 /* ROM failed checksum test */
#define ERR_CRC_FAIL 0x32 /* CRC circuit failed test */

/* Controller commands. */
#define CMD_TEST_DRIVE_READY     0x00
#define CMD_RECALIBRATE          0x01
/* reserved                      0x02 */
#define CMD_STATUS               0x03
#define CMD_FORMAT_DRIVE         0x04
#define CMD_VERIFY               0x05
#define CMD_FORMAT_TRACK         0x06
#define CMD_FORMAT_BAD_TRACK     0x07
#define CMD_READ                 0x08
#define CMD_REASSIGN             0x09
#define CMD_WRITE                0x0a
#define CMD_SEEK                 0x0b
#define CMD_SPECIFY              0x0c
#define CMD_READ_ECC_BURST_LEN   0x0d
#define CMD_READ_BUFFER          0x0e
#define CMD_WRITE_BUFFER         0x0f
#define CMD_ALT_TRACK            0x11
#define CMD_INQUIRY_ST11         0x12 /* ST-11 BIOS */
#define CMD_V86P_POWEROFF        0x1a /* Victor V86P */
#define CMD_RAM_DIAGNOSTIC       0xe0
/* reserved                      0xe1 */
/* reserved                      0xe2 */
#define CMD_DRIVE_DIAGNOSTIC     0xe3
#define CMD_CTRLR_DIAGNOSTIC     0xe4
#define CMD_READ_LONG            0xe5
#define CMD_WRITE_LONG           0xe6

#define CMD_FORMAT_ST11          0xf6 /* ST-11 BIOS */
#define CMD_GET_GEOMETRY_ST11    0xf8 /* ST-11 BIOS */
#define CMD_SET_GEOMETRY_ST11    0xfa /* ST-11 BIOS */
#define CMD_WRITE_GEOMETRY_ST11  0xfc /* ST-11 BIOS 2.0 */

#define CMD_GET_DRIVE_PARAMS_DTC 0xfb /* DTC */
#define CMD_SET_STEP_RATE_DTC    0xfc /* DTC */
#define CMD_SET_GEOMETRY_DTC     0xfe /* DTC */
#define CMD_GET_GEOMETRY_DTC     0xff /* DTC */

enum {
    STATE_IDLE,
    STATE_RECEIVE_COMMAND,
    STATE_START_COMMAND,
    STATE_RECEIVE_DATA,
    STATE_RECEIVED_DATA,
    STATE_SEND_DATA,
    STATE_SENT_DATA,
    STATE_COMPLETION_BYTE,
    STATE_DONE
};

typedef struct drive_t {
    int8_t  present;
    uint8_t hdd_num;

    uint8_t interleave; /* default interleave */
    char    pad;

    uint16_t cylinder; /* current cylinder */

    uint8_t  spt; /* physical parameters */
    uint8_t  hpc;
    uint16_t tracks;

    uint8_t  cfg_spt; /* configured parameters */
    uint8_t  cfg_hpc;
    uint16_t cfg_cyl;
} drive_t;

typedef struct hdc_t {
    uint8_t type; /* controller type */

    uint8_t spt; /* sectors-per-track for controller */

    uint16_t base; /* controller configuration */
    int8_t   irq;
    int8_t   dma;
    uint8_t  switches;
    uint8_t  misc;
    uint8_t  nr_err;
    uint8_t  err_bv;
    uint8_t  cur_sec;
    uint8_t  pad;
    uint32_t bios_addr;
    uint32_t bios_size;
    uint32_t bios_ram;
    rom_t    bios_rom;

    int        state; /* operational data */
    uint8_t    irq_dma;
    uint8_t    error;
    uint8_t    status;
    int8_t     cyl_off; /* for ST-11, cylinder0 offset */
    pc_timer_t timer;

    uint8_t command[6]; /* current command request */
    int     drive_sel;
    int     sector;
    int     head;
    int     cylinder;
    int     count;
    uint8_t compl ; /* current request completion code */

    int buff_pos; /* pointers to the RAM buffer */
    int buff_cnt;

    drive_t drives[MFM_NUM];       /* the attached drives */
    uint8_t scratch[64];           /* ST-11 scratchpad RAM */
    uint8_t buff[SECTOR_SIZE + 4]; /* sector buffer RAM (+ ECC bytes) */
} hdc_t;

/* Supported drives table for the Xebec controller. */
typedef struct hd_type_t {
    uint16_t tracks;
    uint8_t  hpc;
    uint8_t  spt;
} hd_type_t;

hd_type_t hd_types[4] = {
  // clang-format off
    { 306,  4, MFM_SECTORS}, /* type 0  */
    { 612,  4, MFM_SECTORS}, /* type 16 */
    { 615,  4, MFM_SECTORS}, /* type 2  */
    { 306,  8, MFM_SECTORS}  /* type 13 */
  // clang-format on
};

hd_type_t hd_types_olivetti[16] = {
  // clang-format off
    { 697,  5, MFM_SECTORS},
    { 612,  4, MFM_SECTORS}, /* type 16 */
    { 612,  4, MFM_SECTORS}, /* type 16 */
    { 306,  4, MFM_SECTORS}, /* type 0  */
    { 612,  8, MFM_SECTORS},
    { 820,  6, MFM_SECTORS},
    { 820,  6, MFM_SECTORS},
    { 823, 10, MFM_SECTORS},
    { 981,  5, MFM_SECTORS},
    { 981,  5, MFM_SECTORS},
    {1024,  8, MFM_SECTORS},
    {1024,  9, MFM_SECTORS},
    { 872,  5, MFM_SECTORS},
    { 612,  4, MFM_SECTORS}, /* type 16 */
    { 612,  4, MFM_SECTORS}, /* type 16 */
    { 306,  4, MFM_SECTORS}  /* "not present" with the second hard disk */
  // clang-format on
};

#ifdef ENABLE_ST506_XT_LOG
int st506_xt_do_log = ENABLE_ST506_XT_LOG;

static void
st506_xt_log(const char *fmt, ...)
{
    va_list ap;

    if (st506_xt_do_log) {
        va_start(ap, fmt);
        pclog_ex(fmt, ap);
        va_end(ap);
    }
}
#else
#    define st506_xt_log(fmt, ...)
#endif

static void
st506_complete(hdc_t *dev)
{
    dev->status = STAT_REQ | STAT_CD | STAT_IO | STAT_BSY;
    dev->state  = STATE_COMPLETION_BYTE;

    if (dev->irq_dma & DMA_ENA)
        dma_set_drq(dev->dma, 0);

    if (dev->irq_dma & IRQ_ENA) {
        dev->status |= STAT_IRQ;
        picint(1 << dev->irq);
    }
}

static void
st506_error(hdc_t *dev, uint8_t err)
{
    dev->compl |= 0x02;
    dev->error = err;
}

static int
get_sector(hdc_t *dev, drive_t *drive, off64_t *addr)
{
    if (!drive->present) {
        /* No need to log this. */
        dev->error = dev->nr_err;
        return 0;
    }

#if 0
    if (drive->cylinder != dev->cylinder) {
#    ifdef ENABLE_ST506_XT_LOG
        st506_xt_log("ST506: get_sector: wrong cylinder\n");
#    endif
        dev->error = ERR_ILLEGAL_ADDR;
        return(0);
    }
#endif

    if (dev->head >= drive->cfg_hpc) {
#ifdef ENABLE_ST506_XT_LOG
        st506_xt_log("ST506: get_sector: past end of configured heads\n");
#endif
        dev->error = ERR_ILLEGAL_ADDR;
        return 0;
    }
    if (dev->sector >= drive->cfg_spt) {
#ifdef ENABLE_ST506_XT_LOG
        st506_xt_log("ST506: get_sector: past end of configured sectors\n");
#endif
        dev->error = ERR_ILLEGAL_ADDR;
        return 0;
    }

    *addr = ((((off64_t) dev->cylinder * drive->cfg_hpc) + dev->head) * drive->cfg_spt) + dev->sector;

    return 1;
}

static void
next_sector(hdc_t *dev, drive_t *drive)
{
    if (++dev->sector >= drive->cfg_spt) {
        dev->sector = 0;
        if (++dev->head >= drive->cfg_hpc) {
            dev->head = 0;
            if (++drive->cylinder >= drive->cfg_cyl) {
                /*
                 * This really is an error, we cannot move
                 * past the end of the drive, which should
                 * result in an ERR_ILLEGAL_ADDR.  --FvK
                 */
                drive->cylinder = drive->cfg_cyl - 1;
            } else
                dev->cylinder++;
        }
    }
}

/* Extract the CHS info from a command block. */
static int
get_chs(hdc_t *dev, drive_t *drive)
{
    dev->err_bv = 0x80;

    dev->head = dev->command[1] & 0x1f;
    /* 6 bits are used for the sector number even on the IBM PC controller. */
    dev->sector = dev->command[2] & 0x3f;
    dev->count  = dev->command[4];
    if (((dev->type == ST506_XT_TYPE_ST11M) || (dev->type == ST506_XT_TYPE_ST11R)) && (dev->command[0] >= 0xf0))
        dev->cylinder = 0;
    else {
        dev->cylinder = dev->command[3] | ((dev->command[2] & 0xc0) << 2);
        dev->cylinder += dev->cyl_off; /* for ST-11 */
    }

    if (dev->cylinder >= drive->cfg_cyl) {
        /*
         * This really is an error, we cannot move
         * past the end of the drive, which should
         * result in an ERR_ILLEGAL_ADDR.  --FvK
         */
        drive->cylinder = drive->cfg_cyl - 1;
        return 0;
    }

    drive->cylinder = dev->cylinder;

    return 1;
}

static int
get_chs_format(hdc_t *dev, drive_t *drive)
{
    dev->err_bv = 0x80;

    dev->head = dev->command[1] & 0x1f;
    /* 6 bits are used for the sector number even on the IBM PC controller. */
    dev->sector = 1;
    dev->count  = dev->command[4];
    if (((dev->type == ST506_XT_TYPE_ST11M) || (dev->type == ST506_XT_TYPE_ST11R)) && (dev->command[0] >= 0xf0))
        dev->cylinder = 0;
    else {
        dev->cylinder = dev->command[3] | ((dev->command[2] & 0xc0) << 2);
        dev->cylinder += dev->cyl_off; /* for ST-11 */
    }

    if (dev->cylinder >= drive->cfg_cyl) {
        /*
         * This really is an error, we cannot move
         * past the end of the drive, which should
         * result in an ERR_ILLEGAL_ADDR.  --FvK
         */
        drive->cylinder = drive->cfg_cyl - 1;
        return 0;
    }

    drive->cylinder = dev->cylinder;

    return 1;
}

static void
st506_callback(void *priv)
{
    hdc_t   *dev = (hdc_t *) priv;
    drive_t *drive;
    off64_t  addr;
    uint32_t capac;
    int      val;

    /* Get the drive info. Note that the API supports up to 8 drives! */
    dev->drive_sel = (dev->command[1] >> 5) & 0x07;
    drive          = &dev->drives[dev->drive_sel];

    /* Preset the completion byte to "No error" and the selected drive. */
    dev->compl = (dev->drive_sel << 5) | ERR_NONE;

    if (dev->command[0] != 3)
        dev->err_bv = 0x00;

    switch (dev->command[0]) {
        case CMD_TEST_DRIVE_READY:
            st506_xt_log("ST506: TEST_READY(%i) = %i\n",
                         dev->drive_sel, drive->present);
            if (!drive->present)
                st506_error(dev, dev->nr_err);
            st506_complete(dev);
            break;

        case CMD_RECALIBRATE:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    st506_xt_log("ST506: RECALIBRATE(%i) [%i]\n",
                                 dev->drive_sel, drive->present);
                    if (!drive->present) {
                        st506_error(dev, dev->nr_err);
                        st506_complete(dev);
                    } else {
                        double seek_us = hdd_seek_get_time(&hdd[drive->hdd_num], 0, HDD_OP_SEEK, 0, 0.0);
                        timer_advance_u64(&dev->timer, (uint64_t)(seek_us * TIMER_USEC));

                        hdd_audio_seek(&hdd[drive->hdd_num], 0);
                        hdd[drive->hdd_num].cur_cylinder = 0;

                        dev->cylinder   = dev->cyl_off;
                        drive->cylinder = dev->cylinder;
                        dev->state      = STATE_DONE;
                    }
                    break;

                case STATE_DONE:
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_STATUS:
            switch (dev->state) {
                case STATE_START_COMMAND:
#ifdef ENABLE_ST506_XT_LOG
                    st506_xt_log("ST506: STATUS\n");
#endif
                    dev->buff_pos = 0;
                    dev->buff_cnt = 4;
                    dev->buff[0]  = dev->err_bv | dev->error;
                    dev->error    = 0;

                    /* Give address of last operation. */
                    dev->buff[1] = (dev->drive_sel ? 0x20 : 0) | dev->head;
                    dev->buff[2] = ((dev->cylinder & 0x0300) >> 2) | dev->sector;
                    dev->buff[3] = (dev->cylinder & 0xff);

                    dev->status = STAT_BSY | STAT_IO | STAT_REQ;
                    dev->state  = STATE_SEND_DATA;
                    break;

                case STATE_SENT_DATA:
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_FORMAT_DRIVE:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    (void) get_chs(dev, drive);
                    st506_xt_log("ST506: FORMAT_DRIVE(%i) interleave=%i\n",
                                 dev->drive_sel, dev->command[4]);
                    ui_sb_update_icon_write(SB_HDD | HDD_BUS_MFM, 1);
                    timer_advance_u64(&dev->timer, ST506_TIME);
                    dev->state = STATE_SEND_DATA;
                    break;

                case STATE_SEND_DATA: /* wrong, but works */
                    if (!get_sector(dev, drive, &addr)) {
                        ui_sb_update_icon_write(SB_HDD | HDD_BUS_MFM, 0);
                        st506_error(dev, dev->error);
                        st506_complete(dev);
                        return;
                    }

                    /* FIXME: should be drive->capac, not ->spt */
                    capac = (drive->tracks - 1) * drive->hpc * drive->spt;
                    hdd_image_zero(drive->hdd_num, addr, capac);

                    /* Wait 20msec per cylinder. */
                    timer_advance_u64(&dev->timer, ST506_TIME_MS * 20);

                    dev->state = STATE_SENT_DATA;
                    break;

                case STATE_SENT_DATA:
                    ui_sb_update_icon_write(SB_HDD | HDD_BUS_MFM, 0);
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_VERIFY:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    (void) get_chs(dev, drive);
                    st506_xt_log("ST506: VERIFY(%i, %i/%i/%i, %i)\n",
                                 dev->drive_sel, dev->cylinder,
                                 dev->head, dev->sector, dev->count);
                    ui_sb_update_icon(SB_HDD | HDD_BUS_MFM, 1);
                    timer_advance_u64(&dev->timer, ST506_TIME);
                    dev->state = STATE_SEND_DATA;
                    break;

                case STATE_SEND_DATA:
                    if (dev->count-- == 0) {
                        ui_sb_update_icon(SB_HDD | HDD_BUS_MFM, 0);
                        st506_complete(dev);
                    }

                    if (!get_sector(dev, drive, &addr)) {
                        ui_sb_update_icon(SB_HDD | HDD_BUS_MFM, 0);
                        st506_error(dev, dev->error);
                        st506_complete(dev);
                        return;
                    }

                    next_sector(dev, drive);

                    timer_advance_u64(&dev->timer, ST506_TIME);
                    break;

                default:
                    break;
            }
            break;

        case CMD_FORMAT_ST11: /* This is really "Format cylinder 0" */
            if ((dev->type < ST506_XT_TYPE_ST11M) || (dev->type > ST506_XT_TYPE_ST11R)) {
                st506_error(dev, ERR_BAD_COMMAND);
                st506_complete(dev);
                break;
            }
            fallthrough;
        case CMD_FORMAT_TRACK:
        case CMD_FORMAT_BAD_TRACK:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    (void) get_chs_format(dev, drive);
                    st506_xt_log("ST506: FORMAT_%sTRACK(%i, %i/%i)\n",
                                 (dev->command[0] == CMD_FORMAT_BAD_TRACK) ? "BAD_" : "",
                                 dev->drive_sel, dev->cylinder, dev->head);
                    ui_sb_update_icon_write(SB_HDD | HDD_BUS_MFM, 1);
                    timer_advance_u64(&dev->timer, ST506_TIME);
                    dev->state = STATE_SEND_DATA;
                    break;

                case STATE_SEND_DATA: /* wrong, but works */
                    if (!get_sector(dev, drive, &addr)) {
                        ui_sb_update_icon_write(SB_HDD | HDD_BUS_MFM, 0);
                        st506_error(dev, dev->error);
                        st506_complete(dev);
                        return;
                    }

                    hdd_image_zero(drive->hdd_num,
                                   addr, drive->cfg_spt);

                    /* Wait 20 msec per cylinder. */
                    timer_advance_u64(&dev->timer, ST506_TIME_MS * 20);

                    dev->state = STATE_SENT_DATA;
                    break;

                case STATE_SENT_DATA:
                    ui_sb_update_icon_write(SB_HDD | HDD_BUS_MFM, 0);
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_GET_GEOMETRY_ST11: /* "Get geometry" is really "Read cylinder 0" */
            if ((dev->type < ST506_XT_TYPE_ST11M) || (dev->type > ST506_XT_TYPE_ST11R)) {
                st506_error(dev, ERR_BAD_COMMAND);
                st506_complete(dev);
                break;
            }
            fallthrough;
        case CMD_READ:
#if 0
        case CMD_READ_LONG:
#endif
            switch (dev->state) {
                case STATE_START_COMMAND:
                    (void) get_chs(dev, drive);
                    st506_xt_log("ST506: READ%s(%i, %i/%i/%i, %i)\n",
                                 (dev->command[0] == CMD_READ_LONG) ? "_LONG" : "",
                                 dev->drive_sel, dev->cylinder,
                                 dev->head, dev->sector, dev->count);

                    if (!get_sector(dev, drive, &addr)) {
read_error_start:
                        st506_error(dev, dev->error);
                        st506_complete(dev);
                        return;
                    }
                    ui_sb_update_icon(SB_HDD | HDD_BUS_MFM, 1);

                    /* Read data from the image. */
                    if (hdd_image_read(drive->hdd_num, addr, 1,
                                       (uint8_t *) dev->buff) < 0) {
                        dev->error = ERR_UNC_ERR;
                        goto read_error_start;
                    }

                    /* Set up the data transfer. */
                    dev->buff_pos = 0;
                    dev->buff_cnt = SECTOR_SIZE;
                    if (dev->command[0] == CMD_READ_LONG)
                        dev->buff_cnt += 4;
                    dev->status = STAT_BSY | STAT_IO | STAT_REQ;
                    if (dev->irq_dma & DMA_ENA) {
                        double seek_us = hdd_seek_get_time(&hdd[drive->hdd_num], (uint32_t) addr, HDD_OP_READ, 0, 0.0);
                        timer_advance_u64(&dev->timer, (uint64_t)(seek_us * TIMER_USEC));
                        dma_set_drq(dev->dma, 1);
                    }
                    dev->state = STATE_SEND_DATA;
                    break;

                case STATE_SEND_DATA:
                    for (; dev->buff_pos < dev->buff_cnt; dev->buff_pos++) {
                        val = dma_channel_write(dev->dma, dev->buff[dev->buff_pos]);
                        if (val == DMA_NODATA) {
#ifdef ENABLE_ST506_XT_LOG
                            st506_xt_log("ST506: CMD_READ out of data!\n");
#endif
                            st506_error(dev, ERR_NO_RECOVERY);
                            st506_complete(dev);
                            return;
                        }
                    }
                    dma_set_drq(dev->dma, 0);
                    timer_advance_u64(&dev->timer, ST506_TIME);
                    dev->state = STATE_SENT_DATA;
                    break;

                case STATE_SENT_DATA:
                    if (--dev->count == 0) {
                        ui_sb_update_icon(SB_HDD | HDD_BUS_MFM, 0);
                        st506_complete(dev);
                        break;
                    }

                    next_sector(dev, drive);

                    if (!get_sector(dev, drive, &addr)) {
read_error_sent:
                        ui_sb_update_icon(SB_HDD | HDD_BUS_MFM, 0);
                        st506_error(dev, dev->error);
                        st506_complete(dev);
                        return;
                    }

                    /* Read data from the image. */
                    if (hdd_image_read(drive->hdd_num, addr, 1,
                                       (uint8_t *) dev->buff) < 0) {
                        dev->error = ERR_UNC_ERR;
                        goto read_error_sent;
                    }

                    /* Set up the data transfer. */
                    dev->buff_pos = 0;
                    dev->buff_cnt = SECTOR_SIZE;
                    dev->status   = STAT_BSY | STAT_IO | STAT_REQ;
                    if (dev->irq_dma & DMA_ENA) {
                        double seek_us = hdd_seek_get_time(&hdd[drive->hdd_num], (uint32_t) addr, HDD_OP_READ, 1, 0.0);
                        timer_advance_u64(&dev->timer, (uint64_t)(seek_us * TIMER_USEC));
                        dma_set_drq(dev->dma, 1);
                    }
                    dev->state = STATE_SEND_DATA;
                    break;

                default:
                    break;
            }
            break;

        case CMD_SET_GEOMETRY_ST11: /* "Set geometry" is really "Write cylinder 0" */
            if (dev->type == ST506_XT_TYPE_DTC_5150X) {
                /* DTC sends this... */
                st506_complete(dev);
                break;
            } else if ((dev->type < ST506_XT_TYPE_ST11M) || (dev->type > ST506_XT_TYPE_ST11R)) {
                st506_error(dev, ERR_BAD_COMMAND);
                st506_complete(dev);
                break;
            }
            fallthrough;
        case CMD_WRITE:
#if 0
        case CMD_WRITE_LONG:
#endif
            switch (dev->state) {
                case STATE_START_COMMAND:
                    (void) get_chs(dev, drive);
                    st506_xt_log("ST506: WRITE%s(%i, %i/%i/%i, %i)\n",
                                 (dev->command[0] == CMD_WRITE_LONG) ? "_LONG" : "",
                                 dev->drive_sel, dev->cylinder,
                                 dev->head, dev->sector, dev->count);

                    if (!get_sector(dev, drive, &addr)) {
                        st506_error(dev, ERR_BAD_PARAMETER);
                        st506_complete(dev);
                        return;
                    }

                    ui_sb_update_icon_write(SB_HDD | HDD_BUS_MFM, 1);

                    /* Set up the data transfer. */
                    dev->buff_pos = 0;
                    dev->buff_cnt = SECTOR_SIZE;
                    if (dev->command[0] == CMD_WRITE_LONG)
                        dev->buff_cnt += 4;
                    dev->status = STAT_BSY | STAT_REQ;
                    if (dev->irq_dma & DMA_ENA) {
                        double seek_us = hdd_seek_get_time(&hdd[drive->hdd_num], (uint32_t) addr, HDD_OP_WRITE, 0, 0.0);
                        timer_advance_u64(&dev->timer, (uint64_t)(seek_us * TIMER_USEC));
                        dma_set_drq(dev->dma, 1);
                    }
                    dev->state = STATE_RECEIVE_DATA;
                    break;

                case STATE_RECEIVE_DATA:
                    for (; dev->buff_pos < dev->buff_cnt; dev->buff_pos++) {
                        val = dma_channel_read(dev->dma);
                        if (val == DMA_NODATA) {
#ifdef ENABLE_ST506_XT_LOG
                            st506_xt_log("ST506: CMD_WRITE out of data!\n");
#endif
                            st506_error(dev, ERR_NO_RECOVERY);
                            st506_complete(dev);
                            return;
                        }
                        dev->buff[dev->buff_pos] = val & 0xff;
                    }

                    dma_set_drq(dev->dma, 0);
                    timer_advance_u64(&dev->timer, ST506_TIME);
                    dev->state = STATE_RECEIVED_DATA;
                    break;

                case STATE_RECEIVED_DATA:
                    if (!get_sector(dev, drive, &addr)) {
write_error:
                        ui_sb_update_icon_write(SB_HDD | HDD_BUS_MFM, 0);
                        st506_error(dev, dev->error);
                        st506_complete(dev);
                        return;
                    }

                    /* Write data to image. */
                    if (hdd_image_write(drive->hdd_num, addr, 1,
                                        (uint8_t *) dev->buff) < 0) {
                        dev->error = ERR_UNC_ERR;
                        goto write_error;
                    }

                    if (--dev->count == 0) {
                        ui_sb_update_icon_write(SB_HDD | HDD_BUS_MFM, 0);
                        st506_complete(dev);
                        break;
                    }

                    next_sector(dev, drive);

                    /* Set up the data transfer. */
                    dev->buff_pos = 0;
                    dev->buff_cnt = SECTOR_SIZE;
                    dev->status   = STAT_BSY | STAT_REQ;
                    if (dev->irq_dma & DMA_ENA) {
                        double seek_us = hdd_seek_get_time(&hdd[drive->hdd_num], (uint32_t) addr, HDD_OP_WRITE, 1, 0.0);
                        timer_advance_u64(&dev->timer, (uint64_t)(seek_us * TIMER_USEC));
                        dma_set_drq(dev->dma, 1);
                    }
                    dev->state = STATE_RECEIVE_DATA;
                    break;

                default:
                    break;
            }
            break;

        case CMD_SEEK:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    st506_xt_log("ST506: RECALIBRATE(%i) [%i]\n",
                                 dev->drive_sel, drive->present);
                    if (!drive->present) {
                        st506_error(dev, dev->nr_err);
                        st506_complete(dev);
                    } else {
                        val = get_chs(dev, drive);

                        if (val) {
                            if (get_sector(dev, drive, &addr)) {
                                double seek_us = hdd_seek_get_time(&hdd[drive->hdd_num], addr, HDD_OP_SEEK, 0, 0.0);
                                timer_advance_u64(&dev->timer, (uint64_t)(seek_us * TIMER_USEC));

                                uint16_t new_cylinder = (uint16_t) dev->cylinder;
                                hdd_audio_seek(&hdd[drive->hdd_num], new_cylinder);
                                hdd[drive->hdd_num].cur_cylinder = new_cylinder;

                                dev->state      = STATE_DONE;
                            } else {
                                st506_error(dev, ERR_BAD_PARAMETER);
                                st506_complete(dev);
                            }
                        } else {
                            st506_error(dev, ERR_SEEK_ERROR);
                            st506_complete(dev);
                        }
                    }
                    break;

                case STATE_DONE:
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_SPECIFY:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    dev->buff_pos = 0;
                    dev->buff_cnt = 8;
                    dev->status   = STAT_BSY | STAT_REQ;
                    dev->state    = STATE_RECEIVE_DATA;
                    break;

                case STATE_RECEIVED_DATA:
                    drive->cfg_cyl = dev->buff[1] | (dev->buff[0] << 8);
                    drive->cfg_hpc = dev->buff[2];
                    /* For a 615/4/26 we get 666/2/31 geometry. */
                    st506_xt_log("ST506: drive%i: cyls=%i, heads=%i\n",
                                 dev->drive_sel, drive->cfg_cyl, drive->cfg_hpc);
                    if ((dev->type >= ST506_XT_TYPE_VICTOR_V86P) && (drive->cfg_hpc == 2)) {
                        /*
                         * On Victor V86P, there's a disagreement between
                         * the physical geometry, what the controller
                         * pretends it to be, and what the BIOS uses.
                         *
                         * The disk physically has 2/34 heads/sectors per
                         * track, but it is treated as 4/17 in order to
                         * look like a regular type 3 drive (see [1],
                         * line 1859). The controller accepts the 4/17
                         * geometry, so this should not really matter.
                         *
                         * However, the BIOS issues SPECIFY (see [1],
                         * line 2089) with head count of two. Let's
                         * hardwire the correct number instead, just like
                         * the real hardware seems to.
                         *
                         * [1] https://archive.org/download/v86p-hd/V86P-HD.TXT
                         */
                        drive->cfg_hpc = 4;
                        st506_xt_log("ST506: drive%i: corrected to heads=%i\n",
                                     dev->drive_sel, drive->cfg_hpc);
                    }
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_READ_ECC_BURST_LEN:
            switch (dev->state) {
                case STATE_START_COMMAND:
#ifdef ENABLE_ST506_XT_LOG
                    st506_xt_log("ST506: READ_ECC_BURST_LEN\n");
#endif
                    dev->buff_pos = 0;
                    dev->buff_cnt = 1;
                    dev->buff[0]  = 0; /* 0 bits */
                    dev->status   = STAT_BSY | STAT_IO | STAT_REQ;
                    dev->state    = STATE_SEND_DATA;
                    break;

                case STATE_SENT_DATA:
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_READ_BUFFER:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    dev->buff_pos = 0;
                    dev->buff_cnt = SECTOR_SIZE;
                    st506_xt_log("ST506: READ_BUFFER (%i)\n",
                                 dev->buff_cnt);

                    dev->status = STAT_BSY | STAT_IO | STAT_REQ;
                    if (dev->irq_dma & DMA_ENA) {
                        timer_advance_u64(&dev->timer, ST506_TIME);
                        dma_set_drq(dev->dma, 1);
                    }
                    dev->state = STATE_SEND_DATA;
                    break;

                case STATE_SEND_DATA:
                    for (; dev->buff_pos < dev->buff_cnt; dev->buff_pos++) {
                        val = dma_channel_write(dev->dma, dev->buff[dev->buff_pos]);
                        if (val == DMA_NODATA) {
#ifdef ENABLE_ST506_XT_LOG
                            st506_xt_log("ST506: CMD_READ_BUFFER out of data!\n");
#endif
                            st506_error(dev, ERR_NO_RECOVERY);
                            st506_complete(dev);
                            return;
                        }
                    }

                    dma_set_drq(dev->dma, 0);
                    timer_advance_u64(&dev->timer, ST506_TIME);
                    dev->state = STATE_SENT_DATA;
                    break;

                case STATE_SENT_DATA:
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_WRITE_BUFFER:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    dev->buff_pos = 0;
                    dev->buff_cnt = SECTOR_SIZE;
                    st506_xt_log("ST506: WRITE_BUFFER (%i)\n",
                                 dev->buff_cnt);

                    dev->status = STAT_BSY | STAT_REQ;
                    if (dev->irq_dma & DMA_ENA) {
                        timer_advance_u64(&dev->timer, ST506_TIME);
                        dma_set_drq(dev->dma, 1);
                    }
                    dev->state = STATE_RECEIVE_DATA;
                    break;

                case STATE_RECEIVE_DATA:
                    for (; dev->buff_pos < dev->buff_cnt; dev->buff_pos++) {
                        val = dma_channel_read(dev->dma);
                        if (val == DMA_NODATA) {
#ifdef ENABLE_ST506_XT_LOG
                            st506_xt_log("ST506: CMD_WRITE_BUFFER out of data!\n");
#endif
                            st506_error(dev, ERR_NO_RECOVERY);
                            st506_complete(dev);
                            return;
                        }
                        dev->buff[dev->buff_pos] = val & 0xff;
                    }

                    dma_set_drq(dev->dma, 0);
                    timer_advance_u64(&dev->timer, ST506_TIME);
                    dev->state = STATE_RECEIVED_DATA;
                    break;

                case STATE_RECEIVED_DATA:
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_INQUIRY_ST11:
            if (dev->type == ST506_XT_TYPE_ST11M || dev->type == ST506_XT_TYPE_ST11R)
                switch (dev->state) {
                    case STATE_START_COMMAND:
                        st506_xt_log("ST506: INQUIRY (type=%i)\n", dev->type);
                        dev->buff_pos = 0;
                        dev->buff_cnt = 2;
                        dev->buff[0]  = 0x80; /* "ST-11" */
                        if (dev->spt == 17)
                            dev->buff[0] |= 0x40; /* MFM */
                        dev->buff[1] = dev->misc; /* revision */
                        dev->status  = STAT_BSY | STAT_IO | STAT_REQ;
                        dev->state   = STATE_SEND_DATA;
                        break;

                    case STATE_SENT_DATA:
                        st506_complete(dev);
                        break;

                    default:
                        break;
                }
            else {
                st506_error(dev, ERR_BAD_COMMAND);
                st506_complete(dev);
            }
            break;

        case CMD_V86P_POWEROFF:
            if (dev->type >= ST506_XT_TYPE_VICTOR_V86P) {
                /*
                 * Main BIOS (not the option ROM on disk) issues this.
                 * Not much we can do, since we don't have a physical disk
                 * to spin down, but handle this anyways so that we log
                 * something more reasonable than "unknown command".
                 *
                 * Entirely undocumented, but this is what's been observed:
                 * BIOS setting | Command sent
                 * 1 minutes    | 1a 00 00 0c 02 00
                 * 2 minutes    | 1a 00 00 18 02 00
                 * 3 minutes    | 1a 00 00 24 02 00
                 * 4 minutes    | 1a 00 00 30 02 00
                 * 5 minutes    | 1a 00 00 3c 02 00
                 * off          | 1a 00 00 00 02 00
                 */
                if (dev->command[3])
                    st506_xt_log("ST506: Auto power-off in %d seconds (type=%i)\n",
                                 dev->command[3] * 5, dev->type);
                else
                    st506_xt_log("ST506: Auto power-off disabled (type=%i)\n", dev->type);
            } else {
                st506_error(dev, ERR_BAD_COMMAND);
            }
            st506_complete(dev);
            break;

        case CMD_RAM_DIAGNOSTIC:
#ifdef ENABLE_ST506_XT_LOG
            st506_xt_log("ST506: RAM_DIAG\n");
#endif
            st506_complete(dev);
            break;

        case CMD_CTRLR_DIAGNOSTIC:
#ifdef ENABLE_ST506_XT_LOG
            st506_xt_log("ST506: CTRLR_DIAG\n");
#endif
            st506_complete(dev);
            break;

        case CMD_SET_STEP_RATE_DTC:
            if (dev->type == ST506_XT_TYPE_DTC_5150X) {
                /* For DTC, we are done. */
                st506_complete(dev);
            } else if (dev->type == ST506_XT_TYPE_ST11M || dev->type == ST506_XT_TYPE_ST11R) {
                /*
                 * For Seagate ST-11, this is WriteGeometry.
                 *
                 * This writes the contents of the buffer to track 0.
                 *
                 * By the time this command is sent, it will have
                 * formatted the first track, so it should be good,
                 * and our sector buffer contains the magic data
                 * (see above) we need to write to it.
                 */
                (void) get_chs(dev, drive);
                st506_xt_log("ST506: WRITE BUFFER (%i, %i/%i/%i, %i)\n",
                             dev->drive_sel, dev->cylinder,
                             dev->head, dev->sector, dev->count);

                if (!get_sector(dev, drive, &addr)) {
                    st506_error(dev, ERR_BAD_PARAMETER);
                    st506_complete(dev);
                    return;
                }

                ui_sb_update_icon(SB_HDD | HDD_BUS_MFM, 1);

                /* Write data to image. */
                if (hdd_image_write(drive->hdd_num, addr, 1,
                                    (uint8_t *) dev->buff) < 0) {
                    st506_error(dev, ERR_UNC_ERR);
                    st506_complete(dev);
                    return;
                }

                if (--dev->count == 0) {
                    ui_sb_update_icon(SB_HDD | HDD_BUS_MFM, 0);
                    st506_complete(dev);
                    break;
                }

                next_sector(dev, drive);
                timer_advance_u64(&dev->timer, ST506_TIME);
                break;
            } else {
                st506_error(dev, ERR_BAD_COMMAND);
                st506_complete(dev);
            }
            break;

        case CMD_GET_DRIVE_PARAMS_DTC:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    dev->buff_pos = 0;
                    dev->buff_cnt = 4;
                    memset(dev->buff, 0x00, dev->buff_cnt);
                    dev->buff[0] = drive->tracks & 0xff;
                    dev->buff[1] = ((drive->tracks >> 2) & 0xc0) | dev->spt;
                    dev->buff[2] = drive->hpc - 1;
                    dev->status  = STAT_BSY | STAT_IO | STAT_REQ;
                    dev->state   = STATE_SEND_DATA;
                    break;

                case STATE_SENT_DATA:
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_SET_GEOMETRY_DTC:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    val = dev->command[1] & 0x01;
                    st506_xt_log("ST506: DTC_GET_GEOMETRY(%i) %i\n",
                                 dev->drive_sel, val);
                    dev->buff_pos = 0;
                    dev->buff_cnt = 16;
                    dev->status   = STAT_BSY | STAT_REQ;
                    dev->state    = STATE_RECEIVE_DATA;
                    break;

                case STATE_RECEIVED_DATA:
                    /* FIXME: ignore the results. */
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        case CMD_GET_GEOMETRY_DTC:
            switch (dev->state) {
                case STATE_START_COMMAND:
                    val = dev->command[1] & 0x01;
                    st506_xt_log("ST506: DTC_GET_GEOMETRY(%i) %i\n",
                                 dev->drive_sel, val);
                    dev->buff_pos = 0;
                    dev->buff_cnt = 16;
                    memset(dev->buff, 0x00, dev->buff_cnt);
                    dev->buff[4]  = drive->tracks & 0xff;
                    dev->buff[5]  = (drive->tracks >> 8) & 0xff;
                    dev->buff[10] = drive->hpc;
                    dev->status   = STAT_BSY | STAT_IO | STAT_REQ;
                    dev->state    = STATE_SEND_DATA;
                    break;

                case STATE_SENT_DATA:
                    st506_complete(dev);
                    break;

                default:
                    break;
            }
            break;

        default:
            if (dev->command[0] == CMD_WRITE_GEOMETRY_ST11)
                fatal("CMD_WRITE_GEOMETRY_ST11\n");
#ifdef ENABLE_ST506_XT_LOG
            st506_xt_log("ST506: unknown command:\n");
#endif
            st506_xt_log("ST506: %02x %02x %02x %02x %02x %02x\n",
                         dev->command[0], dev->command[1], dev->command[2],
                         dev->command[3], dev->command[4], dev->command[5]);
            st506_error(dev, ERR_BAD_COMMAND);
            st506_complete(dev);
    }
}

/* Read from one of the registers. */
static uint8_t
st506_read(uint16_t port, void *priv)
{
    hdc_t  *dev = (hdc_t *) priv;
    uint8_t ret = 0xff;

    switch (port & 3) {
        case 0: /* read data */
            dev->status &= ~STAT_IRQ;
            switch (dev->state) {
                case STATE_COMPLETION_BYTE:
                    ret         = dev->compl ;
                    dev->status = 0x00;
                    dev->state  = STATE_IDLE;
                    break;

                case STATE_SEND_DATA:
                    ret = dev->buff[dev->buff_pos++];
                    if (dev->buff_pos == dev->buff_cnt) {
                        dev->buff_pos = 0;
                        dev->buff_cnt = 0;
                        dev->status   = STAT_BSY;
                        dev->state    = STATE_SENT_DATA;
                        timer_set_delay_u64(&dev->timer, ST506_TIME);
                    }
                    break;

                default:
                    break;
            }
            break;

        case 1: /* read status */
            ret = dev->status;
            if ((dev->irq_dma & DMA_ENA) && dma_get_drq(dev->dma))
                ret |= STAT_DRQ;
            break;

        case 2: /* read option jumpers */
            ret = dev->switches;
            break;

        default:
            break;
    }
    st506_xt_log("ST506: read(%04x) = %02x\n", port, ret);

    return ret;
}

/* Write to one of the registers. */
static void
st506_write(uint16_t port, uint8_t val, void *priv)
{
    hdc_t *dev = (hdc_t *) priv;

    st506_xt_log("ST506: write(%04x, %02x)\n", port, val);
    switch (port & 3) {
        case 0: /* write data */
            switch (dev->state) {
                case STATE_RECEIVE_COMMAND: /* command data */
                    /* Write directly to the command buffer to avoid overwriting
                       the data buffer. */
                    dev->command[dev->buff_pos++] = val;
                    if (dev->buff_pos == dev->buff_cnt) {
                        /* We have a new command. */
                        dev->buff_pos = 0;
                        dev->buff_cnt = 0;
                        dev->status   = STAT_BSY;
                        dev->state    = STATE_START_COMMAND;
                        timer_set_delay_u64(&dev->timer, ST506_TIME);
                    }
                    break;

                case STATE_RECEIVE_DATA: /* data */
                    dev->buff[dev->buff_pos++] = val;
                    if (dev->buff_pos == dev->buff_cnt) {
                        dev->buff_pos = 0;
                        dev->buff_cnt = 0;
                        dev->status   = STAT_BSY;
                        dev->state    = STATE_RECEIVED_DATA;
                        timer_set_delay_u64(&dev->timer, ST506_TIME);
                    }
                    break;

                default:
                    break;
            }
            break;

        case 1: /* controller reset */
            dev->status = 0x00;
            break;

        case 2: /* generate controller-select-pulse */
            dev->status   = STAT_BSY | STAT_CD | STAT_REQ;
            dev->buff_pos = 0;
            dev->buff_cnt = sizeof(dev->command);
            dev->state    = STATE_RECEIVE_COMMAND;
            break;

        case 3: /* DMA/IRQ enable register */
            dev->irq_dma = val;

            if (!(dev->irq_dma & DMA_ENA))
                dma_set_drq(dev->dma, 0);

            if (!(dev->irq_dma & IRQ_ENA)) {
                dev->status &= ~STAT_IRQ;
                picintc(1 << dev->irq);
            }
            break;

        default:
            break;
    }
}

/* Write to ROM (or scratchpad RAM.) */
static void
mem_write(uint32_t addr, uint8_t val, void *priv)
{
    hdc_t   *dev = (hdc_t *) priv;
    uint32_t ptr;
    uint32_t mask = 0;

    /* Ignore accesses to anything below the configured address,
       needed because of the emulator's 4k mapping granularity. */
    if (addr < dev->bios_addr)
        return;

    addr -= dev->bios_addr;

    switch (dev->type) {
        case ST506_XT_TYPE_ST11M: /* ST-11M */
        case ST506_XT_TYPE_ST11R: /* ST-11R */
            mask = 0x1fff;        /* ST-11 decodes RAM on each 8K block */
            break;

        default:
            break;
    }

    addr &= dev->bios_rom.mask;

    ptr = (dev->bios_rom.mask & mask) - dev->bios_ram;
    if (mask && ((addr & mask) > ptr) && ((addr & mask) <= (ptr + dev->bios_ram)))
        dev->scratch[addr & (dev->bios_ram - 1)] = val;
}

static uint8_t
mem_read(uint32_t addr, void *priv)
{
    const hdc_t *dev = (hdc_t *) priv;
    uint32_t     ptr;
    uint32_t     mask = 0;
    uint8_t      ret = 0xff;

    /* Ignore accesses to anything below the configured address,
       needed because of the emulator's 4k mapping granularity. */
    if (addr < dev->bios_addr)
        return 0xff;

    addr -= dev->bios_addr;

    switch (dev->type) {
        case ST506_XT_TYPE_XEBEC: /* Xebec */
            if (addr >= 0x001000) {
#ifdef ENABLE_ST506_XT_LOG
                st506_xt_log("ST506: Xebec ROM access(0x%06lx)\n", addr);
#endif
                return 0xff;
            }
            break;

        case ST506_XT_TYPE_WDXT_GEN: /* WDXT-GEN */
            if (addr >= 0x002000) {
#ifdef ENABLE_ST506_XT_LOG
                st506_xt_log("ST506: WDXT-GEN ROM access(0x%06lx)\n", addr);
#endif
                return 0xff;
            }
            break;

        case ST506_XT_TYPE_DTC_5150X: /* DTC */
        default:
            if (addr >= 0x002000) {
#ifdef ENABLE_ST506_XT_LOG
                st506_xt_log("ST506: DTC-5150X ROM access(0x%06lx)\n", addr);
#endif
                return 0xff;
            }
            break;

        case ST506_XT_TYPE_ST11M: /* ST-11M */
        case ST506_XT_TYPE_ST11R: /* ST-11R */
            mask = 0x1fff;        /* ST-11 decodes RAM on each 8K block */
            break;
#if 0
        default:
                    break;
#endif
    }

    addr = addr & dev->bios_rom.mask;

    ptr = (dev->bios_rom.mask & mask) - dev->bios_ram;
    if (mask && ((addr & mask) > ptr) && ((addr & mask) <= (ptr + dev->bios_ram)))
        ret = dev->scratch[addr & (dev->bios_ram - 1)];
    else
        ret = dev->bios_rom.rom[addr];

    return ret;
}

/*
 * Set up and load the ROM BIOS for this controller.
 *
 * This is straightforward for most, but some (like the ST-11x)
 * map part of the area as scratchpad RAM, so we cannot use the
 * standard 'rom_init' function here.
 */
static void
loadrom(hdc_t *dev, const char *fn)
{
    uint32_t size;
    FILE    *fp;

    if (fn == NULL) {
#ifdef ENABLE_ST506_XT_LOG
        st506_xt_log("ST506: NULL BIOS ROM file pointer!\n");
#endif
        return;
    }

    if ((fp = rom_fopen((char *) fn, "rb")) == NULL) {
        st506_xt_log("ST506: BIOS ROM '%s' not found!\n", fn);
        return;
    }

    /* Initialize the ROM entry. */
    memset(&dev->bios_rom, 0x00, sizeof(rom_t));

    /* Manually load and process the ROM image. */
    (void) fseek(fp, 0L, SEEK_END);
    size = ftell(fp);
    (void) fseek(fp, 0L, SEEK_SET);

    /* Load the ROM data. */
    dev->bios_rom.rom = (uint8_t *) calloc(1, size);
    memset(dev->bios_rom.rom, 0xff, size);
    if (fread(dev->bios_rom.rom, 1, size, fp) != size)
        fatal("ST-506 XT loadrom(): Error reading data\n");
    (void) fclose(fp);

    /* Set up an address mask for this memory. */
    dev->bios_size     = size;
    dev->bios_rom.mask = (size - 1);

    /* Map this system into the memory map. */
    mem_mapping_add(&dev->bios_rom.mapping, dev->bios_addr, size,
                    mem_read, NULL, NULL, mem_write, NULL, NULL,
                    dev->bios_rom.rom, MEM_MAPPING_EXTERNAL, dev);
}

static void
loadhd(hdc_t *dev, int c, int d, UNUSED(const char *fn))
{
    drive_t *drive = &dev->drives[c];

    if (!hdd_image_load(d)) {
        drive->present = 0;
        return;
    }

    /* Make sure we can do this. */
    /* Allow 31 sectors per track on RLL controllers, for the
       ST225R, which is 667/2/31. */
    if ((hdd[d].spt != dev->spt) && (hdd[d].spt != 31) && (dev->spt != 26)) {
        /*
         * Uh-oh, MFM/RLL mismatch.
         *
         * Although this would be no issue in the code itself,
         * most of the BIOSes were hardwired to whatever their
         * native SPT setting was, so, do not allow this here.
         */
        st506_xt_log("ST506: drive%i: MFM/RLL mismatch (%i/%i)\n",
                     c, hdd[d].spt, dev->spt);
        hdd_image_close(d);
        drive->present = 0;
        return;
    }

    drive->spt    = (uint8_t) hdd[d].spt;
    drive->hpc    = (uint8_t) hdd[d].hpc;
    drive->tracks = (uint16_t) hdd[d].tracks;

    drive->hdd_num = d;
    drive->present = 1;
}

/* Set the "drive type" switches for the IBM Xebec controller. */
static void
set_switches(hdc_t *dev, hd_type_t *hdt, int num)
{
    const drive_t *drive;
    int            c;
    int            e;

    dev->switches = 0x00;

    for (uint8_t d = 0; d < MFM_NUM; d++) {
        drive = &dev->drives[d];

        if (!drive->present) {
            if (dev->type == ST506_XT_TYPE_WD1002A_WX1_NOBIOS)
                dev->switches |= (0x33 << (d ? 0 : 2));
            continue;
        }

        for (c = 0; c < num; c++) {
            /* Does the Xebec also support more than 4 types? */
            if ((drive->spt == hdt[c].spt) && (drive->hpc == hdt[c].hpc) && (drive->tracks == hdt[c].tracks)) {
                /* Olivetti M24/M240: Move the upper 2 bites up by 2 bits, as the
                   layout is as follows: D0_3 D0_2 D1_3 D1_2 D0_1 D0_0 D1_1 D1_0. */
                if (dev->type == ST506_XT_TYPE_WD1002A_WX1_NOBIOS)
                    e = (c & 0x03) | ((c >> 2) << 4);
                else
                    e = c;
                dev->switches |= (e << (d ? 0 : 2));
                break;
            }
        }

#ifdef ENABLE_ST506_XT_LOG
        st506_xt_log("ST506: ");
        if (c == num)
            st506_xt_log("*WARNING* drive%i unsupported", d);
        else
            st506_xt_log("drive%i is type %i", d, c);
        st506_xt_log(" (%i/%i/%i)\n", drive->tracks, drive->hpc, drive->spt);
#endif
    }
}

static void *
st506_init(const device_t *info)
{
    const char *fn = NULL;
    hdc_t      *dev;
    int         i;
    int         c;

    dev = (hdc_t *) calloc(1, sizeof(hdc_t));
    dev->type = info->local & 255;

    /* Set defaults for the controller. */
    dev->spt       = MFM_SECTORS;
    dev->base      = 0x0320;
    dev->irq       = 5;
    dev->dma       = 3;
    dev->bios_addr = 0xc8000;
    dev->nr_err    = ERR_NOT_READY;

    switch (dev->type) {
        case ST506_XT_TYPE_XEBEC: /* Xebec (MFM) */
            fn = XEBEC_BIOS_FILE;
            break;

        case ST506_XT_TYPE_WDXT_GEN: /* WDXT-GEN (MFM) */
            fn = WDXT_GEN_BIOS_FILE;
            break;

        case ST506_XT_TYPE_DTC_5150X: /* DTC5150 (MFM) */
            fn            = DTC_BIOS_FILE;
            dev->switches = 0xff;
            break;

        case ST506_XT_TYPE_ST11R: /* Seagate ST-11R (RLL) */
            dev->spt = RLL_SECTORS;
            fallthrough;

        case ST506_XT_TYPE_ST11M: /* Seagate ST-11M (MFM) */
            dev->nr_err   = ERR_NOT_AVAILABLE;
            dev->switches = 0x01; /* fixed */
            dev->misc     = device_get_config_int("revision");
            switch (dev->misc) {
                case 5: /* v1.7 */
                    fn = ST11_BIOS_FILE_OLD;
                    break;

                case 19: /* v2.0 */
                    fn = ST11_BIOS_FILE_NEW;
                    break;

                default:
                    break;
            }
            dev->base      = device_get_config_hex16("base");
            dev->irq       = device_get_config_int("irq");
            dev->bios_addr = device_get_config_hex20("bios_addr");
            dev->bios_ram  = 64; /* scratch RAM size */

            /*
             * Industrial Madness Alert.
             *
             * With the ST-11 controller, Seagate decided to act
             * like they owned the industry, and reserved the
             * first cylinder of a drive for the controller. So,
             * when the host accessed cylinder 0, that would be
             * the actual cylinder 1 on the drive, and so on.
             */
            dev->cyl_off = 1;
            break;

        case ST506_XT_TYPE_WD1002A_WX1: /* Western Digital WD1002A-WX1 (MFM) */
            dev->nr_err = ERR_NOT_AVAILABLE;
            fn          = WD1002A_WX1_BIOS_FILE;
            /* The switches are read in reverse: 0 = closed, 1 = open.
               Both open means MFM, 17 sectors per track. */
            dev->switches = 0x30; /* autobios */
            dev->base     = device_get_config_hex16("base");
            dev->irq      = device_get_config_int("irq");
            if (dev->irq == 2)
                dev->switches |= 0x80;
            dev->bios_addr = device_get_config_hex20("bios_addr");
            break;

        case ST506_XT_TYPE_WD1002A_WX1_NOBIOS: /* Western Digital WD1002A-WX1 (MFM, No BIOS) */
            /* Supported base addresses: 320h, 324h, 328h, 32Ch. */
            dev->nr_err = ERR_NOT_AVAILABLE;
            fn          = NULL;
            break;

        case ST506_XT_TYPE_WD1004A_WX1: /* Western Digital WD1004A-WX1 (MFM) */
            dev->nr_err = ERR_NOT_AVAILABLE;
            fn          = WD1004A_WX1_BIOS_FILE;
            /* The switches are read in reverse: 0 = closed, 1 = open.
               Both open means MFM, 17 sectors per track. */
            dev->switches = 0x30; /* autobios */
            dev->base     = device_get_config_hex16("base");
            dev->irq      = device_get_config_int("irq");
            if (dev->irq == 2)
                dev->switches |= 0x80;
            dev->bios_addr = device_get_config_hex20("bios_addr");
            break;

        case ST506_XT_TYPE_WD1002A_27X: /* Western Digital WD1002A-27X (RLL) */
            dev->nr_err = ERR_NOT_AVAILABLE;
            fn          = WD1002A_27X_BIOS_FILE;
            /* The switches are read in reverse: 0 = closed, 1 = open.
               Both closed means translate 26 sectors per track to 17,
               SW6 closed, SW5 open means 26 sectors per track. */
            dev->switches = device_get_config_int("translate") ? 0x00 : 0x10; /* autobios */
            dev->spt      = RLL_SECTORS;
            dev->base     = device_get_config_hex16("base");
            dev->irq      = device_get_config_int("irq");
            if (dev->irq == 2)
                dev->switches |= 0x80;
            dev->bios_addr = device_get_config_hex20("bios_addr");
            break;

        case ST506_XT_TYPE_WD1004_27X: /* Western Digital WD1004-27X (RLL) */
            dev->nr_err = ERR_NOT_AVAILABLE;
            fn          = WD1004_27X_BIOS_FILE;
            /* The switches are read in reverse: 0 = closed, 1 = open.
               Both closed means translate 26 sectors per track to 17,
               SW6 closed, SW5 open means 26 sectors per track. */
            dev->switches = device_get_config_int("translate") ? 0x00 : 0x10; /* autobios */
            dev->spt      = RLL_SECTORS;
            dev->base     = device_get_config_hex16("base");
            dev->irq      = device_get_config_int("irq");
            if (dev->irq == 2)
                dev->switches |= 0x80;
            dev->bios_addr = device_get_config_hex20("bios_addr");
            break;

        case ST506_XT_TYPE_WD1004A_27X: /* Western Digital WD1004A-27X (RLL) */
            dev->nr_err = ERR_NOT_AVAILABLE;
            fn          = WD1004A_27X_BIOS_FILE;
            /* The switches are read in reverse: 0 = closed, 1 = open.
               Both closed means translate 26 sectors per track to 17,
               SW6 closed, SW5 open means 26 sectors per track. */
            dev->switches = device_get_config_int("translate") ? 0x00 : 0x10; /* autobios */
            dev->spt      = RLL_SECTORS;
            dev->base     = device_get_config_hex16("base");
            dev->irq      = device_get_config_int("irq");
            if (dev->irq == 2)
                dev->switches |= 0x80;
            dev->bios_addr = device_get_config_hex20("bios_addr");
            break;

        case ST506_XT_TYPE_VICTOR_V86P: /* Victor V86P Fixed Disk Adapter (RLL) */
            fn = VICTOR_V86P_BIOS_FILE;
            break;

        case ST506_XT_TYPE_TOSHIBA_T1200: /* Toshiba T1200 */
            fn            = NULL;
            dev->base     = 0x01f0;
            dev->switches = 0x0c;
            break;

        default:
            break;
    }

    /* Load the ROM BIOS. */
    loadrom(dev, fn);

    /* Set up the I/O region. */
    io_sethandler(dev->base, 4,
                  st506_read, NULL, NULL, st506_write, NULL, NULL, dev);

    /* Add the timer. */
    timer_add(&dev->timer, st506_callback, dev, 0);

    st506_xt_log("ST506: %s (I/O=%03X, IRQ=%i, DMA=%i, BIOS @0x%06lX, size %lu)\n",
                 info->name, dev->base, dev->irq, dev->dma, dev->bios_addr, dev->bios_size);

    /* Load any drives configured for us. */
#ifdef ENABLE_ST506_XT_LOG
    st506_xt_log("ST506: looking for disks...\n");
#endif
    for (c = 0, i = 0; i < HDD_NUM; i++) {
        if ((hdd[i].bus_type == HDD_BUS_MFM) && (hdd[i].mfm_channel < MFM_NUM)) {
            st506_xt_log("ST506: disk '%s' on channel %i\n",
                         hdd[i].fn, hdd[i].mfm_channel);
            loadhd(dev, hdd[i].mfm_channel, i, hdd[i].fn);

            if (++c > MFM_NUM)
                break;
        }
    }
    st506_xt_log("ST506: %i disks loaded.\n", c);

    /* For the Xebec, set the switches now. */
    if (dev->type == ST506_XT_TYPE_XEBEC)
        set_switches(dev, (hd_type_t *) hd_types, 4);
    else if (dev->type == ST506_XT_TYPE_WD1002A_WX1_NOBIOS)
        set_switches(dev, (hd_type_t *) hd_types_olivetti, 16);

    /* Initial "active" drive parameters. */
    for (c = 0; c < MFM_NUM; c++) {
        dev->drives[c].cfg_cyl = dev->drives[c].tracks;
        dev->drives[c].cfg_hpc = dev->drives[c].hpc;
        dev->drives[c].cfg_spt = dev->drives[c].spt;
    }

    return dev;
}

static void
st506_close(void *priv)
{
    hdc_t         *dev = (hdc_t *) priv;
    const drive_t *drive;

    for (uint8_t d = 0; d < MFM_NUM; d++) {
        drive = &dev->drives[d];

        hdd_image_close(drive->hdd_num);
    }

    if (dev->bios_rom.rom != NULL) {
        free(dev->bios_rom.rom);
        dev->bios_rom.rom = NULL;
    }

    free(dev);
}

static int
xebec_available(void)
{
    return (rom_present(XEBEC_BIOS_FILE));
}

static int
wdxt_available(void)
{
    return (rom_present(WDXT_GEN_BIOS_FILE));
}

static int
dtc5150x_available(void)
{
    return (rom_present(DTC_BIOS_FILE));
}

static int
st11_m_available(void)
{
    return (rom_present(ST11_BIOS_FILE_OLD) && rom_present(ST11_BIOS_FILE_NEW));
}

static int
st11_r_available(void)
{
    return (rom_present(ST11_BIOS_FILE_OLD) && rom_present(ST11_BIOS_FILE_NEW));
}

static int
wd1002a_wx1_available(void)
{
    return (rom_present(WD1002A_WX1_BIOS_FILE));
}

static int
wd1002a_27x_available(void)
{
    return (rom_present(WD1002A_27X_BIOS_FILE));
}

static int
wd1004a_wx1_available(void)
{
    return (rom_present(WD1004A_WX1_BIOS_FILE));
}

static int
wd1004_27x_available(void)
{
    return (rom_present(WD1004_27X_BIOS_FILE));
}

static int
wd1004a_27x_available(void)
{
    return (rom_present(WD1004A_27X_BIOS_FILE));
}

static int
victor_v86p_available(void)
{
    return (rom_present(VICTOR_V86P_BIOS_FILE));
}

// clang-format off
static const device_config_t dtc_config[] = {
    {
        .name           = "bios_addr",
        .description    = "BIOS address",
        .type           = CONFIG_HEX20,
        .default_string = NULL,
        .default_int    = 0xc8000,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "Disabled", .value = 0x00000 },
            { .description = "C800H",    .value = 0xc8000 },
            { .description = "CA00H",    .value = 0xca000 },
            { .description = "D800H",    .value = 0xd8000 },
            { .description = "F400H",    .value = 0xf4000 },
            { .description = ""                           }
        },
        .bios           = { { 0 } }
    },
    { .name = "", .description = "", .type = CONFIG_END }
};

static const device_config_t st11_config[] = {
    {
        .name           = "base",
        .description    = "Address",
        .type           = CONFIG_HEX16,
        .default_string = NULL,
        .default_int    = 0x0320,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "320H", .value = 0x0320 },
            { .description = "324H", .value = 0x0324 },
            { .description = "328H", .value = 0x0328 },
            { .description = "32CH", .value = 0x032c },
            { .description = ""                      }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "irq",
        .description    = "IRQ",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 5,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "IRQ 2", .value = 2 },
            { .description = "IRQ 5", .value = 5 },
            { .description = ""                  }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "bios_addr",
        .description    = "BIOS address",
        .type           = CONFIG_HEX20,
        .default_string = NULL,
        .default_int    = 0xc8000,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "Disabled", .value = 0x00000 },
            { .description = "C800H",    .value = 0xc8000 },
            { .description = "D000H",    .value = 0xd0000 },
            { .description = "D800H",    .value = 0xd8000 },
            { .description = "E000H",    .value = 0xe0000 },
            { .description = ""                           }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "revision",
        .description    = "BIOS Revision",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 19,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "v1.7", .value =  5 },
            { .description = "v2.0", .value = 19 },
            { .description = ""                            }
        },
        .bios           = { { 0 } }
    },
    { .name = "", .description = "", .type = CONFIG_END }
};

static const device_config_t wd_config[] = {
    {
        .name           = "bios_addr",
        .description    = "BIOS address",
        .type           = CONFIG_HEX20,
        .default_string = NULL,
        .default_int    = 0xc8000,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "Disabled", .value = 0x00000 },
            { .description = "C800H",    .value = 0xc8000 },
            { .description = ""                           }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "base",
        .description    = "Address",
        .type           = CONFIG_HEX16,
        .default_string = NULL,
        .default_int    = 0x0320,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "320H", .value = 0x0320 },
            { .description = "324H", .value = 0x0324 },
            { .description = ""                      }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "irq",
        .description    = "IRQ",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 5,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "IRQ 2", .value = 2 },
            { .description = "IRQ 5", .value = 5 },
            { .description = ""                  }
        },
        .bios           = { { 0 } }
    },
    { .name = "", .description = "", .type = CONFIG_END }
};

static const device_config_t wd_nobios_config[] = {
    {
        .name           = "base",
        .description    = "Address",
        .type           = CONFIG_HEX16,
        .default_string = NULL,
        .default_int    = 0x0320,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "320H", .value = 0x0320 },
            { .description = "324H", .value = 0x0324 },
            { .description = ""                      }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "irq",
        .description    = "IRQ",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 5,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "IRQ 2", .value = 2 },
            { .description = "IRQ 5", .value = 5 },
            { .description = ""                  }
        },
        .bios           = { { 0 } }
    },
    { .name = "", .description = "", .type = CONFIG_END }
};

static const device_config_t wd_rll_config[] = {
    {
        .name           = "bios_addr",
        .description    = "BIOS address",
        .type           = CONFIG_HEX20,
        .default_string = NULL,
        .default_int    = 0xc8000,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "Disabled", .value = 0x00000 },
            { .description = "C800H",    .value = 0xc8000 },
            { .description = ""                           }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "base",
        .description    = "Address",
        .type           = CONFIG_HEX16,
        .default_string = NULL,
        .default_int    = 0x0320,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "320H", .value = 0x0320 },
            { .description = "324H", .value = 0x0324 },
            { .description = ""                      }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "irq",
        .description    = "IRQ",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 5,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "IRQ 2", .value = 2 },
            { .description = "IRQ 5", .value = 5 },
            { .description = ""                  }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "translate",
        .description    = "Translate 26 -> 17",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 0,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "Off", .value = 0 },
            { .description = "On",  .value = 1 },
            { .description = ""                }
        },
        .bios           = { { 0 } }
    },
    { .name = "", .description = "", .type = CONFIG_END }
};

static const device_config_t wd1004a_config[] = {
    {
        .name           = "bios_addr",
        .description    = "BIOS address",
        .type           = CONFIG_HEX20,
        .default_string = NULL,
        .default_int    = 0xc8000,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "Disabled", .value = 0x00000 },
            { .description = "C800H",    .value = 0xc8000 },
            { .description = ""                           }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "base",
        .description    = "Address",
        .type           = CONFIG_HEX16,
        .default_string = NULL,
        .default_int    = 0x0320,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "320H", .value = 0x0320 },
            { .description = "324H", .value = 0x0324 },
            { .description = ""                      }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "irq",
        .description    = "IRQ",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 5,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "IRQ 2", .value = 2 },
            { .description = "IRQ 5", .value = 5 },
            { .description = ""                  }
        },
        .bios           = { { 0 } }
    },
    { .name = "", .description = "", .type = CONFIG_END }
};

static const device_config_t wd1004_rll_config[] = {
    {
        .name           = "bios_addr",
        .description    = "BIOS address",
        .type           = CONFIG_HEX20,
        .default_string = NULL,
        .default_int    = 0xc8000,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "Disabled", .value = 0x00000 },
            { .description = "C800H",    .value = 0xc8000 },
            { .description = "CA00H",    .value = 0xca000 },
            { .description = "CC00H",    .value = 0xcc000 },
            { .description = "CE00H",    .value = 0xce000 },
            { .description = ""                           }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "base",
        .description    = "Address",
        .type           = CONFIG_HEX16,
        .default_string = NULL,
        .default_int    = 0x0320,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "320H", .value = 0x0320 },
            { .description = "324H", .value = 0x0324 },
            { .description = "328H", .value = 0x0328 },
            { .description = "32CH", .value = 0x032c },
            { .description = ""                      }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "irq",
        .description    = "IRQ",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 5,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "IRQ 2", .value = 2 },
            { .description = "IRQ 5", .value = 5 },
            { .description = ""                  }
        },
        .bios           = { { 0 } }
    },
    {
        .name           = "translate",
        .description    = "Translate 26 -> 17",
        .type           = CONFIG_SELECTION,
        .default_string = NULL,
        .default_int    = 0,
        .file_filter    = NULL,
        .spinner        = { 0 },
        .selection      = {
            { .description = "Off", .value = 0 },
            { .description = "On",  .value = 1 },
            { .description = ""                }
        },
        .bios           = { { 0 } }
    },
    { .name = "", .description = "", .type = CONFIG_END }
};

// clang-format on

const device_t st506_xt_xebec_device = {
    .name          = "IBM PC Fixed Disk Adapter (MFM)",
    .internal_name = "st506_xt",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_XEBEC,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = xebec_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t st506_xt_wdxt_gen_device = {
    .name          = "WDXT-GEN (MFM)",
    .internal_name = "st506_xt_gen",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_WDXT_GEN,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = wdxt_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t st506_xt_dtc5150x_device = {
    .name          = "DTC 5150X (MFM)",
    .internal_name = "st506_xt_dtc5150x",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_DTC_5150X,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = dtc5150x_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = dtc_config
};

const device_t st506_xt_st11_m_device = {
    .name          = "ST-11M (MFM)",
    .internal_name = "st506_xt_st11_m",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_ST11M,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = st11_m_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = st11_config
};

const device_t st506_xt_st11_r_device = {
    .name          = "ST-11R (RLL)",
    .internal_name = "st506_xt_st11_r",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_ST11R,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = st11_r_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = st11_config
};

const device_t st506_xt_wd1002a_wx1_device = {
    .name          = "WD1002A-WX1 (MFM)",
    .internal_name = "st506_xt_wd1002a_wx1",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_WD1002A_WX1,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = wd1002a_wx1_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = wd_config
};

const device_t st506_xt_wd1002a_wx1_nobios_device = {
    .name          = "WD1002A-WX1 (MFM) (No BIOS)",
    .internal_name = "st506_xt_wd1002a_wx1",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_WD1002A_WX1_NOBIOS,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = wd1002a_wx1_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = wd_nobios_config
};

const device_t st506_xt_wd1002a_27x_device = {
    .name          = "WD1002A-27X (RLL)",
    .internal_name = "st506_xt_wd1002a_27x",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_WD1002A_27X,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = wd1002a_27x_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = wd_rll_config
};

const device_t st506_xt_wd1004a_wx1_device = {
    .name          = "WD1004A-WX1 (MFM)",
    .internal_name = "st506_xt_wd1004a_wx1",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_WD1004A_WX1,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = wd1004a_wx1_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = wd1004a_config
};

const device_t st506_xt_wd1004_27x_device = {
    .name          = "WD1004-27X (RLL)",
    .internal_name = "st506_xt_wd1004_27x",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_WD1004_27X,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = wd1004_27x_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = wd1004_rll_config
};

const device_t st506_xt_wd1004a_27x_device = {
    .name          = "WD1004a-27X (RLL)",
    .internal_name = "st506_xt_wd1004a_27x",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_WD1004A_27X,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = wd1004a_27x_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = wd_rll_config
};

const device_t st506_xt_victor_v86p_device = {
    .name          = "Victor V86P Fixed Disk Adapter (RLL)",
    .internal_name = "st506_xt_victor_v86p",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_VICTOR_V86P,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = victor_v86p_available,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};

const device_t st506_xt_toshiba_t1200_device = {
    .name          = "Toshiba T1200 RLL Fixed Disk Adapter",
    .internal_name = "st506_xt_toshiba_t1200",
    .flags         = DEVICE_ISA,
    .local         = (HDD_BUS_MFM << 8) | ST506_XT_TYPE_TOSHIBA_T1200,
    .init          = st506_init,
    .close         = st506_close,
    .reset         = NULL,
    .available     = NULL,
    .speed_changed = NULL,
    .force_redraw  = NULL,
    .config        = NULL
};
