diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp index 0c026a450..cf9c3233e 100644 --- a/Analyser/Static/StaticAnalyser.cpp +++ b/Analyser/Static/StaticAnalyser.cpp @@ -51,6 +51,7 @@ #include "Storage/Disk/DiskImage/Formats/G64.hpp" #include "Storage/Disk/DiskImage/Formats/DMK.hpp" #include "Storage/Disk/DiskImage/Formats/FAT12.hpp" +#include "Storage/Disk/DiskImage/Formats/FD.hpp" #include "Storage/Disk/DiskImage/Formats/HFE.hpp" #include "Storage/Disk/DiskImage/Formats/IPF.hpp" #include "Storage/Disk/DiskImage/Formats/IMD.hpp" @@ -240,6 +241,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: accumulator.try_standard>(TargetPlatform::MSX, "dsk"); accumulator.try_standard>(TargetPlatform::Oric, "dsk"); + accumulator.try_standard>(TargetPlatform::ThomsonMO, "fd"); + accumulator.try_standard>(TargetPlatform::Commodore8bit, "g64"); accumulator.try_standard(TargetPlatform::AppleII, "hdv"); diff --git a/Analyser/Static/Thomson/StaticAnalyser.cpp b/Analyser/Static/Thomson/StaticAnalyser.cpp index 844464a6e..a14ca0d46 100644 --- a/Analyser/Static/Thomson/StaticAnalyser.cpp +++ b/Analyser/Static/Thomson/StaticAnalyser.cpp @@ -45,6 +45,12 @@ Analyser::Static::TargetList Analyser::Static::Thomson::GetTargets( } } + // Currently: accept all floppy disks. + if(!media.disks.empty()) { + target->floppy = MOTarget::Floppy::CD90_640; + target->media.disks = media.disks; + } + if(!target->media.empty()) { destination.push_back(std::move(target)); } diff --git a/Analyser/Static/Thomson/Target.hpp b/Analyser/Static/Thomson/Target.hpp index be20f6754..6cf60d7d2 100644 --- a/Analyser/Static/Thomson/Target.hpp +++ b/Analyser/Static/Thomson/Target.hpp @@ -18,12 +18,16 @@ namespace Analyser::Static::Thomson { struct MOTarget: public Analyser::Static::Target, public Reflection::StructImpl { std::string loading_command; + ReflectableEnum(Floppy, None, CD90_640); + Floppy floppy = Floppy::None; + MOTarget() : Analyser::Static::Target(Machine::ThomsonMO) {} private: friend Reflection::StructImpl; void declare_fields() { - // None yet. + AnnounceEnum(Floppy); + DeclareField(floppy); } }; diff --git a/Components/1770/1770.cpp b/Components/1770/1770.cpp index e1be73aba..fda3469f3 100644 --- a/Components/1770/1770.cpp +++ b/Components/1770/1770.cpp @@ -207,6 +207,7 @@ void WD1770::posit_event(const int new_event_type) { status.data_request = false; status.spin_up = false; }); + // TODO: potentially should disable motors? } else { if(!(interesting_event_mask_ & int(new_event_type))) return; interesting_event_mask_ &= ~new_event_type; diff --git a/Machines/Acorn/Electron/Plus3.cpp b/Machines/Acorn/Electron/Plus3.cpp index 207dd91ff..3ff02de4c 100644 --- a/Machines/Acorn/Electron/Plus3.cpp +++ b/Machines/Acorn/Electron/Plus3.cpp @@ -15,7 +15,7 @@ Plus3::Plus3() : WD1770(P1770) { set_control_register(last_control_, 0xff); } -void Plus3::set_disk(std::shared_ptr disk, size_t drive) { +void Plus3::set_disk(std::shared_ptr disk, const size_t drive) { get_drive(drive).set_disk(disk); } @@ -31,7 +31,7 @@ const Storage::Disk::Disk *Plus3::disk(const std::string &name) { } -void Plus3::set_control_register(uint8_t control) { +void Plus3::set_control_register(const uint8_t control) { // bit 0 => enable or disable drive 1 // bit 1 => enable or disable drive 2 // bit 2 => side select @@ -43,7 +43,7 @@ void Plus3::set_control_register(uint8_t control) { set_control_register(control, changes); } -void Plus3::set_control_register(uint8_t control, uint8_t changes) { +void Plus3::set_control_register(const uint8_t control, const uint8_t changes) { if(changes&3) { set_drive(control&3); } @@ -57,13 +57,13 @@ void Plus3::set_control_register(uint8_t control, uint8_t changes) { if(changes & 0x08) set_is_double_density(!(control & 0x08)); } -void Plus3::set_motor_on(bool on) { +void Plus3::set_motor_on(const bool on) { // TODO: this status should transfer if the selected drive changes. But the same goes for // writing state, so plenty of work to do in general here. get_drive().set_motor_on(on); } -void Plus3::set_activity_observer(Activity::Observer *observer) { +void Plus3::set_activity_observer(Activity::Observer *const observer) { for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) { drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true); }); diff --git a/Machines/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp new file mode 100644 index 000000000..f3e5cc3a8 --- /dev/null +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -0,0 +1,76 @@ +// +// CD90-640.cpp +// Clock Signal +// +// Created by Thomas Harte on 04/04/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#include "CD90-640.hpp" + +using namespace Thomson; + +CD90_640::CD90_640() : WD::WD1770(P1770) { + // 325 is a peculiar RPM, but seems to match a spin-up test in the disk ROM that polls the WD1770's status register + // index hole bit and counts time. Furthermore there are other machines with unusual RPMs. Could definitely + // still just imply an issue elsewhere in the emulator though. + emplace_drives(2, 8000000, 325, 2); +} + +uint8_t CD90_640::control() { + return control_; // Possibly only b7 is loaded? +} + +void CD90_640::set_control(const uint8_t value) { + control_ = value; + + // Following along from the schematic in https://github.com/OlivierP-To8/CD90-640/ : + // + // The 74LS123 on the second page is fed with: + // + // D0 = external data line 7 + // D1 = line 0 + // D2 = line 1 + // D3 = line 2 + // + // It then routes: + // + // Q0 = DDEN select + // Q1 = side sleect + // Q2, Q3 = drive selects + // + set_is_double_density(!(value & 0x80)); + for_all_drives( [&](Storage::Disk::Drive &drive, size_t) { + drive.set_head(value & 1); + }); + set_drive((value >> 1) & 3); +} + +void CD90_640::set_motor_on(const bool motor) { + for_all_drives( [&](Storage::Disk::Drive &drive, size_t) { + drive.set_motor_on(motor); + }); +} + +void CD90_640::set_activity_observer(Activity::Observer *const observer) { + for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) { + drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true); + }); +} + +// TODO: the code below is fairly boilerplate; can it be factored out? + +void CD90_640::set_disk(std::shared_ptr disk, const size_t drive) { + get_drive(drive).set_disk(disk); +} + +const Storage::Disk::Disk *CD90_640::disk(const std::string &name) { + const Storage::Disk::Disk *result = nullptr; + for_all_drives( [&](Storage::Disk::Drive &drive, size_t) { + const auto disk = drive.disk(); + if(disk && disk->represents(name)) { + result = disk; + } + }); + return result; +} diff --git a/Machines/Thomson/MO5/CD90-640.hpp b/Machines/Thomson/MO5/CD90-640.hpp new file mode 100644 index 000000000..9fff33b52 --- /dev/null +++ b/Machines/Thomson/MO5/CD90-640.hpp @@ -0,0 +1,59 @@ +// +// CD90-640.hpp +// Clock Signal +// +// Created by Thomas Harte on 04/04/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "Components/1770/1770.hpp" +#include "Outputs/Log.hpp" + +#include + +namespace Thomson { + +class CD90_640 final : public WD::WD1770 { +public: + CD90_640(); + + void set_disk(std::shared_ptr, size_t drive); + const Storage::Disk::Disk *disk(const std::string &); + void set_activity_observer(Activity::Observer *); + + template + uint8_t read() { + const uint8_t result = [&] { + if(address & 8) { + return control(); + } else { + return WD::WD1770::read(address); + } + } (); + Logger::info().append("Read %02x <- %04x", result, address); + return result; + } + + template + void write(const uint8_t value) { + Logger::info().append("Write %02x -> %04x", value, address); + if(address & 8) { + set_control(value); + } else { + WD::WD1770::write(address, value); + } + } + +private: + uint8_t control(); + void set_control(uint8_t); + uint8_t control_; + + using Logger = Log::Logger; + + void set_motor_on(bool) override; +}; + +} diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index e53b9282c..b9280e2f3 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -8,6 +8,7 @@ #include "MO5.hpp" +#include "CD90-640.hpp" #include "Video.hpp" #include "Activity/Source.hpp" @@ -36,6 +37,7 @@ using Log = Log::Logger; static constexpr int ClockRate = 1'000'000; static constexpr uint8_t MusicExpansionMask = 63; +template struct ConcreteMachine: public Activity::Source, public Configurable::Device, @@ -64,15 +66,24 @@ struct ConcreteMachine: speaker_.set_input_rate(ClockRate); construct_joysticks(); - const auto request = ROM::Request(ROM::Name::ThomasonMO5v11); + auto request = ROM::Request(ROM::Name::ThomsonMO5v11); + if(has_floppy) { + request = request && ROM::Request(ROM::Name::ThomsonCD90_640); + } + auto roms = rom_fetcher(request); if(!request.validate(roms)) { throw ROMMachine::Error::MissingROMs; } - const auto &rom = roms.find(ROM::Name::ThomasonMO5v11)->second; + const auto &rom = roms.find(ROM::Name::ThomsonMO5v11)->second; std::copy_n(rom.begin(), rom.size(), rom_.begin()); + if(has_floppy) { + const auto &floppy_rom = roms.find(ROM::Name::ThomsonCD90_640)->second; + std::copy_n(floppy_rom.begin(), floppy_rom.size(), floppy_rom_.begin()); + } + Memory::Fuzz(ram_); system_pia_.refresh(); @@ -112,18 +123,28 @@ struct ConcreteMachine: ) { static constexpr auto duration = CPU::M6809::duration(bus_phase); if(video_ += duration) { - system_pia_.set(video_.last_valid()->irq()); + system_pia_.template set(video_.last_valid()->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. + } - if constexpr (read_write == CPU::M6809::ReadWrite::NoData) { - return Cycles(0); - } else { + 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) { case 0xa7c0: access<0xa7c0, read_write>(system_pia_, value); break; case 0xa7c1: access<0xa7c1, read_write>(system_pia_, value); break; @@ -141,19 +162,22 @@ struct ConcreteMachine: } 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; + default: - 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); - } + unmapped(); break; } } else { if constexpr (CPU::M6809::is_read(read_write)) { if(address < 0x2000) { value = start_pointer_[address]; + } else if(address >= 0xa000 && address < 0xa7c0) { + value = has_floppy ? floppy_rom_[address - 0xa000] : 0xff; } else if(address >= 0xc000) { value = rom_[address - 0xc000]; @@ -174,7 +198,7 @@ struct ConcreteMachine: if(!serialiser) return; Storage::Tape::Thomson::MO::Parser parser; - const auto dp = m6809_.registers().reg(); + const auto dp = m6809_.registers().template reg(); auto &polarity = ram_[size_t((dp << 8) | 0x44)]; auto &data = ram_[size_t((dp << 8) | 0x45)]; @@ -193,7 +217,7 @@ struct ConcreteMachine: if(!*bit) { polarity ^= 0xff; } - m6809_.registers().reg() = *bit ? 0xff : 0x00; + m6809_.registers().template reg() = *bit ? 0xff : 0x00; // The parser reads up to the end of the bit. The ROM routine ends about two-thirds // of the way through the bit. So 'rewind' the tape a little. @@ -212,7 +236,7 @@ struct ConcreteMachine: start_pointer_[address] = value; } else { if constexpr (!CPU::M6809::is_read(read_write)) { - if(address >= 0xc000) { + if(address >= 0xa000) { Log::info().append("ROM write at %04x", +address); } } @@ -234,6 +258,7 @@ private: std::array ram_; std::array rom_; + std::array floppy_rom_; uint8_t *start_pointer_ = nullptr; uint8_t *video_page(const bool pixels) { @@ -299,11 +324,11 @@ private: template void set(const bool active) { if constexpr (irq == Motorola::MC6821::IRQ::A) { - machine_.m6809_.set(active); + machine_.m6809_.template set(active); } if constexpr (irq == Motorola::MC6821::IRQ::B) { - machine_.m6809_.set(active); + machine_.m6809_.template set(active); } } @@ -419,17 +444,17 @@ private: apply(port_b_, index ? 0x08 : 0x04); if(index) { - machine_.sound_and_game_pia_.set(is_active); + machine_.sound_and_game_pia_.template set(is_active); } else { - machine_.sound_and_game_pia_.set(is_active); + machine_.sound_and_game_pia_.template set(is_active); } } else { apply(port_b_, index ? 0x80 : 0x40); if(index) { - machine_.sound_and_game_pia_.set(is_active); + machine_.sound_and_game_pia_.template set(is_active); } else { - machine_.sound_and_game_pia_.set(is_active); + machine_.sound_and_game_pia_.template set(is_active); } } break; @@ -469,9 +494,14 @@ private: Motorola::MC6821::MC6821 sound_and_game_pia_; JustInTimeActor video_; + + // MARK: - Tape and disk. + Storage::Tape::BinaryTapePlayer tape_player_; bool allow_fast_tape_hack_ = true; + Thomson::CD90_640 fdc_; + // MARK: - AudioProducer. Concurrency::AsyncTaskQueue audio_queue_; @@ -546,7 +576,7 @@ private: } HalfCycles get_typer_delay(const std::string &) const final { - if(m6809_.get()) { + if(m6809_.template get()) { return Cycles(1'000'000); } else { return Cycles(0); @@ -564,7 +594,16 @@ private: tape_player_.set_tape(media.tapes.front(), TargetPlatform::ThomsonMO); } - return !media.tapes.empty(); + if(!has_floppy) { + return !media.tapes.empty(); + } + + size_t index = 0; + for(auto &disk: media.disks) { + fdc_.set_disk(disk, index); + ++ index; + } + return !media.tapes.empty() || !media.disks.empty(); } ChangeEffect effect_for_file_did_change(const std::string &) const override { @@ -573,8 +612,11 @@ private: // MARK: - Activity Source. - void set_activity_observer(Activity::Observer *observer) override { + void set_activity_observer(Activity::Observer *const observer) override { tape_player_.set_activity_observer(observer); + if(has_floppy) { + fdc_.set_activity_observer(observer); + } } // MARK: - Configuration options. @@ -611,5 +653,9 @@ std::unique_ptr Machine::ThomsonMO( ) { using Target = Analyser::Static::Thomson::MOTarget; const Target *const thomson_target = dynamic_cast(target); - return std::make_unique(*thomson_target, rom_fetcher); + + switch(thomson_target->floppy) { + case Target::Floppy::None: return std::make_unique>(*thomson_target, rom_fetcher); + case Target::Floppy::CD90_640: return std::make_unique>(*thomson_target, rom_fetcher); + } } diff --git a/Machines/Utility/ROMCatalogue.cpp b/Machines/Utility/ROMCatalogue.cpp index 251ff5912..81236b015 100644 --- a/Machines/Utility/ROMCatalogue.cpp +++ b/Machines/Utility/ROMCatalogue.cpp @@ -1021,7 +1021,7 @@ const std::vector &Description::all_roms() { // Thomson MO and TO machines. // { - ThomasonMO5v1, + ThomsonMO5v1, "Thomson", "the Thomson MO5 ROM, version 1", "mo5-v1.0.rom", @@ -1029,7 +1029,7 @@ const std::vector &Description::all_roms() { 0xf62d2f3au }, { - ThomasonMO5v11, + ThomsonMO5v11, "Thomson", "the Thomson MO5 ROM, version 1.1", "mo5-v1.1.rom", @@ -1037,6 +1037,15 @@ const std::vector &Description::all_roms() { 0x237c60bfu }, + { + ThomsonCD90_640, + "Thomson", + "the Thomson CD90-640 ROM", + "cd90-640.rom", + 0x7c0, + 0x5114c0a5u + }, + // // Vic-20. // diff --git a/Machines/Utility/ROMCatalogue.hpp b/Machines/Utility/ROMCatalogue.hpp index bcb9a6fb8..72b19229e 100644 --- a/Machines/Utility/ROMCatalogue.hpp +++ b/Machines/Utility/ROMCatalogue.hpp @@ -176,8 +176,10 @@ enum Name { SinclairQLJS, // Thomson MO and TO machines. - ThomasonMO5v1, - ThomasonMO5v11, + ThomsonMO5v1, + ThomsonMO5v11, + + ThomsonCD90_640, // Vic-20. Vic20BASIC, diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index f385ced30..4da662ed5 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -133,6 +133,12 @@ 4B055AEE1FAE9BBF0060FFFF /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B86E2591F8C628F006FAA45 /* Keyboard.cpp */; }; 4B055AEF1FAE9BF00060FFFF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; }; 4B055AF21FAE9C1C0060FFFF /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B055AF01FAE9C080060FFFF /* OpenGL.framework */; }; + 4B062D1A2F81427D00472717 /* CD90-640.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D192F81427D00472717 /* CD90-640.cpp */; }; + 4B062D1B2F81427D00472717 /* CD90-640.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D192F81427D00472717 /* CD90-640.cpp */; }; + 4B062D1C2F81427D00472717 /* CD90-640.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D192F81427D00472717 /* CD90-640.cpp */; }; + 4B062D1F2F81589C00472717 /* FD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D1E2F81589C00472717 /* FD.cpp */; }; + 4B062D202F81589C00472717 /* FD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D1E2F81589C00472717 /* FD.cpp */; }; + 4B062D212F81589C00472717 /* FD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B062D1E2F81589C00472717 /* FD.cpp */; }; 4B06AACB2C645ED00034D014 /* AY38910.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4A762E1DB1A3FA007AAE2E /* AY38910.cpp */; }; 4B06AACC2C645EDF0034D014 /* Archimedes.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB505842B9634F30031C43C /* Archimedes.cpp */; }; 4B06AACD2C645EE50034D014 /* Enterprise.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B051CA12676F52200CA44E8 /* Enterprise.cpp */; }; @@ -1405,6 +1411,10 @@ 4B055ABE1FAE98000060FFFF /* MachineForTarget.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MachineForTarget.cpp; sourceTree = ""; }; 4B055ABF1FAE98000060FFFF /* MachineForTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MachineForTarget.hpp; sourceTree = ""; }; 4B055AF01FAE9C080060FFFF /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; + 4B062D182F81427D00472717 /* CD90-640.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = "CD90-640.hpp"; sourceTree = ""; }; + 4B062D192F81427D00472717 /* CD90-640.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "CD90-640.cpp"; sourceTree = ""; }; + 4B062D1D2F81589C00472717 /* FD.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FD.hpp; sourceTree = ""; }; + 4B062D1E2F81589C00472717 /* FD.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = FD.cpp; sourceTree = ""; }; 4B08A2741EE35D56008B7065 /* Z80InterruptTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Z80InterruptTests.swift; sourceTree = ""; }; 4B08A2761EE39306008B7065 /* TestMachine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestMachine.h; sourceTree = ""; }; 4B08A2771EE39306008B7065 /* TestMachine.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TestMachine.mm; sourceTree = ""; }; @@ -3413,6 +3423,7 @@ 4B4518911F75FD1B00926311 /* D64.cpp */, 4BAF2B4C2004580C00480230 /* DMK.cpp */, 4BEBFB4B2002C4BF000708CC /* FAT12.cpp */, + 4B062D1E2F81589C00472717 /* FD.cpp */, 4B4518931F75FD1B00926311 /* G64.cpp */, 4B4518951F75FD1B00926311 /* HFE.cpp */, 42EB81262B23AAC300429AF4 /* IMD.cpp */, @@ -3420,13 +3431,13 @@ 4B7773512DE894E400933F03 /* JFD.cpp */, 4BB4BFAE22A42F290069048D /* MacintoshIMG.cpp */, 4B58601C1F806AB200AEE2E3 /* MFMSectorDump.cpp */, + 4B5617322F42ADB8003CB7FE /* MOOF.cpp */, 4BC131782346DF2B00E4FF3D /* MSA.cpp */, 4B0F94FC208C1A1600FE41D9 /* NIB.cpp */, 4B4518971F75FD1B00926311 /* OricMFMDSK.cpp */, 423820422B1A90BE00964EFE /* PCBooter.cpp */, 4B4518991F75FD1B00926311 /* SSD.cpp */, 4B7BA03323C58B1E00B98D9E /* STX.cpp */, - 4B5617322F42ADB8003CB7FE /* MOOF.cpp */, 4B6ED2EE208E2F8A0047B343 /* WOZ.cpp */, 4B80CD75256CA15E00176FCC /* 2MG.hpp */, 4B45188E1F75FD1B00926311 /* AcornADF.hpp */, @@ -3436,6 +3447,7 @@ 4B4518921F75FD1B00926311 /* D64.hpp */, 4BAF2B4D2004580C00480230 /* DMK.hpp */, 4BEBFB4C2002C4BF000708CC /* FAT12.hpp */, + 4B062D1D2F81589C00472717 /* FD.hpp */, 4B4518941F75FD1B00926311 /* G64.hpp */, 4B4518961F75FD1B00926311 /* HFE.hpp */, 42EB81272B23AAC300429AF4 /* IMD.hpp */, @@ -3443,13 +3455,13 @@ 4B7773502DE894E400933F03 /* JFD.hpp */, 4BB4BFAF22A42F290069048D /* MacintoshIMG.hpp */, 4B58601D1F806AB200AEE2E3 /* MFMSectorDump.hpp */, + 4B5617312F42ADB8003CB7FE /* MOOF.hpp */, 4BC131792346DF2B00E4FF3D /* MSA.hpp */, 4B0F94FD208C1A1600FE41D9 /* NIB.hpp */, 4B4518981F75FD1B00926311 /* OricMFMDSK.hpp */, 423820432B1A90BE00964EFE /* PCBooter.hpp */, 4B45189A1F75FD1B00926311 /* SSD.hpp */, 4B7BA03223C58B1E00B98D9E /* STX.hpp */, - 4B5617312F42ADB8003CB7FE /* MOOF.hpp */, 4B6ED2EF208E2F8A0047B343 /* WOZ.hpp */, 4BFDD7891F7F2DB4008579B9 /* Utility */, ); @@ -5350,8 +5362,10 @@ 4BCA6FAA2F589C0C000B9537 /* MO5 */ = { isa = PBXGroup; children = ( + 4B062D192F81427D00472717 /* CD90-640.cpp */, 4BCA6FA92F589C0C000B9537 /* MO5.cpp */, 4BBC7DC02F6D94E500E71794 /* Video.cpp */, + 4B062D182F81427D00472717 /* CD90-640.hpp */, 4BBC7DC52F70BAFB00E71794 /* Keyboard.hpp */, 4BCA6FA82F589C0C000B9537 /* MO5.hpp */, 4BBC7DBF2F6D94E500E71794 /* Video.hpp */, @@ -6538,6 +6552,7 @@ 4B0ACC03237756F6008902D0 /* Line.cpp in Sources */, 4B055AB11FAE86070060FFFF /* Tape.cpp in Sources */, 4B96F7CF263E33B10092AEE1 /* DSK.cpp in Sources */, + 4B062D1C2F81427D00472717 /* CD90-640.cpp in Sources */, 4B2B946626377C0200E7097C /* SZX.cpp in Sources */, 4BEDA43225B3C700000C2DBD /* Executor.cpp in Sources */, 4BC1317B2346DF2B00E4FF3D /* MSA.cpp in Sources */, @@ -6588,6 +6603,7 @@ 4B055A911FAE85B50060FFFF /* Cartridge.cpp in Sources */, 4B8DD39826360DDF00B3C866 /* Z80.cpp in Sources */, 4B894525201967B4007DE474 /* Tape.cpp in Sources */, + 4B062D1F2F81589C00472717 /* FD.cpp in Sources */, 4B055AB21FAE860F0060FFFF /* CommodoreTAP.cpp in Sources */, 4BC6236F26F426B400F83DFE /* FAT.cpp in Sources */, 4B0F1BFD260300D900B85C66 /* ZXSpectrum.cpp in Sources */, @@ -6860,11 +6876,13 @@ 4B894528201967B4007DE474 /* Disk.cpp in Sources */, 4B7773542DE894E400933F03 /* JFD.cpp in Sources */, 4B2E86CF25D8D8C70024F1E9 /* Keyboard.cpp in Sources */, + 4B062D202F81589C00472717 /* FD.cpp in Sources */, 4B4C81CA28B56CF800F84AE9 /* MacintoshVolume.cpp in Sources */, 4BBB70A4202011C2002FE009 /* MultiMediaTarget.cpp in Sources */, 4B7C681627517A59001671EC /* Sprites.cpp in Sources */, 4B0ACC02237756ED008902D0 /* Line.cpp in Sources */, 4B5617402F44D533003CB7FE /* ROMCatalogue.cpp in Sources */, + 4B062D1B2F81427D00472717 /* CD90-640.cpp in Sources */, 4B89453A201967B4007DE474 /* StaticAnalyser.cpp in Sources */, 4BB697CB1D4B6D3E00248BDF /* TimedEventLoop.cpp in Sources */, 4BDACBEC22FFA5D20045EF7E /* ncr5380.cpp in Sources */, @@ -7014,10 +7032,12 @@ 4B778F5923A5F2D00000D260 /* Z80.cpp in Sources */, 4BD971392BFC3D9D00C907AA /* ArchimedesStaticAnalyserTests.mm in Sources */, 4BBC7DC22F6D94E500E71794 /* Video.cpp in Sources */, + 4B062D212F81589C00472717 /* FD.cpp in Sources */, 4B08A2751EE35D56008B7065 /* Z80InterruptTests.swift in Sources */, 4B778F0E23A5EC4F0000D260 /* Tape.cpp in Sources */, 4B778F2D23A5EF190000D260 /* MFMDiskController.cpp in Sources */, 4B06AAD92C645F5D0034D014 /* SN76489.cpp in Sources */, + 4B062D1A2F81427D00472717 /* CD90-640.cpp in Sources */, 423BDC4A2AB24699008E37B6 /* 8088Tests.mm in Sources */, 4B778F2723A5EEF60000D260 /* BinaryDump.cpp in Sources */, 4BFCA1241ECBDCB400AC40C1 /* AllRAMProcessor.cpp in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index 791091967..8a06b7185 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -875,6 +875,28 @@ NSDocumentClass $(PRODUCT_MODULE_NAME).MachineDocument + + CFBundleTypeExtensions + + fd + + CFBundleTypeIconFile + floppy525 + CFBundleTypeName + Thomson Disk Image + CFBundleTypeOSTypes + + ???? + + CFBundleTypeRole + Editor + LSHandlerRank + Owner + LSTypeIsPackage + + NSDocumentClass + $(PRODUCT_MODULE_NAME).MachineDocument + CFBundleExecutable $(EXECUTABLE_NAME) diff --git a/ROMImages/Thomson/readme.txt b/ROMImages/Thomson/readme.txt index a713f4db2..47494f48c 100644 --- a/ROMImages/Thomson/readme.txt +++ b/ROMImages/Thomson/readme.txt @@ -3,4 +3,5 @@ ROM files would ordinarily go here; the copyright status of these is uncertain s Expected files: mo5-v1.0.rom — the MO5 ROM, version 1; -mo5-v1.1.rom — the MO5 ROM, version 1.1. +mo5-v1.1.rom — the MO5 ROM, version 1.1; +cd90-640.rom — the CD90-640 floppy disk drive ROM. diff --git a/Storage/Disk/DiskImage/Formats/AcornADF.cpp b/Storage/Disk/DiskImage/Formats/AcornADF.cpp index 4a3d4f4fb..65904cd6c 100644 --- a/Storage/Disk/DiskImage/Formats/AcornADF.cpp +++ b/Storage/Disk/DiskImage/Formats/AcornADF.cpp @@ -51,7 +51,7 @@ AcornADF::AcornADF(const std::string &file_name) : MFMSectorDump(file_name) { // // So then .ADF files might be track-interleaved and might not be. - const auto has_identifier = [&](long location, bool permit_hugo, bool permit_nick) -> bool { + const auto has_identifier = [&](const long location, const bool permit_hugo, const bool permit_nick) -> bool { file_.seek(location, Whence::SET); uint8_t bytes[4]; @@ -119,6 +119,6 @@ int AcornADF::head_count() const { return head_count_; } -long AcornADF::get_file_offset_for_position(Track::Address address) const { +long AcornADF::get_file_offset_for_position(const Track::Address address) const { return (address.position.as_int() * head_count_ + address.head) * (128 << sector_size_) * sectors_per_track_; } diff --git a/Storage/Disk/DiskImage/Formats/FD.cpp b/Storage/Disk/DiskImage/Formats/FD.cpp new file mode 100644 index 000000000..0bf8dfe94 --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/FD.cpp @@ -0,0 +1,57 @@ +// +// FD.cpp +// Clock Signal +// +// Created by Thomas Harte on 04/04/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#include "FD.hpp" + +namespace { +// Per https://en.wikipedia.org/wiki/List_of_floppy_disk_formats the Thomson +// uses 16 256-byte sectors per track. +constexpr int sectors_per_track = 16; +constexpr int sector_size = 1; + +constexpr int bytes_per_track = sectors_per_track * (128 << sector_size); +} + +using namespace Storage::Disk; + +FD::FD(const std::string &file_name) : MFMSectorDump(file_name) { + // Guess disk geometry from file size. + switch(file_.stats().st_size) { + default: throw Error::InvalidFormat; + + case bytes_per_track * 40: + head_count_ = 1; + track_count_ = 40; + break; + + case bytes_per_track * 80: + head_count_ = 1; + track_count_ = 80; + break; + + case bytes_per_track * 160: + head_count_ = 2; + track_count_ = 80; + break; + } + + set_geometry(sectors_per_track, sector_size, 1, Encodings::MFM::Density::Double); +} + +HeadPosition FD::maximum_head_position() const { + return HeadPosition(track_count_); +} + +int FD::head_count() const { + return head_count_; +} + +long FD::get_file_offset_for_position(const Track::Address address) const { + // Determined empirically: disk images are not interleaved. + return ((address.head * track_count_) + address.position.as_int()) * bytes_per_track; +} diff --git a/Storage/Disk/DiskImage/Formats/FD.hpp b/Storage/Disk/DiskImage/Formats/FD.hpp new file mode 100644 index 000000000..81130e55b --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/FD.hpp @@ -0,0 +1,37 @@ +// +// FD.hpp +// Clock Signal +// +// Created by Thomas Harte on 04/04/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "MFMSectorDump.hpp" + +namespace Storage::Disk { + +/*! + Provides a @c Disk containing an FD disk image: a decoded sector dump of a Thomson disk. +*/ +class FD: public MFMSectorDump { +public: + /*! + Construct an @c FD containing content from the file with name @c file_name. + + @throws Storage::FileHolder::Error::CantOpen if this file can't be opened. + @throws Error::InvalidFormat if the file doesn't appear to contain a .FD format image. + */ + FD(const std::string &file_name); + + HeadPosition maximum_head_position() const final; + int head_count() const final; + +private: + long get_file_offset_for_position(Track::Address) const final; + int head_count_; + int track_count_; +}; + +} diff --git a/Storage/Disk/DiskImage/Formats/MFMSectorDump.cpp b/Storage/Disk/DiskImage/Formats/MFMSectorDump.cpp index ff31fa1a3..a653c03c0 100644 --- a/Storage/Disk/DiskImage/Formats/MFMSectorDump.cpp +++ b/Storage/Disk/DiskImage/Formats/MFMSectorDump.cpp @@ -14,14 +14,19 @@ using namespace Storage::Disk; MFMSectorDump::MFMSectorDump(const std::string &file_name) : file_(file_name) {} -void MFMSectorDump::set_geometry(int sectors_per_track, uint8_t sector_size, uint8_t first_sector, Encodings::MFM::Density density) { +void MFMSectorDump::set_geometry( + const int sectors_per_track, + const uint8_t sector_size, + const uint8_t first_sector, + const Encodings::MFM::Density density +) { sectors_per_track_ = sectors_per_track; sector_size_ = sector_size; density_ = density; first_sector_ = first_sector; } -std::unique_ptr MFMSectorDump::track_at_position(Track::Address address) const { +std::unique_ptr MFMSectorDump::track_at_position(const Track::Address address) const { if(address.head >= head_count()) return nullptr; if(address.position.as_largest() >= maximum_head_position().as_largest()) return nullptr; diff --git a/Storage/Disk/Drive.cpp b/Storage/Disk/Drive.cpp index 0e8dd3d03..42ad569c9 100644 --- a/Storage/Disk/Drive.cpp +++ b/Storage/Disk/Drive.cpp @@ -352,8 +352,8 @@ void Drive::process_next_event() { } cycles_since_index_hole_ = 0; - // Begin a 2ms period of holding the index line pulse active. - index_pulse_remaining_ = Cycles((get_input_clock_rate() * 2) / 1000); + // Begin a 1ms period of holding the index line pulse active. + index_pulse_remaining_ = Cycles((get_input_clock_rate() * 1) / 1000); } if( event_delegate_ && diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index bde3d0331..a8a683dc8 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -20,7 +20,7 @@ TapeSerialiser::TapeSerialiser(std::unique_ptr &&serialiser) : std::unique_ptr Tape::serialiser(const TargetPlatform::Type platform) const { auto serialiser = format_serialiser(); - if(auto *recipient = dynamic_cast(serialiser.get())) { + if(auto recipient = dynamic_cast(serialiser.get())) { recipient->set_target_platforms(platform); } @@ -57,7 +57,7 @@ void TapeSerialiser::reset() { Pulse TapeSerialiser::next_pulse() { pulse_ = serialiser_->next_pulse(); - offset_++; + ++offset_; return pulse_; } @@ -84,7 +84,7 @@ ClockingHint::Preference TapePlayer::preferred_clocking() const { return (!tape_ || serialiser_->is_at_end()) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime; } -void TapePlayer::set_tape(std::shared_ptr tape, TargetPlatform::Type platform) { +void TapePlayer::set_tape(std::shared_ptr tape, const TargetPlatform::Type platform) { tape_ = tape; serialiser_ = tape->serialiser(platform); @@ -106,7 +106,6 @@ bool TapePlayer::has_tape() const { } void TapePlayer::next_pulse() { - // get the new pulse if(tape_) { current_pulse_ = serialiser_->next_pulse(); if(serialiser_->is_at_end()) update_clocking_observer(); @@ -171,7 +170,7 @@ void BinaryTapePlayer::set_motor_control(const bool enabled) { void BinaryTapePlayer::update_observer() { const bool lit = motor_is_running_ && !is_at_end(); if(observer_ && lit != observer_lit_) { - observer_->set_led_status(LEDName, motor_is_running_ && !is_at_end()); + observer_->set_led_status(LEDName, lit); observer_lit_ = lit; } } diff --git a/cmake/CLK_SOURCES.cmake b/cmake/CLK_SOURCES.cmake index 3d066c7d0..6cdb58b2c 100644 --- a/cmake/CLK_SOURCES.cmake +++ b/cmake/CLK_SOURCES.cmake @@ -151,6 +151,7 @@ set(CLK_SOURCES Machines/Sinclair/ZX8081/Video.cpp Machines/Sinclair/ZX8081/ZX8081.cpp Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp + Machines/Thomson/MO5/CD90-640.cpp Machines/Thomson/MO5/MO5.cpp Machines/Thomson/MO5/Video.cpp Machines/Utility/MachineForTarget.cpp @@ -206,6 +207,7 @@ set(CLK_SOURCES Storage/Disk/DiskImage/Formats/D64.cpp Storage/Disk/DiskImage/Formats/DMK.cpp Storage/Disk/DiskImage/Formats/FAT12.cpp + Storage/Disk/DiskImage/Formats/FD.cpp Storage/Disk/DiskImage/Formats/G64.cpp Storage/Disk/DiskImage/Formats/HFE.cpp Storage/Disk/DiskImage/Formats/IMD.cpp