// // Plus4.cpp // Clock Signal // // Created by Thomas Harte on 06/12/2024. // Copyright © 2024 Thomas Harte. All rights reserved. // #include "Plus4.hpp" #include "Audio.hpp" #include "Interrupts.hpp" #include "Keyboard.hpp" #include "Pager.hpp" #include "Video.hpp" #include "Machines/MachineTypes.hpp" #include "Machines/Utility/MemoryFuzzer.hpp" #include "Processors/6502/6502.hpp" #include "Analyser/Static/Commodore/Target.hpp" #include "Outputs/Log.hpp" #include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "Configurable/StandardOptions.hpp" #include "Analyser/Dynamic/ConfidenceCounter.hpp" #include "Analyser/Static/Commodore/Target.hpp" #include "Storage/Tape/Parsers/Commodore.hpp" #include "Storage/Tape/Tape.hpp" #include "Machines/Commodore/SerialBus.hpp" #include "Machines/Commodore/1540/C1540.hpp" #include "Processors/6502Esque/Implementation/LazyFlags.hpp" #include #include #include using namespace Commodore; using namespace Commodore::Plus4; namespace { using Logger = Log::Logger; class Joystick: public Inputs::ConcreteJoystick { public: Joystick() : ConcreteJoystick({ Input(Input::Up), Input(Input::Down), Input(Input::Left), Input(Input::Right), Input(Input::Fire) }) {} void did_set_input(const Input &digital_input, bool is_active) final { const auto apply = [&](uint8_t mask) { if(is_active) mask_ &= ~mask; else mask_ |= mask; }; switch(digital_input.type) { default: return; case Input::Right: apply(0x08); break; case Input::Left: apply(0x04); break; case Input::Down: apply(0x02); break; case Input::Up: apply(0x01); break; case Input::Fire: apply(0xc0); break; } } uint8_t mask() const { return mask_; } private: uint8_t mask_ = 0xff; }; class Timers { public: Timers(Interrupts &interrupts) : interrupts_(interrupts) {} template void write(const uint8_t value) { const auto load_low = [&](uint16_t &target) { target = uint16_t((target & 0xff00) | (value << 0)); }; const auto load_high = [&](uint16_t &target) { target = uint16_t((target & 0x00ff) | (value << 8)); }; static constexpr auto timer = offset >> 1; paused_[timer] = ~offset & 1; if constexpr (offset & 1) { load_high(timers_[timer]); if constexpr (!timer) { load_high(timer0_reload_); } } else { load_low(timers_[timer]); if constexpr (!timer) { load_low(timer0_reload_); } } } template uint8_t read() { static constexpr auto timer = offset >> 1; if constexpr (offset & 1) { return uint8_t(timers_[timer] >> 8); } else { return uint8_t(timers_[timer] >> 0); } } void tick(int count) { // Quick hack here; do better than stepping through one at a time. while(count--) { decrement<0>(); decrement<1>(); decrement<2>(); } } private: template void decrement() { if(paused_[timer]) return; // Check for reload. if(!timer && !timers_[timer]) { timers_[timer] = timer0_reload_; } -- timers_[timer]; // Check for interrupt. if(!timers_[timer]) { switch(timer) { case 0: interrupts_.apply(Interrupts::Flag::Timer1); break; case 1: interrupts_.apply(Interrupts::Flag::Timer2); break; case 2: interrupts_.apply(Interrupts::Flag::Timer3); break; } } } uint16_t timers_[3]{}; uint16_t timer0_reload_ = 0xffff; bool paused_[3]{}; Interrupts &interrupts_; }; class SerialPort: public Serial::Port { public: void set_input(const Serial::Line line, const Serial::LineLevel value) override { levels_[size_t(line)] = value; } Serial::LineLevel level(const Serial::Line line) const { return levels_[size_t(line)]; } private: Serial::LineLevel levels_[5]; }; class ConcreteMachine: public Activity::Source, public BusController, public ClockingHint::Observer, public Configurable::Device, public CPU::MOS6502::BusHandler, public MachineTypes::AudioProducer, public MachineTypes::JoystickMachine, public MachineTypes::MappedKeyboardMachine, public MachineTypes::TimedMachine, public MachineTypes::ScanProducer, public MachineTypes::MediaTarget, public Machine, public Utility::TypeRecipient { public: ConcreteMachine(const Analyser::Static::Commodore::Plus4Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : m6502_(*this), interrupts_(*this), timers_(interrupts_), video_(video_map_, interrupts_), audio_(audio_queue_), speaker_(audio_) { const auto clock = clock_rate(false); media_divider_ = Cycles(clock); set_clock_rate(clock); speaker_.set_input_rate(float(clock)); const auto kernel = ROM::Name::Plus4KernelPALv5; const auto basic = ROM::Name::Plus4BASIC; ROM::Request request = ROM::Request(basic) && ROM::Request(kernel); if(target.has_c1541) { request = request && C1540::Machine::rom_request(C1540::Personality::C1541); } auto roms = rom_fetcher(request); if(!request.validate(roms)) { throw ROMMachine::Error::MissingROMs; } kernel_ = roms.find(kernel)->second; basic_ = roms.find(basic)->second; Memory::Fuzz(ram_); map_.page(ram_.data()); page_cpu_rom(); video_map_.page(ram_.data()); if(target.has_c1541) { c1541_ = std::make_unique(C1540::Personality::C1541, roms); c1541_->set_serial_bus(serial_bus_); Serial::attach(serial_port_, serial_bus_); c1541_->run_for(Cycles(2000000)); } tape_player_ = std::make_unique(clock); tape_player_->set_clocking_hint_observer(this); joysticks_.emplace_back(std::make_unique()); joysticks_.emplace_back(std::make_unique()); insert_media(target.media); if(!target.loading_command.empty()) { // Prefix a space as a delaying technique. type_string(std::string(" ") + target.loading_command); } } ~ConcreteMachine() { audio_queue_.flush(); } // HACK. NOCOMMIT. // int pulse_num_ = 0; Cycles perform_bus_operation( const CPU::MOS6502::BusOperation operation, const uint16_t address, uint8_t *const value ) { // Determine from the TED video subsystem the length of this clock cycle as perceived by the 6502, // relative to the master clock. const auto length = video_.cycle_length(operation == CPU::MOS6502::BusOperation::Ready); // Update other subsystems. advance_timers_and_tape(length); if(!superspeed_) { video_.run_for(length); if(c1541_) { c1541_cycles_ += length * Cycles(1'000'000); c1541_->run_for(c1541_cycles_.divide(media_divider_)); } time_since_audio_update_ += length; } if(operation == CPU::MOS6502::BusOperation::Ready) { return length; } // Perform actual access. if(address < 0x0002) { // 0x0000: data directions for parallel IO; 1 = output. // 0x0001: // b7 = serial data in; // b6 = serial clock in and cassette write; // b5 = [unconnected]; // b4 = cassette read; // b3 = cassette motor, 1 = off; // b2 = serial ATN out; // b1 = serial clock out and cassette write; // b0 = serial data out. if(is_read(operation)) { if(!address) { *value = io_direction_; } else { *value = io_input(); } } else { if(!address) { io_direction_ = *value; } else { io_output_ = *value; } const auto output = io_output_ | ~io_direction_; update_tape_motor(); serial_port_.set_output(Serial::Line::Data, Serial::LineLevel(~output & 0x01)); serial_port_.set_output(Serial::Line::Clock, Serial::LineLevel(~output & 0x02)); serial_port_.set_output(Serial::Line::Attention, Serial::LineLevel(~output & 0x04)); } } else if(address < 0xfd00 || address >= 0xff40) { // if( // use_fast_tape_hack_ && // operation == CPU::MOS6502Esque::BusOperation::ReadOpcode // ) { // superspeed_ |= address == 0xe5fd; // superspeed_ &= (address != 0xe68b) && (address != 0xe68d); // } static constexpr bool use_hle = true; // if( // use_fast_tape_hack_ && // operation == CPU::MOS6502Esque::BusOperation::ReadOpcode && // address == 0xe5fd // ) { // printf("Pulse %d from %lld ", // pulse_num_, // tape_player_->serialiser()->offset() // ); // } if( use_fast_tape_hack_ && operation == CPU::MOS6502Esque::BusOperation::ReadOpcode && ( (use_hle && address == 0xe5fd) || address == 0xe68b || address == 0xe68d ) ) { // ++pulse_num_; if(use_hle) { read_dipole(); } // using Flag = CPU::MOS6502::Flag; // using Register = CPU::MOS6502::Register; // const auto flags = m6502_.value_of(Register::Flags); // printf("to %lld: %c%c%c\n", // tape_player_->serialiser()->offset(), // flags & Flag::Sign ? 'n' : '-', // flags & Flag::Overflow ? 'v' : '-', // flags & Flag::Carry ? 'c' : '-' // ); *value = 0x60; } else { if(is_read(operation)) { *value = map_.read(address); } else { map_.write(address) = *value; } } // TODO: rdbyte and ldsync is probably sufficient? // if(use_fast_tape_hack_ && operation == CPU::MOS6502Esque::BusOperation::ReadOpcode) { // static constexpr uint16_t ldsync = 0; // switch(address) { // default: break; // // case ldsync: // break; // } // // if(address == 0xe9cc) { // // Skip the `jsr rdblok` that opens `fah` (i.e. find any header), performing // // its function as a high-level emulation. // Storage::Tape::Commodore::Parser parser(TargetPlatform::Plus4); // auto header = parser.get_next_header(*tape_player_->serialiser()); // // const auto tape_position = tape_player_->serialiser()->offset(); // if(header) { // // Copy to in-memory buffer and set type. // std::memcpy(&ram_[0x0333], header->data.data(), 191); // map_.write(0xb6) = 0x33; // map_.write(0xb7) = 0x03; // map_.write(0xf8) = header->type_descriptor(); //// hold_tape_ = true; // Logger::info().append("Found header"); // } else { // // no header found, so pretend this hack never interceded // tape_player_->serialiser()->set_offset(tape_position); //// hold_tape_ = false; // Logger::info().append("Didn't find header"); // } // // // Clear status and the verify flags. // ram_[0x90] = 0; // ram_[0x93] = 0; // // *value = 0x0c; // NOP abs. // } // } } else if(address < 0xff00) { // Miscellaneous hardware. All TODO. if(is_read(operation)) { switch(address & 0xfff0) { case 0xfd10: // 6529 parallel port, about which I know only what I've found in kernel ROM disassemblies. // If play button is not currently pressed and this read is immediately followed by // an AND 4, press it. The kernel will deal with motor control subsequently. if(!play_button_) { const uint16_t pc = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter); const uint8_t next[] = { map_.read(pc+0), map_.read(pc+1), map_.read(pc+2), map_.read(pc+3), }; // TODO: boil this down to a PC check. It's currently in this form as I'm unclear what // diversity of kernels exist. if(next[0] == 0x29 && next[1] == 0x04 && next[2] == 0xd0 && next[3] == 0xf4) { play_button_ = true; update_tape_motor(); } } *value = 0xff ^ (play_button_ ? 0x4 :0x0); break; case 0xfdd0: case 0xfdf0: *value = uint8_t(address >> 8); break; default: Logger::info().append("TODO: read @ %04x", address); break; } } else { switch(address & 0xfff0) { case 0xfd30: keyboard_mask_ = *value; break; case 0xfdd0: { // const auto low = address & 3; // const auto high = (address >> 2) & 3; // TODO: set up ROMs. } break; default: Logger::info().append("TODO: write of %02x @ %04x", *value, address); break; } } } else { const auto pc = m6502_.value_of(CPU::MOS6502::Register::ProgramCounter); const bool is_from_rom = (rom_is_paged_ && pc >= 0x8000) || (pc >= 0x400 && pc < 0x500) || (pc >= 0x700 && pc < 0x800); bool is_hit = true; if(is_read(operation)) { switch(address) { case 0xff00: *value = timers_.read<0>(); break; case 0xff01: *value = timers_.read<1>(); break; case 0xff02: *value = timers_.read<2>(); break; case 0xff03: *value = timers_.read<3>(); break; case 0xff04: *value = timers_.read<4>(); break; case 0xff05: *value = timers_.read<5>(); break; case 0xff06: *value = video_.read<0xff06>(); break; case 0xff07: *value = video_.read<0xff07>(); break; case 0xff08: { const uint8_t keyboard_input = ~( ((keyboard_mask_ & 0x01) ? 0x00 : key_states_[0]) | ((keyboard_mask_ & 0x02) ? 0x00 : key_states_[1]) | ((keyboard_mask_ & 0x04) ? 0x00 : key_states_[2]) | ((keyboard_mask_ & 0x08) ? 0x00 : key_states_[3]) | ((keyboard_mask_ & 0x10) ? 0x00 : key_states_[4]) | ((keyboard_mask_ & 0x20) ? 0x00 : key_states_[5]) | ((keyboard_mask_ & 0x40) ? 0x00 : key_states_[6]) | ((keyboard_mask_ & 0x80) ? 0x00 : key_states_[7]) ); const uint8_t joystick_mask = 0xff & ((joystick_mask_ & 0x02) ? 0xff : (joystick(0).mask() | 0x40)) & ((joystick_mask_ & 0x04) ? 0xff : (joystick(1).mask() | 0x80)); *value = keyboard_input & joystick_mask; } break; case 0xff09: *value = interrupts_.status(); break; case 0xff0a: *value = interrupts_.mask() | video_.read<0xff0a>() | 0xa0; break; case 0xff0b: *value = video_.read<0xff0b>(); break; case 0xff0c: *value = video_.read<0xff0c>(); break; case 0xff0d: *value = video_.read<0xff0d>(); break; case 0xff0e: *value = ff0e_; break; case 0xff0f: *value = ff0f_; break; case 0xff10: *value = ff10_ | 0xfc; break; case 0xff11: *value = ff11_; break; case 0xff12: *value = ff12_ | 0xc0; break; case 0xff13: *value = ff13_ | (rom_is_paged_ ? 1 : 0); break; case 0xff14: *value = video_.read<0xff14>(); break; case 0xff15: *value = video_.read<0xff15>(); break; case 0xff16: *value = video_.read<0xff16>(); break; case 0xff17: *value = video_.read<0xff17>(); break; case 0xff18: *value = video_.read<0xff18>(); break; case 0xff19: *value = video_.read<0xff19>(); break; case 0xff1a: *value = video_.read<0xff1a>(); break; case 0xff1b: *value = video_.read<0xff1b>(); break; case 0xff1c: *value = video_.read<0xff1c>(); break; case 0xff1d: *value = video_.read<0xff1d>(); break; case 0xff1e: *value = video_.read<0xff1e>(); break; case 0xff1f: *value = video_.read<0xff1f>(); break; case 0xff3e: *value = 0; break; case 0xff3f: *value = 0; break; default: Logger::info().append("TODO: TED read at %04x", address); is_hit = false; } } else { switch(address) { case 0xff00: timers_.write<0>(*value); break; case 0xff01: timers_.write<1>(*value); break; case 0xff02: timers_.write<2>(*value); break; case 0xff03: timers_.write<3>(*value); break; case 0xff04: timers_.write<4>(*value); break; case 0xff05: timers_.write<5>(*value); break; case 0xff06: video_.write<0xff06>(*value); break; case 0xff07: video_.write<0xff07>(*value); update_audio(); audio_.set_divider(*value); break; case 0xff08: // Observation here: the kernel posts a 0 to this // address upon completing each keyboard scan cycle, // once per frame. if(typer_ && !*value) { if(!typer_->type_next_character()) { clear_all_keys(); typer_.reset(); } } joystick_mask_ = *value; break; case 0xff09: interrupts_.set_status(*value); break; case 0xff0a: interrupts_.set_mask(*value); video_.write<0xff0a>(*value); break; case 0xff0b: video_.write<0xff0b>(*value); break; case 0xff0c: video_.write<0xff0c>(*value); break; case 0xff0d: video_.write<0xff0d>(*value); break; case 0xff0e: ff0e_ = *value; update_audio(); audio_.set_frequency_low<0>(*value); break; case 0xff0f: ff0f_ = *value; update_audio(); audio_.set_frequency_low<1>(*value); break; case 0xff10: ff10_ = *value; update_audio(); audio_.set_frequency_high<1>(*value); break; case 0xff11: ff11_ = *value; update_audio(); audio_.set_control(*value); break; case 0xff12: ff12_ = *value & 0x3f; video_.write<0xff12>(*value); if((*value & 4)) { page_video_rom(); } else { page_video_ram(); } update_audio(); audio_.set_frequency_high<0>(*value); break; case 0xff13: ff13_ = *value & 0xfe; video_.write<0xff13>(*value); break; case 0xff14: video_.write<0xff14>(*value); break; case 0xff15: video_.write<0xff15>(*value); break; case 0xff16: video_.write<0xff16>(*value); break; case 0xff17: video_.write<0xff17>(*value); break; case 0xff18: video_.write<0xff18>(*value); break; case 0xff19: video_.write<0xff19>(*value); break; case 0xff1a: video_.write<0xff1a>(*value); break; case 0xff1b: video_.write<0xff1b>(*value); break; case 0xff1c: video_.write<0xff1c>(*value); break; case 0xff1d: video_.write<0xff1d>(*value); break; case 0xff1e: video_.write<0xff1e>(*value); break; case 0xff1f: video_.write<0xff1f>(*value); break; case 0xff3e: page_cpu_rom(); break; case 0xff3f: page_cpu_ram(); break; default: Logger::info().append("TODO: TED write at %04x", address); is_hit = false; } } if(!is_from_rom) { if(is_hit) confidence_.add_hit(); else confidence_.add_miss(); } } return superspeed_ ? Cycles(0) : length; } private: using Processor = CPU::MOS6502::Processor; Processor m6502_; Outputs::Speaker::Speaker *get_speaker() override { return &speaker_; } void set_activity_observer(Activity::Observer *const observer) final { if(c1541_) c1541_->set_activity_observer(observer); } void set_irq_line(bool active) override { m6502_.set_irq_line(active); } void set_ready_line(bool active) override { m6502_.set_ready_line(active); } void page_video_rom() { video_map_.page(basic_.data()); video_map_.page(kernel_.data()); } void page_video_ram() { video_map_.page(&ram_[0x8000]); } void page_cpu_rom() { // TODO: allow other ROM selection. And no ROM? map_.page(basic_.data()); map_.page(kernel_.data()); rom_is_paged_ = true; set_use_fast_tape(); } void page_cpu_ram() { map_.page(&ram_[0x8000]); rom_is_paged_ = false; set_use_fast_tape(); } bool rom_is_paged_ = false; void set_scan_target(Outputs::Display::ScanTarget *const target) final { video_.set_scan_target(target); } Outputs::Display::ScanStatus get_scaled_scan_status() const final { return video_.get_scaled_scan_status(); } void set_display_type(const Outputs::Display::DisplayType display_type) final { video_.set_display_type(display_type); } Outputs::Display::DisplayType get_display_type() const final { return video_.get_display_type(); } void run_for(const Cycles cycles) final { m6502_.run_for(cycles); // I don't know why. update_audio(); audio_queue_.perform(); } void flush_output(int outputs) override { if(outputs & Output::Audio) { update_audio(); audio_queue_.perform(); } } bool insert_media(const Analyser::Static::Media &media) final { if(!media.tapes.empty()) { tape_player_->set_tape(media.tapes[0], TargetPlatform::Plus4); } if(!media.disks.empty() && c1541_) { c1541_->set_disk(media.disks[0]); } return true; } Plus4::Pager map_; Plus4::Pager video_map_; std::array ram_; std::vector kernel_; std::vector basic_; uint8_t ff0e_, ff0f_, ff10_, ff11_, ff12_, ff13_; Interrupts interrupts_; Cycles timers_subcycles_; Timers timers_; Video video_; Concurrency::AsyncTaskQueue audio_queue_; Audio audio_; Cycles time_since_audio_update_; Outputs::Speaker::PullLowpass