// // CoCo.cpp // Clock Signal // // Created by Thomas Harte on 01/05/2026. // Copyright © 2026 Thomas Harte. All rights reserved. // #include "CoCo.hpp" #include "DiskController.hpp" #include "Keyboard.hpp" #include "Processors/6809/6809.hpp" #include "Components/6821/6821.hpp" #include "Components/6847/6847.hpp" #include "Activity/Source.hpp" #include "Analyser/Static/TandyCoCo/Target.hpp" #include "ClockReceiver/JustInTime.hpp" #include "Machines/MachineTypes.hpp" #include "Machines/Utility/MemoryFuzzer.hpp" #include "Components/AudioToggle/AudioToggle.hpp" #include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "Outputs/Speaker/Implementation/BufferSource.hpp" #include using namespace Tandy::CoCo; namespace { struct MemoryMap { uint8_t read(const uint16_t address) const { return read_[address >> 13] ? read_[address >> 13][address] : 0xff; } void write(const uint16_t address, uint8_t value) { if(!write_[address >> 13]) return; write_[address >> 13][address] = value; } void set_read(const size_t begin, const size_t end, const uint8_t *data) { assert(!(begin & 0x1fff)); assert(begin >= 0 && begin <= 0x10000); assert(!(end & 0x1fff)); assert(end >= 0 && end <= 0x10000); for(auto page = begin >> 13; page < end >> 13; page++) { read_[page] = data - begin; } } void set_readwrite(const size_t begin, const size_t end, uint8_t *data) { assert(!(begin & 0x1fff)); assert(begin >= 0 && begin <= 0x10000); assert(!(end & 0x1fff)); assert(end >= 0 && end <= 0x10000); for(auto page = begin >> 13; page < end >> 13; page++) { read_[page] = write_[page] = data - begin; } } void reset() { std::fill(std::begin(write_), std::end(write_), nullptr); std::fill(std::begin(read_), std::end(read_), nullptr); } private: uint8_t *write_[8]{}; const uint8_t *read_[8]{}; }; struct Joystick: public Inputs::ConcreteJoystick { Joystick() : ConcreteJoystick({ Input(Input::Horizontal), Input(Input::Vertical), Input(Input::Fire, 0), Input(Input::Fire, 1) }) {} void did_set_input(const Input &input, const float value) final { const size_t index = input.type == Input::Type::Vertical; axes[index] = std::clamp(uint8_t(value * 63.0), 0, 63); } void did_set_input(const Input &input, const bool value) final { if(input.type != Input::Type::Fire) { return; } buttons[input.info.control.index] = value; } float digital_minimum() const { return 0.0f; } float digital_maximum() const { return 1.0f; } uint8_t axes[2]{32, 32}; bool buttons[2]{}; }; // On the CoCo the most significant VDG data bit is connected to the VDG's '!alpha/semi_g' input. // This allows it to switch automatically between alphanumeric mode and semi-graphics mode based // on the most significant data bit, allowing mixed text and block-graphics on the same screen. // // Cf. https://web.archive.org/web/20210214054301/http://www.cs.unc.edu/~yakowenk/coco/text/semigraphics.html struct ModeMapper { Motorola::MC6847::Mode::IntT operator()(const uint8_t data, const Motorola::MC6847::Mode::IntT mode) { return mode | (data & 0x80 ? Motorola::MC6847::Mode::Semigraphics : 0); } }; struct SAM { SAM(MemoryMap &memory, const bool is_64kb) : memory_(memory), is_64kb_(is_64kb) { Memory::Fuzz(ram_); update_memory(); } void reset() { speed_ = ClockSpeed::Half; set_all_ram(false); page_ram(0); } uint8_t operator[](const uint16_t address) { b1b3_ += ((previous_6847_address_ ^ address) & previous_6847_address_ & 1) << 1; previous_6847_address_ = address; x_ += b1b3_ >> 4; b1b3_ &= 0b0'1110; if(x_ == x_divider_) { x_ = 0; b4_ += 0b1'0000; y_ += b4_ >> 5; b4_ &= 0b1'0000; if(y_ == y_divider_) { y_ = 0; b5b15_ += 0b10'0000; } } // TODO: determine whether this should be able to read ROM. return memory_.read((address & 1) | b1b3_ | b4_ | b5b15_); } template void set_hsync() { if(active) { b1b3_ &= clear_mask_; b4_ &= clear_mask_; previous_6847_address_ = 0; } } template void set_field_sync() { if(!active) { b5b15_ = graphics_address_; b4_ = 0; b1b3_ = 0; previous_6847_address_ = 0; x_ = y_ = 0; } } template void set_row_preset() {} template void access() { if(!is_write(read_write)) { return; } const auto set = [&](auto &target, auto bit) { target = target & ~bit; target |= address & 1 ? bit : 0; }; const auto configure_counter = [&] { switch(graphics_mode_) { case 0: x_divider_ = 1; y_divider_ = 12; clear_mask_ = 0b1111'1111'1110'0000; break; case 1: x_divider_ = 3; y_divider_ = 1; clear_mask_ = 0b1111'1111'1111'0000; break; case 2: x_divider_ = 1; y_divider_ = 3; clear_mask_ = 0b1111'1111'1110'0000; break; case 3: x_divider_ = 2; y_divider_ = 1; clear_mask_ = 0b1111'1111'1111'0000; break; case 4: x_divider_ = 1; y_divider_ = 2; clear_mask_ = 0b1111'1111'1110'0000; break; case 5: x_divider_ = 1; y_divider_ = 1; clear_mask_ = 0b1111'1111'1111'0000; break; case 6: x_divider_ = 1; y_divider_ = 1; clear_mask_ = 0b1111'1111'1110'0000; break; case 7: x_divider_ = 1; y_divider_ = 1; clear_mask_ = 0b1111'1111'1111'1111; break; default: __builtin_unreachable(); } }; switch((address >> 1) & 0xf) { default: printf("Unhandled SAM access %04x\n", address); break; case 0: set(graphics_mode_, 1); configure_counter(); break; case 1: set(graphics_mode_, 2); configure_counter(); break; case 2: set(graphics_mode_, 4); configure_counter(); break; case 3: set(graphics_address_, 0x0200); break; case 4: set(graphics_address_, 0x0400); break; case 5: set(graphics_address_, 0x0800); break; case 6: set(graphics_address_, 0x1000); break; case 7: set(graphics_address_, 0x2000); break; case 8: set(graphics_address_, 0x4000); break; case 9: set(graphics_address_, 0x8000); break; case 11: { int tc = int(speed_); set(tc, 1); speed_ = ClockSpeed(tc); } break; case 12: { int tc = int(speed_); set(tc, 2); speed_ = ClockSpeed(tc); } break; // Likely these are to effect refresh; TODO: look into this. case 13: set(ram_size_, 1); break; case 14: set(ram_size_, 2); break; case 10: page_ram(address & 1); break; case 15: set_all_ram(address & 1); break; } } template < CPU::M6809::BusPhase bus_phase, CPU::M6809::ReadWrite read_write, typename AddressT > Cycles cycle_cost(const AddressT address, const Cycles phase) { // // Without documentation I've made a guess here that the half-speed bus has an alignment requirement, // so switching from full-speed to half-speed might result in a two-cycle access but might result in // a three-cycle access (followed by two-cycle accesses). // // If that proves to be untrue, `phase` can be eliminated as an argument. // switch(speed_) { case ClockSpeed::Full1: case ClockSpeed::Full2: return Cycles(0); case ClockSpeed::Half: return duration(bus_phase) + phase; case ClockSpeed::HalfInRAM: if constexpr (read_write == CPU::M6809::ReadWrite::NoData) { return Cycles(0); } else { if(address < 0x8000 || all_ram_) { return duration(bus_phase) + phase; } else { return Cycles(0); } } break; default: break; } __builtin_unreachable(); } uint16_t graphics_address() const { return graphics_address_; } bool insert_cartridge(const std::vector &contents) { if(contents.size() > 16 * 1024) { return false; } // My memory map requires things to be a multiple of 0x2000 bytes; pad to that. cartridge_ = contents; if(cartridge_.size() & 0x1fff) { const auto new_size = (cartridge_.size() + 0x2000) & ~size_t(0x1fff); cartridge_.resize(new_size, 0xff); } update_memory(); return true; } bool has_cartridge() const { return !cartridge_.empty(); } void set_basic(const std::vector rom) { std::fill(basic_.begin(), basic_.end(), 0xff); std::copy_n(rom.begin(), rom.size(), basic_.end() - rom.size()); } void set_extended_basic(const std::vector rom) { std::copy_n(rom.begin(), rom.size(), basic_.begin()); } void set_alternate_basic(const std::vector rom) { std::fill(alternate_basic_.begin(), alternate_basic_.end(), 0xff); std::copy_n(rom.begin(), rom.size(), alternate_basic_.end() - rom.size()); } void set_no_alternate_basic() { std::copy(basic_.begin(), basic_.end(), alternate_basic_.begin()); } private: MemoryMap &memory_; enum class ClockSpeed { Half, HalfInRAM, Full1, Full2, }; int graphics_mode_ = 0; uint16_t graphics_address_ = 0; ClockSpeed speed_ = ClockSpeed::Half; int ram_size_ = 0; // 'X' and 'Y' are the data sheet names for these values, which is unfortunate given that they're involved // in data fetch but don't correlate with dimensions. // // Requirements: // // The low bit of the address coming from the 6847 acts as the low bit of the address fetched. // It also clocks counting. // // Above that is a three-bit counter, which it clocks. // // That three bit counter feeds into the 'x' divider. So if the x divider is 3 then that counter must overflow // three times in order to propagate carry upwards. // // Carry from that counter is fed into a one-bit counter. // // That one bit counter then clocks the 'y' divider, carry from which clocks the remaining 11 bits of output. // // At field sync the top seven bits of the counter are reloaded from the programmed base address. // At horizontal sync the programmed mask is applied to the counters. // Upon row preset, both x and y dividers are reset to 0. // // Observations: the hsync clearing mask never affects the top 11 bits — the portiona after the y divider. uint16_t b1b3_, x_, b4_, y_, b5b15_; uint16_t x_divider_ = 1, y_divider_ = 1; uint16_t clear_mask_ = 0xffff; uint16_t previous_6847_address_ = 0; std::vector cartridge_; std::array basic_; std::array alternate_basic_; std::array ram_; int ram_page_ = 0; bool all_ram_ = false; bool is_64kb_ = true; void page_ram(const int page) { ram_page_ = page; update_memory(); } void set_all_ram(const bool all_ram) { all_ram_ = all_ram; update_memory(); } void update_memory() { memory_.reset(); if(all_ram_ && is_64kb_) { memory_.set_readwrite(0x0000, 0x1'0000, ram_.data()); } else { memory_.set_readwrite(0x0000, 0x8000, ram_.data() + (is_64kb_ ? ram_page_ : 0) * 32768); if(ram_page_) { memory_.set_read(0x8000, 0xc000, alternate_basic_.data()); } else { memory_.set_read(0x8000, 0xc000, basic_.data()); } memory_.set_read(0xc000, 0xc000 + cartridge_.size(), cartridge_.data()); } } }; constexpr bool is_64kb(const Analyser::Static::TandyCoCo::Target::MemorySize size) { return size == Analyser::Static::TandyCoCo::Target::MemorySize::SixtyFourKB; } static constexpr uint8_t MaxDACLevel = 0b111'111; static constexpr double ClockRate = 1'789'772.5; static constexpr auto AudioDivider = Cycles(1); } namespace TandyCoCo { template class ConcreteMachine: public Activity::Source, public Configurable::Device, public Machine, public MachineTypes::AudioProducer, public MachineTypes::JoystickMachine, public MachineTypes::MappedKeyboardMachine, public MachineTypes::MediaChangeObserver, public MachineTypes::MediaTarget, public MachineTypes::ScanProducer, public MachineTypes::SoftResettable, public MachineTypes::TimedMachine, public Utility::TypeRecipient { public: ConcreteMachine(const Analyser::Static::TandyCoCo::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : m6809_(*this), pia0_handler_(*this), pia0_(pia0_handler_), pia1_handler_(*this), pia1_(pia1_handler_), sam_(memory_, is_64kb(target.memory_size)), m6847_(sam_, sam_), tape_player_(int(ClockRate)), audio_(audio_queue_, MaxDACLevel), speaker_(audio_) { set_clock_rate(ClockRate); speaker_.set_input_rate(ClockRate / AudioDivider.as()); construct_joysticks(); is_dragon_ = Analyser::Static::TandyCoCo::is_dragon(target.model); auto request = [&]() { if(is_dragon_) { if(is_64kb(target.memory_size)) { return ROM::Request(ROM::Name::Dragon64ROM1) && ROM::Request(ROM::Name::Dragon64ROM2); } else { return ROM::Request(ROM::Name::Dragon32); } } auto request = ROM::Request(ROM::Name::TandyExtendedBASIC10) || ROM::Request(ROM::Name::TandyExtendedBASIC11); if(!has_disk_drive) request = request.optional(); if(has_disk_drive) { auto disk_request = ROM::Request(ROM::Name::TandyCoCoDiskBASIC10) || ROM::Request(ROM::Name::TandyCoCoDiskBASIC11) || ROM::Request(ROM::Name::TandyCoCoDiskBASIC21); request = request && disk_request; } // BASIC 1.3 is not used unless explicitly requested for now, as it doesn't 'just work' with disks // as the rest of this code proceeds. I don't know whether its fussy about other ROMs or whether I've // got a blocking implementation issue elsewhere. switch(target.rom_version) { using enum Analyser::Static::TandyCoCo::Target::ROMVersion; case V10: request = request && ROM::Request(ROM::Name::TandyCoCoColourBasic10); break; case V11: request = request && ROM::Request(ROM::Name::TandyCoCoColourBasic11); break; case V12: request = request && ROM::Request(ROM::Name::TandyCoCoColourBasic12); break; case V13: request = request && ROM::Request(ROM::Name::TandyCoCoColourBasic13); break; default: { auto basic_request = ROM::Request(ROM::Name::TandyCoCoColourBasic11) || ROM::Request(ROM::Name::TandyCoCoColourBasic12); if(target.rom_version != V11OrAbove) { basic_request = basic_request || ROM::Request(ROM::Name::TandyCoCoColourBasic10); } request = request && basic_request; } break; } return request; } (); auto roms = rom_fetcher(request); if(!request.validate(roms)) { throw ROMMachine::Error::MissingROMs; } const auto any_of = [&](const std::initializer_list &options) -> std::optional> { for(auto &key: options) { const auto rom = roms.find(key); if(rom != roms.end()) { return rom->second; } } return std::nullopt; }; const auto basic = any_of({ ROM::Name::Dragon64ROM1, ROM::Name::Dragon32, ROM::Name::TandyCoCoColourBasic13, ROM::Name::TandyCoCoColourBasic12, ROM::Name::TandyCoCoColourBasic11, ROM::Name::TandyCoCoColourBasic10, }); sam_.set_basic(*basic); const auto extended_basic = any_of({ ROM::Name::TandyExtendedBASIC11, ROM::Name::TandyExtendedBASIC10, }); if(extended_basic.has_value()) { sam_.set_extended_basic(*extended_basic); } const auto alternate_basic = any_of({ ROM::Name::Dragon64ROM2, }); if(alternate_basic.has_value()) { sam_.set_alternate_basic(*alternate_basic); } else { sam_.set_no_alternate_basic(); } const auto disk_rom = any_of({ ROM::Name::TandyCoCoDiskBASIC21, ROM::Name::TandyCoCoDiskBASIC11, ROM::Name::TandyCoCoDiskBASIC10, }); if(disk_rom.has_value()) { sam_.insert_cartridge(*disk_rom); } insert_media(target.media); type_string(target.loading_command); } ~ConcreteMachine() { audio_queue_.lock_flush(); } template < CPU::M6809::BusPhase bus_phase, CPU::M6809::LIC lic, CPU::M6809::ReadWrite read_write, CPU::M6809::BusState bus_state, typename AddressT > Cycles perform( const AddressT address, CPU::M6809::data_t value ) { // TODO, maybe: pull the switch inside this SAM call outside the loop? const auto delay = sam_.cycle_cost(address, bus_phase_); const auto duration = delay + CPU::M6809::duration(bus_phase); bus_phase_ += duration; bus_phase_ &= 1; if constexpr (has_disk_drive) { // Update after the fact due to the internal mechanics of WD177x posting; these signals from the WD should // occur when it observes that the access cycle is over. It is now over because the next has begun. m6809_.template set(disk_controller_.nmi()); m6809_.template set(disk_controller_.halt()); // Multiply by 4.5 to get very close to 8Mhz for the controller. cycles_16mhz_ += duration * 9; disk_controller_.run_for(cycles_16mhz_.divide(2)); } if(m6847_ += duration) { pia0_.template set(m6847_.get()->hsync()); pia0_.template set(m6847_.get()->fsync()); } tape_player_.run_for(duration); if(typer_) { typer_->run_for(duration); } time_since_audio_update_ += duration; if(!has_disk_drive && sam_.has_cartridge()) { // When a cartridge is inserted: "the clock signal (Q) is shorted to the cartridge interrupt pin." pia1_.template set(true); pia1_.template set(false); } using namespace CPU::M6809; if(address >> 8 == 0xff) { if constexpr (read_write != ReadWrite::NoData) { switch(address) { default: printf("Unhandled at %04x\n", +address); break; case 0xff00: case 0xff04: case 0xff08: case 0xff0c: case 0xff10: case 0xff14: case 0xff18: case 0xff1c: access<0xff00, read_write>(pia0_, value); break; case 0xff01: case 0xff05: case 0xff09: case 0xff0d: case 0xff11: case 0xff15: case 0xff19: case 0xff1d: access<0xff01, read_write>(pia0_, value); break; case 0xff02: case 0xff06: case 0xff0a: case 0xff0e: case 0xff12: case 0xff16: case 0xff1a: case 0xff1e: access<0xff02, read_write>(pia0_, value); break; case 0xff03: case 0xff07: case 0xff0b: case 0xff0f: case 0xff13: case 0xff17: case 0xff1b: case 0xff1f: access<0xff03, read_write>(pia0_, value); break; case 0xff20: case 0xff24: case 0xff28: case 0xff2c: case 0xff30: case 0xff34: case 0xff38: case 0xff3c: access<0xff20, read_write>(pia1_, value); break; case 0xff21: case 0xff25: case 0xff29: case 0xff2d: case 0xff31: case 0xff35: case 0xff39: case 0xff3d: access<0xff21, read_write>(pia1_, value); break; case 0xff22: case 0xff26: case 0xff2a: case 0xff2e: case 0xff32: case 0xff36: case 0xff3a: case 0xff3e: access<0xff22, read_write>(pia1_, value); break; case 0xff23: case 0xff27: case 0xff2b: case 0xff2f: case 0xff33: case 0xff37: case 0xff3b: case 0xff3f: access<0xff23, read_write>(pia1_, value); break; case 0xff40: if(has_disk_drive) access<0xff40, read_write>(disk_controller_, value); break; case 0xff48: if(has_disk_drive) access<0xff48, read_write>(disk_controller_, value); break; case 0xff49: if(has_disk_drive) access<0xff49, read_write>(disk_controller_, value); break; case 0xff4a: if(has_disk_drive) access<0xff4a, read_write>(disk_controller_, value); break; case 0xff4b: if(has_disk_drive) access<0xff4b, read_write>(disk_controller_, value); break; case 0xffc0: sam_.access<0xffc0, read_write>(); break; case 0xffc1: sam_.access<0xffc1, read_write>(); break; case 0xffc2: sam_.access<0xffc2, read_write>(); break; case 0xffc3: sam_.access<0xffc3, read_write>(); break; case 0xffc4: sam_.access<0xffc4, read_write>(); break; case 0xffc5: sam_.access<0xffc5, read_write>(); break; case 0xffc6: sam_.access<0xffc6, read_write>(); break; case 0xffc7: sam_.access<0xffc7, read_write>(); break; case 0xffc8: sam_.access<0xffc8, read_write>(); break; case 0xffc9: sam_.access<0xffc9, read_write>(); break; case 0xffca: sam_.access<0xffca, read_write>(); break; case 0xffcb: sam_.access<0xffcb, read_write>(); break; case 0xffcc: sam_.access<0xffcc, read_write>(); break; case 0xffcd: sam_.access<0xffcd, read_write>(); break; case 0xffce: sam_.access<0xffce, read_write>(); break; case 0xffcf: sam_.access<0xffcf, read_write>(); break; case 0xffd0: sam_.access<0xffd0, read_write>(); break; case 0xffd1: sam_.access<0xffd1, read_write>(); break; case 0xffd2: sam_.access<0xffd2, read_write>(); break; case 0xffd3: sam_.access<0xffd3, read_write>(); break; case 0xffd4: sam_.access<0xffd4, read_write>(); break; case 0xffd5: sam_.access<0xffd5, read_write>(); break; case 0xffd6: sam_.access<0xffd6, read_write>(); break; case 0xffd7: sam_.access<0xffd7, read_write>(); break; case 0xffd8: sam_.access<0xffd8, read_write>(); break; case 0xffd9: sam_.access<0xffd9, read_write>(); break; case 0xffda: sam_.access<0xffda, read_write>(); break; case 0xffdb: sam_.access<0xffdb, read_write>(); break; case 0xffdc: sam_.access<0xffdc, read_write>(); break; case 0xffdd: sam_.access<0xffdd, read_write>(); break; case 0xffde: sam_.access<0xffde, read_write>(); break; case 0xffdf: sam_.access<0xffdf, read_write>(); break; case 0xfff2: case 0xfff3: case 0xfff4: case 0xfff5: case 0xfff6: case 0xfff7: case 0xfff8: case 0xfff9: case 0xfffa: case 0xfffb: case 0xfffc: case 0xfffd: case 0xfffe: case 0xffff: if constexpr (is_read(read_write)) { value = memory_.read(address - 0x4000); } break; } } } else { if constexpr (is_read(read_write)) { value = memory_.read(address); if constexpr (lic == CPU::M6809::LIC::InstructionFetch) { if(allow_fast_tape_loading_ && tape_player_.motor_control() && address == 0xa75d) { // To reproduce: // // LA75D CLR CPERTM ; RESET PERIOD TIMER // TST CBTPHA ; CHECK TO SEE IF SYNC’ED ON THE HI-LO TRANSITION OR LO-HI // BNE LA773 ; BRANCH ON HI-LO TRANSITION // // * LO - HI TRANSITION // LA763 BSR LA76C ; READ CASSETTE INPUT BIT 8D 07; 7 cycles // BCS LA763 ; LOOP UNTIL IT IS LO 25 FC; 3 cycles // LA767 BSR LA76C ; READ CASSETTE INPUT DATA // BCC LA767 ; WAIT UNTIL IT GOES HI // RTS // // * READ CASSETTE INPUT BIT OF THE PIA // LA76C INC CPERTM ; INCREMENT PERIOD TIMER 0C 83; 6 cycles // LDB PIA1 ; GET CASSETTE INPUT BIT F6 FF 20; 5 cycles // RORB ; PUT CASSETTE BIT INTO THE CARRY FLAG 56; 2 cycles // RTS 39; 5 cycles // // * WAIT FOR HI - LO TRANSITION // LA773 BSR LA76C ; READ CASSETTE INPUT DATA // BCC LA773 ; LOOP UNTIL IT IS HI // LA777 BSR LA76C ; READ CASSETTE INPUT // BCS LA777 ; LOOP UNTIL IT IS LO // RTS // // where: CPERTM = $0083; CBTPHA = $0084. // // The value of B is immediately discared by the caller so all that's needed is to set // CPERTM to the amount of time taken to observe the two transitions required, divided // by 28 cycles = 28/894886.25 s. // bool polarity = memory_.read(0x0084); Cycles::IntType total = 0; for(int c = 0; c < 2; c++) { while(tape_player_.input() != polarity) { total += tape_player_.get_cycles_until_next_event(); tape_player_.run_for_input_pulse(); } polarity ^= true; } memory_.write(0x0083, uint8_t(total / 56)); value = 0x39; } } } if constexpr (is_write(read_write)) { // TODO: could do better with the test below by having SAM calculate a range? const uint16_t video_start = sam_.graphics_address(); if(address >= video_start && address < video_start + 6144) { m6847_.flush(); } memory_.write(address, value); } } return delay; } private: struct M6809Traits { static constexpr bool uses_mrdy = false; static constexpr auto pause_precision = CPU::M6809::PausePrecision::BetweenInstructions; using BusHandlerT = ConcreteMachine; }; CPU::M6809::Processor m6809_; MemoryMap memory_; // MARK: - TimedMachine. void run_for(const Cycles cycles) final { m6809_.run_for(cycles); } void flush_output(const int outputs) final { if(outputs & Output::Video) { m6847_.flush(); } if(outputs & Output::Audio) { update_audio(); audio_queue_.perform(); } } // MARK: - Resettable. void soft_reset() final { m6847_->toggle_colour_phase(); sam_.reset(); m6809_.template set(true); } // MARK: - PIAs. friend struct PIA0Handler; struct PIA0Handler { PIA0Handler(ConcreteMachine &machine) : machine_(machine) { clear_all_keys(); } // // Port A: // // b7: joystick comparison input // b0–b6: keyboard row [input?] // b0–b1, b2–b3: joystick button inputs; // 0 and 2 = right joystick; 1 and 3 = left joystick; // 0 and 1 = switch 1; 2 and 3 = switch 2. // // CA1: horizontal sync // CA2: select line LSB of joystick MUX // // Port B: // // b0-b7: keyboard column [output?] // // CB1: field sync // CB2: select line MSB of joystick MUX // Interrupt outputs both connected to IRQ. template uint8_t input() { if constexpr (port == Motorola::MC6821::Port::A) { return (keyboard_column_ & 0x80 ? 0xff : key_columns_[7]) & (keyboard_column_ & 0x40 ? 0xff : key_columns_[6]) & (keyboard_column_ & 0x20 ? 0xff : key_columns_[5]) & (keyboard_column_ & 0x10 ? 0xff : key_columns_[4]) & (keyboard_column_ & 0x08 ? 0xff : key_columns_[3]) & (keyboard_column_ & 0x04 ? 0xff : key_columns_[2]) & (keyboard_column_ & 0x02 ? 0xff : key_columns_[1]) & (keyboard_column_ & 0x01 ? 0xff : key_columns_[0]) & ( machine_.dac_level_ > machine_.joystick(joystick_).axes[axis_] ? 0x7f : 0xff ) & (machine_.joystick(1).buttons[1] ? 0xf7 : 0xff) & (machine_.joystick(0).buttons[1] ? 0xfb : 0xff) & (machine_.joystick(1).buttons[0] ? 0xfd : 0xff) & (machine_.joystick(0).buttons[0] ? 0xfe : 0xff); } if constexpr (port == Motorola::MC6821::Port::B) { return (keyboard_row_ & 0x80 ? 0xff : key_rows_[7]) & (keyboard_row_ & 0x40 ? 0xff : key_rows_[6]) & (keyboard_row_ & 0x20 ? 0xff : key_rows_[5]) & (keyboard_row_ & 0x10 ? 0xff : key_rows_[4]) & (keyboard_row_ & 0x08 ? 0xff : key_rows_[3]) & (keyboard_row_ & 0x04 ? 0xff : key_rows_[2]) & (keyboard_row_ & 0x02 ? 0xff : key_rows_[1]) & (keyboard_row_ & 0x01 ? 0xff : key_rows_[0]); } __builtin_unreachable(); } template void output(const uint8_t value) { if constexpr (port == Motorola::MC6821::Port::A) { keyboard_row_ = value; } if constexpr (port == Motorola::MC6821::Port::B) { keyboard_column_ = value; } } template void set(const bool active) { if constexpr (irq == Motorola::MC6821::IRQ::A) { machine_.set_irq<0>(active); } if constexpr (irq == Motorola::MC6821::IRQ::B) { machine_.set_irq<1>(active); } } template void observe(const bool value) { if constexpr (control == Motorola::MC6821::Control::CA2) { axis_ = value; machine_.mux_ = (machine_.mux_ & 1) | (value ? 2 : 0); } if constexpr (control == Motorola::MC6821::Control::CB2) { joystick_ = value; machine_.mux_ = (machine_.mux_ & 2) | (value ? 1 : 0); } // Changes to the MUX might affect audio output, so give it a bump. machine_.set_audio(std::nullopt, std::nullopt); } void set_key_pressed(const int column, const int row, bool is_pressed) { const auto column_mask = uint8_t(1 << column); const auto row_mask = uint8_t(1 << row); key_columns_[row] &= ~column_mask; key_rows_[column] &= ~row_mask; if(!is_pressed) { key_columns_[row] |= column_mask; key_rows_[column] |= row_mask; } } void clear_all_keys() { std::fill(std::begin(key_columns_), std::end(key_columns_), 0xff); std::fill(std::begin(key_rows_), std::end(key_rows_), 0xff); } private: ConcreteMachine &machine_; uint8_t keyboard_column_ = 0xff, keyboard_row_ = 0xff; uint8_t key_columns_[8]{}, key_rows_[8]{}; uint8_t joystick_ = 0; uint8_t axis_ = 0; }; PIA0Handler pia0_handler_; Motorola::MC6821::MC6821 pia0_; bool irqs_[2]{}; template void set_irq(const bool active) { irqs_[slot] = active; m6809_.template set(irqs_[0] || irqs_[1]); } bool firqs_[2]{}; template void set_firq(const bool active) { firqs_[slot] = active; m6809_.template set(firqs_[0] || firqs_[1]); } friend struct PIA1Handler; struct PIA1Handler { PIA1Handler(ConcreteMachine &machine) : machine_(machine) {} // // Port A: // // b2-b7: 6-bit DAC output // b1: RS232 data output // b0: tape input // // CA1: RS232 carrier detect // CA2: tape motor control // // Port B: // // b7: 6847 alpha/graphics select (0 = alphanumeric) // b4–b6: VDG GM inputs; also b5 = 6847 invert; b4 = 6847 shift toggle // b3: colour set select (and RGB monitor detecting input? Probably CoCo3) // b2: ram size input // b1: 1-bit audio output // b0: RS232 data input // // Note to self: semigraphic 6847 line is connected to b7 of the data bus; it is set or unset depending on // the data fetched for that column. // // CB1: cartridge interrupt input // CB2: sound enable // Interrupt output connected to FIRQ. template uint8_t input() { if constexpr (port == Motorola::MC6821::Port::A) { return 0xfe | (machine_.tape_player_.input() ? 0x01 : 0x00); } if constexpr (port == Motorola::MC6821::Port::B) { return 0xff; } __builtin_unreachable(); } template void output(const uint8_t value) { if constexpr (port == Motorola::MC6821::Port::A) { machine_.dac_level_ = value >> 2; // The DAC might be the output, so allow it to inspect. machine_.set_audio(std::nullopt, std::nullopt); } if constexpr (port == Motorola::MC6821::Port::B) { machine_.set_audio(std::nullopt, value & 0x2); machine_.m6847_->set_mode( value & 0x80, // Alpha/graphics. false, // Graphics/semigraphics. value & 0x10, // External ROM. false, // Invert. (value >> 4) & 7, // Graphics mode. value & 0x08 // Colour select. ); } } template void set(const bool active) { if constexpr (irq == Motorola::MC6821::IRQ::A) { machine_.set_firq<0>(active); } if constexpr (irq == Motorola::MC6821::IRQ::B) { machine_.set_firq<1>(active); } } template void observe(const bool value) { if constexpr (control == Motorola::MC6821::Control::CA2) { machine_.tape_player_.set_motor_control(value); } if constexpr (control == Motorola::MC6821::Control::CB2) { machine_.set_audio(value, std::nullopt); } } private: ConcreteMachine &machine_; }; PIA1Handler pia1_handler_; Motorola::MC6821::MC6821 pia1_; // MARK: - SAM. Cycles bus_phase_; SAM sam_; // MARK: - Video and ScanProducer. JustInTimeActor< Motorola::MC6847::MC6847< is_pal ? Motorola::MC6847::FrameTiming::PALPaddedVsync : Motorola::MC6847::FrameTiming::NTSC, SAM, SAM, ModeMapper >, Cycles, 4 > m6847_; void set_scan_target(Outputs::Display::ScanTarget *const target) final { m6847_.get()->set_scan_target(target); } Outputs::Display::ScanStatus get_scaled_scan_status() const final { return m6847_.get()->get_scaled_scan_status() / 4.0; } void set_display_type(Outputs::Display::DisplayType display_type) final { m6847_.get()->set_display_type(display_type); } Outputs::Display::DisplayType get_display_type() const final { return m6847_.get()->get_display_type(); } // MARK: - MappedKeyboardMachine. bool is_dragon_ = false; Tandy::CoCo::Keyboard::KeyboardMapper keyboard_mapper_; KeyboardMapper *keyboard_mapper() final { return &keyboard_mapper_; } void set_key_state(const uint16_t key, const bool is_pressed) final { // The CoCo and Dragon differ in columns; a mapping from CoCo to Dragon column is: // // 0 -> 2 // 1 -> 3 // 2 -> 4 // 3 -> 5 // 4 -> 0 // 5 -> 1 // 6 -> 6 // 7 -> 7 const auto mapped_column = [&](const int column) { if(!is_dragon_ || column >= 6) { return column; } return (column + 2) % 6; }; pia0_handler_.set_key_pressed( mapped_column(Keyboard::column(key)), Keyboard::row(key), is_pressed ); } void clear_all_keys() final { pia0_handler_.clear_all_keys(); } void type_string(const std::wstring &string) final { Utility::TypeRecipient::add_typer(string); } bool can_type(const wchar_t c) const final { return Utility::TypeRecipient::can_type(c); } HalfCycles typer_delay(const std::wstring &) const final { if(m6809_.template get()) { return Cycles(2'750'000); } else { return Cycles(0); } } HalfCycles typer_frequency() const final { return Cycles(40'000); } // MARK: - MediaTarget and MediaChangeObserver. Storage::Tape::BinaryTapePlayer tape_player_; bool allow_fast_tape_loading_ = true; bool insert_media(const Analyser::Static::Media &media) override { if(!media.tapes.empty()) { tape_player_.set_tape(media.tapes.front(), TargetPlatform::ThomsonMO); } if(has_disk_drive && !media.disks.empty()) { for(size_t c = 0; c < media.disks.size() && c < 4; c++) { disk_controller_.set_disk(media.disks[c], c); } } const bool had_cartridge = !has_disk_drive && !media.cartridges.empty() && sam_.insert_cartridge(media.cartridges.front()->segments().front().data); return !media.tapes.empty() || had_cartridge || (has_disk_drive && !media.disks.empty()); } ChangeEffect effect_for_file_did_change(const std::string &) const override { return ChangeEffect::RestartMachine; } // MARK: - Configuration options. std::unique_ptr get_options() const final { auto options = std::make_unique(Configurable::OptionsType::UserFriendly); options->quick_load = allow_fast_tape_loading_; options->output = get_video_signal_configurable(); return options; } void set_options(const std::unique_ptr &str) final { const auto options = dynamic_cast(str.get()); allow_fast_tape_loading_ = options->quick_load; set_video_signal_configurable(options->output); } // MARK: - Activity Source. void set_activity_observer(Activity::Observer *const observer) override { tape_player_.set_activity_observer(observer); if(has_disk_drive) { disk_controller_.set_activity_observer(observer); } } // MARK: - Joysticks. std::vector> joysticks_; void construct_joysticks() { joysticks_.emplace_back(new Joystick); joysticks_.emplace_back(new Joystick); } const std::vector> &get_joysticks() final { return joysticks_; } Joystick &joystick(const size_t index) { return *static_cast(joysticks_[index].get()); } // MARK: - AudioProducer. Concurrency::AsyncTaskQueue audio_queue_; Audio::DAC audio_; Outputs::Speaker::PullLowpass speaker_; Cycles time_since_audio_update_; void update_audio() { const auto cycles = time_since_audio_update_.divide(AudioDivider); speaker_.run_for(audio_queue_, cycles); } Outputs::Speaker::Speaker *get_speaker() override { return &speaker_; } uint8_t dac_level_ = 0; uint8_t mux_ = 0; bool audio_enabled_ = false; bool audio_toggle_ = false; uint8_t audio_output_ = 0; void set_audio(const std::optional enabled, std::optional toggle) { const auto new_audio_enabled = enabled.value_or(audio_enabled_); audio_enabled_ = new_audio_enabled; const auto new_audio_toggle = toggle.value_or(audio_toggle_); audio_toggle_ = new_audio_toggle; // When audio is enabled, the MUX determines the audio source. Source 0 is the DAC. const auto new_audio_output = [&]() -> uint8_t { if(!audio_enabled_) { return audio_toggle_ ? 48 : 0; } switch(mux_) { default: return 0; case 0: return dac_level_; // Omitted at present (with a possibility that I've read the two mux lines the wrong way around): // // 1 -> cassette; // 2 -> cartridge; // 3 -> not used. } } (); if(new_audio_output == audio_output_) { return; } audio_output_ = new_audio_output; update_audio(); audio_.set_output(new_audio_output); } // MARK: - Disk. DiskController disk_controller_; Cycles cycles_16mhz_; }; } namespace { template std::unique_ptr create( const Analyser::Static::TandyCoCo::Target &target, const ROMMachine::ROMFetcher &rom_fetcher ) { if(Analyser::Static::TandyCoCo::is_pal(target.model)) { return std::make_unique>(target, rom_fetcher); } else { return std::make_unique>(target, rom_fetcher); } } } std::unique_ptr Machine::create( const Analyser::Static::Target &target, const ROMMachine::ROMFetcher &rom_fetcher ) { const auto &coco_target = static_cast(target); if(coco_target.has_disk_drive) { return ::create(coco_target, rom_fetcher); } else { return ::create(coco_target, rom_fetcher); } }