From 072ce6b9fc95e67d6961076cdf6cb515f27c8f44 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 3 Apr 2026 22:40:57 -0400 Subject: [PATCH 01/23] Avoid test repetition. --- Storage/Tape/Tape.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index bde3d0331..7ab67f684 100644 --- a/Storage/Tape/Tape.cpp +++ b/Storage/Tape/Tape.cpp @@ -171,7 +171,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; } } From cef90320b8c58efa56f8b0c05fc0076a8308c8f9 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 3 Apr 2026 22:44:24 -0400 Subject: [PATCH 02/23] Make minor style adjustments. --- Storage/Tape/Tape.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Storage/Tape/Tape.cpp b/Storage/Tape/Tape.cpp index 7ab67f684..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(); From e7a40fbf720dcc1affb9ef30ab5c18e69db5a160 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 3 Apr 2026 23:05:01 -0400 Subject: [PATCH 03/23] Correct typo; add CD90-640 ROM. --- Machines/Thomson/MO5/MO5.cpp | 4 ++-- Machines/Utility/ROMCatalogue.cpp | 13 +++++++++++-- Machines/Utility/ROMCatalogue.hpp | 6 ++++-- ROMImages/Thomson/readme.txt | 3 ++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index e53b9282c..0cf67ab93 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -64,13 +64,13 @@ struct ConcreteMachine: speaker_.set_input_rate(ClockRate); construct_joysticks(); - const auto request = ROM::Request(ROM::Name::ThomasonMO5v11); + const auto request = ROM::Request(ROM::Name::ThomsonMO5v11); 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()); Memory::Fuzz(ram_); 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/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. From aabb056af44dda3f797d35e074e7043b9605454d Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 3 Apr 2026 23:20:16 -0400 Subject: [PATCH 04/23] Start sketching out floppy support. The ROM accesses a7d0, a7d1, a7d3 and a7d8. --- Machines/Thomson/MO5/MO5.cpp | 41 +++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 0cf67ab93..1ac68caa8 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -36,6 +36,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,7 +65,11 @@ struct ConcreteMachine: speaker_.set_input_rate(ClockRate); construct_joysticks(); - const auto request = ROM::Request(ROM::Name::ThomsonMO5v11); + 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; @@ -73,6 +78,11 @@ struct ConcreteMachine: 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,7 +122,7 @@ 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); @@ -120,9 +130,7 @@ struct ConcreteMachine: typer_->run_for(duration); } - 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) { switch(address) { case 0xa7c0: access<0xa7c0, read_write>(system_pia_, value); break; @@ -154,6 +162,8 @@ struct ConcreteMachine: 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 +184,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 +203,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. @@ -234,6 +244,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 +310,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 +430,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; @@ -546,7 +557,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); @@ -611,5 +622,5 @@ 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); + return std::make_unique>(*thomson_target, rom_fetcher); } From 646d638cd4764ec9e699e0d09c0d6944c3a53b21 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 09:08:06 -0400 Subject: [PATCH 05/23] Add `const`s. --- Machines/Acorn/Electron/Plus3.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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); }); From 48507726c877a04c0d2bfc0f37f169c00bf1fb33 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 09:12:01 -0400 Subject: [PATCH 06/23] Assert that the CD90-640 is WD based, wire that up. --- Machines/Thomson/MO5/CD90-640.cpp | 30 ++++++++++++ Machines/Thomson/MO5/CD90-640.hpp | 49 +++++++++++++++++++ Machines/Thomson/MO5/MO5.cpp | 36 +++++++++++--- .../Clock Signal.xcodeproj/project.pbxproj | 10 ++++ cmake/CLK_SOURCES.cmake | 1 + 5 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 Machines/Thomson/MO5/CD90-640.cpp create mode 100644 Machines/Thomson/MO5/CD90-640.hpp diff --git a/Machines/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp new file mode 100644 index 000000000..97b3c6601 --- /dev/null +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -0,0 +1,30 @@ +// +// 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) { + emplace_drives(2, 8000000, 300, 2); +} + +uint8_t CD90_640::control() { + return control_; +} + +void CD90_640::set_control(const uint8_t value) { + control_ = value; + // TODO: What do these bits mean? +} + +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); + }); +} diff --git a/Machines/Thomson/MO5/CD90-640.hpp b/Machines/Thomson/MO5/CD90-640.hpp new file mode 100644 index 000000000..da2339c7f --- /dev/null +++ b/Machines/Thomson/MO5/CD90-640.hpp @@ -0,0 +1,49 @@ +// +// 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 + +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() { + if(address & 8) { + return control(); + } else { + return WD::WD1770::read(address); + } + } + + template + void write(const uint8_t value) { + if(address & 8) { + set_control(value); + } else { + WD::WD1770::write(address, value); + } + } + +private: + uint8_t control(); + void set_control(uint8_t); + uint8_t control_; +}; + +} diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 1ac68caa8..3d9175b69 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" @@ -129,9 +130,21 @@ struct ConcreteMachine: 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) { 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; @@ -149,13 +162,14 @@ 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 { @@ -480,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_; @@ -584,8 +603,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. diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index f385ced30..642255bc5 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -133,6 +133,9 @@ 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 */; }; 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 +1408,8 @@ 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 = ""; }; 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 = ""; }; @@ -5350,8 +5355,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 +6545,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 */, @@ -6865,6 +6873,7 @@ 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 */, @@ -7018,6 +7027,7 @@ 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/cmake/CLK_SOURCES.cmake b/cmake/CLK_SOURCES.cmake index 3d066c7d0..655b44af7 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 From 3be3772abfe30fafdf74506052f4489612689e1f Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 09:58:00 -0400 Subject: [PATCH 07/23] Increase logging, guess that this is a 1793? --- Machines/Thomson/MO5/CD90-640.cpp | 4 +++- Machines/Thomson/MO5/CD90-640.hpp | 5 +++++ Outputs/Log.hpp | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Machines/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp index 97b3c6601..0eff1dc54 100644 --- a/Machines/Thomson/MO5/CD90-640.cpp +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -10,15 +10,17 @@ using namespace Thomson; -CD90_640::CD90_640() : WD::WD1770(P1770) { +CD90_640::CD90_640() : WD::WD1770(P1793) { emplace_drives(2, 8000000, 300, 2); } uint8_t CD90_640::control() { + Logger::info().append("Read control: %02x", control_); return control_; } void CD90_640::set_control(const uint8_t value) { + Logger::info().append("Set control to %02x", value); control_ = value; // TODO: What do these bits mean? } diff --git a/Machines/Thomson/MO5/CD90-640.hpp b/Machines/Thomson/MO5/CD90-640.hpp index da2339c7f..b9701ae96 100644 --- a/Machines/Thomson/MO5/CD90-640.hpp +++ b/Machines/Thomson/MO5/CD90-640.hpp @@ -9,6 +9,7 @@ #pragma once #include "Components/1770/1770.hpp" +#include "Outputs/Log.hpp" #include @@ -24,6 +25,7 @@ public: template uint8_t read() { + Logger::info().append("Read %04x", address); if(address & 8) { return control(); } else { @@ -33,6 +35,7 @@ public: template void write(const uint8_t value) { + Logger::info().append("Write %02x -> %04x", value, address); if(address & 8) { set_control(value); } else { @@ -44,6 +47,8 @@ private: uint8_t control(); void set_control(uint8_t); uint8_t control_; + + using Logger = Log::Logger; }; } diff --git a/Outputs/Log.hpp b/Outputs/Log.hpp index 293202210..f303f18d3 100644 --- a/Outputs/Log.hpp +++ b/Outputs/Log.hpp @@ -122,9 +122,9 @@ constexpr EnabledLevel enabled_level(const Source source) { // case Source::PCPOST: return EnabledLevel::None; - case Source::Floppy: +// case Source::Floppy: // case Source::Keyboard: - return EnabledLevel::Errors; +// return EnabledLevel::Errors; } } From ec915504d123e3faa8f3223012762bd8ebafbff9 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 10:49:33 -0400 Subject: [PATCH 08/23] Attempt FD file support. --- Analyser/Static/StaticAnalyser.cpp | 3 ++ Analyser/Static/Thomson/StaticAnalyser.cpp | 6 +++ Analyser/Static/Thomson/Target.hpp | 6 ++- Machines/Thomson/MO5/CD90-640.cpp | 17 +++++++ Machines/Thomson/MO5/MO5.cpp | 17 ++++++- .../Clock Signal.xcodeproj/project.pbxproj | 14 +++++- OSBindings/Mac/Clock Signal/Info.plist | 22 ++++++++ Storage/Disk/DiskImage/Formats/FD.cpp | 50 +++++++++++++++++++ Storage/Disk/DiskImage/Formats/FD.hpp | 36 +++++++++++++ cmake/CLK_SOURCES.cmake | 1 + 10 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 Storage/Disk/DiskImage/Formats/FD.cpp create mode 100644 Storage/Disk/DiskImage/Formats/FD.hpp 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/Machines/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp index 0eff1dc54..094389034 100644 --- a/Machines/Thomson/MO5/CD90-640.cpp +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -30,3 +30,20 @@ void CD90_640::set_activity_observer(Activity::Observer *const observer) { 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/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 3d9175b69..b249907f3 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -594,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 { @@ -644,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/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 642255bc5..4da662ed5 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -136,6 +136,9 @@ 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 */; }; @@ -1410,6 +1413,8 @@ 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 = ""; }; @@ -3418,6 +3423,7 @@ 4B4518911F75FD1B00926311 /* D64.cpp */, 4BAF2B4C2004580C00480230 /* DMK.cpp */, 4BEBFB4B2002C4BF000708CC /* FAT12.cpp */, + 4B062D1E2F81589C00472717 /* FD.cpp */, 4B4518931F75FD1B00926311 /* G64.cpp */, 4B4518951F75FD1B00926311 /* HFE.cpp */, 42EB81262B23AAC300429AF4 /* IMD.cpp */, @@ -3425,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 */, @@ -3441,6 +3447,7 @@ 4B4518921F75FD1B00926311 /* D64.hpp */, 4BAF2B4D2004580C00480230 /* DMK.hpp */, 4BEBFB4C2002C4BF000708CC /* FAT12.hpp */, + 4B062D1D2F81589C00472717 /* FD.hpp */, 4B4518941F75FD1B00926311 /* G64.hpp */, 4B4518961F75FD1B00926311 /* HFE.hpp */, 42EB81272B23AAC300429AF4 /* IMD.hpp */, @@ -3448,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 */, ); @@ -6596,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 */, @@ -6868,6 +6876,7 @@ 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 */, @@ -7023,6 +7032,7 @@ 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 */, 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/Storage/Disk/DiskImage/Formats/FD.cpp b/Storage/Disk/DiskImage/Formats/FD.cpp new file mode 100644 index 000000000..85b8573a8 --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/FD.cpp @@ -0,0 +1,50 @@ +// +// 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 tracks_per_side = 40; +constexpr int sectors_per_track = 16; +constexpr int sector_size = 1; + +constexpr int bytes_per_track = sectors_per_track * (128 << sector_size); +constexpr int bytes_per_side = tracks_per_side * bytes_per_track; +} + +using namespace Storage::Disk; + +FD::FD(const std::string &file_name) : MFMSectorDump(file_name) { + // Disk has two heads if the suffix is .dsd or if it's too large to be an SSD. + const bool is_double_sided = file_.stats().st_size > bytes_per_side; + + if( + file_.stats().st_size != bytes_per_side && + file_.stats().st_size != bytes_per_side * 2 + ) throw Error::InvalidFormat; + + head_count_ = is_double_sided ? 2 : 1; + + set_geometry(sectors_per_track, sector_size, 0, Encodings::MFM::Density::Double); +} + +HeadPosition FD::maximum_head_position() const { + return HeadPosition(tracks_per_side); +} + +int FD::head_count() const { + return head_count_; +} + +long FD::get_file_offset_for_position(const Track::Address address) const { + // TODO: RESOLVE GUESS HERE. + // Guess is that data is interleaved. + return (address.position.as_int() * head_count_ + address.head) * 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..a491dc033 --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/FD.hpp @@ -0,0 +1,36 @@ +// +// 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_; +}; + +} diff --git a/cmake/CLK_SOURCES.cmake b/cmake/CLK_SOURCES.cmake index 655b44af7..6cdb58b2c 100644 --- a/cmake/CLK_SOURCES.cmake +++ b/cmake/CLK_SOURCES.cmake @@ -207,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 From 9e7d1d79e95412a2b97179d60cf4ec93e5dc6644 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 10:55:14 -0400 Subject: [PATCH 09/23] Force motor on, as initial step. --- Machines/Thomson/MO5/CD90-640.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Machines/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp index 094389034..c24e21fbf 100644 --- a/Machines/Thomson/MO5/CD90-640.cpp +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -35,6 +35,9 @@ void CD90_640::set_activity_observer(Activity::Observer *const observer) { void CD90_640::set_disk(std::shared_ptr disk, const size_t drive) { get_drive(drive).set_disk(disk); + + // TODO: what is really in charge of motor control? + get_drive(drive).set_motor_on(true); } const Storage::Disk::Disk *CD90_640::disk(const std::string &name) { From 9ab3d52eb2188df5186dc0ffe33b0d8636ba9f2a Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 11:05:01 -0400 Subject: [PATCH 10/23] Force drive selection; verify that the index hole is going by. --- Machines/Thomson/MO5/CD90-640.cpp | 3 ++- Machines/Thomson/MO5/CD90-640.hpp | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Machines/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp index c24e21fbf..bc8fcdf98 100644 --- a/Machines/Thomson/MO5/CD90-640.cpp +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -36,8 +36,9 @@ void CD90_640::set_activity_observer(Activity::Observer *const observer) { void CD90_640::set_disk(std::shared_ptr disk, const size_t drive) { get_drive(drive).set_disk(disk); - // TODO: what is really in charge of motor control? + // TODO: what is really in charge of motor control and drive selection? get_drive(drive).set_motor_on(true); + set_drive(1 << drive); } const Storage::Disk::Disk *CD90_640::disk(const std::string &name) { diff --git a/Machines/Thomson/MO5/CD90-640.hpp b/Machines/Thomson/MO5/CD90-640.hpp index b9701ae96..4af1f4b5e 100644 --- a/Machines/Thomson/MO5/CD90-640.hpp +++ b/Machines/Thomson/MO5/CD90-640.hpp @@ -25,12 +25,15 @@ public: template uint8_t read() { - Logger::info().append("Read %04x", address); - if(address & 8) { - return control(); - } else { - return WD::WD1770::read(address); - } + 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 From 04c59b3b42beb3f545403f28fd857117bb30ddf7 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 17:18:21 -0400 Subject: [PATCH 11/23] Experiment with head load for motor. --- Machines/Thomson/MO5/CD90-640.cpp | 9 +++++++++ Machines/Thomson/MO5/CD90-640.hpp | 2 ++ 2 files changed, 11 insertions(+) diff --git a/Machines/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp index bc8fcdf98..39b80e873 100644 --- a/Machines/Thomson/MO5/CD90-640.cpp +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -51,3 +51,12 @@ const Storage::Disk::Disk *CD90_640::disk(const std::string &name) { }); return result; } + +//void CD90_640::set_head_load_request(const bool head_load) { +// // Turn all motors on or off; if off then unload the head instantly. +// for_all_drives([head_load] (Storage::Disk::Drive &drive, size_t) { +// drive.set_motor_on(head_load); +// }); +// Logger::info().append("Head load: %d", head_load); +// set_head_loaded(head_load); +//} diff --git a/Machines/Thomson/MO5/CD90-640.hpp b/Machines/Thomson/MO5/CD90-640.hpp index 4af1f4b5e..9cf6228eb 100644 --- a/Machines/Thomson/MO5/CD90-640.hpp +++ b/Machines/Thomson/MO5/CD90-640.hpp @@ -52,6 +52,8 @@ private: uint8_t control_; using Logger = Log::Logger; + +// void set_head_load_request(bool) override; }; } From 899898508231c563735914f0b466429c67dd7226 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 18:07:22 -0400 Subject: [PATCH 12/23] Add `const`s. --- Storage/Disk/DiskImage/Formats/AcornADF.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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_; } From 43bb7e8a2807e91be44750af739d2fe2a878611f Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 18:08:50 -0400 Subject: [PATCH 13/23] Add `const`s, enforce line length. --- Storage/Disk/DiskImage/Formats/MFMSectorDump.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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; From 02c17a5b53b779a2aaf81fe410b321c4d0876a17 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 22:38:03 -0400 Subject: [PATCH 14/23] Apply what a schematic seems to imply. --- Components/1770/1770.cpp | 1 + Machines/Thomson/MO5/CD90-640.cpp | 47 ++++++++++++++++++++----------- Machines/Thomson/MO5/CD90-640.hpp | 2 +- 3 files changed, 32 insertions(+), 18 deletions(-) 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/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp index 39b80e873..34c81c8a3 100644 --- a/Machines/Thomson/MO5/CD90-640.cpp +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -10,19 +10,45 @@ using namespace Thomson; -CD90_640::CD90_640() : WD::WD1770(P1793) { +CD90_640::CD90_640() : WD::WD1770(P1770) { emplace_drives(2, 8000000, 300, 2); } uint8_t CD90_640::control() { - Logger::info().append("Read control: %02x", control_); +// Logger::info().append("Read control: %02x [?]", control_); return control_; } void CD90_640::set_control(const uint8_t value) { - Logger::info().append("Set control to %02x", value); +// Logger::info().append("Write control: %02x", value); control_ = value; - // TODO: What do these bits mean? + + // 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) { @@ -35,10 +61,6 @@ void CD90_640::set_activity_observer(Activity::Observer *const observer) { void CD90_640::set_disk(std::shared_ptr disk, const size_t drive) { get_drive(drive).set_disk(disk); - - // TODO: what is really in charge of motor control and drive selection? - get_drive(drive).set_motor_on(true); - set_drive(1 << drive); } const Storage::Disk::Disk *CD90_640::disk(const std::string &name) { @@ -51,12 +73,3 @@ const Storage::Disk::Disk *CD90_640::disk(const std::string &name) { }); return result; } - -//void CD90_640::set_head_load_request(const bool head_load) { -// // Turn all motors on or off; if off then unload the head instantly. -// for_all_drives([head_load] (Storage::Disk::Drive &drive, size_t) { -// drive.set_motor_on(head_load); -// }); -// Logger::info().append("Head load: %d", head_load); -// set_head_loaded(head_load); -//} diff --git a/Machines/Thomson/MO5/CD90-640.hpp b/Machines/Thomson/MO5/CD90-640.hpp index 9cf6228eb..9fff33b52 100644 --- a/Machines/Thomson/MO5/CD90-640.hpp +++ b/Machines/Thomson/MO5/CD90-640.hpp @@ -53,7 +53,7 @@ private: using Logger = Log::Logger; -// void set_head_load_request(bool) override; + void set_motor_on(bool) override; }; } From 7b3799b8b652f82eec5a2d89b542971b346a8145 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 23:04:33 -0400 Subject: [PATCH 15/23] Shorten index pulse time. --- Storage/Disk/Drive.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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_ && From 377e75f4d42a3193e9fe54bee59e377d4b34b347 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 23:04:43 -0400 Subject: [PATCH 16/23] Log more unexpected writes. --- Machines/Thomson/MO5/MO5.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index b249907f3..b9280e2f3 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -236,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); } } From 011ec055d5f91780187dfbcdedb506f049a63a41 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 23:14:24 -0400 Subject: [PATCH 17/23] Increase RPM; these are 5.25" drives. --- Machines/Thomson/MO5/CD90-640.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Machines/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp index 34c81c8a3..3ce073f8d 100644 --- a/Machines/Thomson/MO5/CD90-640.cpp +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -11,7 +11,7 @@ using namespace Thomson; CD90_640::CD90_640() : WD::WD1770(P1770) { - emplace_drives(2, 8000000, 300, 2); + emplace_drives(2, 8000000, 360, 2); } uint8_t CD90_640::control() { From e37c9d759d2b47a8df691ead928e16e093fa4ea5 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 23:27:33 -0400 Subject: [PATCH 18/23] Empirically it seems like sectors are 1-indexed. --- Storage/Disk/DiskImage/Formats/FD.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Storage/Disk/DiskImage/Formats/FD.cpp b/Storage/Disk/DiskImage/Formats/FD.cpp index 85b8573a8..96192ea37 100644 --- a/Storage/Disk/DiskImage/Formats/FD.cpp +++ b/Storage/Disk/DiskImage/Formats/FD.cpp @@ -32,7 +32,7 @@ FD::FD(const std::string &file_name) : MFMSectorDump(file_name) { head_count_ = is_double_sided ? 2 : 1; - set_geometry(sectors_per_track, sector_size, 0, Encodings::MFM::Density::Double); + set_geometry(sectors_per_track, sector_size, 1, Encodings::MFM::Density::Double); } HeadPosition FD::maximum_head_position() const { From b8b81530544aa5d9b0fc847b4d6fef7e9b56bf2e Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 23:27:55 -0400 Subject: [PATCH 19/23] Slow drives slightly. --- Machines/Thomson/MO5/CD90-640.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Machines/Thomson/MO5/CD90-640.cpp b/Machines/Thomson/MO5/CD90-640.cpp index 3ce073f8d..f3e5cc3a8 100644 --- a/Machines/Thomson/MO5/CD90-640.cpp +++ b/Machines/Thomson/MO5/CD90-640.cpp @@ -11,16 +11,17 @@ using namespace Thomson; CD90_640::CD90_640() : WD::WD1770(P1770) { - emplace_drives(2, 8000000, 360, 2); + // 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() { -// Logger::info().append("Read control: %02x [?]", control_); - return control_; + return control_; // Possibly only b7 is loaded? } void CD90_640::set_control(const uint8_t value) { -// Logger::info().append("Write control: %02x", value); control_ = value; // Following along from the schematic in https://github.com/OlivierP-To8/CD90-640/ : From 701af5758ce8b21a7cad04fcb22de44de9801c35 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 23:29:45 -0400 Subject: [PATCH 20/23] Updated guess: not interleaved. --- Storage/Disk/DiskImage/Formats/FD.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Storage/Disk/DiskImage/Formats/FD.cpp b/Storage/Disk/DiskImage/Formats/FD.cpp index 96192ea37..b8fd1bb31 100644 --- a/Storage/Disk/DiskImage/Formats/FD.cpp +++ b/Storage/Disk/DiskImage/Formats/FD.cpp @@ -44,7 +44,6 @@ int FD::head_count() const { } long FD::get_file_offset_for_position(const Track::Address address) const { - // TODO: RESOLVE GUESS HERE. - // Guess is that data is interleaved. - return (address.position.as_int() * head_count_ + address.head) * bytes_per_track; + // Determined empirically: disk images are not interleaved. + return address.head * bytes_per_side + address.position.as_int() * bytes_per_track; } From b7e10b3c3ac849d2f45d423d3e6d42af89c66d37 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 23:35:30 -0400 Subject: [PATCH 21/23] Remove space, add brackets for clarity. --- Storage/Disk/DiskImage/Formats/FD.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Storage/Disk/DiskImage/Formats/FD.cpp b/Storage/Disk/DiskImage/Formats/FD.cpp index b8fd1bb31..93862e035 100644 --- a/Storage/Disk/DiskImage/Formats/FD.cpp +++ b/Storage/Disk/DiskImage/Formats/FD.cpp @@ -45,5 +45,5 @@ int FD::head_count() const { long FD::get_file_offset_for_position(const Track::Address address) const { // Determined empirically: disk images are not interleaved. - return address.head * bytes_per_side + address.position.as_int() * bytes_per_track; + return (address.head * bytes_per_side) + (address.position.as_int() * bytes_per_track); } From 8ec21f058bd2eebe3157e97754107067f0bc2f96 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 4 Apr 2026 23:41:20 -0400 Subject: [PATCH 22/23] Reduce logging. --- Outputs/Log.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Outputs/Log.hpp b/Outputs/Log.hpp index f303f18d3..293202210 100644 --- a/Outputs/Log.hpp +++ b/Outputs/Log.hpp @@ -122,9 +122,9 @@ constexpr EnabledLevel enabled_level(const Source source) { // case Source::PCPOST: return EnabledLevel::None; -// case Source::Floppy: + case Source::Floppy: // case Source::Keyboard: -// return EnabledLevel::Errors; + return EnabledLevel::Errors; } } From b3300e487fde1ea48163d32d80cc5f5cc6d99eb0 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 5 Apr 2026 10:04:01 -0400 Subject: [PATCH 23/23] Revise heads/tracks guesswork. --- Storage/Disk/DiskImage/Formats/FD.cpp | 30 +++++++++++++++++---------- Storage/Disk/DiskImage/Formats/FD.hpp | 1 + 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Storage/Disk/DiskImage/Formats/FD.cpp b/Storage/Disk/DiskImage/Formats/FD.cpp index 93862e035..0bf8dfe94 100644 --- a/Storage/Disk/DiskImage/Formats/FD.cpp +++ b/Storage/Disk/DiskImage/Formats/FD.cpp @@ -11,32 +11,40 @@ namespace { // Per https://en.wikipedia.org/wiki/List_of_floppy_disk_formats the Thomson // uses 16 256-byte sectors per track. -constexpr int tracks_per_side = 40; constexpr int sectors_per_track = 16; constexpr int sector_size = 1; constexpr int bytes_per_track = sectors_per_track * (128 << sector_size); -constexpr int bytes_per_side = tracks_per_side * bytes_per_track; } using namespace Storage::Disk; FD::FD(const std::string &file_name) : MFMSectorDump(file_name) { - // Disk has two heads if the suffix is .dsd or if it's too large to be an SSD. - const bool is_double_sided = file_.stats().st_size > bytes_per_side; + // Guess disk geometry from file size. + switch(file_.stats().st_size) { + default: throw Error::InvalidFormat; - if( - file_.stats().st_size != bytes_per_side && - file_.stats().st_size != bytes_per_side * 2 - ) throw Error::InvalidFormat; + case bytes_per_track * 40: + head_count_ = 1; + track_count_ = 40; + break; - head_count_ = is_double_sided ? 2 : 1; + 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(tracks_per_side); + return HeadPosition(track_count_); } int FD::head_count() const { @@ -45,5 +53,5 @@ int FD::head_count() const { long FD::get_file_offset_for_position(const Track::Address address) const { // Determined empirically: disk images are not interleaved. - return (address.head * bytes_per_side) + (address.position.as_int() * bytes_per_track); + 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 index a491dc033..81130e55b 100644 --- a/Storage/Disk/DiskImage/Formats/FD.hpp +++ b/Storage/Disk/DiskImage/Formats/FD.hpp @@ -31,6 +31,7 @@ public: private: long get_file_offset_for_position(Track::Address) const final; int head_count_; + int track_count_; }; }