Merge pull request #1746 from TomHarte/Yeti

MO5: Add provisional floppy drive support.
This commit is contained in:
Thomas Harte authored and GitHub committed 2026-04-05 21:47:10 -04:00
commit a7b490abca
20 files changed
+398 -49

No files matched your search

+3
View File
@@ -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<Disk::DiskImageHolder<Disk::FAT12>>(TargetPlatform::MSX, "dsk");
accumulator.try_standard<Disk::DiskImageHolder<Disk::OricMFMDSK>>(TargetPlatform::Oric, "dsk");
accumulator.try_standard<Disk::DiskImageHolder<Disk::FD>>(TargetPlatform::ThomsonMO, "fd");
accumulator.try_standard<Disk::DiskImageHolder<Disk::G64>>(TargetPlatform::Commodore8bit, "g64");
accumulator.try_standard<MassStorage::HDV>(TargetPlatform::AppleII, "hdv");
@@ -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));
}
+5 -1
View File
@@ -18,12 +18,16 @@ namespace Analyser::Static::Thomson {
struct MOTarget: public Analyser::Static::Target, public Reflection::StructImpl<MOTarget> {
std::string loading_command;
ReflectableEnum(Floppy, None, CD90_640);
Floppy floppy = Floppy::None;
MOTarget() : Analyser::Static::Target(Machine::ThomsonMO) {}
private:
friend Reflection::StructImpl<MOTarget>;
void declare_fields() {
// None yet.
AnnounceEnum(Floppy);
DeclareField(floppy);
}
};
+1
View File
@@ -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;
+5 -5
View File
@@ -15,7 +15,7 @@ Plus3::Plus3() : WD1770(P1770) {
set_control_register(last_control_, 0xff);
}
void Plus3::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
void Plus3::set_disk(std::shared_ptr<Storage::Disk::Disk> 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);
});
+76
View File
@@ -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<Storage::Disk::Disk> 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;
}
+59
View File
@@ -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 <string>
namespace Thomson {
class CD90_640 final : public WD::WD1770 {
public:
CD90_640();
void set_disk(std::shared_ptr<Storage::Disk::Disk>, size_t drive);
const Storage::Disk::Disk *disk(const std::string &);
void set_activity_observer(Activity::Observer *);
template <int address>
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 <int address>
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<Log::Source::Floppy>;
void set_motor_on(bool) override;
};
}
+71 -25
View File
@@ -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<Log::Source::MO5>;
static constexpr int ClockRate = 1'000'000;
static constexpr uint8_t MusicExpansionMask = 63;
template <bool has_floppy>
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<Cycles>(bus_phase);
if(video_ += duration) {
system_pia_.set<Motorola::MC6821::Control::CB1>(video_.last_valid()->irq());
system_pia_.template set<Motorola::MC6821::Control::CB1>(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<CPU::M6809::R8::DP>();
const auto dp = m6809_.registers().template reg<CPU::M6809::R8::DP>();
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<CPU::M6809::R8::A>() = *bit ? 0xff : 0x00;
m6809_.registers().template reg<CPU::M6809::R8::A>() = *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<uint8_t, 0x10000 + 0x2000> ram_;
std::array<uint8_t, 0x4000> rom_;
std::array<uint8_t, 0x7c0> floppy_rom_;
uint8_t *start_pointer_ = nullptr;
uint8_t *video_page(const bool pixels) {
@@ -299,11 +324,11 @@ private:
template <Motorola::MC6821::IRQ irq>
void set(const bool active) {
if constexpr (irq == Motorola::MC6821::IRQ::A) {
machine_.m6809_.set<CPU::M6809::Line::FIRQ>(active);
machine_.m6809_.template set<CPU::M6809::Line::FIRQ>(active);
}
if constexpr (irq == Motorola::MC6821::IRQ::B) {
machine_.m6809_.set<CPU::M6809::Line::IRQ>(active);
machine_.m6809_.template set<CPU::M6809::Line::IRQ>(active);
}
}
@@ -419,17 +444,17 @@ private:
apply(port_b_, index ? 0x08 : 0x04);
if(index) {
machine_.sound_and_game_pia_.set<Motorola::MC6821::Control::CB1>(is_active);
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CB1>(is_active);
} else {
machine_.sound_and_game_pia_.set<Motorola::MC6821::Control::CA1>(is_active);
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CA1>(is_active);
}
} else {
apply(port_b_, index ? 0x80 : 0x40);
if(index) {
machine_.sound_and_game_pia_.set<Motorola::MC6821::Control::CB2>(is_active);
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CB2>(is_active);
} else {
machine_.sound_and_game_pia_.set<Motorola::MC6821::Control::CA2>(is_active);
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CA2>(is_active);
}
}
break;
@@ -469,9 +494,14 @@ private:
Motorola::MC6821::MC6821<SoundAndGamePIAPortHandler, 2, 1> sound_and_game_pia_;
JustInTimeActor<Video, Cycles> video_;
// MARK: - Tape and disk.
Storage::Tape::BinaryTapePlayer tape_player_;
bool allow_fast_tape_hack_ = true;
Thomson::CD90_640 fdc_;
// MARK: - AudioProducer.
Concurrency::AsyncTaskQueue<false> audio_queue_;
@@ -546,7 +576,7 @@ private:
}
HalfCycles get_typer_delay(const std::string &) const final {
if(m6809_.get<CPU::M6809::Line::PowerOnReset>()) {
if(m6809_.template get<CPU::M6809::Line::PowerOnReset>()) {
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> Machine::ThomsonMO(
) {
using Target = Analyser::Static::Thomson::MOTarget;
const Target *const thomson_target = dynamic_cast<const Target *>(target);
return std::make_unique<ConcreteMachine>(*thomson_target, rom_fetcher);
switch(thomson_target->floppy) {
case Target::Floppy::None: return std::make_unique<ConcreteMachine<false>>(*thomson_target, rom_fetcher);
case Target::Floppy::CD90_640: return std::make_unique<ConcreteMachine<true>>(*thomson_target, rom_fetcher);
}
}
+11 -2
View File
@@ -1021,7 +1021,7 @@ const std::vector<Description> &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> &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> &Description::all_roms() {
0x237c60bfu
},
{
ThomsonCD90_640,
"Thomson",
"the Thomson CD90-640 ROM",
"cd90-640.rom",
0x7c0,
0x5114c0a5u
},
//
// Vic-20.
//
+4 -2
View File
@@ -176,8 +176,10 @@ enum Name {
SinclairQLJS,
// Thomson MO and TO machines.
ThomasonMO5v1,
ThomasonMO5v11,
ThomsonMO5v1,
ThomsonMO5v11,
ThomsonCD90_640,
// Vic-20.
Vic20BASIC,
@@ -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 = "<group>"; };
4B055ABF1FAE98000060FFFF /* MachineForTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MachineForTarget.hpp; sourceTree = "<group>"; };
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 = "<group>"; };
4B062D192F81427D00472717 /* CD90-640.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = "CD90-640.cpp"; sourceTree = "<group>"; };
4B062D1D2F81589C00472717 /* FD.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FD.hpp; sourceTree = "<group>"; };
4B062D1E2F81589C00472717 /* FD.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = FD.cpp; sourceTree = "<group>"; };
4B08A2741EE35D56008B7065 /* Z80InterruptTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Z80InterruptTests.swift; sourceTree = "<group>"; };
4B08A2761EE39306008B7065 /* TestMachine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestMachine.h; sourceTree = "<group>"; };
4B08A2771EE39306008B7065 /* TestMachine.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TestMachine.mm; sourceTree = "<group>"; };
@@ -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 */,
+22
View File
@@ -875,6 +875,28 @@
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>fd</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>floppy525</string>
<key>CFBundleTypeName</key>
<string>Thomson Disk Image</string>
<key>CFBundleTypeOSTypes</key>
<array>
<string>????</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSTypeIsPackage</key>
<false/>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
+2 -1
View File
@@ -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.
+2 -2
View File
@@ -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_;
}
+57
View File
@@ -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;
}
+37
View File
@@ -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_;
};
}
@@ -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<Track> MFMSectorDump::track_at_position(Track::Address address) const {
std::unique_ptr<Track> 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;
+2 -2
View File
@@ -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_ &&
+4 -5
View File
@@ -20,7 +20,7 @@ TapeSerialiser::TapeSerialiser(std::unique_ptr<FormatSerialiser> &&serialiser) :
std::unique_ptr<TapeSerialiser> Tape::serialiser(const TargetPlatform::Type platform) const {
auto serialiser = format_serialiser();
if(auto *recipient = dynamic_cast<TargetPlatform::Recipient *>(serialiser.get())) {
if(auto recipient = dynamic_cast<TargetPlatform::Recipient *>(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<Storage::Tape::Tape> tape, TargetPlatform::Type platform) {
void TapePlayer::set_tape(std::shared_ptr<Storage::Tape::Tape> 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;
}
}
+2
View File
@@ -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