mirror of
https://github.com/TomHarte/CLK.git
synced 2024-12-27 01:31:42 +00:00
866 lines
28 KiB
C++
866 lines
28 KiB
C++
//
|
||
// Macintosh.cpp
|
||
// Clock Signal
|
||
//
|
||
// Created by Thomas Harte on 03/05/2019.
|
||
// Copyright © 2019 Thomas Harte. All rights reserved.
|
||
//
|
||
|
||
#include "Macintosh.hpp"
|
||
|
||
#include <array>
|
||
|
||
#include "DeferredAudio.hpp"
|
||
#include "DriveSpeedAccumulator.hpp"
|
||
#include "Keyboard.hpp"
|
||
#include "RealTimeClock.hpp"
|
||
#include "Video.hpp"
|
||
|
||
#include "../../MachineTypes.hpp"
|
||
#include "../../../Activity/Source.hpp"
|
||
#include "../../../Configurable/Configurable.hpp"
|
||
|
||
#include "../../../Inputs/QuadratureMouse/QuadratureMouse.hpp"
|
||
#include "../../../Outputs/Log.hpp"
|
||
|
||
#include "../../../ClockReceiver/JustInTime.hpp"
|
||
#include "../../../ClockReceiver/ClockingHintSource.hpp"
|
||
#include "../../../Configurable/StandardOptions.hpp"
|
||
|
||
//#define LOG_TRACE
|
||
|
||
#include "../../../Components/5380/ncr5380.hpp"
|
||
#include "../../../Components/6522/6522.hpp"
|
||
#include "../../../Components/8530/z8530.hpp"
|
||
#include "../../../Components/DiskII/IWM.hpp"
|
||
#include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp"
|
||
#include "../../../Processors/68000/68000.hpp"
|
||
|
||
#include "../../../Storage/MassStorage/SCSI/SCSI.hpp"
|
||
#include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp"
|
||
#include "../../../Storage/MassStorage/Encodings/MacintoshVolume.hpp"
|
||
|
||
#include "../../../Analyser/Static/Macintosh/Target.hpp"
|
||
|
||
#include "../../Utility/MemoryPacker.hpp"
|
||
#include "../../Utility/MemoryFuzzer.hpp"
|
||
|
||
namespace {
|
||
|
||
constexpr int CLOCK_RATE = 7833600;
|
||
|
||
}
|
||
|
||
namespace Apple {
|
||
namespace Macintosh {
|
||
|
||
template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachine:
|
||
public Machine,
|
||
public MachineTypes::TimedMachine,
|
||
public MachineTypes::ScanProducer,
|
||
public MachineTypes::AudioProducer,
|
||
public MachineTypes::MediaTarget,
|
||
public MachineTypes::MouseMachine,
|
||
public MachineTypes::MappedKeyboardMachine,
|
||
public CPU::MC68000::BusHandler,
|
||
public Zilog::SCC::z8530::Delegate,
|
||
public Activity::Source,
|
||
public Configurable::Device,
|
||
public DriveSpeedAccumulator::Delegate,
|
||
public ClockingHint::Observer {
|
||
public:
|
||
using Target = Analyser::Static::Macintosh::Target;
|
||
|
||
ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
|
||
MachineTypes::MappedKeyboardMachine({
|
||
Inputs::Keyboard::Key::LeftShift, Inputs::Keyboard::Key::RightShift,
|
||
Inputs::Keyboard::Key::LeftOption, Inputs::Keyboard::Key::RightOption,
|
||
Inputs::Keyboard::Key::LeftMeta, Inputs::Keyboard::Key::RightMeta,
|
||
}),
|
||
mc68000_(*this),
|
||
iwm_(CLOCK_RATE),
|
||
video_(audio_, drive_speed_accumulator_),
|
||
via_(via_port_handler_),
|
||
via_port_handler_(*this, clock_, keyboard_, audio_, iwm_, mouse_),
|
||
scsi_bus_(CLOCK_RATE * 2),
|
||
scsi_(scsi_bus_, CLOCK_RATE * 2),
|
||
hard_drive_(scsi_bus_, 6 /* SCSI ID */),
|
||
drives_{
|
||
{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke},
|
||
{CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke}
|
||
},
|
||
mouse_(1) {
|
||
|
||
// Select a ROM name and determine the proper ROM and RAM sizes
|
||
// based on the machine model.
|
||
using Model = Analyser::Static::Macintosh::Target::Model;
|
||
const std::string machine_name = "Macintosh";
|
||
uint32_t ram_size, rom_size;
|
||
std::vector<ROMMachine::ROM> rom_descriptions;
|
||
switch(model) {
|
||
default:
|
||
case Model::Mac128k:
|
||
ram_size = 128*1024;
|
||
rom_size = 64*1024;
|
||
rom_descriptions.emplace_back(machine_name, "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28);
|
||
break;
|
||
case Model::Mac512k:
|
||
ram_size = 512*1024;
|
||
rom_size = 64*1024;
|
||
rom_descriptions.emplace_back(machine_name, "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d);
|
||
break;
|
||
case Model::Mac512ke:
|
||
case Model::MacPlus: {
|
||
ram_size = ((model == Model::MacPlus) ? 4096 : 512)*1024;
|
||
rom_size = 128*1024;
|
||
const std::initializer_list<uint32_t> crc32s = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e };
|
||
rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s);
|
||
} break;
|
||
}
|
||
ram_mask_ = ram_size - 1;
|
||
rom_mask_ = rom_size - 1;
|
||
ram_.resize(ram_size);
|
||
video_.set_ram(reinterpret_cast<uint16_t *>(ram_.data()), ram_mask_ >> 1);
|
||
|
||
// Grab a copy of the ROM and convert it into big-endian data.
|
||
const auto roms = rom_fetcher(rom_descriptions);
|
||
if(!roms[0]) {
|
||
throw ROMMachine::Error::MissingROMs;
|
||
}
|
||
roms[0]->resize(rom_size);
|
||
Memory::PackBigEndian16(*roms[0], rom_);
|
||
|
||
// Randomise memory contents.
|
||
Memory::Fuzz(ram_);
|
||
|
||
// Attach the drives to the IWM.
|
||
iwm_->set_drive(0, &drives_[0]);
|
||
iwm_->set_drive(1, &drives_[1]);
|
||
|
||
// If they are 400kb drives, also attach them to the drive-speed accumulator.
|
||
if(!drives_[0].is_800k() || !drives_[1].is_800k()) {
|
||
drive_speed_accumulator_.set_delegate(this);
|
||
}
|
||
|
||
// Make sure interrupt changes from the SCC are observed.
|
||
scc_.set_delegate(this);
|
||
|
||
// Also watch for changes in clocking requirement from the SCSI chip.
|
||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||
scsi_bus_.set_clocking_hint_observer(this);
|
||
}
|
||
|
||
// The Mac runs at 7.8336mHz.
|
||
set_clock_rate(double(CLOCK_RATE));
|
||
audio_.speaker.set_input_rate(float(CLOCK_RATE) / 2.0f);
|
||
|
||
// Insert any supplied media.
|
||
insert_media(target.media);
|
||
|
||
// Set the immutables of the memory map.
|
||
setup_memory_map();
|
||
}
|
||
|
||
~ConcreteMachine() {
|
||
audio_.queue.flush();
|
||
}
|
||
|
||
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
|
||
video_.set_scan_target(scan_target);
|
||
}
|
||
|
||
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
|
||
return video_.get_scaled_scan_status();
|
||
}
|
||
|
||
Outputs::Speaker::Speaker *get_speaker() final {
|
||
return &audio_.speaker;
|
||
}
|
||
|
||
void run_for(const Cycles cycles) final {
|
||
mc68000_.run_for(cycles);
|
||
}
|
||
|
||
using Microcycle = CPU::MC68000::Microcycle;
|
||
|
||
forceinline HalfCycles perform_bus_operation(const Microcycle &cycle, int) {
|
||
// Advance time.
|
||
advance_time(cycle.length);
|
||
|
||
// A null cycle leaves nothing else to do.
|
||
if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return HalfCycles(0);
|
||
|
||
// Grab the address.
|
||
auto address = cycle.host_endian_byte_address();
|
||
|
||
// Everything above E0 0000 is signalled as being on the peripheral bus.
|
||
mc68000_.set_is_peripheral_address(address >= 0xe0'0000);
|
||
|
||
// All code below deals only with reads and writes — cycles in which a
|
||
// data select is active. So quit now if this is not the active part of
|
||
// a read or write.
|
||
//
|
||
// The 68000 uses 6800-style autovectored interrupts, so the mere act of
|
||
// having set VPA above deals with those given that the generated address
|
||
// for interrupt acknowledge cycles always has all bits set except the
|
||
// lowest explicit address lines.
|
||
if(!cycle.data_select_active() || (cycle.operation & Microcycle::InterruptAcknowledge)) return HalfCycles(0);
|
||
|
||
// Grab the word-precision address being accessed.
|
||
uint8_t *memory_base = nullptr;
|
||
HalfCycles delay;
|
||
switch(memory_map_[address >> 17]) {
|
||
default: assert(false);
|
||
|
||
case BusDevice::Unassigned:
|
||
fill_unmapped(cycle);
|
||
return delay;
|
||
|
||
case BusDevice::VIA: {
|
||
if(*cycle.address & 1) {
|
||
fill_unmapped(cycle);
|
||
} else {
|
||
const int register_address = address >> 9;
|
||
|
||
// VIA accesses are via address 0xefe1fe + register*512,
|
||
// which at word precision is 0x77f0ff + register*256.
|
||
if(cycle.operation & Microcycle::Read) {
|
||
cycle.value->halves.low = via_.read(register_address);
|
||
} else {
|
||
via_.write(register_address, cycle.value->halves.low);
|
||
}
|
||
|
||
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
|
||
}
|
||
} return delay;
|
||
|
||
case BusDevice::PhaseRead: {
|
||
if(cycle.operation & Microcycle::Read) {
|
||
cycle.value->halves.low = phase_ & 7;
|
||
}
|
||
|
||
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
|
||
} return delay;
|
||
|
||
case BusDevice::IWM: {
|
||
if(*cycle.address & 1) {
|
||
const int register_address = address >> 9;
|
||
|
||
// The IWM; this is a purely polled device, so can be run on demand.
|
||
if(cycle.operation & Microcycle::Read) {
|
||
cycle.value->halves.low = iwm_->read(register_address);
|
||
} else {
|
||
iwm_->write(register_address, cycle.value->halves.low);
|
||
}
|
||
|
||
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
|
||
} else {
|
||
fill_unmapped(cycle);
|
||
}
|
||
} return delay;
|
||
|
||
case BusDevice::SCSI: {
|
||
const int register_address = address >> 4;
|
||
const bool dma_acknowledge = address & 0x200;
|
||
|
||
// Even accesses = read; odd = write.
|
||
if(*cycle.address & 1) {
|
||
// Odd access => this is a write. Data will be in the upper byte.
|
||
if(cycle.operation & Microcycle::Read) {
|
||
scsi_.write(register_address, 0xff, dma_acknowledge);
|
||
} else {
|
||
if(cycle.operation & Microcycle::SelectWord) {
|
||
scsi_.write(register_address, cycle.value->halves.high, dma_acknowledge);
|
||
} else {
|
||
scsi_.write(register_address, cycle.value->halves.low, dma_acknowledge);
|
||
}
|
||
}
|
||
} else {
|
||
// Even access => this is a read.
|
||
if(cycle.operation & Microcycle::Read) {
|
||
const auto result = scsi_.read(register_address, dma_acknowledge);
|
||
if(cycle.operation & Microcycle::SelectWord) {
|
||
// Data is loaded on the top part of the bus only.
|
||
cycle.value->full = uint16_t((result << 8) | 0xff);
|
||
} else {
|
||
cycle.value->halves.low = result;
|
||
}
|
||
}
|
||
}
|
||
} return delay;
|
||
|
||
case BusDevice::SCCReadResetPhase: {
|
||
// Any word access here adjusts phase.
|
||
if(cycle.operation & Microcycle::SelectWord) {
|
||
adjust_phase();
|
||
} else {
|
||
// A0 = 1 => reset; A0 = 0 => read.
|
||
if(*cycle.address & 1) {
|
||
scc_.reset();
|
||
|
||
if(cycle.operation & Microcycle::Read) {
|
||
cycle.value->halves.low = 0xff;
|
||
}
|
||
} else {
|
||
const auto read = scc_.read(int(address >> 1));
|
||
if(cycle.operation & Microcycle::Read) {
|
||
cycle.value->halves.low = read;
|
||
}
|
||
}
|
||
}
|
||
} return delay;
|
||
|
||
case BusDevice::SCCWrite: {
|
||
// Any word access here adjusts phase.
|
||
if(cycle.operation & Microcycle::SelectWord) {
|
||
adjust_phase();
|
||
} else {
|
||
if(*cycle.address & 1) {
|
||
if(cycle.operation & Microcycle::Read) {
|
||
scc_.write(int(address >> 1), 0xff);
|
||
cycle.value->halves.low = 0xff;
|
||
} else {
|
||
scc_.write(int(address >> 1), cycle.value->halves.low);
|
||
}
|
||
} else {
|
||
fill_unmapped(cycle);
|
||
}
|
||
}
|
||
} return delay;
|
||
|
||
case BusDevice::RAM: {
|
||
// This is coupled with the Macintosh implementation of video; the magic
|
||
// constant should probably be factored into the Video class.
|
||
// It embodies knowledge of the fact that video (and audio) will always
|
||
// be fetched from the final $d900 bytes of memory.
|
||
// (And that ram_mask_ = ram size - 1).
|
||
if(address > ram_mask_ - 0xd900)
|
||
update_video();
|
||
|
||
memory_base = ram_.data();
|
||
address &= ram_mask_;
|
||
|
||
// Apply a delay due to video contention if applicable; scheme applied:
|
||
// only every other access slot is available during the period of video
|
||
// output. I believe this to be correct for the 128k, 512k and Plus.
|
||
// More research to do on other models.
|
||
if(video_is_outputting() && ram_subcycle_ < 8) {
|
||
delay = HalfCycles(8 - ram_subcycle_);
|
||
advance_time(delay);
|
||
}
|
||
} break;
|
||
|
||
case BusDevice::ROM: {
|
||
if(!(cycle.operation & Microcycle::Read)) return delay;
|
||
memory_base = rom_;
|
||
address &= rom_mask_;
|
||
} break;
|
||
}
|
||
|
||
// If control has fallen through to here, the access is either a read from ROM, or a read or write to RAM.
|
||
switch(cycle.operation & (Microcycle::SelectWord | Microcycle::SelectByte | Microcycle::Read)) {
|
||
default:
|
||
break;
|
||
|
||
case Microcycle::SelectWord | Microcycle::Read:
|
||
cycle.value->full = *reinterpret_cast<uint16_t *>(&memory_base[address]);
|
||
break;
|
||
case Microcycle::SelectByte | Microcycle::Read:
|
||
cycle.value->halves.low = memory_base[address];
|
||
break;
|
||
case Microcycle::SelectWord:
|
||
*reinterpret_cast<uint16_t *>(&memory_base[address]) = cycle.value->full;
|
||
break;
|
||
case Microcycle::SelectByte:
|
||
memory_base[address] = cycle.value->halves.low;
|
||
break;
|
||
}
|
||
|
||
return delay;
|
||
}
|
||
|
||
void flush() {
|
||
// Flush the video before the audio queue; in a Mac the
|
||
// video is responsible for providing part of the
|
||
// audio signal, so the two aren't as distinct as in
|
||
// most machines.
|
||
update_video();
|
||
|
||
// As above: flush audio after video.
|
||
via_.flush();
|
||
audio_.queue.perform();
|
||
|
||
// This avoids deferring IWM costs indefinitely, until
|
||
// they become artbitrarily large.
|
||
iwm_.flush();
|
||
}
|
||
|
||
void set_rom_is_overlay(bool rom_is_overlay) {
|
||
ROM_is_overlay_ = rom_is_overlay;
|
||
|
||
using Model = Analyser::Static::Macintosh::Target::Model;
|
||
switch(model) {
|
||
case Model::Mac128k:
|
||
case Model::Mac512k:
|
||
case Model::Mac512ke:
|
||
populate_memory_map(0, [rom_is_overlay] (std::function<void(int target, BusDevice device)> map_to) {
|
||
// Addresses up to $80 0000 aren't affected by this bit.
|
||
if(rom_is_overlay) {
|
||
// Up to $60 0000 mirrors of the ROM alternate with unassigned areas every $10 0000 byes.
|
||
for(int c = 0; c < 0x600000; c += 0x100000) {
|
||
map_to(c + 0x100000, (c & 0x100000) ? BusDevice::Unassigned : BusDevice::ROM);
|
||
}
|
||
map_to(0x800000, BusDevice::RAM);
|
||
} else {
|
||
map_to(0x400000, BusDevice::RAM);
|
||
map_to(0x500000, BusDevice::ROM);
|
||
map_to(0x800000, BusDevice::Unassigned);
|
||
}
|
||
});
|
||
break;
|
||
|
||
case Model::MacPlus:
|
||
populate_memory_map(0, [rom_is_overlay] (std::function<void(int target, BusDevice device)> map_to) {
|
||
// Addresses up to $80 0000 aren't affected by this bit.
|
||
if(rom_is_overlay) {
|
||
for(int c = 0; c < 0x580000; c += 0x20000) {
|
||
map_to(c + 0x20000, ((c & 0x100000) || (c & 0x20000)) ? BusDevice::Unassigned : BusDevice::ROM);
|
||
}
|
||
map_to(0x600000, BusDevice::SCSI);
|
||
map_to(0x800000, BusDevice::RAM);
|
||
} else {
|
||
map_to(0x400000, BusDevice::RAM);
|
||
for(int c = 0x400000; c < 0x580000; c += 0x20000) {
|
||
map_to(c + 0x20000, ((c & 0x100000) || (c & 0x20000)) ? BusDevice::Unassigned : BusDevice::ROM);
|
||
}
|
||
map_to(0x600000, BusDevice::SCSI);
|
||
map_to(0x800000, BusDevice::Unassigned);
|
||
}
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
|
||
bool video_is_outputting() {
|
||
return video_.is_outputting(time_since_video_update_);
|
||
}
|
||
|
||
void set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use_alternate_audio_buffer) {
|
||
update_video();
|
||
video_.set_use_alternate_buffers(use_alternate_screen_buffer, use_alternate_audio_buffer);
|
||
}
|
||
|
||
bool insert_media(const Analyser::Static::Media &media) final {
|
||
if(media.disks.empty() && media.mass_storage_devices.empty())
|
||
return false;
|
||
|
||
// TODO: shouldn't allow disks to be replaced like this, as the Mac
|
||
// uses software eject. Will need to expand messaging ability of
|
||
// insert_media.
|
||
if(!media.disks.empty()) {
|
||
if(drives_[0].has_disk())
|
||
drives_[1].set_disk(media.disks[0]);
|
||
else
|
||
drives_[0].set_disk(media.disks[0]);
|
||
}
|
||
|
||
// TODO: allow this only at machine startup?
|
||
if(!media.mass_storage_devices.empty()) {
|
||
const auto volume = dynamic_cast<Storage::MassStorage::Encodings::Macintosh::Volume *>(media.mass_storage_devices.front().get());
|
||
if(volume) {
|
||
volume->set_drive_type(Storage::MassStorage::Encodings::Macintosh::DriveType::SCSI);
|
||
}
|
||
hard_drive_->set_storage(media.mass_storage_devices.front());
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// MARK: Keyboard input.
|
||
|
||
KeyboardMapper *get_keyboard_mapper() final {
|
||
return &keyboard_mapper_;
|
||
}
|
||
|
||
void set_key_state(uint16_t key, bool is_pressed) final {
|
||
keyboard_.enqueue_key_state(key, is_pressed);
|
||
}
|
||
|
||
// TODO: clear all keys.
|
||
|
||
// MARK: Interrupt updates.
|
||
|
||
void did_change_interrupt_status(Zilog::SCC::z8530 *, bool) final {
|
||
update_interrupt_input();
|
||
}
|
||
|
||
void update_interrupt_input() {
|
||
// Update interrupt input.
|
||
// TODO: does this really cascade like this?
|
||
if(scc_.get_interrupt_line()) {
|
||
mc68000_.set_interrupt_level(2);
|
||
} else if(via_.get_interrupt_line()) {
|
||
mc68000_.set_interrupt_level(1);
|
||
} else {
|
||
mc68000_.set_interrupt_level(0);
|
||
}
|
||
}
|
||
|
||
// MARK: - Activity Source
|
||
void set_activity_observer(Activity::Observer *observer) final {
|
||
iwm_->set_activity_observer(observer);
|
||
|
||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||
scsi_bus_.set_activity_observer(observer);
|
||
}
|
||
}
|
||
|
||
// MARK: - Configuration options.
|
||
std::unique_ptr<Reflection::Struct> get_options() final {
|
||
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
|
||
options->quickboot = quickboot_;
|
||
return options;
|
||
}
|
||
|
||
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
|
||
// TODO: should this really be a runtime option?
|
||
// It should probably be a construction option.
|
||
|
||
const auto options = dynamic_cast<Options *>(str.get());
|
||
quickboot_ = options->quickboot;
|
||
if(quickboot_) {
|
||
// Cf. Big Mess o' Wires' disassembly of the Mac Plus ROM, and the
|
||
// test at $E00. TODO: adapt as(/if?) necessary for other Macs.
|
||
ram_[0x02ae] = 0x40;
|
||
ram_[0x02af] = 0x00;
|
||
ram_[0x02b0] = 0x00;
|
||
ram_[0x02b1] = 0x00;
|
||
}
|
||
}
|
||
|
||
private:
|
||
bool quickboot_ = false;
|
||
|
||
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final {
|
||
scsi_bus_is_clocked_ = scsi_bus_.preferred_clocking() != ClockingHint::Preference::None;
|
||
}
|
||
|
||
void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) final {
|
||
iwm_.flush();
|
||
drives_[0].set_rotation_speed(speed);
|
||
drives_[1].set_rotation_speed(speed);
|
||
}
|
||
|
||
forceinline void adjust_phase() {
|
||
++phase_;
|
||
}
|
||
|
||
forceinline void fill_unmapped(const Microcycle &cycle) {
|
||
if(!(cycle.operation & Microcycle::Read)) return;
|
||
cycle.set_value16(0xffff);
|
||
}
|
||
|
||
/// Advances all non-CPU components by @c duration half cycles.
|
||
forceinline void advance_time(HalfCycles duration) {
|
||
time_since_video_update_ += duration;
|
||
iwm_ += duration;
|
||
ram_subcycle_ = (ram_subcycle_ + duration.as_integral()) & 15;
|
||
|
||
// The VIA runs at one-tenth of the 68000's clock speed, in sync with the E clock.
|
||
// See: Guide to the Macintosh Hardware Family p149 (PDF p188). Some extra division
|
||
// may occur here in order to provide VSYNC at a proper moment.
|
||
// Possibly route vsync.
|
||
if(time_since_video_update_ < time_until_video_event_) {
|
||
via_clock_ += duration;
|
||
via_.run_for(via_clock_.divide(HalfCycles(10)));
|
||
} else {
|
||
auto via_time_base = time_since_video_update_ - duration;
|
||
auto via_cycles_outstanding = duration;
|
||
while(time_until_video_event_ < time_since_video_update_) {
|
||
const auto via_cycles = time_until_video_event_ - via_time_base;
|
||
via_time_base = HalfCycles(0);
|
||
via_cycles_outstanding -= via_cycles;
|
||
|
||
via_clock_ += via_cycles;
|
||
via_.run_for(via_clock_.divide(HalfCycles(10)));
|
||
|
||
video_.run_for(time_until_video_event_);
|
||
time_since_video_update_ -= time_until_video_event_;
|
||
time_until_video_event_ = video_.get_next_sequence_point();
|
||
|
||
via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !video_.vsync());
|
||
}
|
||
|
||
via_clock_ += via_cycles_outstanding;
|
||
via_.run_for(via_clock_.divide(HalfCycles(10)));
|
||
}
|
||
|
||
// The keyboard also has a clock, albeit a very slow one — 100,000 cycles/second.
|
||
// Its clock and data lines are connected to the VIA.
|
||
keyboard_clock_ += duration;
|
||
const auto keyboard_ticks = keyboard_clock_.divide(HalfCycles(CLOCK_RATE / 100000));
|
||
if(keyboard_ticks > HalfCycles(0)) {
|
||
keyboard_.run_for(keyboard_ticks);
|
||
via_.set_control_line_input(MOS::MOS6522::Port::B, MOS::MOS6522::Line::Two, keyboard_.get_data());
|
||
via_.set_control_line_input(MOS::MOS6522::Port::B, MOS::MOS6522::Line::One, keyboard_.get_clock());
|
||
}
|
||
|
||
// Feed mouse inputs within at most 1250 cycles of each other.
|
||
if(mouse_.has_steps()) {
|
||
time_since_mouse_update_ += duration;
|
||
const auto mouse_ticks = time_since_mouse_update_.divide(HalfCycles(2500));
|
||
if(mouse_ticks > HalfCycles(0)) {
|
||
mouse_.prepare_step();
|
||
scc_.set_dcd(0, mouse_.get_channel(1) & 1);
|
||
scc_.set_dcd(1, mouse_.get_channel(0) & 1);
|
||
}
|
||
}
|
||
|
||
// TODO: SCC should be clocked at a divide-by-two, if and when it actually has
|
||
// anything connected.
|
||
|
||
// Consider updating the real-time clock.
|
||
real_time_clock_ += duration;
|
||
auto ticks = real_time_clock_.divide_cycles(Cycles(CLOCK_RATE)).as_integral();
|
||
while(ticks--) {
|
||
clock_.update();
|
||
// TODO: leave a delay between toggling the input rather than using this coupled hack.
|
||
via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two, true);
|
||
via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::Two, false);
|
||
}
|
||
|
||
// Update the SCSI if currently active.
|
||
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
|
||
if(scsi_bus_is_clocked_) scsi_bus_.run_for(duration);
|
||
}
|
||
}
|
||
|
||
forceinline void update_video() {
|
||
video_.run_for(time_since_video_update_.flush<HalfCycles>());
|
||
time_until_video_event_ = video_.get_next_sequence_point();
|
||
}
|
||
|
||
Inputs::Mouse &get_mouse() final {
|
||
return mouse_;
|
||
}
|
||
|
||
using IWMActor = JustInTimeActor<IWM, 1, 1, HalfCycles, Cycles>;
|
||
|
||
class VIAPortHandler: public MOS::MOS6522::PortHandler {
|
||
public:
|
||
VIAPortHandler(ConcreteMachine &machine, RealTimeClock &clock, Keyboard &keyboard, DeferredAudio &audio, IWMActor &iwm, Inputs::QuadratureMouse &mouse) :
|
||
machine_(machine), clock_(clock), keyboard_(keyboard), audio_(audio), iwm_(iwm), mouse_(mouse) {}
|
||
|
||
using Port = MOS::MOS6522::Port;
|
||
using Line = MOS::MOS6522::Line;
|
||
|
||
void set_port_output(Port port, uint8_t value, uint8_t) {
|
||
/*
|
||
Peripheral lines: keyboard data, interrupt configuration.
|
||
(See p176 [/215])
|
||
*/
|
||
switch(port) {
|
||
case Port::A:
|
||
/*
|
||
Port A:
|
||
b7: [input] SCC wait/request (/W/REQA and /W/REQB wired together for a logical OR)
|
||
b6: 0 = alternate screen buffer, 1 = main screen buffer
|
||
b5: floppy disk SEL state control (upper/lower head "among other things")
|
||
b4: 1 = use ROM overlay memory map, 0 = use ordinary memory map
|
||
b3: 0 = use alternate sound buffer, 1 = use ordinary sound buffer
|
||
b2–b0: audio output volume
|
||
*/
|
||
iwm_->set_select(!!(value & 0x20));
|
||
|
||
machine_.set_use_alternate_buffers(!(value & 0x40), !(value&0x08));
|
||
machine_.set_rom_is_overlay(!!(value & 0x10));
|
||
|
||
audio_.flush();
|
||
audio_.audio.set_volume(value & 7);
|
||
break;
|
||
|
||
case Port::B:
|
||
/*
|
||
Port B:
|
||
b7: 0 = sound enabled, 1 = sound disabled
|
||
b6: [input] 0 = video beam in visible portion of line, 1 = outside
|
||
b5: [input] mouse y2
|
||
b4: [input] mouse x2
|
||
b3: [input] 0 = mouse button down, 1 = up
|
||
b2: 0 = real-time clock enabled, 1 = disabled
|
||
b1: clock's data-clock line
|
||
b0: clock's serial data line
|
||
*/
|
||
if(value & 0x4) clock_.abort();
|
||
else clock_.set_input(!!(value & 0x2), !!(value & 0x1));
|
||
|
||
audio_.flush();
|
||
audio_.audio.set_enabled(!(value & 0x80));
|
||
break;
|
||
}
|
||
}
|
||
|
||
uint8_t get_port_input(Port port) {
|
||
switch(port) {
|
||
case Port::A:
|
||
// printf("6522 r A\n");
|
||
return 0x00; // TODO: b7 = SCC wait/request
|
||
|
||
case Port::B:
|
||
return uint8_t(
|
||
((mouse_.get_button_mask() & 1) ? 0x00 : 0x08) |
|
||
((mouse_.get_channel(0) & 2) << 3) |
|
||
((mouse_.get_channel(1) & 2) << 4) |
|
||
(clock_.get_data() ? 0x02 : 0x00) |
|
||
(machine_.video_is_outputting() ? 0x00 : 0x40)
|
||
);
|
||
}
|
||
|
||
// Should be unreachable.
|
||
return 0xff;
|
||
}
|
||
|
||
void set_control_line_output(Port port, Line line, bool value) {
|
||
/*
|
||
Keyboard wiring (I believe):
|
||
CB2 = data (input/output)
|
||
CB1 = clock (input)
|
||
|
||
CA2 is used for receiving RTC interrupts.
|
||
CA1 is used for receiving vsync.
|
||
*/
|
||
if(port == Port::B && line == Line::Two) {
|
||
keyboard_.set_input(value);
|
||
}
|
||
else LOG("Unhandled control line output: " << (port ? 'B' : 'A') << int(line));
|
||
}
|
||
|
||
void run_for(HalfCycles duration) {
|
||
// The 6522 enjoys a divide-by-ten, so multiply back up here to make the
|
||
// divided-by-two clock the audio works on.
|
||
audio_.time_since_update += HalfCycles(duration.as_integral() * 5);
|
||
}
|
||
|
||
void flush() {
|
||
audio_.flush();
|
||
}
|
||
|
||
void set_interrupt_status(bool) {
|
||
machine_.update_interrupt_input();
|
||
}
|
||
|
||
private:
|
||
ConcreteMachine &machine_;
|
||
RealTimeClock &clock_;
|
||
Keyboard &keyboard_;
|
||
DeferredAudio &audio_;
|
||
IWMActor &iwm_;
|
||
Inputs::QuadratureMouse &mouse_;
|
||
};
|
||
|
||
CPU::MC68000::Processor<ConcreteMachine, true> mc68000_;
|
||
|
||
DriveSpeedAccumulator drive_speed_accumulator_;
|
||
IWMActor iwm_;
|
||
|
||
DeferredAudio audio_;
|
||
Video video_;
|
||
|
||
RealTimeClock clock_;
|
||
Keyboard keyboard_;
|
||
|
||
MOS::MOS6522::MOS6522<VIAPortHandler> via_;
|
||
VIAPortHandler via_port_handler_;
|
||
|
||
Zilog::SCC::z8530 scc_;
|
||
SCSI::Bus scsi_bus_;
|
||
NCR::NCR5380::NCR5380 scsi_;
|
||
SCSI::Target::Target<SCSI::DirectAccessDevice> hard_drive_;
|
||
bool scsi_bus_is_clocked_ = false;
|
||
|
||
HalfCycles via_clock_;
|
||
HalfCycles real_time_clock_;
|
||
HalfCycles keyboard_clock_;
|
||
HalfCycles time_since_video_update_;
|
||
HalfCycles time_until_video_event_;
|
||
HalfCycles time_since_mouse_update_;
|
||
|
||
bool ROM_is_overlay_ = true;
|
||
int phase_ = 1;
|
||
int ram_subcycle_ = 0;
|
||
|
||
DoubleDensityDrive drives_[2];
|
||
Inputs::QuadratureMouse mouse_;
|
||
|
||
Apple::Macintosh::KeyboardMapper keyboard_mapper_;
|
||
|
||
enum class BusDevice {
|
||
RAM, ROM, VIA, IWM, SCCWrite, SCCReadResetPhase, SCSI, PhaseRead, Unassigned
|
||
};
|
||
|
||
/// Divides the 24-bit address space up into $20000 (i.e. 128kb) segments, recording
|
||
/// which device is current mapped in each area. Keeping it in a table is a bit faster
|
||
/// than the multi-level address inspection that is otherwise required, as well as
|
||
/// simplifying slightly the handling of different models.
|
||
///
|
||
/// So: index with the top 7 bits of the 24-bit address.
|
||
BusDevice memory_map_[128];
|
||
|
||
void setup_memory_map() {
|
||
// Apply the power-up memory map, i.e. assume that ROM_is_overlay_ = true;
|
||
// start by calling into set_rom_is_overlay to seed everything up to $800000.
|
||
set_rom_is_overlay(true);
|
||
|
||
populate_memory_map(0x800000, [] (std::function<void(int target, BusDevice device)> map_to) {
|
||
map_to(0x900000, BusDevice::Unassigned);
|
||
map_to(0xa00000, BusDevice::SCCReadResetPhase);
|
||
map_to(0xb00000, BusDevice::Unassigned);
|
||
map_to(0xc00000, BusDevice::SCCWrite);
|
||
map_to(0xd00000, BusDevice::Unassigned);
|
||
map_to(0xe00000, BusDevice::IWM);
|
||
map_to(0xe80000, BusDevice::Unassigned);
|
||
map_to(0xf00000, BusDevice::VIA);
|
||
map_to(0xf80000, BusDevice::PhaseRead);
|
||
map_to(0x1000000, BusDevice::Unassigned);
|
||
});
|
||
}
|
||
|
||
void populate_memory_map(int start_address, std::function<void(std::function<void(int, BusDevice)>)> populator) {
|
||
// Define semantics for below; map_to will write from the current cursor position
|
||
// to the supplied 24-bit address, setting a particular mapped device.
|
||
int segment = start_address >> 17;
|
||
auto map_to = [&segment, this](int address, BusDevice device) {
|
||
for(; segment < address >> 17; ++segment) {
|
||
this->memory_map_[segment] = device;
|
||
}
|
||
};
|
||
|
||
populator(map_to);
|
||
}
|
||
|
||
uint32_t ram_mask_ = 0;
|
||
uint32_t rom_mask_ = 0;
|
||
uint8_t rom_[128*1024];
|
||
std::vector<uint8_t> ram_;
|
||
};
|
||
|
||
}
|
||
}
|
||
|
||
using namespace Apple::Macintosh;
|
||
|
||
Machine *Machine::Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||
auto *const mac_target = dynamic_cast<const Analyser::Static::Macintosh::Target *>(target);
|
||
|
||
using Model = Analyser::Static::Macintosh::Target::Model;
|
||
switch(mac_target->model) {
|
||
default:
|
||
case Model::Mac128k: return new ConcreteMachine<Model::Mac128k>(*mac_target, rom_fetcher);
|
||
case Model::Mac512k: return new ConcreteMachine<Model::Mac512k>(*mac_target, rom_fetcher);
|
||
case Model::Mac512ke: return new ConcreteMachine<Model::Mac512ke>(*mac_target, rom_fetcher);
|
||
case Model::MacPlus: return new ConcreteMachine<Model::MacPlus>(*mac_target, rom_fetcher);
|
||
}
|
||
}
|
||
|
||
Machine::~Machine() {}
|