mirror of
https://github.com/TomHarte/CLK.git
synced 2024-09-06 20:54:31 +00:00
Specifically: now that the implementation is contained within the CPP file, there's no need to embed the keyboard, tape player and VIA port handler as private classes. Also the pain of additional syntax is reduced, so the keyboard has been bumped up to a fully data-hiding class. I've also transferred overall ownership of the tape player, AY and keyboard up to the Oric itself, with the VIA merely being wired to them, and added a whole bunch of extra documentation.
458 lines
13 KiB
C++
458 lines
13 KiB
C++
//
|
|
// Oric.cpp
|
|
// Clock Signal
|
|
//
|
|
// Created by Thomas Harte on 11/10/2016.
|
|
// Copyright © 2016 Thomas Harte. All rights reserved.
|
|
//
|
|
|
|
#include "Oric.hpp"
|
|
|
|
#include "Video.hpp"
|
|
#include "Microdisc.hpp"
|
|
#include "CharacterMapper.hpp"
|
|
|
|
#include "../MemoryFuzzer.hpp"
|
|
#include "../Typer.hpp"
|
|
|
|
#include "../../Processors/6502/6502.hpp"
|
|
#include "../../Components/6522/6522.hpp"
|
|
#include "../../Components/AY38910/AY38910.hpp"
|
|
|
|
#include "../../Storage/Tape/Tape.hpp"
|
|
#include "../../Storage/Tape/Parsers/Oric.hpp"
|
|
|
|
#include "../../ClockReceiver/ForceInline.hpp"
|
|
|
|
#include <memory>
|
|
|
|
namespace Oric {
|
|
|
|
/*!
|
|
Models the Oric's keyboard: eight key rows, containing a bitfield of keys set.
|
|
|
|
Active line is selected through a port on the Oric's VIA, and a column mask is
|
|
selected via a port on the AY, returning a single Boolean representation of the
|
|
logical OR of every key selected by the column mask on the active row.
|
|
*/
|
|
class Keyboard {
|
|
public:
|
|
Keyboard() {
|
|
clear_all_keys();
|
|
}
|
|
|
|
/// Sets whether @c key is or is not pressed, per @c is_pressed.
|
|
void set_key_state(uint16_t key, bool is_pressed) {
|
|
uint8_t mask = key & 0xff;
|
|
int line = key >> 8;
|
|
|
|
if(is_pressed) rows_[line] |= mask;
|
|
else rows_[line] &= ~mask;
|
|
}
|
|
|
|
/// Sets all keys as unpressed.
|
|
void clear_all_keys() {
|
|
memset(rows_, 0, sizeof(rows_));
|
|
}
|
|
|
|
/// Selects the active row.
|
|
void set_active_row(uint8_t row) {
|
|
row_ = row & 7;
|
|
}
|
|
|
|
/// Queries the keys on the active row specified by @c mask.
|
|
bool query_column(uint8_t column_mask) {
|
|
return !!(rows_[row_] & column_mask);
|
|
}
|
|
|
|
private:
|
|
uint8_t row_ = 0;
|
|
uint8_t rows_[8];
|
|
};
|
|
|
|
/*!
|
|
Provide's the Oric's tape player: a standard binary-sampled tape which also holds
|
|
an instance of the Oric tape parser, to provide fast-tape loading.
|
|
*/
|
|
class TapePlayer: public Storage::Tape::BinaryTapePlayer {
|
|
public:
|
|
TapePlayer() : Storage::Tape::BinaryTapePlayer(1000000) {}
|
|
|
|
/*!
|
|
Parses the incoming tape event stream to obtain the next stored byte.
|
|
|
|
@param use_fast_encoding If set to @c true , inspects the tape as though it
|
|
is encoded in the Oric's fast-loading scheme. Otherwise looks for a slow-encoded byte.
|
|
|
|
@returns The next byte from the tape.
|
|
*/
|
|
uint8_t get_next_byte(bool use_fast_encoding) {
|
|
return (uint8_t)parser_.get_next_byte(get_tape(), use_fast_encoding);
|
|
}
|
|
|
|
private:
|
|
Storage::Tape::Oric::Parser parser_;
|
|
};
|
|
|
|
/*!
|
|
Implements the Oric's VIA's port handler. On the Oric the VIA's ports connect
|
|
to the AY, the tape's motor control signal and the keyboard.
|
|
*/
|
|
class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
|
|
public:
|
|
VIAPortHandler(TapePlayer &tape_player, Keyboard &keyboard) : tape_player_(tape_player), keyboard_(keyboard) {}
|
|
|
|
/*!
|
|
Reponds to the 6522's control line output change signal; on an Oric A2 is connected to
|
|
the AY's BDIR, and B2 is connected to the AY's A2.
|
|
*/
|
|
void set_control_line_output(MOS::MOS6522::Port port, MOS::MOS6522::Line line, bool value) {
|
|
if(line) {
|
|
if(port) ay_bdir_ = value; else ay_bc1_ = value;
|
|
update_ay();
|
|
ay8910_->set_control_lines( (GI::AY38910::ControlLines)((ay_bdir_ ? GI::AY38910::BDIR : 0) | (ay_bc1_ ? GI::AY38910::BC1 : 0) | GI::AY38910::BC2));
|
|
}
|
|
}
|
|
|
|
/*!
|
|
Reponds to changes in the 6522's port output. On an Oric port B sets the tape motor control
|
|
and the keyboard's active row. Port A is connected to the AY's data bus.
|
|
*/
|
|
void set_port_output(MOS::MOS6522::Port port, uint8_t value, uint8_t direction_mask) {
|
|
if(port) {
|
|
keyboard_.set_active_row(value);
|
|
tape_player_.set_motor_control(value & 0x40);
|
|
} else {
|
|
update_ay();
|
|
ay8910_->set_data_input(value);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
Provides input data for the 6522. Port B reads the keyboard, and port A reads from the AY.
|
|
*/
|
|
uint8_t get_port_input(MOS::MOS6522::Port port) {
|
|
if(port) {
|
|
uint8_t column = ay8910_->get_port_output(false) ^ 0xff;
|
|
return keyboard_.query_column(column) ? 0x08 : 0x00;
|
|
} else {
|
|
return ay8910_->get_data_output();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
Advances time. This class manages the AY's concept of time to permit updating-on-demand.
|
|
*/
|
|
inline void run_for(const Cycles cycles) {
|
|
cycles_since_ay_update_ += cycles;
|
|
}
|
|
|
|
/// Flushes any queued behaviour (which, specifically, means on the AY).
|
|
void flush() {
|
|
ay8910_->run_for(cycles_since_ay_update_.flush());
|
|
ay8910_->flush();
|
|
}
|
|
|
|
/// Sets the AY in use by the machine the VIA that uses this port handler sits within.
|
|
void set_ay(GI::AY38910::AY38910 *ay) {
|
|
ay8910_ = ay;
|
|
}
|
|
|
|
private:
|
|
void update_ay() {
|
|
ay8910_->run_for(cycles_since_ay_update_.flush());
|
|
}
|
|
bool ay_bdir_ = false;
|
|
bool ay_bc1_ = false;
|
|
Cycles cycles_since_ay_update_;
|
|
|
|
GI::AY38910::AY38910 *ay8910_ = nullptr;
|
|
TapePlayer &tape_player_;
|
|
Keyboard &keyboard_;
|
|
};
|
|
|
|
class ConcreteMachine:
|
|
public CPU::MOS6502::BusHandler,
|
|
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
|
|
public Utility::TypeRecipient,
|
|
public Storage::Tape::BinaryTapePlayer::Delegate,
|
|
public Microdisc::Delegate,
|
|
public Machine {
|
|
|
|
public:
|
|
ConcreteMachine() :
|
|
m6502_(*this),
|
|
paged_rom_(rom_),
|
|
via_(via_port_handler_),
|
|
via_port_handler_(tape_player_, keyboard_) {
|
|
set_clock_rate(1000000);
|
|
via_port_handler_.set_interrupt_delegate(this);
|
|
tape_player_.set_delegate(this);
|
|
Memory::Fuzz(ram_, sizeof(ram_));
|
|
}
|
|
|
|
void set_rom(ROM rom, const std::vector<uint8_t> &data) override final {
|
|
switch(rom) {
|
|
case BASIC11: basic11_rom_ = std::move(data); break;
|
|
case BASIC10: basic10_rom_ = std::move(data); break;
|
|
case Microdisc: microdisc_rom_ = std::move(data); break;
|
|
case Colour:
|
|
colour_rom_ = std::move(data);
|
|
if(video_output_) video_output_->set_colour_rom(colour_rom_);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void set_key_state(uint16_t key, bool is_pressed) override final {
|
|
if(key == KeyNMI) {
|
|
m6502_.set_nmi_line(is_pressed);
|
|
} else {
|
|
keyboard_.set_key_state(key, is_pressed);
|
|
}
|
|
}
|
|
|
|
void clear_all_keys() override final {
|
|
keyboard_.clear_all_keys();
|
|
}
|
|
|
|
void set_use_fast_tape_hack(bool activate) override final {
|
|
use_fast_tape_hack_ = activate;
|
|
}
|
|
|
|
void set_output_device(Outputs::CRT::OutputDevice output_device) override final {
|
|
video_output_->set_output_device(output_device);
|
|
}
|
|
|
|
// to satisfy ConfigurationTarget::Machine
|
|
void configure_as_target(const StaticAnalyser::Target &target) override final {
|
|
if(target.oric.has_microdisc) {
|
|
microdisc_is_enabled_ = true;
|
|
microdisc_did_change_paging_flags(µdisc_);
|
|
microdisc_.set_delegate(this);
|
|
}
|
|
|
|
if(target.loadingCommand.length()) {
|
|
set_typer_for_string(target.loadingCommand.c_str());
|
|
}
|
|
|
|
if(target.oric.use_atmos_rom) {
|
|
memcpy(rom_, basic11_rom_.data(), std::min(basic11_rom_.size(), sizeof(rom_)));
|
|
|
|
is_using_basic11_ = true;
|
|
tape_get_byte_address_ = 0xe6c9;
|
|
scan_keyboard_address_ = 0xf495;
|
|
tape_speed_address_ = 0x024d;
|
|
} else {
|
|
memcpy(rom_, basic10_rom_.data(), std::min(basic10_rom_.size(), sizeof(rom_)));
|
|
|
|
is_using_basic11_ = false;
|
|
tape_get_byte_address_ = 0xe630;
|
|
scan_keyboard_address_ = 0xf43c;
|
|
tape_speed_address_ = 0x67;
|
|
}
|
|
|
|
insert_media(target.media);
|
|
}
|
|
|
|
bool insert_media(const StaticAnalyser::Media &media) override final {
|
|
if(media.tapes.size()) {
|
|
tape_player_.set_tape(media.tapes.front());
|
|
}
|
|
|
|
int drive_index = 0;
|
|
for(auto disk : media.disks) {
|
|
if(drive_index < 4) microdisc_.set_disk(disk, drive_index);
|
|
drive_index++;
|
|
}
|
|
|
|
return !media.tapes.empty() || (!media.disks.empty() && microdisc_is_enabled_);
|
|
}
|
|
|
|
// to satisfy CPU::MOS6502::BusHandler
|
|
forceinline Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
|
|
if(address > ram_top_) {
|
|
if(isReadOperation(operation)) *value = paged_rom_[address - ram_top_ - 1];
|
|
|
|
// 024D = 0 => fast; otherwise slow
|
|
// E6C9 = read byte: return byte in A
|
|
if( address == tape_get_byte_address_ &&
|
|
paged_rom_ == rom_ &&
|
|
use_fast_tape_hack_ &&
|
|
operation == CPU::MOS6502::BusOperation::ReadOpcode &&
|
|
tape_player_.has_tape() &&
|
|
!tape_player_.get_tape()->is_at_end()) {
|
|
|
|
uint8_t next_byte = tape_player_.get_next_byte(!ram_[tape_speed_address_]);
|
|
m6502_.set_value_of_register(CPU::MOS6502::A, next_byte);
|
|
m6502_.set_value_of_register(CPU::MOS6502::Flags, next_byte ? 0 : CPU::MOS6502::Flag::Zero);
|
|
*value = 0x60; // i.e. RTS
|
|
}
|
|
} else {
|
|
if((address & 0xff00) == 0x0300) {
|
|
if(microdisc_is_enabled_ && address >= 0x0310) {
|
|
switch(address) {
|
|
case 0x0310: case 0x0311: case 0x0312: case 0x0313:
|
|
if(isReadOperation(operation)) *value = microdisc_.get_register(address);
|
|
else microdisc_.set_register(address, *value);
|
|
break;
|
|
case 0x314: case 0x315: case 0x316: case 0x317:
|
|
if(isReadOperation(operation)) *value = microdisc_.get_interrupt_request_register();
|
|
else microdisc_.set_control_register(*value);
|
|
break;
|
|
case 0x318: case 0x319: case 0x31a: case 0x31b:
|
|
if(isReadOperation(operation)) *value = microdisc_.get_data_request_register();
|
|
break;
|
|
}
|
|
} else {
|
|
if(isReadOperation(operation)) *value = via_.get_register(address);
|
|
else via_.set_register(address, *value);
|
|
}
|
|
} else {
|
|
if(isReadOperation(operation))
|
|
*value = ram_[address];
|
|
else {
|
|
if(address >= 0x9800 && address <= 0xc000) update_video();
|
|
ram_[address] = *value;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(typer_ && address == scan_keyboard_address_ && operation == CPU::MOS6502::BusOperation::ReadOpcode) {
|
|
// the Oric 1 misses any key pressed on the very first entry into the read keyboard routine, so don't
|
|
// do anything until at least the second, regardless of machine
|
|
if(!keyboard_read_count_) keyboard_read_count_++;
|
|
else if(!typer_->type_next_character()) {
|
|
clear_all_keys();
|
|
typer_.reset();
|
|
}
|
|
}
|
|
|
|
via_.run_for(Cycles(1));
|
|
via_port_handler_.run_for(Cycles(1));
|
|
tape_player_.run_for(Cycles(1));
|
|
if(microdisc_is_enabled_) microdisc_.run_for(Cycles(8));
|
|
cycles_since_video_update_++;
|
|
return Cycles(1);
|
|
}
|
|
|
|
forceinline void flush() {
|
|
update_video();
|
|
via_port_handler_.flush();
|
|
}
|
|
|
|
// to satisfy CRTMachine::Machine
|
|
void setup_output(float aspect_ratio) override final {
|
|
ay8910_.reset(new GI::AY38910::AY38910());
|
|
ay8910_->set_clock_rate(1000000);
|
|
via_port_handler_.set_ay(ay8910_.get());
|
|
|
|
video_output_.reset(new VideoOutput(ram_));
|
|
if(!colour_rom_.empty()) video_output_->set_colour_rom(colour_rom_);
|
|
}
|
|
|
|
void close_output() override final {
|
|
video_output_.reset();
|
|
ay8910_.reset();
|
|
via_port_handler_.set_ay(nullptr);
|
|
}
|
|
|
|
std::shared_ptr<Outputs::CRT::CRT> get_crt() override final {
|
|
return video_output_->get_crt();
|
|
}
|
|
|
|
std::shared_ptr<Outputs::Speaker> get_speaker() override final {
|
|
return ay8910_;
|
|
}
|
|
|
|
void run_for(const Cycles cycles) override final {
|
|
m6502_.run_for(cycles);
|
|
}
|
|
|
|
// to satisfy MOS::MOS6522IRQDelegate::Delegate
|
|
void mos6522_did_change_interrupt_status(void *mos6522) override final {
|
|
set_interrupt_line();
|
|
}
|
|
|
|
// to satisfy Storage::Tape::BinaryTapePlayer::Delegate
|
|
void tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape_player) override final {
|
|
// set CB1
|
|
via_.set_control_line_input(MOS::MOS6522::Port::B, MOS::MOS6522::Line::One, !tape_player->get_input());
|
|
}
|
|
|
|
// for Utility::TypeRecipient::Delegate
|
|
void set_typer_for_string(const char *string) override final {
|
|
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper);
|
|
Utility::TypeRecipient::set_typer_for_string(string, std::move(mapper));
|
|
}
|
|
|
|
// for Microdisc::Delegate
|
|
void microdisc_did_change_paging_flags(class Microdisc *microdisc) override final {
|
|
int flags = microdisc->get_paging_flags();
|
|
if(!(flags&Microdisc::PagingFlags::BASICDisable)) {
|
|
ram_top_ = 0xbfff;
|
|
paged_rom_ = rom_;
|
|
} else {
|
|
if(flags&Microdisc::PagingFlags::MicrodscDisable) {
|
|
ram_top_ = 0xffff;
|
|
} else {
|
|
ram_top_ = 0xdfff;
|
|
paged_rom_ = microdisc_rom_.data();
|
|
}
|
|
}
|
|
}
|
|
|
|
void wd1770_did_change_output(WD::WD1770 *wd1770) override final {
|
|
set_interrupt_line();
|
|
}
|
|
|
|
private:
|
|
CPU::MOS6502::Processor<ConcreteMachine, false> m6502_;
|
|
|
|
// RAM and ROM
|
|
std::vector<uint8_t> basic11_rom_, basic10_rom_, microdisc_rom_, colour_rom_;
|
|
uint8_t ram_[65536], rom_[16384];
|
|
Cycles cycles_since_video_update_;
|
|
inline void update_video() {
|
|
video_output_->run_for(cycles_since_video_update_.flush());
|
|
}
|
|
|
|
// ROM bookkeeping
|
|
bool is_using_basic11_ = false;
|
|
uint16_t tape_get_byte_address_ = 0, scan_keyboard_address_ = 0, tape_speed_address_ = 0;
|
|
int keyboard_read_count_ = 0;
|
|
|
|
// Outputs
|
|
std::unique_ptr<VideoOutput> video_output_;
|
|
std::shared_ptr<GI::AY38910::AY38910> ay8910_;
|
|
|
|
// The tape
|
|
TapePlayer tape_player_;
|
|
bool use_fast_tape_hack_ = false;
|
|
|
|
VIAPortHandler via_port_handler_;
|
|
MOS::MOS6522::MOS6522<VIAPortHandler> via_;
|
|
Keyboard keyboard_;
|
|
|
|
// the Microdisc, if in use
|
|
class Microdisc microdisc_;
|
|
bool microdisc_is_enabled_ = false;
|
|
uint16_t ram_top_ = 0xbfff;
|
|
uint8_t *paged_rom_;
|
|
|
|
inline void set_interrupt_line() {
|
|
m6502_.set_irq_line(
|
|
via_.get_interrupt_line() ||
|
|
(microdisc_is_enabled_ && microdisc_.get_interrupt_request_line()));
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
using namespace Oric;
|
|
|
|
Machine *Machine::Oric() {
|
|
return new ConcreteMachine;
|
|
}
|
|
|
|
Machine::~Machine() {}
|