1
0
mirror of https://github.com/livingcomputermuseum/UniBone.git synced 2026-04-14 15:55:25 +00:00
Files
livingcomputermuseum.UniBone/10.02_devices/2_src/rp_drive.cpp
2020-04-02 02:55:33 +02:00

754 lines
22 KiB
C++

/*
rp_drive.cpp: Implementation of RP0X disks.
Copyright Vulcan Inc. 2020 via Living Computers: Museum + Labs, Seattle, WA.
Contributed under the BSD 2-clause license.
*/
#include <assert.h>
#include <memory>
using namespace std;
#include "logger.hpp"
#include "utils.hpp"
#include "rh11.hpp"
#include "massbus_rp.hpp"
#include "rp_drive.hpp"
rp_drive_c::rp_drive_c(rh11_c *controller, massbus_rp_c* bus, uint32_t driveNumber) :
storagedrive_c(controller),
_driveNumber(driveNumber),
_desiredCylinder(0),
_currentCylinder(0),
_desiredTrack(0),
_offset(0),
_ata(false),
_ned(false),
_rmr(false),
_ready(true),
_lst(false),
_aoe(false),
_iae(false),
_wle(false),
_pip(false),
_vv(false),
_workerState(WorkerState::Idle),
_workerWakeupCond(PTHREAD_COND_INITIALIZER),
_workerMutex(PTHREAD_MUTEX_INITIALIZER)
{
set_workers_count(1);
log_label = "RP";
SetDriveType("RP06");
_newCommand = { 0 };
_controller = controller;
_bus = bus;
}
rp_drive_c::~rp_drive_c()
{
if (file_is_open())
{
file_close();
}
}
void
rp_drive_c::Reset()
{
INFO ("Drive %d reset", _driveNumber);
// TODO: how to deal with the worker thread.
// In the case of a Controller Clear, for example,
// it needs to terminate its work immediately, which may
// not be possible if a DMA transfer is in progress.
// If we can make the above happen, it's reasonable to block
// here until the worker has returned to idle.
// (Maybe?)
}
// on_param_changed():
// Handles configuration parameter changes.
bool
rp_drive_c::on_param_changed(parameter_c *param)
{
// no own "enable" logic
if (&type_name == param)
{
return SetDriveType(type_name.new_value.c_str());
}
else if (&image_filepath == param)
{
// Try to open the image file.
if (file_open(image_filepath.new_value, true))
{
image_filepath.value = image_filepath.new_value;
// New image; this is analogous to a new pack being spun up.
// Set the Volume Valid bit so software knows things have changed.
_vv = true;
UpdateStatus(false, false);
return true;
}
return false;
}
return device_c::on_param_changed(param); // more actions (for enable)false;
}
//
// GetSectorSize():
// Returns the size, in bytes, of a single sector on this drive.
// This is either 512 or 576 bytes.
//
uint32_t
rp_drive_c::GetSectorSize()
{
//
// For the time being this is always 512 bytes.
//
return 512;
}
//
// IsPackLoaded():
// Indicates whether this drive has a pack loaded (i.e. has an image
// assigned to it)
//
bool
rp_drive_c::IsPackLoaded()
{
return file_is_open();
}
void
rp_drive_c::Select()
{
// Just force an update of status registers for this disk.
UpdateStatus(false, false);
}
void rp_drive_c::DoCommand(
uint16_t command)
{
FunctionCode function = static_cast<FunctionCode>((command & RP_FUNC) >> 1);
// check for GO bit; if unset we have nothing to do here,
if ((command & RP_GO) == 0)
{
return;
}
INFO ("RP function 0%o, unit %o", function, _driveNumber);
if (!_ready && FunctionCode::DriveClear != function)
{
// For now, halt -- This should never happen with valid code.
// After things are stable, this should set error bits.
FATAL("Unit %d - Command sent while not ready!", _driveNumber);
}
// TODO: when Error Summary bit is set in ER1, the drive will accept no commands
// apart from a DRIVE CLEAR command.
_ned = false;
_ata = false;
switch(static_cast<FunctionCode>(function))
{
case FunctionCode::Nop:
// Nothing.
UpdateStatus(true, false);
break;
case FunctionCode::Unload:
// Unsure how best to implement this:
// This puts the drive in STANDBY state, meaning the heads are
// retracted and the spindle powers down. The command doesn't actually *finish*
// until the pack is manually spun back up. This could be implemented
// by unloading the disk image and waiting until a new one is loaded.
// Right now I'm just treating it as a no-op, at least until I can find a good
// way to test it using real software.
INFO ("RP Unload");
_ata = true;
UpdateStatus(true, false);
break;
case FunctionCode::DriveClear:
// p. 2-11 of EK-RP056-MM-01:
// "The following registers and conditions within the DCL are cleared
// by this command:
// - Status Register (ATA and ERR status bits)
// - All three Error registers
// - Attention Summary Register
// - ECC Position and Pattern Registers
// - The Diagnostic Mode Bit
INFO ("RP Drive Clear");
_ata = false;
_rmr = false;
_ned = false;
UpdateStatus(false, false);
break;
case FunctionCode::Release:
INFO ("RP Release");
// This is a no-op, this only applies to dual-ported configurations,
// which we are not.
_ata = false;
UpdateStatus(false, false);
break;
case FunctionCode::ReadInPreset:
INFO ("RP Read-In Preset");
//
// "This command sets the VV (volume valid) bit, clears the desired
// sector/track address register, and clears the FMT, HCI, and ECI
// bits in the offset register. It is used to bootstrap the device."
//
_vv = true;
_ready = true;
_desiredSector = 0;
_desiredTrack = 0;
_offset = 0;
UpdateStatus(false, false);
break;
case FunctionCode::PackAcknowledge:
INFO ("RP Pack Acknowledge");
_vv = true;
UpdateStatus(false, false);
break;
case FunctionCode::ReadData:
case FunctionCode::WriteData:
case FunctionCode::WriteHeaderAndData:
case FunctionCode::ReadHeaderAndData:
case FunctionCode::Search:
case FunctionCode::Seek:
case FunctionCode::Recalibrate:
case FunctionCode::Offset:
case FunctionCode::ReturnToCenterline:
INFO ("RP Read/Write Data or head-motion command");
{
// If this drive is not connected, head-motion commands cannot be
// executed.
if (!IsConnected())
{
_ata = true;
_ned = true;
UpdateStatus(true, false);
return;
}
// Clear DRY.
_ready = false;
if (function == FunctionCode::Search ||
function == FunctionCode::Seek ||
function == FunctionCode::Recalibrate ||
function == FunctionCode::Offset ||
function == FunctionCode::ReturnToCenterline)
{
// Positioning in progress.
_pip = true;
}
else
{
// This is a data transfer command,
// let the controller know so it can clear its READY bit
// for the duration.
_controller->StartDataTransfer();
}
UpdateStatus(false, false);
pthread_mutex_lock(&_workerMutex);
// Sanity check: no other command should be executing on this drive
// right now.
if (_newCommand.ready)
{
FATAL("Command o%o sent while worker for drive %d is busy!",
function, _driveNumber);
}
// Save a copy of command data for the worker to consume
_newCommand.function = function;
_newCommand.bus_address = _controller->GetBusAddress();
_newCommand.word_count = _controller->GetWordCount();
_newCommand.ready = true;
// Wake the worker
pthread_cond_signal(&_workerWakeupCond);
pthread_mutex_unlock(&_workerMutex);
INFO ("Command queued for worker.");
}
break;
default:
FATAL("Unimplemented RP function.");
break;
}
}
// Background worker function
void
rp_drive_c::worker(unsigned instance)
{
UNUSED(instance);
// worker_init_realtime_priority(rt_device);
_workerState = WorkerState::Idle;
WorkerCommand command = { 0 };
timeout_c timeout;
INFO ("rp_drive %d worker started.", _driveNumber);
while (!workers_terminate)
{
switch(_workerState)
{
case WorkerState::Idle:
// Wait for work.
pthread_mutex_lock(&_workerMutex);
while (!_newCommand.ready)
{
pthread_cond_wait(
&_workerWakeupCond,
&_workerMutex);
}
// Make a local copy of the new command
command = _newCommand;
pthread_mutex_unlock(&_workerMutex);
_workerState = WorkerState::Execute;
break;
case WorkerState::Execute:
{
INFO ("Worker executing function o%o", command.function);
switch(command.function)
{
case FunctionCode::ReadHeaderAndData:
case FunctionCode::ReadData:
{
INFO ("Read wc %d", command.word_count);
if (FunctionCode::ReadHeaderAndData == command.function)
{
// Per EK-RP056-MM-01 a READ HEADER AND DATA command
// is functionally identical to a READ DATA command
// except four extra words are sent prior to the sector data.
// These are:
// 1. Cylinder address and format bit:
// TODO: where is this format bit?
// 2. Sector/Track address:
// bits 0-4 specify sector, bits 8-12 indicate track.
// (i.e identical to the DA register format)
// 3, 4. Key Field - programmer defined, at format time.
// We return a header that matches the expected disk address.
uint16_t* header = new uint16_t[4];
assert(header);
header[0] = _desiredCylinder;
header[1] = _desiredSector | (_desiredTrack << 8);
header[2] = 0;
header[3] = 0;
_controller->DiskReadTransfer(
command.bus_address,
std::min(4, command.word_count),
header);
command.bus_address += 8;
command.word_count -= 4;
delete[] header;
}
// Any words left?
if (command.word_count > 0)
{
uint16_t* buffer = nullptr;
if (Read(
command.word_count,
&buffer))
{
//
// Data read: do DMA transfer to memory.
//
_controller->DiskReadTransfer(
command.bus_address,
command.word_count,
buffer);
// Free buffer
delete[] buffer;
}
else
{
// Read failed:
INFO ("Read failed.");
_ata = true;
}
}
_workerState = WorkerState::Finish;
}
break;
case FunctionCode::WriteData:
{
//
// Data write: do DMA transfer from memory.
//
uint16_t* buffer = _controller->DiskWriteTransfer(
command.bus_address,
command.word_count);
if (!buffer || !Write(
command.word_count,
buffer))
{
// Write failed:
INFO ("Write failed.");
_ata = true;
}
delete[] buffer;
_workerState = WorkerState::Finish;
}
break;
case FunctionCode::Search:
{
if (!Search())
{
// Search failed
INFO ("Search failed");
}
// Return to ready state, set attention bit.
_ata = true;
_workerState = WorkerState::Finish;
}
break;
case FunctionCode::Recalibrate:
INFO ("RECALIBRATE");
// Treat a Recal as a seek to zero.
_desiredCylinder = 0;
// Fall through to seek.
case FunctionCode::Seek:
if (!SeekTo())
{
// Seek failed
INFO ("Seek failed");
}
_ata = true;
_workerState = WorkerState::Finish;
break;
case FunctionCode::Offset:
case FunctionCode::ReturnToCenterline:
// We don't support adjusting the head position between
// cylinders so these are both effectively no-ops, but
// they're no-ops that need to take a small amount of time
// to complete.
INFO ("OFFSET/RETURN TO CL");
timeout.wait_ms(10);
_ata = true;
_workerState = WorkerState::Finish;
break;
default:
FATAL("Unimplemented drive function %d", command.function);
break;
}
}
break;
case WorkerState::Finish:
_workerState = WorkerState::Idle;
// Drive is ready again.
_ready = true;
_pip = false;
pthread_mutex_lock(&_workerMutex);
_newCommand.ready = false;
pthread_mutex_unlock(&_workerMutex);
UpdateStatus(true, false);
break;
}
}
}
void
rp_drive_c::ForceDiagnosticError()
{
UpdateStatus(false, true);
}
//
// Register update functions
//
void
rp_drive_c::UpdateStatus(
bool complete,
bool diagForceError)
{
uint16_t error1 =
(_rmr ? 04 : 0) |
(_aoe ? 01000 : 0) |
(_iae ? 02000 : 0) |
(_wle ? 04000 : 0);
bool err = false;
if (error1 != 0)
{
err = true;
_ata = true;
}
else if (diagForceError)
{
_ata = true;
}
uint16_t status =
(_vv ? 0100 : 0) |
(_ready ? 0200 : 0) |
(0400) | // Drive preset -- always set for a single-controller disk
(_lst ? 02000 : 0) | // Last sector read
(IsWriteLocked() ? 04000 : 0) | // Write lock
(IsPackLoaded() ? 010000 : 0) | // Medium online
(_pip ? 020000 : 0) | // PIP
(err ? 040000 : 0) | // Composite error
(_ata ? 0100000 : 0);
INFO ("Unit %d error1: o%o", _driveNumber, error1);
INFO ("Unit %d Status: o%o", _driveNumber, status);
// Inform controller of status update.
_bus->DriveStatus(_driveNumber, status, error1, _ata, complete);
}
bool
rp_drive_c::SeekTo()
{
// TODO: delay by appropriate amount
timeout_c timeout;
_iae = !(_desiredCylinder < _driveInfo.Cylinders);
if (IsConnected() && IsPackLoaded() && !_iae)
{
timeout.wait_ms(20);
_currentCylinder = _desiredCylinder;
return true;
}
else
{
return false; // no good
}
}
//
// TODO: on all reads/writes, an implied seek takes place if the
// specified cylinder is not the current cylinder.
//
//
// Writes the specified number of words from the provided buffer,
// starting at the specified C/H/S address.
//
bool
rp_drive_c::Write(
size_t countInWords,
uint16_t* buffer)
{
_iae = !ValidateCHS(_desiredCylinder, _desiredTrack, _desiredSector);
_wle = IsWriteLocked();
// TODO: handle address overflow
if (!IsConnected() || !IsPackLoaded() || _iae || _wle)
{
return false;
}
else
{
_currentCylinder = _desiredCylinder;
uint32_t offset = GetSectorForCHS(_currentCylinder, _desiredTrack, _desiredSector);
file_write(reinterpret_cast<uint8_t*>(buffer), offset * GetSectorSize(), countInWords * 2);
timeout_c timeout;
timeout.wait_us(2500);
return true;
}
}
//
// Reads the specifed number of words starting at the specified logical
// block. Returns a pointer to a buffer containing the data read.
// Caller is responsible for freeing this buffer.
//
bool
rp_drive_c::Read(
size_t countInWords,
uint16_t** buffer)
{
_iae = !ValidateCHS(_desiredCylinder, _desiredTrack, _desiredSector);
_wle = false;
if (!IsConnected() || !IsPackLoaded() || _iae)
{
*buffer = nullptr;
INFO ("Failure: connected %d loaded %d valid %d", IsConnected(), IsPackLoaded(), _iae);
return false;
}
else
{
_currentCylinder = _desiredCylinder;
*buffer = new uint16_t[countInWords];
assert(nullptr != *buffer);
uint32_t offset = GetSectorForCHS(_currentCylinder, _desiredTrack, _desiredSector);
INFO ("Read from sector offset o%o", offset);
file_read(reinterpret_cast<uint8_t*>(*buffer), offset * GetSectorSize(), countInWords * 2);
timeout_c timeout;
timeout.wait_us(2500);
return true;
}
}
bool
rp_drive_c::Search(void)
{
_iae = !ValidateCHS(_desiredCylinder, _desiredTrack, _desiredSector);
if (!IsConnected() || !IsPackLoaded() || _iae)
{
INFO ("Failure: connected &d loaded %d valid %d", IsConnected(), IsPackLoaded(), _iae);
return false;
}
else
{
// This is just a no-op, as we don't emulate read errors. We just delay a tiny bit.
timeout_c timeout;
INFO ("Search commencing.");
timeout.wait_ms(20);
INFO ("Search completed.");
_currentCylinder = _desiredCylinder;
return true;
}
}
uint32_t
rp_drive_c::GetSectorForCHS(
uint32_t cylinder,
uint32_t track,
uint32_t sector)
{
return (cylinder * _driveInfo.Tracks * _driveInfo.Sectors) +
(track * _driveInfo.Sectors) +
sector;
}
//
// Updates the capacity parameter of the drive based on the block count and block size.
//
void
rp_drive_c::UpdateCapacity()
{
// TODO: implement
}
bool
rp_drive_c::ValidateCHS(
uint32_t cylinder,
uint32_t track,
uint32_t sector)
{
if (cylinder >= _driveInfo.Cylinders ||
track >= _driveInfo.Tracks ||
sector >= _driveInfo.Sectors)
{
return false; // Out of range
}
else
{
return true;
}
}
//
//
// SetDriveType():
// Updates this drive's type to the specified type (i.e.
// RP05 or RM80).
// If the specified type is not found in our list of known
// drive types, the drive's type is not changed and false
// is returned.
//
bool
rp_drive_c::SetDriveType(
const char* typeName)
{
//
// Search through drive data table for name,
// and if valid, set the type appropriately.
//
int index = 0;
while (g_driveTable[index].Cylinders != 0)
{
if (!strcasecmp(typeName, g_driveTable[index].TypeName))
{
_driveInfo = g_driveTable[index];
type_name.value = _driveInfo.TypeName;
UpdateCapacity();
return true;
}
index++;
}
// Not found
return false;
}
//
// on_power_changed():
// Handle power change notifications.
//
void
rp_drive_c::on_power_changed(void)
{
}
//
// on_init_changed():
// Handle INIT signal.
void
rp_drive_c::on_init_changed(void)
{
}