// // MO.cpp // Clock Signal // // Created by Thomas Harte on 04/03/2026. // Copyright © 2026 Thomas Harte. All rights reserved. // #include "MO.hpp" #include "CD90-640.hpp" #include "FastTape.hpp" #include "Keyboard.hpp" #include "MemoryMap.hpp" #include "Video.hpp" #include "Activity/Source.hpp" #include "Machines/MachineTypes.hpp" #include "Processors/6809/6809.hpp" #include "Components/6821/6821.hpp" #include "ClockReceiver/JustInTime.hpp" #include "Components/AudioToggle/AudioToggle.hpp" #include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "Outputs/Speaker/Implementation/BufferSource.hpp" #include "Outputs/Log.hpp" #include "Analyser/Static/Thomson/Target.hpp" using namespace Thomson::MO; namespace { using Log = Log::Logger; static constexpr int ClockRate = 1'000'000; static constexpr uint8_t MusicExpansionMask = 63; using Target = Analyser::Static::Thomson::MOTarget; constexpr Keyboard::Machine machine(const Target::Model model) { switch(model) { case Target::Model::MO5v1: case Target::Model::MO5v11: return Keyboard::Machine::MO5; case Target::Model::MO6v1: case Target::Model::MO6v2: case Target::Model::MO6v3: return Keyboard::Machine::MO6; case Target::Model::Prodest128: return Keyboard::Machine::Prodest128; default: __builtin_unreachable(); } } template struct ConcreteMachine: public Activity::Source, public Configurable::Device, public MachineTypes::AudioProducer, public MachineTypes::JoystickMachine, public MachineTypes::MappedKeyboardMachine, public MachineTypes::MediaChangeObserver, public MachineTypes::MediaTarget, public MachineTypes::SoftResettable, public MachineTypes::ScanProducer, public MachineTypes::TimedMachine, public Machine, public Utility::TypeRecipient { ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : Utility::TypeRecipient(machine(target.model)), m6809_(*this), system_pia_port_handler_(*this), system_pia_(system_pia_port_handler_), sound_and_game_pia_port_handler_(*this), sound_and_game_pia_(sound_and_game_pia_port_handler_), video_(memory_.video(true), memory_.video(false)), tape_player_(ClockRate), audio_(audio_queue_, MusicExpansionMask), speaker_(audio_), keyboard_mapper_(machine(target.model)) { set_clock_rate(ClockRate); speaker_.set_input_rate(ClockRate); construct_joysticks(); const auto BasicROM = [](const Target::Model model) { switch(model) { using enum Target::Model; default: __builtin_unreachable(); case MO5v1: return ROM::Name::ThomsonMO5v1; case MO5v11: return ROM::Name::ThomsonMO5v11; case MO6v1: return ROM::Name::ThomsonMO6v1; case MO6v2: return ROM::Name::ThomsonMO6v2; case MO6v3: return ROM::Name::ThomsonMO6v3; case Prodest128: return ROM::Name::OlivettiProdest128; } } (target.model); auto request = ROM::Request(BasicROM); if(has_floppy) { request = request && ROM::Request(ROM::Name::ThomsonCD90_640); } auto roms = rom_fetcher(request); if(!request.validate(roms)) { throw ROMMachine::Error::MissingROMs; } { auto rom = roms.find(BasicROM)->second; memory_.set_rom(rom); } if(has_floppy) { memory_.set_floppy_rom(roms.find(ROM::Name::ThomsonCD90_640)->second); } system_pia_.refresh(); 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 ) { static constexpr auto duration = CPU::M6809::duration(bus_phase); if(video_ += duration) { system_pia_.template set(video_.get()->irq()); } time_since_audio_update_ += duration; tape_player_.run_for(duration); if(typer_) { typer_->run_for(duration); } if(has_floppy) { fdc_.run_for(duration * 8); // My WD1770 has a nominal clock of 8Mhz. } // // Complete MO5 memory map: // // 0x0000–: video RAM, either pixels or attributes paged. // 0x2000–: RAM // 0xa000–: DOS ROM, if installed // 0xa7c0–: memory-mapped devices // 0xb000–: cartridge, if installed // 0xc000–: BASIC/ROM, if no cartridge installed // 0xf000–: BASIC/ROM // if constexpr (read_write != CPU::M6809::ReadWrite::NoData) { if(address >= 0xa7c0 && address < 0xa800) { const auto unmapped = [&] { if constexpr (CPU::M6809::is_read(read_write)) { value = 0xff; Log::info().append("Unhandled read at %04x", +address); } else { Log::info().append("Unhandled write: %02x -> %04x", +value, +address); } }; switch(address) { using namespace CPU::M6809; case 0xa7c0: access<0xa7c0, read_write>(system_pia_, value); if constexpr (CPU::M6809::is_read(read_write)) { if(allow_fast_tape_loading_ && tape_player_.motor_control()) { fast_tape_loader_.add_tape_read(m6809_.registers().pc.full, memory_); } } break; case 0xa7c1: access<0xa7c1, read_write>(system_pia_, value); break; case 0xa7c2: access<0xa7c2, read_write>(system_pia_, value); break; case 0xa7c3: access<0xa7c3, read_write>(system_pia_, value); break; case 0xa7cb: access<0xa7cb, read_write>(memory_, value); break; case 0xa7cc: access<0xa7cc, read_write>(sound_and_game_pia_, value); break; case 0xa7cd: access<0xa7cd, read_write>(sound_and_game_pia_, value); break; case 0xa7ce: access<0xa7ce, read_write>(sound_and_game_pia_, value); break; case 0xa7cf: access<0xa7cf, read_write>(sound_and_game_pia_, value); break; case 0xa7d0: if(has_floppy) access<0xa7d0, read_write>(fdc_, value); else unmapped(); break; case 0xa7d1: if(has_floppy) access<0xa7d1, read_write>(fdc_, value); else unmapped(); break; case 0xa7d2: if(has_floppy) access<0xa7d2, read_write>(fdc_, value); else unmapped(); break; case 0xa7d3: if(has_floppy) access<0xa7d3, read_write>(fdc_, value); else unmapped(); break; case 0xa7d8: if(has_floppy) access<0xa7d8, read_write>(fdc_, value); else unmapped(); break; case 0xa7e4: if(is_mo6) access<0xa7e4, read_write>(memory_, value); else unmapped(); break; case 0xa7e5: if(is_mo6) memory_.template system_access<0xa7e5, read_write>(video_, value); else unmapped(); break; case 0xa7e6: if(is_mo6) memory_.template system_access<0xa7e6, read_write>(video_, value); else unmapped(); break; case 0xa7e7: if constexpr (CPU::M6809::is_read(read_write)) { if constexpr (is_mo6) { value = video_->vertical_state() & memory_.template read<0xa7e7>(); } else { value = video_->vertical_state(); } } else { if constexpr (is_mo6) { access<0xa7e7, read_write>(video_, value); access<0xa7e7, read_write>(memory_, value); } else { unmapped(); } } break; case 0xa7da: if(is_mo6) access<0xa7da, read_write>(video_, value); else unmapped(); break; case 0xa7db: if(is_mo6) access<0xa7db, read_write>(video_, value); else unmapped(); break; case 0xa7dc: if(is_mo6) access<0xa7dc, read_write>(video_, value); else unmapped(); break; case 0xa7dd: if(is_mo6) { access<0xa7dd, read_write>(memory_, value); access<0xa7dd, read_write>(video_, value); } else { unmapped(); } break; default: unmapped(); break; } } else { if constexpr (CPU::M6809::is_read(read_write)) { value = memory_.read(address); if constexpr (lic == CPU::M6809::LIC::InstructionFetch) { if( allow_fast_tape_loading_ && tape_player_.motor_control() && fast_tape_loader_.trap_address.has_value() && address == *fast_tape_loader_.trap_address ) [[unlikely]] { auto *const serialiser = tape_player_.serialiser(); if(serialiser) { const auto action = fast_tape_loader_.did_trap( address, memory_, m6809_.registers(), tape_player_, *serialiser ); switch(action) { using enum Thomson::TrapAction; case None: break; case NOP: value = 0x12; break; case RTS: value = 0x39; break; default: __builtin_unreachable(); } } } } } else { if(address < 40*200) { video_.flush(); } memory_.write(address, value); } } } return Cycles(0); } 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_; friend struct SystemPIAPortHandler; struct SystemPIAPortHandler { SystemPIAPortHandler(ConcreteMachine &machine) : machine_(machine) {} // // Control lines: // // CA1: lightpen input // CA2: drive motor control output // CB1: 50Hz interrupt input // CB2: "Video incrustation"? // // // Port A: // // b7 [input]: tape data (high = tape attached but not running) // b6 [output]: tape data // b1–b5: ... machine specific ... // b0 [output]: lower 8kb paging // // MO5: // // b5 [input]: light pen button // b1–b3 [output]: border colour // // MO6: // // b5 [output]: ROM page selection // b4 [output]: shift lock LED // b3 [output]: additional bit for keyboard line // b2 [output]: "mute souris" (mute mouse? Probably related to the mouse and 6-bit DAC sharing lines.) // b1 [input]: light pen button (maybe?) // // // Port B: // // b7 [input]: state of selected key // b4–6 [output]: keyboard column // b1–3 [output]: keyboard line // b0 [output]: 1-bit sound // template uint8_t input() { if constexpr (port == Motorola::MC6821::Port::A) { return machine_.tape_player_.input() ? 0x00 : 0x80; } if constexpr (port == Motorola::MC6821::Port::B) { // Cf. R118, R119 and T21 from the schematic; if PA3 (stored here as bit 6) is active then // the output of PB1–3 (here bits 0 to 2) is ineffective. const auto key = key_ & 0x40 ? key_ & ~7 : key_; return key_states_[key] ? 0x00 : 0x80; } __builtin_unreachable(); } template void output(const uint8_t value) { if constexpr (port == Motorola::MC6821::Port::A) { machine_.memory_.page_video(value & 0x01); machine_.memory_.page_monitor(value & 0x20); if constexpr (!is_mo6) { machine_.video_->set_border_colour((value >> 1) & 0xf); } else { // TODO: determine why I'm having to invert this bit. key_ = (key_ & 0b0'111'111) | ((~value << 3) & 0b1'000'000); if(machine_.activity_observer_) { machine_.activity_observer_->set_led_status(machine_.ShiftLED, value & 0x10); } } } if constexpr (port == Motorola::MC6821::Port::B) { key_ = (key_ & 0b1'000'000) | ((value >> 1) & 0b0'111'111); machine_.set_audio(value & 1, std::nullopt); } } template void set(const bool active) { if constexpr (irq == Motorola::MC6821::IRQ::A) { machine_.m6809_.template set(active); } if constexpr (irq == Motorola::MC6821::IRQ::B) { machine_.m6809_.template set(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) { Log::info().append("Video encrustation (?) is %d", value); } } void clear_all_keys() { std::fill(std::begin(key_states_), std::end(key_states_), false); } void set_key_state(const uint16_t key, const bool is_pressed) { key_states_[key] = is_pressed; } private: ConcreteMachine &machine_; uint8_t key_ = 0; bool key_states_[0x80]{}; // There are 64 key addresses on an MO5; 128 on an MO6. }; SystemPIAPortHandler system_pia_port_handler_; Motorola::MC6821::MC6821 system_pia_; friend struct SoundAndGamePIAPortHandler; struct SoundAndGamePIAPortHandler { SoundAndGamePIAPortHandler(ConcreteMachine &machine) : machine_(machine) {} template void observe(const bool) {} template void output(const uint8_t value) { // Port B: // // b0–b5: DAC output if constexpr (port == Motorola::MC6821::Port::B) { machine_.set_audio(std::nullopt, value & MusicExpansionMask); } } template void set(const bool) {} template uint8_t input() { // Port A: // // b0: joystick 0 up / left mouse click // b1: joystick 0 down / right mouse click // b2: joystick 0 left / XB // b3: joystick 0 right / YB // b4: joystick 1 up // b5: joystick 1 down // b6: joystick 1 left // b7: joystick 1 right if constexpr (port == Motorola::MC6821::Port::A) { return port_a_; } // Port B: // // b0: common 0 // b1: common 1 // b2: joystick 0 button 2 / XA // b3: joystick 1 button 2 // b4: // b5: // b6: joystick 0 button 1 / YA // b7: joystick 1 button 1 if constexpr (port == Motorola::MC6821::Port::B) { return port_b_; } return 0xff; } // Control ports: (CE/CF?) // // CA1: joystick 0 button 2 // CA2: joystick 0 button 1 // CB1: joystick 1 button 2 // CB2: joystick 1 button 1 void set_joystick_input( const int index, const Inputs::Joystick::Input &input, bool is_active ) { const auto apply = [&](uint8_t &port, const uint8_t mask) { if(is_active) { port &= ~mask; } else { port |= mask; } }; switch(input.type) { using enum Inputs::Joystick::Input::Type; case Up: apply(port_a_, index ? 0x10 : 0x01); break; case Down: apply(port_a_, index ? 0x20 : 0x02); break; case Left: apply(port_a_, index ? 0x40 : 0x04); break; case Right: apply(port_a_, index ? 0x80 : 0x08); break; case Fire: if(input.info.control.index) { apply(port_b_, index ? 0x08 : 0x04); if(index) { machine_.sound_and_game_pia_.template set(is_active); } else { machine_.sound_and_game_pia_.template set(is_active); } } else { apply(port_b_, index ? 0x80 : 0x40); if(index) { machine_.sound_and_game_pia_.template set(is_active); } else { machine_.sound_and_game_pia_.template set(is_active); } } break; default: break; } } private: ConcreteMachine &machine_; uint8_t port_a_ = 0xff; uint8_t port_b_ = 0xff; }; struct Joystick: public Inputs::ConcreteJoystick { Joystick(SoundAndGamePIAPortHandler &pia_port_handler, const int index) : ConcreteJoystick({ Input(Input::Up), Input(Input::Down), Input(Input::Left), Input(Input::Right), Input(Input::Fire, 0), Input(Input::Fire, 1), }), pia_port_handler_(pia_port_handler), index_(index) {} void did_set_input(const Input &digital_input, bool is_active) final { pia_port_handler_.set_joystick_input(index_, digital_input, is_active); } private: SoundAndGamePIAPortHandler &pia_port_handler_; int index_; }; SoundAndGamePIAPortHandler sound_and_game_pia_port_handler_; Motorola::MC6821::MC6821 sound_and_game_pia_; JustInTimeActor video_; // MARK: - Tape and disk. Storage::Tape::BinaryTapePlayer tape_player_; bool allow_fast_tape_loading_ = true; Thomson::FastTapeLoader fast_tape_loader_; Thomson::CD90_640 fdc_; // 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_.flush(); speaker_.run_for(audio_queue_, cycles); } Outputs::Speaker::Speaker *get_speaker() override { return &speaker_; } bool audio_enabled_ = false; uint8_t audio_level_ = MusicExpansionMask; void set_audio(const std::optional enabled, const std::optional level) { const auto new_audio_enabled = enabled.value_or(audio_enabled_); const auto new_audio_level = level.value_or(audio_level_); if(new_audio_level == audio_level_ && new_audio_enabled == audio_enabled_) { return; } audio_level_ = new_audio_level; audio_enabled_ = new_audio_enabled; update_audio(); audio_.set_output(audio_enabled_ ? audio_level_ : 0); } // MARK: - ScanProducer. void set_scan_target(Outputs::Display::ScanTarget *const target) final { video_.get()->set_scan_target(target); } Outputs::Display::ScanStatus get_scaled_scan_status() const final { return video_.get()->get_scaled_scan_status(); } // MARK: - TimedMachine. void run_for(const Cycles cycles) final { m6809_.run_for(cycles); } void flush_output(const int outputs) final { if(outputs & Output::Video) { video_.flush(); } if(outputs & Output::Audio) { update_audio(); audio_queue_.perform(); } } // MARK: - Resettable. void soft_reset() final { m6809_.template set(true); memory_.reset(); } // MARK: - MappedKeyboardMachine. Thomson::MO::Keyboard::KeyboardMapper keyboard_mapper_; KeyboardMapper *keyboard_mapper() final { return &keyboard_mapper_; } void set_key_state(const uint16_t key, const bool is_pressed) final { system_pia_port_handler_.set_key_state(key, is_pressed); } void clear_all_keys() final { system_pia_port_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 is_mo6 ? Cycles(1'000'000) : Cycles(1'000'000); } else { return Cycles(0); } } HalfCycles typer_frequency() const final { return Cycles(30'000); } // MARK: - MediaTarget and MediaChangeObserver. bool insert_media(const Analyser::Static::Media &media) override { if(!media.tapes.empty()) { tape_player_.set_tape(media.tapes.front(), TargetPlatform::ThomsonMO); } if(!media.cartridges.empty()) { auto rom = media.cartridges.front()->segments().front().data; if(rom.size() < 16384) { rom.resize(16384); } memory_.set_cartridge(rom); } if(has_floppy) { size_t index = 0; for(auto &disk: media.disks) { fdc_.set_disk(disk, index); ++ index; } } return !media.tapes.empty() || (!media.disks.empty() && has_floppy) || !media.cartridges.empty(); } ChangeEffect effect_for_file_did_change(const std::string &) const override { // TODO: unless it's a disk that's already inserted. return ChangeEffect::RestartMachine; } // MARK: - Activity Source. void set_activity_observer(Activity::Observer *const observer) override { if(is_mo6) { activity_observer_ = observer; activity_observer_->register_led(ShiftLED, Activity::Observer::LEDPresentation::Persistent); } tape_player_.set_activity_observer(observer); if(has_floppy) { fdc_.set_activity_observer(observer); } } static constexpr char ShiftLED[] = "Shift Lock"; Activity::Observer *activity_observer_ = nullptr; // 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_; 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; } // MARK: - Joystick Machine. const std::vector> &get_joysticks() override { return joysticks_; } std::vector> joysticks_; void construct_joysticks() { joysticks_.push_back(std::make_unique(sound_and_game_pia_port_handler_, 0)); joysticks_.push_back(std::make_unique(sound_and_game_pia_port_handler_, 1)); } }; } namespace { template std::unique_ptr machine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) { switch(target.floppy) { using enum Target::Floppy; case None: return std::make_unique>(target, rom_fetcher); case CD90_640: return std::make_unique>(target, rom_fetcher); default: __builtin_unreachable(); } } } std::unique_ptr Machine::create( const Analyser::Static::Target &target, const ROMMachine::ROMFetcher &rom_fetcher ) { const auto &thomson_target = static_cast(target); if(Analyser::Static::Thomson::is_mo6(thomson_target.model)) { return machine(thomson_target, rom_fetcher); } else { return machine(thomson_target, rom_fetcher); } }