// license:BSD-3-Clause
// copyright-holders:68bit
/***************************************************************************
Western Digital WD1000 Winchester Disk Controller
***************************************************************************/
#include "emu.h"
#include "machine/wd1000.h"
//**************************************************************************
// DEVICE DEFINITIONS
//**************************************************************************
DEFINE_DEVICE_TYPE(WD1000, wd1000_device, "wd1000", "Western Digital WD1000 Winchester Disk Controller")
//**************************************************************************
// LIVE DEVICE
//**************************************************************************
//-------------------------------------------------
// wd1000_device - constructor
//-------------------------------------------------
wd1000_device::wd1000_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock)
: device_t(mconfig, WD1000, tag, owner, clock)
, m_intrq_cb(*this)
, m_drq_cb(*this)
, m_drives(*this, "%u", 0)
, m_sector_base(0)
, m_buffer_index(0)
, m_buffer_end(0)
, m_intrq(0)
, m_drq(0)
, m_stepping_rate(0x00)
, m_command(0x00)
, m_error(0x00)
, m_precomp(0x00)
, m_sector_count(0x00)
, m_sector_number(0x00)
, m_cylinder(0x0000)
, m_sdh(0x00)
, m_status(0x00)
{
}
void wd1000_device::set_sector_base(uint32_t base)
{
m_sector_base = base;
}
//-------------------------------------------------
// device_start - device-specific startup
//-------------------------------------------------
void wd1000_device::device_start()
{
// Allocate timers
m_seek_timer = timer_alloc(FUNC(wd1000_device::update_seek), this);
m_drq_timer = timer_alloc(FUNC(wd1000_device::delayed_drq), this);
// Empty buffer.
m_buffer_index = 0;
for (int i = 0; i < 4; i++)
m_drive_cylinder[i] = 0;
// Initialize the status as ready if the initial drive exists,
// and assume it has been restored so seek is complete.
if (m_drives[drive()] && m_drives[drive()]->exists())
m_status |= S_RDY | S_SC;
save_item(NAME(m_drive_cylinder));
save_item(NAME(m_sector_base));
save_item(NAME(m_buffer));
save_item(NAME(m_buffer_index));
save_item(NAME(m_buffer_end));
save_item(NAME(m_intrq));
save_item(NAME(m_drq));
save_item(NAME(m_stepping_rate));
save_item(NAME(m_command));
save_item(NAME(m_error));
save_item(NAME(m_precomp));
save_item(NAME(m_sector_count));
save_item(NAME(m_sector_number));
save_item(NAME(m_cylinder));
save_item(NAME(m_sdh));
save_item(NAME(m_status));
}
//-------------------------------------------------
// device_reset - device-specific reset
//-------------------------------------------------
void wd1000_device::device_reset()
{
m_buffer_index = 0;
m_sdh = 0x20;
for (int i = 0; i < 4; i++)
m_drive_cylinder[i] = 0;
if (m_drives[drive()] && m_drives[drive()]->exists())
m_status |= S_RDY | S_SC;
}
//-------------------------------------------------
// update_seek -
//-------------------------------------------------
TIMER_CALLBACK_MEMBER(wd1000_device::update_seek)
{
m_drive_cylinder[drive()] = param;
m_status |= S_SC;
switch (m_command >> 4)
{
case CMD_RESTORE:
cmd_restore();
break;
case CMD_SEEK:
cmd_seek();
break;
case CMD_READ_SECTOR:
cmd_read_sector();
break;
case CMD_WRITE_SECTOR:
cmd_write_sector();
break;
case CMD_WRITE_FORMAT:
cmd_format_sector();
break;
}
}
//-------------------------------------------------
// delayed_drq - set DRQ after a necessary delay
//-------------------------------------------------
TIMER_CALLBACK_MEMBER(wd1000_device::delayed_drq)
{
set_drq();
}
void wd1000_device::set_error(int error)
{
if (error)
{
m_error |= error;
m_status |= S_ERR;
}
else
{
m_error = 0;
m_status &= ~S_ERR;
}
}
void wd1000_device::set_intrq(int state)
{
if (m_intrq == 0 && state == 1)
{
m_intrq = 1;
m_intrq_cb(1);
}
else if (m_intrq == 1 && state == 0)
{
m_intrq = 0;
m_intrq_cb(0);
}
}
void wd1000_device::set_drq()
{
if ((m_status & S_DRQ) == 0)
{
m_status |= S_DRQ;
m_drq_cb(true);
}
}
void wd1000_device::drop_drq()
{
if (m_status & S_DRQ)
{
m_status &= ~S_DRQ;
m_drq_cb(false);
}
}
attotime wd1000_device::get_stepping_rate()
{
if (m_stepping_rate)
return attotime::from_usec(500 * m_stepping_rate);
else
return attotime::from_usec(10);
}
void wd1000_device::start_command()
{
m_status |= S_BSY;
set_error(0);
switch (m_command >> 4)
{
case CMD_RESTORE:
m_stepping_rate = m_command & 0x0f;
break;
case CMD_READ_SECTOR:
break;
case CMD_WRITE_SECTOR:
break;
case CMD_WRITE_FORMAT:
break;
case CMD_SEEK:
m_stepping_rate = m_command & 0x0f;
break;
}
}
void wd1000_device::end_command()
{
m_status &= ~S_BSY;
set_intrq(1);
}
int wd1000_device::get_lbasector()
{
harddisk_image_device *file = m_drives[drive()];
const auto &info = file->get_info();
int lbasector;
if (m_cylinder > info.cylinders)
{
logerror("%s: Unexpected cylinder %d for range 0 to %d\n", machine().describe_context(), m_cylinder, info.cylinders - 1);
}
if (head() >= info.heads)
{
logerror("%s: Unexpected head %d for range 0 to %d\n", machine().describe_context(), head(), info.heads - 1);
}
int16_t sector = m_sector_number - m_sector_base;
if (sector < 0 || sector >= info.sectors)
{
logerror("%s: Unexpected sector number %d for range %d to %d\n", machine().describe_context(), m_sector_number, m_sector_base, info.sectors + m_sector_base);
}
if (sector_bytes() != info.sectorbytes)
{
logerror("%s: Unexpected sector bytes %d, expected %d\n", machine().describe_context(), sector_bytes(), info.sectorbytes);
}
lbasector = m_cylinder;
lbasector *= info.heads;
lbasector += head();
lbasector *= info.sectors;
lbasector += sector;
return lbasector;
}
uint8_t wd1000_device::data_r()
{
uint8_t data = 0x00;
if (machine().side_effects_disabled())
return data;
drop_drq();
if (m_buffer_index >= m_buffer_end)
{
// Transfer has already completed.
logerror("%s: Unexpected buffer read at %d beyond tail %d\n", machine().describe_context(), m_buffer_index, m_buffer_end);
return data;
}
data = m_buffer[m_buffer_index++];
if (m_buffer_index == m_buffer_end)
{
// Tranfer completed.
if ((m_command >> 4) == CMD_READ_SECTOR)
{
uint8_t dma = BIT(m_command, 3);
if (dma)
{
set_intrq(1);
}
}
else
{
logerror("%s: Unexpected buffer read transfer for command %02x\n", machine().describe_context(), m_command);
}
}
else
{
// Continue the transfer. A delay is implemented here to avoid
// recursion in the DRQ handler.
m_drq_timer->adjust(attotime::from_usec(1));
}
return data;
}
uint8_t wd1000_device::read(offs_t offset)
{
if ((m_status & S_BSY) && offset != 7)
{
logerror("%s Unexpected register %d read while busy\n", machine().describe_context(), offset & 0x07);
return m_status;
}
switch (offset & 0x07)
{
case 0:
// Data register.
return data_r();
case 1:
// Error register:
// bit 7 bad block detect
// bit 6 CRC error, data field
// bit 5 CRC error, ID field
// bit 4 ID not found
// bit 3 unused
// bit 2 Aborted Command
// bit 1 TR000 (track zero) error
// bit 0 DAM not found
// The error register is only valid if the error bit in the
// status register is set
return m_error;
case 2:
// Sector count. This is only used in the format command where
// it is decremented to zero.
return m_sector_count;
case 3:
// Sector number.
return m_sector_number;
case 4:
// Cylinder low byte C0-C7
// This read clears DRQ. This is intended for the handling
// aborted sector reads that leave DRQ asserted.
if (!machine().side_effects_disabled())
{
set_intrq(0);
drop_drq();
}
return m_cylinder & 0xff;
case 5:
// Cylinder high byte C8-C9
return m_cylinder >> 8;
case 6:
// Size / head / drive
return m_sdh;
case 7:
// Status.
// bit 7 Busy
// bit 6 Ready
// bit 5 Write fault
// bit 4 Seek complete
// bit 3 Data request (DRQ)
// bit 2 -
// bit 1 -
// bit 0 Error
// return 'ready' + 'seek complete'
// If the busy bit is set then no other bits are valid!!!
//
// Reading the status register clears the interrupt.
//
// TODO should reading the status register while busy reset
// the interrupt?
if (!machine().side_effects_disabled())
set_intrq(0);
return m_status;
}
return 0x00;
}
void wd1000_device::data_w(uint8_t data)
{
drop_drq();
if (m_buffer_index >= m_buffer_end)
{
// Transfer has already completed.
logerror("%s: Unexpected buffer write at %d beyond tail %d\n", machine().describe_context(), m_buffer_index, m_buffer_end);
return;
}
m_buffer[m_buffer_index++] = data;
if (m_buffer_index == m_buffer_end)
{
// Tranfer completed.
if ((m_command >> 4) == CMD_WRITE_SECTOR ||
(m_command >> 4) == CMD_WRITE_FORMAT)
{
m_status |= S_BSY;
set_error(0);
// Implied seek
m_status &= ~S_SC;
int amount = abs(m_drive_cylinder[drive()] - m_cylinder);
int target = m_cylinder;
m_seek_timer->adjust(get_stepping_rate() * amount, target);
}
else
{
logerror("%s: Unexpected buffer write transfer for command %02x\n", machine().describe_context(), m_command);
}
}
else
{
// Continue the transfer
m_drq_timer->adjust(attotime::from_usec(1));
}
}
void wd1000_device::write(offs_t offset, uint8_t data)
{
switch (offset & 0x07)
{
case 0:
// Data register
data_w(data);
break;
case 1:
// Write precomp
m_precomp = data;
break;
case 2:
// Sector Count
m_sector_count = data;
break;
case 3:
// Sector Number
m_sector_number = data;
break;
case 4:
// Cylinder Low
m_cylinder = (m_cylinder & 0xff00) | (data << 0);
break;
case 5:
// Cylinder High
m_cylinder = (m_cylinder & 0x00ff) | (data << 8);
break;
case 6:
{
// Size / drive / head
// bit 7 : Honour error correction (WD-1001 only?)
// bit 6,5 : sector size (0: 256, 1: 512, 3: 128)
// bit 4,3 : drive select (0,1,2,3)
// bit 2,1,0: head select (0-7)
uint8_t drive = (data & 0x18) >> 3;
m_sdh = data;
// Update the drive ready flag in the status register which
// indicates the ready state for the selected drive.
// TODO should this also set the SC flag?
if (m_drives[drive] && m_drives[drive]->exists())
m_status |= S_RDY;
else
m_status &= ~S_RDY;
break;
}
case 7:
// Command register
// Load while the controller is not busy.
// Task file must have been loaded??
// Not if 'Seek complete' or 'Ready' are false, or 'Write fault' is true.
if (m_status & S_BSY)
{
logerror("%s: Unexpected command %02x issued when already busy with %02x\n", machine().describe_context(), data, m_command);
// TODO should it be ignored?
//return;
}
// Interrupts are cleared and the error bit is reset
// on receipt of a new command.
set_intrq(0);
set_error(0);
if (!(m_status & S_RDY))
{
logerror("%s: Unexpected command %02x when not ready\n", machine().describe_context(), m_command);
set_error(ERR_AC);
end_command();
}
else
{
m_command = data;
switch (m_command >> 4)
{
case CMD_RESTORE:
{
start_command();
// Schedule an implied seek.
m_status &= ~S_SC;
int amount = m_drive_cylinder[drive()];
int target = 0;
m_seek_timer->adjust(get_stepping_rate() * amount, target);
break;
}
case CMD_SEEK:
case CMD_READ_SECTOR:
{
start_command();
int amount = abs(m_drive_cylinder[drive()] - m_cylinder);
int target = m_cylinder;
m_seek_timer->adjust(get_stepping_rate() * amount, target);
break;
}
case CMD_WRITE_SECTOR:
case CMD_WRITE_FORMAT:
// These commands do not perform an implied
// seek until all the data has been
// transferred, so just start the transfer
// here.
m_buffer_index = 0;
m_buffer_end = 512;
set_drq();
break;
}
}
break;
}
}
void wd1000_device::cmd_restore()
{
end_command();
}
// These commands should only be reachable if the status flag S_RDY is
// true, and that should only occur if the m_drives element is there,
// so it is not necessary to guard that case in these functions.
void wd1000_device::cmd_read_sector()
{
harddisk_image_device *file = m_drives[drive()];
uint8_t dma = BIT(m_command, 3);
file->read(get_lbasector(), m_buffer);
m_buffer_index = 0;
m_buffer_end = 512;
m_status &= ~S_BSY;
set_drq();
if (!dma)
{
// Interrupt now, rather than at the end of the DMA transfer.
set_intrq(1);
}
}
void wd1000_device::cmd_write_sector()
{
harddisk_image_device *file = m_drives[drive()];
if (m_buffer_index != sector_bytes())
{
logerror("%s: Unexpected unfilled buffer on write, only %d or %d bytes filled\n", machine().describe_context(), m_buffer_index, sector_bytes());
}
file->write(get_lbasector(), m_buffer);
end_command();
}
void wd1000_device::cmd_format_sector()
{
harddisk_image_device *file = m_drives[drive()];
uint8_t buffer[512];
// The m_buffer appears to be loaded with an interleave table which is
// not used here. The sectors are zero filled.
for (int i = 0; i < m_sector_count; i++)
{
std::fill(std::begin(buffer), std::end(buffer), 0);
file->write(get_lbasector(), buffer);
}
m_sector_count = 0;
end_command();
}
void wd1000_device::cmd_seek()
{
end_command();
}