1
0
mirror of https://github.com/livingcomputermuseum/UniBone.git synced 2026-01-28 12:49:08 +00:00
Files
livingcomputermuseum.UniBone/10.02_devices/2_src/rl0102.cpp
Joerg Hoppe 974aeed8eb Big summer rework:
Interrupt and DMA system now handles multiple levels and multiple devices in parallel
Interrupt Register changes synced with INTR transaction
DL11 and KW11 clock pass the ZDLDI0 diagnostic.
Devices can now be enabled and disabled individually.
2019-08-02 16:45:54 +02:00

690 lines
22 KiB
C++

/* rl02.cpp: implementation of RL01/RL02 disk drive, attached to RL11 controller
Copyright (c) 2018, Joerg Hoppe
j_hoppe@t-online.de, www.retrocmp.com
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
JOERG HOPPE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12-nov-2018 JH entered beta phase
*/
#include <assert.h>
using namespace std;
#include "logger.hpp"
#include "utils.hpp"
#include "rl11.hpp"
#include "rl0102.hpp"
RL0102_c::RL0102_c(storagecontroller_c *controller) :
storagedrive_c(controller) {
log_label = "RL0102"; // to be overwritten by RL11 on create
status_word = 0;
set_type(2); // default: RL02
runstop_button.value = false; // force user to load file assume drive is LOAD
fault_lamp.value = false;
cover_open.value = false;
}
// return false, if illegal parameter value.
// verify "new_value", must output error messages
bool RL0102_c::on_param_changed(parameter_c *param) {
if (param == &enabled) {
if (!enabled.new_value) {
// disable switches power OFF.
// must be power on by caller or user after enable
power_switch.value = false;
change_state(RL0102_STATE_power_off);
}
} else if (param == &type_name) {
if (!strcasecmp(type_name.new_value.c_str(), "RL01"))
set_type(1);
else if (!strcasecmp(type_name.new_value.c_str(), "RL02"))
set_type(2);
else {
// throw bad_parameter_check("drive type must be RL01 or RL02") ;
ERROR("drive type must be RL01 or RL02");
return false;
}
}
return storagedrive_c::on_param_changed(param); // more actions (for enable)
}
void RL0102_c::set_type(uint8_t drivetype) {
this->drivetype = drivetype;
switch (drivetype) {
case 1:
cylinder_count = 256;
head_count = 2;
sector_count = 40;
type_name.value = "RL01";
break;
case 2:
cylinder_count = 512;
head_count = 2;
sector_count = 40;
type_name.value = "RL02";
break;
}
block_count = cylinder_count * head_count * sector_count;
sector_size_bytes = block_size_bytes = 256; // in byte
capacity.value = block_size_bytes * block_count;
}
/* CRC16 as implemented by the DEC 9401 chip
* simh/PDP11\pdp11_rl.c
*/
uint16_t RL0102_c::calc_crc(const int wc, const uint16_t *data) {
uint32_t crc, j, d;
int32_t i;
crc = 0;
for (i = 0; i < wc; i++) {
d = *data++;
/* cribbed from KG11-A */
for (j = 0; j < 16; j++) {
crc = (crc & ~01) | ((crc & 01) ^ (d & 01));
crc = (crc & 01) ? (crc >> 1) ^ 0120001 : crc >> 1;
d >>= 1;
}
}
return (uint16_t) crc;
}
void RL0102_c::on_power_changed(void) {
// called at high priority.
// mutex?
if (power_down) {
// FAULT lamp, while RL11 evals DC_LO
// power-on defaults
change_state( RL0102_STATE_power_off);
update_status_word(drive_ready_line, true); // RL11 starts with error
}
}
// if seeking or ontrack: retrack head to 0
void RL0102_c::on_init_changed(void) {
// called at high priority.
// mutex?
if (init_asserted) {
// seems not to retract head on INIT
/*
if (state.value == RL0102_STATE_seek || state.value == RL0102_STATE_lock_on) {
seek_destination_cylinder = 0;
seek_destination_head = 0;
change_state( RL0102_STATE_seek) ;
}
*/
// clear_error_register(); !NO! voume check remains
}
}
// seek is only possible if ready
bool RL0102_c::cmd_seek(unsigned destination_cylinder, unsigned destination_head) {
assert(destination_cylinder < cylinder_count); // RL11 must calc correct #
if (state.value != RL0102_STATE_lock_on) {
// if (state.value != RL0102_STATE_seek && state.value != RL0102_STATE_lock_on) {
WARNING("Drive seek to cyl.head=%d.%d failed, wrong state %d!", destination_cylinder,
destination_head, state.value);
return false; // illegal state
}
DEBUG("Drive start seek from cyl.head %d.%d to %d.%d", cylinder, head, destination_cylinder,
destination_head);
// seek may stop running seek ?
// worker_mutex.lock() ;
this->seek_destination_cylinder = destination_cylinder;
this->seek_destination_head = destination_head; // time needed to center on track
head = 0xff; // invalid, to get extra seek time
// RL11 must see "READY=false" immediately
update_status_word(/*drive_ready_line*/false, drive_error_line);
change_state(RL0102_STATE_seek);
// worker_mutex.unlock() ;
return true;
}
// separate proc, to have a testpoint
void RL0102_c::change_state(unsigned new_state) {
unsigned old_state = state.value;
uint16_t old_status_word = status_word;
// TODO: only in update_status_word() the visible state of state of ready line and error line should be set.!
//renome "update_status_word()" to "update_visible_status()"
state.value = new_state;
update_status_word(); // contains state
if (old_state != new_state)
DEBUG("Change drive %s state from %d to %d. Status word %06o -> %06o.",
name.value.c_str(), old_state, state.value, old_status_word, status_word);
}
/*** state functions, called repeatedly ***/
void RL0102_c::state_power_off() {
// drive_ready_line = false; // verified
// drive_error_line = true; // real RL02: RL11 show a DRIVE ERROR after power on / DC_LO
type_name.readonly = false; // may be changed between RL01/RL02
volume_check = true; // starts with volume check?
cover_open.readonly = true;
update_status_word(/*drive_ready_line*/false, /*drive_error_line*/true);
ready_lamp.value = false;
load_lamp.value = false;
fault_lamp.value = false;
writeprotect_lamp.value = false;
// image_filepath.readonly = true ; // "door locked", disk can not be changed
image_filepath.readonly = false; // don't be so complicated
if (power_switch.value == true)
change_state(RL0102_STATE_load_cartridge);
state_timeout.wait_ms(100);
}
// drive stop, door unlocked, cartridge can be loaded
void RL0102_c::state_load_cartridge() {
// drive_ready_line = false; // verified
type_name.readonly = true; // must be powered of to changed between RL01/RL02
update_status_word(/*drive_ready_line*/false, drive_error_line);
load_lamp.value = 1;
ready_lamp.value = 0;
writeprotect_lamp.value = writeprotect_button.value;
cover_open.readonly = false; // can be changed ("opened") only in LOAD state
// only in "load" state may the "media" be changed
image_filepath.readonly = false;
// Path to FAULT: try to load illegal file
// Path out of FAULT: File OK, or RUN runstop_button released
// FAULT => state is "load cartridge"
if (runstop_button.value == true && cover_open.value == false) {
// LOAD released: start spinning, if file image OK
// this test is repeated endlessly if button pressed and filename illegal
if (file_open(image_filepath.value, /*create*/true)) {
fault_lamp.value = false;
change_state(RL0102_STATE_spin_up);
return; // no wait
} else {
if (!fault_lamp.value) // error message only once
ERROR("Could not open/create file \"%s\".", image_filepath.value.c_str());
fault_lamp.value = true; // disables effect of runstop button
}
} else {
// load cartridge: unlock file
fault_lamp.value = false;
if (file_is_open())
file_close();
}
state_timeout.wait_ms(100);
}
void RL0102_c::state_spin_up() {
unsigned calcperiod_ms = 100;
// change of rpm in 0.1 secs
unsigned rpm_increment = full_rpm / (time_spinup_sec * (1000 / calcperiod_ms));
volume_check = true; // SIMH RLDS_VCK
cover_open.readonly = true; // can be changed ("opened") only in LOAD state
// drive_ready_line = false; // verified
update_status_word(/*drive_ready_line*/false, drive_error_line);
if (runstop_button.value == false || fault_lamp.value == true) { // stop spinning
change_state(RL0102_STATE_spin_down);
return;
}
rpm_increment *= emulation_speed.value;
INFO("Spin up drive speed = %d", rotation_umin.value);
rotation_umin.value += rpm_increment;
if (rotation_umin.value > full_rpm) {
rotation_umin.value = full_rpm;
cylinder = 0;
change_state(RL0102_STATE_brush_cycle);
return;
}
load_lamp.value = 0;
ready_lamp.value = 0;
writeprotect_lamp.value = writeprotect_button.value || file_readonly;
image_filepath.readonly = true; // "door locked", disk can not be changed
state_timeout.wait_ms(calcperiod_ms);
}
void RL0102_c::state_brush_cycle() {
// a real brush was used only on early RL01
// drive_ready_line = false ;
update_status_word(/*drive_ready_line*/false, drive_error_line);
state_timeout.wait_ms(100);
change_state(RL0102_STATE_load_heads);
}
void RL0102_c::state_load_heads() {
// drive_ready_line = false ;
update_status_word(/*drive_ready_line*/false, drive_error_line);
state_timeout.wait_ms(time_heads_out_ms);
cylinder = 0;
this->seek_destination_cylinder = 0;
this->seek_destination_head = 0; // time needed to center on track
head = 0xff; // invalid, to get extra seek time
// now perform a "guard band seek": seek head 0, track 0
change_state(RL0102_STATE_seek);
// next_sector_segment_under_heads = 0; // init rotation angle
next_sector_segment_under_heads = 12; // next header is 6
}
// DEC: seek = 100ms for 512/256 tracks
void RL0102_c::state_seek() {
// drive_ready_line = false;
update_status_word(/*drive_ready_line*/false, drive_error_line);
unsigned calcperiod_ms = 10;
// head can pass this much tracks per loop
// calc for RL02
unsigned trackmove_increment = 512 * calcperiod_ms / 100;
unsigned trackmove_time_ms; // time for increment seek or part of it
if (drivetype == 1)
trackmove_increment /= 2; // RL01 tracks are wider apart
// trackmove_increment *= emulation_speed.value;
if (runstop_button.value == false || fault_lamp.value == true) { // stop spinning
change_state(RL0102_STATE_spin_down);
return;
}
load_lamp.value = 0;
ready_lamp.value = 0;
writeprotect_lamp.value = writeprotect_button.value || file_readonly;
// need delay for head search (ZRLI, test 9)
// here done BEFORE cylinder search ...
// set cur head to "invalid" to get the extra head seek time
if (seek_destination_head != head) {
head = seek_destination_head;
// wait for .. say .. 3 sector passes
// unsigned sector_time_us = (60 * MILLION) / (full_rpm * sector_count); // = 625us
// trackmove_time_ms = (5 * sector_time_us) / 1000;
// ZRLJ test 1: any seek > 3ms
trackmove_time_ms = 5;
state_timeout.wait_ms(trackmove_time_ms); // must be > 0!
DEBUG("Seek: head switch to %d", head);
return;
}
// cylinder changes, velocity mode
trackmove_time_ms = calcperiod_ms; // default: max movement
if (seek_destination_cylinder > cylinder) {
DEBUG("drive seeking outward, cyl = %d", cylinder);
cylinder += trackmove_increment;
if (cylinder >= seek_destination_cylinder) {
// seek head outward finished
// proportionally reduced seek time
cylinder = seek_destination_cylinder;
trackmove_time_ms = calcperiod_ms * (seek_destination_cylinder - cylinder)
/ trackmove_increment;
DEBUG("drive seek outwards complete, cyl = %d", cylinder);
// DEBUG("Seek: trackmove_time_ms =%d", trackmove_time_ms);
state_timeout.wait_ms(trackmove_time_ms);
change_state(RL0102_STATE_lock_on);
} else
state_timeout.wait_ms(trackmove_time_ms);
} else {
// seek head inwards
if ((cylinder - seek_destination_cylinder) <= trackmove_increment) {
// proportionally reduced seek time
trackmove_time_ms = calcperiod_ms * (cylinder - seek_destination_cylinder)
/ trackmove_increment;
cylinder = seek_destination_cylinder;
DEBUG("drive seek inwards complete, cyl = %d", cylinder);
// DEBUG("Seek: trackmove_time_ms =%d", trackmove_time_ms);
state_timeout.wait_ms(trackmove_time_ms);
change_state(RL0102_STATE_lock_on);
return;
} else {
DEBUG("drive seeking inwards, cyl = %d", cylinder);
cylinder -= trackmove_increment;
state_timeout.wait_ms(trackmove_time_ms);
}
}
}
void RL0102_c::state_lock_on() {
if (runstop_button.value == false || fault_lamp.value == true) { // stop spinning
change_state(RL0102_STATE_unload_heads);
return;
}
// drive_ready_line = true;
update_status_word(/*drive_ready_line*/true, drive_error_line);
load_lamp.value = 0;
ready_lamp.value = 1;
writeprotect_lamp.value = writeprotect_button.value || file_readonly;
// fast polling, if ZRLI tests time of 0 cly seek with head switch
state_timeout.wait_ms(1);
// state_wait_ms = 100 ;
}
void RL0102_c::state_unload_heads() {
drive_ready_line = false;
state_timeout.wait_ms(time_heads_out_ms);
change_state(RL0102_STATE_spin_down);
}
void RL0102_c::state_spin_down() {
unsigned calcperiod_ms = 100;
// change of rpm in 0.1 secs
unsigned rpm_increment = full_rpm / (time_spinup_sec * (1000 / calcperiod_ms));
rpm_increment *= emulation_speed.value;
// drive_ready_line = false; // verified
update_status_word(/*drive_ready_line*/false, drive_error_line);
INFO("Spin down drive speed = %d", rotation_umin.value);
if (rotation_umin.value <= rpm_increment) {
rotation_umin.value = 0;
change_state(RL0102_STATE_load_cartridge);
return;
} else
rotation_umin.value -= rpm_increment;
load_lamp.value = 0;
ready_lamp.value = 0;
writeprotect_lamp.value = writeprotect_button.value || file_readonly;
image_filepath.readonly = true; // "door locked", disk can not be changed
state_timeout.wait_ms(calcperiod_ms);
}
// clear volatile error conditions in status word
void RL0102_c::clear_error_register(void) {
error_wge = false;
volume_check = false;
update_status_word(drive_ready_line, /*drive_error_line*/false);
}
// return drive status word for controller MP registers
void RL0102_c::update_status_word(bool new_drive_ready_line, bool new_drive_error_line) {
uint16_t tmp = 0;
if (state.value != RL0102_STATE_power_off)
tmp |= state.value;
if (state.value != RL0102_STATE_brush_cycle)
tmp |= RL0102_STATUS_BH; // brush home
if (state.value == RL0102_STATE_load_heads || state.value == RL0102_STATE_seek
|| state.value == RL0102_STATE_lock_on)
tmp |= RL0102_STATUS_HO; // heads out
if (cover_open.value == true)
tmp |= RL0102_STATUS_CO;
if (head == 1) // which head is selected after last seek/read/write?
tmp |= RL0102_STATUS_HS;
if (drivetype == 2)
tmp |= RL0102_STATUS_DT; // rl02
/* OPI on RL11 controller
if (state.value == RL0102_STATE_power_off) {
tmp |= RL0102_STATUS_DSE; // drive select
drive_error_line = true;
}
*/
if (volume_check) {
tmp |= RL0102_STATUS_VC;
new_drive_error_line = true; // VC is error, tested on real RL02
}
if (error_wge) {
tmp |= RL0102_STATUS_WGE; // write not possible
new_drive_error_line = true;
}
if (file_readonly || writeprotect_button.value == true) {
// writeprotect_lamp.value = true ; not here!!
tmp |= RL0102_STATUS_WL;
}
// notify the RL11 CSR?
if (new_drive_ready_line != drive_ready_line || new_drive_error_line != drive_error_line
|| tmp != status_word) {
drive_ready_line = new_drive_ready_line;
drive_error_line = new_drive_error_line;
status_word = tmp;
controller->on_drive_status_changed(this);
}
}
// update, if neither error nor ready changed
void RL0102_c::update_status_word(void) {
update_status_word(drive_ready_line, drive_error_line);
}
// is sector with given header on current track?
bool RL0102_c::header_on_track(uint16_t header) {
// fields of disk address word (read/write data, read header)
unsigned header_cyl = (header >> 7) & 0x1ff; // bits <15:7>
unsigned header_hd = (header >> 6) & 0x01; // bit 6
unsigned header_sec = header & 0x03f; // bit <5:0>
if (header_cyl != cylinder || header_hd != head || header_sec >= 40)
return false;
else
return true;
}
/*
simulate an alternating stream of sector_headers and sector_data on the rotating platter.
sector_segment_under_heads counts 0..79
if even: sector header under head
if odd: data under heads
time for one sector on platter:
1 rotation = 40 sectors = (60/2400)= 25ms
1 sector (header+data( = 25ms/40 = 625 us.
*/
#define NEXT_SECTOR_SEGMENT_ADVANCE do { \
next_sector_segment_under_heads = (next_sector_segment_under_heads + 1) % 80 ; \
} while(0)
#ifdef OLD
#define NEXT_SECTOR_SEGMENT_ADVANCE do { \
next_sector_segment_under_heads = (next_sector_segment_under_heads + 1) % 80 ; \
if (next_sector_segment_under_heads & 1) \
/* time to pass one sector 600 data, 25 header? */ \
rotational_timeout.wait_us(600) ; \
else rotational_timeout.wait_us(25) ; \
} while(0)
#endif
// read next sector header from rotating platter
// then increments "sector_segment_under_heads" to next data
// sector header has the format
// 3 words: diskaddress, 0x0000, CRC
// samples from real RL02: cyl=0,head. each header(=sector) and crc
// (3,042000);(4,030001);(6,104000);(7,072001);(16,164002);(17,012003);(24,170005);
// (25,006004);(26,044004);(27,132005);(31,056007);(33,162006);(34,110007);
// (37,152007);(40,140013);(43,102013);(44,170012);(46,044013)
bool RL0102_c::cmd_read_next_sector_header(uint16_t *buffer, unsigned buffer_size_words) {
if (state.value != RL0102_STATE_lock_on)
return false; // wrong state
assert(buffer_size_words >= 3);
if (next_sector_segment_under_heads & 1) {
// odd: next is data, let it pass the head
NEXT_SECTOR_SEGMENT_ADVANCE
;
// nanosleep() for rotational delay?
}
unsigned sectorno = next_sector_segment_under_heads >> 1; // LSB is header/data phase
// bits<0:5>=sector, bit<6>=head, bit<7:15>=cylinder
assert(cylinder < 512);
assert(head < 2);
assert(sectorno < 40);
// fill 3 word header
buffer[0] = (cylinder << 7) | (head << 6) | sectorno;
buffer[1] = 0x0000;
buffer[2] = calc_crc(2, &buffer[0]); // header CRC
// circular advance to next header: 40x headers, 40x data
NEXT_SECTOR_SEGMENT_ADVANCE
;
// nanosleep() for rotational delay?
return true;
}
// read next data block from rotating platter
// then increments "sector_segment_under_heads" to next header
// controller must address sector by waiting for it with cmd_read_next_sector_header()
bool RL0102_c::cmd_read_next_sector_data(uint16_t *buffer, unsigned buffer_size_words) {
if (state.value != RL0102_STATE_lock_on)
return false; // wrong state
assert(buffer_size_words * 2 >= sector_size_bytes);
if (!(next_sector_segment_under_heads & 1)) {
// even: next segment is header, let it pass the head
NEXT_SECTOR_SEGMENT_ADVANCE
;
// nanosleep() for rotational delay?
}
unsigned sectorno = next_sector_segment_under_heads >> 1; // LSB is header/data phase
unsigned track_size_bytes = sector_count * sector_size_bytes;
uint64_t offset = (uint64_t) (head_count * cylinder + head) * track_size_bytes
+ sectorno * sector_size_bytes;
// access image file
// LSB saved before MSB -> word/byte conversion on ARM (little endian) is easy
file_read((uint8_t *) buffer, offset, sector_size_bytes);
DEBUG("File Read 0x%x words from c/h/s=%d/%d/%d, file pos=0x%llx, words = %06o, %06o, ...",
sector_size_bytes / 2, cylinder, head, sectorno, offset, (unsigned )(buffer[0]),
(unsigned )(buffer[1]));
// circular advance to next header: 40x headers, 40x data
NEXT_SECTOR_SEGMENT_ADVANCE
;
// nanosleep() for rotational delay?
return true;
}
// write data for current sector under head
// then increments "stuff_under_heads" to next header
// controller must address sector by waiting for it with cmd_read_next_sector_header()
bool RL0102_c::cmd_write_next_sector_data(uint16_t *buffer, unsigned buffer_size_words) {
if (state.value != RL0102_STATE_lock_on)
return false; // wrong state
assert(buffer_size_words * 2 >= sector_size_bytes);
// error: write can not be executed, different reasons
if (file_readonly || writeprotect_button.value == true || !drive_ready_line) {
error_wge = true;
update_status_word();
return true; // function did not fail, WGE is valid result
}
error_wge = false; // can read
if (!(next_sector_segment_under_heads & 1)) {
// even: next segment is header, let it pass the head
NEXT_SECTOR_SEGMENT_ADVANCE
;
// nanosleep() for rotational delay?
}
unsigned sectorno = next_sector_segment_under_heads >> 1; // LSB is header/data phase
unsigned track_size_bytes = sector_count * sector_size_bytes;
uint64_t offset = (uint64_t) (head_count * cylinder + head) * track_size_bytes
+ sectorno * sector_size_bytes;
// access image file
// LSB saved before MSB -> word/byte conversion on ARM (little endian) is easy
file_write((uint8_t *) buffer, offset, sector_size_bytes);
DEBUG("File Write 0x%x words from c/h/s=%d/%d/%d, file pos=0x%llx, words = %06o, %06o, ...",
sector_size_bytes / 2, cylinder, head, sectorno, offset, (unsigned )(buffer[0]),
(unsigned )(buffer[1]));
// circular advance to next header: 40x headers, 40x data
NEXT_SECTOR_SEGMENT_ADVANCE
;
// nanosleep() for rotational delay?
return true;
}
// thread
void RL0102_c::worker(unsigned instance) {
UNUSED(instance); // only one
timeout_c timeout;
// set prio to RT, but less than RL11 controller
worker_init_realtime_priority(rt_device);
while (!workers_terminate) {
// worker_mutex.lock() ; // collision with cmd_seek() and on_xxx_changed()
// states have set error flags not in RL11 CSR: just update
update_status_word(drive_ready_line, drive_error_line);
// global stuff for all states
if (enabled.value && (!controller || !controller->enabled.value))
// RL drive powered, but no controller: no clock -> FAULT
fault_lamp.value = true;
if (power_switch.value == false)
change_state(RL0102_STATE_power_off);
switch (state.value) {
case RL0102_STATE_power_off:
state_power_off();
break;
case RL0102_STATE_load_cartridge:
state_load_cartridge();
break;
case RL0102_STATE_spin_up:
state_spin_up();
break;
case RL0102_STATE_brush_cycle:
state_brush_cycle();
break;
case RL0102_STATE_load_heads:
state_load_heads();
break;
case RL0102_STATE_seek:
state_seek();
break;
case RL0102_STATE_lock_on:
state_lock_on();
break;
case RL0102_STATE_unload_heads:
state_unload_heads();
break;
case RL0102_STATE_spin_down:
state_spin_down();
break;
}
// worker_mutex.unlock() ;
}
}