From ed481cce90b7ed9fdc68024917f38ae8eae969bf Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 19 Mar 2026 18:23:24 -0400 Subject: [PATCH 01/40] Ensure all macros are undefined. --- Processors/6809/6809.hpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Processors/6809/6809.hpp b/Processors/6809/6809.hpp index 980ee3f62..3f5b068e2 100644 --- a/Processors/6809/6809.hpp +++ b/Processors/6809/6809.hpp @@ -950,11 +950,20 @@ struct Processor { goto fetch_decode; } - #undef access_label - #undef attach - #undef join - #undef access + #undef addressing_program + #undef access_program #undef restore_point + #undef join + #undef attach + #undef local_label + #undef access_label + #undef check_pause + #undef read + #undef write + #undef inactive_bus + #undef internal_cycles + #undef perform_operation + #undef addressed_internal_cycle } Registers ®isters() { From 82e9da752ef716479ef9860cc590f768d29658be Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 19 Mar 2026 18:23:34 -0400 Subject: [PATCH 02/40] Add header for spaceship operator. --- Numeric/SizedInt.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Numeric/SizedInt.hpp b/Numeric/SizedInt.hpp index e3d725b6f..b1e5f8d43 100644 --- a/Numeric/SizedInt.hpp +++ b/Numeric/SizedInt.hpp @@ -11,6 +11,7 @@ #include "Sizes.hpp" #include +#include namespace Numeric { From dcd6b611d0a2afb801428dd9cf71e1d299d8ec0f Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 19 Mar 2026 18:24:10 -0400 Subject: [PATCH 03/40] Start establishing more MO5 hardware. --- Components/6821/6821.hpp | 32 ++++++++++ Machines/Thomson/MO5/MO5.cpp | 58 +++++++++++++++---- .../Clock Signal.xcodeproj/project.pbxproj | 10 ++++ 3 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 Components/6821/6821.hpp diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp new file mode 100644 index 000000000..334b40031 --- /dev/null +++ b/Components/6821/6821.hpp @@ -0,0 +1,32 @@ +// +// 6821.hpp +// Clock Signal +// +// Created by Thomas Harte on 19/03/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#ifndef _821_hpp +#define _821_hpp + +#include "Numeric/SizedInt.hpp" + +namespace Motorola::MC6821 { + +template +class MC6821 { +public: + void write(const Numeric::SizedInt<2> address, const uint8_t value) { + (void)address; + (void)value; + } + + uint8_t read(const Numeric::SizedInt<2> address) { + (void)address; + return 0; + } +}; + +}; + +#endif /* _821_hpp */ diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index f4725f040..f06776380 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -10,9 +10,19 @@ #include "Machines/MachineTypes.hpp" #include "Processors/6809/6809.hpp" +#include "Components/6821/6821.hpp" using namespace Thomson::MO5; +// Video timing, as far as auto-translate lets me figure it out: +// +// 64 cycles/line; +// 56 lines post signalled vsync, then 200 of video, then 56 more, for 312 total. +// +// Start of vsync is connected to CPU IRQ. +// +// Within a line: ??? Who knows ??? +// namespace { struct ConcreteMachine: @@ -33,6 +43,8 @@ struct ConcreteMachine: const auto &rom = roms.find(ROM::Name::ThomasonMO5v11)->second; std::copy_n(rom.begin(), rom.size(), rom_.begin()); + + page_lower(false); } void run_for(const Cycles cycles) final { @@ -49,21 +61,38 @@ struct ConcreteMachine: const AddressT address, CPU::M6809::data_t value ) { - if constexpr (read_write != CPU::M6809::ReadWrite::NoData) { - if constexpr (CPU::M6809::is_read(read_write)) { - if(address >= 0xc000) { - value = rom_[address - 0xc000]; - } else { - value = ram_[address]; + if constexpr (read_write == CPU::M6809::ReadWrite::NoData) { + return Cycles(0); + } else { + if(address >= 0xa7c0 && address < 0xa800) { + switch(address) { + case 0xa7c0: case 0xa7c1: case 0xa7c2: case 0xa7c3: + if constexpr (CPU::M6809::is_read(read_write)) { + value = system_pia_.read(+address); + } else { + system_pia_.write(+address, value); + } + break; } } else { - ram_[address] = value; - printf("%04x: RAM <- 0x%02x [S: %04x]\n", +address, value, m6809_.registers().s); + if constexpr (CPU::M6809::is_read(read_write)) { + if(address < 0x2000) { + value = start_pointer_[address]; + } else if(address >= 0xc000) { + value = rom_[address - 0xc000]; + } else { + value = ram_[address]; + } + } else { + if(address < 0x2000) { + start_pointer_[address] = value; + } else { + ram_[address] = value; + } + } } } - // TODO: the lowest 8kb of memory can actually be paged, so the linear representation above isn't accurate. - return Cycles(0); } @@ -75,8 +104,15 @@ private: }; CPU::M6809::Processor m6809_; - std::array ram_; + std::array ram_; std::array rom_; + uint8_t *start_pointer_ = nullptr; + + void page_lower(const bool attributes) { + start_pointer_ = &ram_[attributes ? 0 : 0xf000]; + } + + Motorola::MC6821::MC6821 system_pia_; // MARK: - ScanProducer. diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 53b1eb4ab..f9dbaef9e 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -2531,6 +2531,7 @@ 4BDACBEA22FFA5D20045EF7E /* ncr5380.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ncr5380.cpp; sourceTree = ""; }; 4BDACBEB22FFA5D20045EF7E /* ncr5380.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ncr5380.hpp; sourceTree = ""; }; 4BDB3D8522833321002D3CEE /* Keyboard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = ""; }; + 4BDB734F2F6CA1A200964D69 /* 6821.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = 6821.hpp; sourceTree = ""; }; 4BDCC5F81FB27A5E001220C5 /* ROMMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ROMMachine.hpp; sourceTree = ""; }; 4BDDBA981EF3451200347E61 /* Z80MachineCycleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Z80MachineCycleTests.swift; sourceTree = ""; }; 4BE0151C286A8C8E00EA42E9 /* MemorySwitches.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MemorySwitches.hpp; sourceTree = ""; }; @@ -5276,6 +5277,7 @@ 4BC080DD26A481C100D03FD8 /* 6526 */, 4B1E85791D174DEC001EF87D /* 6532 */, 4BC9DF4C1D04691600F44158 /* 6560 */, + 4BDB73512F6CA1A200964D69 /* 6821 */, 4BE845221F2FF7F400A5EA22 /* 6845 */, 4BB307B8235001C300457D33 /* 6850 */, 4BD9137C1F3115AC009BCF85 /* 8255 */, @@ -5591,6 +5593,14 @@ path = 5380; sourceTree = ""; }; + 4BDB73512F6CA1A200964D69 /* 6821 */ = { + isa = PBXGroup; + children = ( + 4BDB734F2F6CA1A200964D69 /* 6821.hpp */, + ); + path = 6821; + sourceTree = ""; + }; 4BE211FB253FC80800435408 /* AppleIIgs */ = { isa = PBXGroup; children = ( From c67fa9ec3c796f57688e392d69996f84f45a8d03 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 19 Mar 2026 18:28:35 -0400 Subject: [PATCH 04/40] Add note to self. --- Machines/Thomson/MO5/MO5.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index f06776380..a80c02a6b 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -113,6 +113,25 @@ private: } Motorola::MC6821::MC6821 system_pia_; + // TODO: + // + // Port A: + // b0 = lower 8kb RAM paging; + // b1–4: border colour; + // b4: light pen button + // b6: tape output + // b7: tape input [and 0 = no tape; 1 = tape present] + // + // Port B: + // b0 = 1-bit sound output; + // b1–3 = keyboard column; + // b4–6: keyboard line; + // b7: status of key at that position. + // + // CA1: lightpen input (IRQA -> FIRQ) + // CA2: drive motor control + // CB1: 50Hz interrupt (IRQB -> IRQ) + // CB2: genlock enable, maybe? // MARK: - ScanProducer. From e77bc4790c5194cc9f8c8491988f66fead87d65c Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 19 Mar 2026 21:31:09 -0400 Subject: [PATCH 05/40] Sketch out some basics of the PIA. --- Components/6821/6821.hpp | 50 +++++++++++++++++++++++++++++++----- Machines/Thomson/MO5/MO5.cpp | 24 ++++++++++++----- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index 334b40031..7cc3fdbdc 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -9,22 +9,58 @@ #ifndef _821_hpp #define _821_hpp -#include "Numeric/SizedInt.hpp" - namespace Motorola::MC6821 { template class MC6821 { public: - void write(const Numeric::SizedInt<2> address, const uint8_t value) { - (void)address; - (void)value; + template + void write(const uint8_t value) { + static constexpr int port = (address >> 1) & 1; + + if constexpr (address & 1) { + ports_[port].control = value; + } else { + if(ports_[port].control & Control::DataVisible) { + ports_[port].data = value; + } else { + ports_[port].direction = value; + } + } } - uint8_t read(const Numeric::SizedInt<2> address) { - (void)address; + template + uint8_t read() { + static constexpr int port = (address >> 1) & 1; + if constexpr(address & 1) { + return ports_[port].control; + } else { + if(ports_[port].control & Control::DataVisible) { + return + (ports_[port].data & ports_[port].direction) | + (ports_[port].input & ~ports_[port].direction); + } else { + return ports_[port].direction; + } + } return 0; } + +private: + enum Control: uint8_t { + IRQA = 0b1000'0000, + IRQB = 0b0100'0000, + CA2 = 0b0011'1000, + DataVisible = 0b0000'0100, + CA1 = 0b0000'0011, + }; + + struct Port { + uint8_t control; + uint8_t data; + uint8_t direction; + uint8_t input = 0xff; + } ports_[2]; }; }; diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index a80c02a6b..6c795a121 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -51,6 +51,19 @@ struct ConcreteMachine: m6809_.run_for(cycles); } + template < + int address, + CPU::M6809::ReadWrite read_write, + typename ComponentT + > + static void access(ComponentT &component, CPU::M6809::data_t value) { + if constexpr (CPU::M6809::is_read(read_write)) { + value = component.template read
(); + } else { + component.template write
(value); + } + } + template < CPU::M6809::BusPhase bus_phase, CPU::M6809::ReadWrite read_write, @@ -66,13 +79,10 @@ struct ConcreteMachine: } else { if(address >= 0xa7c0 && address < 0xa800) { switch(address) { - case 0xa7c0: case 0xa7c1: case 0xa7c2: case 0xa7c3: - if constexpr (CPU::M6809::is_read(read_write)) { - value = system_pia_.read(+address); - } else { - system_pia_.write(+address, value); - } - break; + case 0xa7c0: access<0xa7c0, read_write>(system_pia_, value); break; + case 0xa7c1: access<0xa7c1, read_write>(system_pia_, value); break; + case 0xa7c2: access<0xa7c2, read_write>(system_pia_, value); break; + case 0xa7c3: access<0xa7c3, read_write>(system_pia_, value); break; } } else { if constexpr (CPU::M6809::is_read(read_write)) { From 64648a0388f1c327ddd800050099e711d96cef72 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 10:17:09 -0400 Subject: [PATCH 06/40] Attempt a through-wiring of PIA ports. --- Components/6821/6821.hpp | 38 ++++++++++++++++----- Machines/Thomson/MO5/MO5.cpp | 66 ++++++++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 24 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index 7cc3fdbdc..37bf6fd88 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -11,14 +11,21 @@ namespace Motorola::MC6821 { -template +enum class Port { + A = 0, + B = 1, +}; + +template class MC6821 { public: + MC6821(PortHandlerT &port_handler) : port_handler_(port_handler) {} + template void write(const uint8_t value) { - static constexpr int port = (address >> 1) & 1; + static constexpr int port = bool(address & RS1Mask); - if constexpr (address & 1) { + if constexpr (address & RS0Mask) { ports_[port].control = value; } else { if(ports_[port].control & Control::DataVisible) { @@ -26,7 +33,19 @@ public: } else { ports_[port].direction = value; } + port_handler_.template output( + ports_[port].data | (~ports_[port].direction) + ); } + +// if(!port) { + printf("[%04x %d [%d]]: %d: %02x; c:%02x d:%02x dir:%02x; output: %02x\n", + uint16_t(address), port, bool(ports_[port].control & Control::DataVisible), + bool(address & RS0Mask), value, + ports_[port].control, ports_[port].data, ports_[port].direction, + uint8_t(ports_[port].data | (~ports_[port].direction)) + ); +// } } template @@ -38,7 +57,7 @@ public: if(ports_[port].control & Control::DataVisible) { return (ports_[port].data & ports_[port].direction) | - (ports_[port].input & ~ports_[port].direction); + (port_handler_.template input() & ~ports_[port].direction); } else { return ports_[port].direction; } @@ -47,6 +66,8 @@ public: } private: + PortHandlerT &port_handler_; + enum Control: uint8_t { IRQA = 0b1000'0000, IRQB = 0b0100'0000, @@ -55,11 +76,10 @@ private: CA1 = 0b0000'0011, }; - struct Port { - uint8_t control; - uint8_t data; - uint8_t direction; - uint8_t input = 0xff; + struct { + uint8_t control = 0; + uint8_t data = 0; + uint8_t direction = 0; // Per bit: 0 = input; 1 = output. } ports_[2]; }; diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 6c795a121..4f2957b1c 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -31,7 +31,9 @@ struct ConcreteMachine: public Machine { ConcreteMachine(const Analyser::Static::Target &, const ROMMachine::ROMFetcher &rom_fetcher) : - m6809_(*this) + m6809_(*this), + system_pia_port_handler_(*this), + system_pia_(system_pia_port_handler_) { set_clock_rate(1'000'000); @@ -122,27 +124,61 @@ private: start_pointer_ = &ram_[attributes ? 0 : 0xf000]; } - Motorola::MC6821::MC6821 system_pia_; + friend struct SystemPIAPortHandler; + struct SystemPIAPortHandler { + SystemPIAPortHandler(ConcreteMachine &machine) : machine_(machine) {} + + template + uint8_t input() { + if constexpr (port == Motorola::MC6821::Port::A) { + // Port A inputs: + // b7: tape input [and 0 = no tape; 1 = tape present] + return 0xff; + } + + if constexpr (port == Motorola::MC6821::Port::B) { + // Port B inputs: + // b7: status of key at that position. + return 0xff; + } + + __builtin_unreachable(); + } + + template + void output(const uint8_t value) { + if constexpr (port == Motorola::MC6821::Port::A) { + machine_.page_lower(value & 1); + + // Port A outputs: + // b0 = lower 8kb RAM paging; + // b1–4: border colour; + // b4: light pen button + // b6: tape output + } + + if constexpr (port == Motorola::MC6821::Port::B) { + // Port B outputs: + // b0 = 1-bit sound output; + // b1–3 = keyboard column; + // b4–6: keyboard line; + } + } + // TODO: // - // Port A: - // b0 = lower 8kb RAM paging; - // b1–4: border colour; - // b4: light pen button - // b6: tape output - // b7: tape input [and 0 = no tape; 1 = tape present] - // - // Port B: - // b0 = 1-bit sound output; - // b1–3 = keyboard column; - // b4–6: keyboard line; - // b7: status of key at that position. - // // CA1: lightpen input (IRQA -> FIRQ) // CA2: drive motor control // CB1: 50Hz interrupt (IRQB -> IRQ) // CB2: genlock enable, maybe? + private: + ConcreteMachine &machine_; + + }; + SystemPIAPortHandler system_pia_port_handler_; + Motorola::MC6821::MC6821 system_pia_; + // MARK: - ScanProducer. void set_scan_target(Outputs::Display::ScanTarget *) final { From bffbf8184a4218974ee3a5a5e5830c87242f7c63 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 10:25:04 -0400 Subject: [PATCH 07/40] Confirm MO5 address wiring; add hysteresis. --- Components/6821/6821.hpp | 20 ++++++++------------ Machines/Thomson/MO5/MO5.cpp | 4 ++-- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index 37bf6fd88..d3a8d8fb3 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -33,19 +33,13 @@ public: } else { ports_[port].direction = value; } - port_handler_.template output( - ports_[port].data | (~ports_[port].direction) - ); - } -// if(!port) { - printf("[%04x %d [%d]]: %d: %02x; c:%02x d:%02x dir:%02x; output: %02x\n", - uint16_t(address), port, bool(ports_[port].control & Control::DataVisible), - bool(address & RS0Mask), value, - ports_[port].control, ports_[port].data, ports_[port].direction, - uint8_t(ports_[port].data | (~ports_[port].direction)) - ); -// } + const uint8_t output = ports_[port].data | (~ports_[port].direction); + if(output != ports_[port].previous_output) { + ports_[port].previous_output = output; + port_handler_.template output(output); + } + } } template @@ -80,6 +74,8 @@ private: uint8_t control = 0; uint8_t data = 0; uint8_t direction = 0; // Per bit: 0 = input; 1 = output. + + uint8_t previous_output = 0x00; } ports_[2]; }; diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 4f2957b1c..62a2f64c7 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -132,6 +132,7 @@ private: uint8_t input() { if constexpr (port == Motorola::MC6821::Port::A) { // Port A inputs: + // b4: light pen button // b7: tape input [and 0 = no tape; 1 = tape present] return 0xff; } @@ -153,7 +154,6 @@ private: // Port A outputs: // b0 = lower 8kb RAM paging; // b1–4: border colour; - // b4: light pen button // b6: tape output } @@ -177,7 +177,7 @@ private: }; SystemPIAPortHandler system_pia_port_handler_; - Motorola::MC6821::MC6821 system_pia_; + Motorola::MC6821::MC6821 system_pia_; // MARK: - ScanProducer. From 49412da7db0fda0b5c23cc29dd2b4e8db427d062 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 10:32:19 -0400 Subject: [PATCH 08/40] Establish a two-stage setup. --- Components/6821/6821.hpp | 28 +++++++++++++++++++++++----- Machines/Thomson/MO5/MO5.cpp | 2 +- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index d3a8d8fb3..a1e3eaa91 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -34,7 +34,7 @@ public: ports_[port].direction = value; } - const uint8_t output = ports_[port].data | (~ports_[port].direction); + const uint8_t output = ports_[port].output(); if(output != ports_[port].previous_output) { ports_[port].previous_output = output; port_handler_.template output(output); @@ -49,9 +49,7 @@ public: return ports_[port].control; } else { if(ports_[port].control & Control::DataVisible) { - return - (ports_[port].data & ports_[port].direction) | - (port_handler_.template input() & ~ports_[port].direction); + return ports_[port].input(port_handler_.template input()); } else { return ports_[port].direction; } @@ -59,6 +57,11 @@ public: return 0; } + void refresh() const { + refresh(); + refresh(); + } + private: PortHandlerT &port_handler_; @@ -75,8 +78,23 @@ private: uint8_t data = 0; uint8_t direction = 0; // Per bit: 0 = input; 1 = output. - uint8_t previous_output = 0x00; + uint8_t output() const { + return data | ~direction; + } + uint8_t input(const uint8_t bus) const { + return (data & direction) | (bus & ~direction); + } + mutable uint8_t previous_output = 0x00; } ports_[2]; + + template + void refresh() const { + static constexpr auto index = int(port); + + const uint8_t output = ports_[index].output(); + ports_[index].previous_output = output; + port_handler_.template output(output); + } }; }; diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 62a2f64c7..b0980784f 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -46,7 +46,7 @@ struct ConcreteMachine: const auto &rom = roms.find(ROM::Name::ThomasonMO5v11)->second; std::copy_n(rom.begin(), rom.size(), rom_.begin()); - page_lower(false); + system_pia_.refresh(); } void run_for(const Cycles cycles) final { From f5e6c350600b9a296193e6879f2d5c224ca06e55 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 10:40:36 -0400 Subject: [PATCH 09/40] Establish that interrupts might happen. --- Components/6821/6821.hpp | 5 +++++ Machines/Thomson/MO5/MO5.cpp | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index a1e3eaa91..0d7459b0e 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -16,6 +16,11 @@ enum class Port { B = 1, }; +enum class IRQ { + A, + B, +}; + template class MC6821 { public: diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index b0980784f..4a6dc2564 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -165,12 +165,23 @@ private: } } + template + void set_interrupt(const bool active) { + if constexpr (irq == Motorola::MC6821::IRQ::A) { + machine_.m6809_.set(active); + } + + if constexpr (irq == Motorola::MC6821::IRQ::B) { + machine_.m6809_.set(active); + } + } + // TODO: // - // CA1: lightpen input (IRQA -> FIRQ) + // CA1: lightpen input // CA2: drive motor control - // CB1: 50Hz interrupt (IRQB -> IRQ) - // CB2: genlock enable, maybe? + // CB1: 50Hz interrupt + // CB2: genlock enable, maybe? Video "encrustation". private: ConcreteMachine &machine_; From 5b4a583a46e0e133af0695771c7d871915595847 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 11:21:53 -0400 Subject: [PATCH 10/40] Add a placeholder for video generation. --- Machines/Thomson/MO5/MO5.cpp | 33 +++++++++++-------- Machines/Thomson/MO5/Video.cpp | 26 +++++++++++++++ Machines/Thomson/MO5/Video.hpp | 23 +++++++++++++ .../Clock Signal.xcodeproj/project.pbxproj | 12 ++++++- cmake/CLK_SOURCES.cmake | 1 + 5 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 Machines/Thomson/MO5/Video.cpp create mode 100644 Machines/Thomson/MO5/Video.hpp diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 4a6dc2564..80cfe9554 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -8,21 +8,16 @@ #include "MO5.hpp" +#include "Video.hpp" + #include "Machines/MachineTypes.hpp" #include "Processors/6809/6809.hpp" #include "Components/6821/6821.hpp" +#include "ClockReceiver/JustInTime.hpp" + using namespace Thomson::MO5; -// Video timing, as far as auto-translate lets me figure it out: -// -// 64 cycles/line; -// 56 lines post signalled vsync, then 200 of video, then 56 more, for 312 total. -// -// Start of vsync is connected to CPU IRQ. -// -// Within a line: ??? Who knows ??? -// namespace { struct ConcreteMachine: @@ -49,10 +44,6 @@ struct ConcreteMachine: system_pia_.refresh(); } - void run_for(const Cycles cycles) final { - m6809_.run_for(cycles); - } - template < int address, CPU::M6809::ReadWrite read_write, @@ -76,6 +67,8 @@ struct ConcreteMachine: const AddressT address, CPU::M6809::data_t value ) { + video_ += m6809_.duration(bus_phase); + if constexpr (read_write == CPU::M6809::ReadWrite::NoData) { return Cycles(0); } else { @@ -97,6 +90,7 @@ struct ConcreteMachine: } } else { if(address < 0x2000) { + if(address < 40*200) video_.flush(); start_pointer_[address] = value; } else { ram_[address] = value; @@ -190,6 +184,8 @@ private: SystemPIAPortHandler system_pia_port_handler_; Motorola::MC6821::MC6821 system_pia_; + JustInTimeActor video_; + // MARK: - ScanProducer. void set_scan_target(Outputs::Display::ScanTarget *) final { @@ -198,6 +194,17 @@ private: Outputs::Display::ScanStatus get_scan_status() const final { return Outputs::Display::ScanStatus(); } + + // MARK: - TimedMachine. + + void run_for(const Cycles cycles) final { + m6809_.run_for(cycles); + } + void flush_output(int outputs) final { + if(outputs & Output::Video) { + video_.flush(); + } + } }; } diff --git a/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp new file mode 100644 index 000000000..bf66ce5fd --- /dev/null +++ b/Machines/Thomson/MO5/Video.cpp @@ -0,0 +1,26 @@ +// +// Video.cpp +// Clock Signal +// +// Created by Thomas Harte on 20/03/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#include "Video.hpp" + +using namespace Thomson::MO5; + +// Video timing, as far as auto-translate lets me figure it out: +// +// 64 cycles/line; +// 56 lines post signalled vsync, then 200 of video, then 56 more, for 312 total. +// +// Start of vsync is connected to CPU IRQ. +// +// Within a line: ??? Who knows ??? +// + +void Video::run_for(const Cycles cycles) { + + (void)cycles; +} diff --git a/Machines/Thomson/MO5/Video.hpp b/Machines/Thomson/MO5/Video.hpp new file mode 100644 index 000000000..f1c3ed8fe --- /dev/null +++ b/Machines/Thomson/MO5/Video.hpp @@ -0,0 +1,23 @@ +// +// Video.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/03/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#ifndef Video_hpp +#define Video_hpp + +#include "ClockReceiver/ClockReceiver.hpp" + +namespace Thomson::MO5 { + +struct Video { +public: + void run_for(Cycles); +}; + +} + +#endif /* Video_hpp */ diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index f9dbaef9e..a48525c23 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -1104,6 +1104,9 @@ 4BBB70A8202014E2002FE009 /* MultiProducer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A6202014E2002FE009 /* MultiProducer.cpp */; }; 4BBB70A9202014E2002FE009 /* MultiProducer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A6202014E2002FE009 /* MultiProducer.cpp */; }; 4BBB77DD2867EBB300D335A1 /* IIgs Memory Map in Resources */ = {isa = PBXBuildFile; fileRef = 4BBB77DC2867EBB300D335A1 /* IIgs Memory Map */; }; + 4BBC7DC12F6D94E500E71794 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBC7DC02F6D94E500E71794 /* Video.cpp */; }; + 4BBC7DC22F6D94E500E71794 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBC7DC02F6D94E500E71794 /* Video.cpp */; }; + 4BBC7DC32F6D94E500E71794 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBC7DC02F6D94E500E71794 /* Video.cpp */; }; 4BBC951E1F368D83008F4C34 /* i8272.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBC951C1F368D83008F4C34 /* i8272.cpp */; }; 4BBF49AF1ED2880200AB3669 /* FUSETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF49AE1ED2880200AB3669 /* FUSETests.swift */; }; 4BBFBB6C1EE8401E00C01E7A /* ZX8081.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBFBB6A1EE8401E00C01E7A /* ZX8081.cpp */; }; @@ -2376,6 +2379,8 @@ 4BBB70A6202014E2002FE009 /* MultiProducer.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MultiProducer.cpp; sourceTree = ""; }; 4BBB70A7202014E2002FE009 /* MultiProducer.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MultiProducer.hpp; sourceTree = ""; }; 4BBB77DC2867EBB300D335A1 /* IIgs Memory Map */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "IIgs Memory Map"; sourceTree = ""; }; + 4BBC7DBF2F6D94E500E71794 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = ""; }; + 4BBC7DC02F6D94E500E71794 /* Video.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Video.cpp; sourceTree = ""; }; 4BBC951C1F368D83008F4C34 /* i8272.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = i8272.cpp; sourceTree = ""; }; 4BBC951D1F368D83008F4C34 /* i8272.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = i8272.hpp; sourceTree = ""; }; 4BBF49AE1ED2880200AB3669 /* FUSETests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FUSETests.swift; sourceTree = ""; }; @@ -5333,8 +5338,10 @@ 4BCA6FAA2F589C0C000B9537 /* MO5 */ = { isa = PBXGroup; children = ( - 4BCA6FA82F589C0C000B9537 /* MO5.hpp */, 4BCA6FA92F589C0C000B9537 /* MO5.cpp */, + 4BBC7DC02F6D94E500E71794 /* Video.cpp */, + 4BCA6FA82F589C0C000B9537 /* MO5.hpp */, + 4BBC7DBF2F6D94E500E71794 /* Video.hpp */, ); path = MO5; sourceTree = ""; @@ -6470,6 +6477,7 @@ 423820452B1A90BE00964EFE /* PCBooter.cpp in Sources */, 4B8318B922D3E56D006DB630 /* MemoryPacker.cpp in Sources */, 4B055ABA1FAE86170060FFFF /* Commodore.cpp in Sources */, + 4BBC7DC12F6D94E500E71794 /* Video.cpp in Sources */, 42EB812E2B4700B700429AF4 /* MemoryMap.cpp in Sources */, 4B0ACC2F23775819008902D0 /* TIA.cpp in Sources */, 4B9BE401203A0C0600FFAE60 /* MultiSpeaker.cpp in Sources */, @@ -6828,6 +6836,7 @@ 4B8805F41DCFD22A003085B1 /* Commodore.cpp in Sources */, 4B3FCC40201EC24200960631 /* MultiMachine.cpp in Sources */, 4B0F1BB22602645900B85C66 /* StaticAnalyser.cpp in Sources */, + 4BBC7DC32F6D94E500E71794 /* Video.cpp in Sources */, 4B8805F01DCFC99C003085B1 /* Acorn.cpp in Sources */, 4B30512D1D989E2200B4FED8 /* Drive.cpp in Sources */, 4BCE005D227D30CC000CA200 /* MemoryPacker.cpp in Sources */, @@ -6964,6 +6973,7 @@ 4BC5C3E022C994CD00795658 /* 68000MoveTests.mm in Sources */, 4B778F5923A5F2D00000D260 /* Z80.cpp in Sources */, 4BD971392BFC3D9D00C907AA /* ArchimedesStaticAnalyserTests.mm in Sources */, + 4BBC7DC22F6D94E500E71794 /* Video.cpp in Sources */, 4B08A2751EE35D56008B7065 /* Z80InterruptTests.swift in Sources */, 4B778F0E23A5EC4F0000D260 /* Tape.cpp in Sources */, 4B778F2D23A5EF190000D260 /* MFMDiskController.cpp in Sources */, diff --git a/cmake/CLK_SOURCES.cmake b/cmake/CLK_SOURCES.cmake index d29c824eb..35aa1fb0c 100644 --- a/cmake/CLK_SOURCES.cmake +++ b/cmake/CLK_SOURCES.cmake @@ -151,6 +151,7 @@ set(CLK_SOURCES Machines/Sinclair/ZX8081/ZX8081.cpp Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp Machines/Thomson/MO5/MO5.cpp + Machines/Thomson/MO5/Video.cpp Machines/Utility/MachineForTarget.cpp Machines/Utility/MemoryFuzzer.cpp Machines/Utility/MemoryPacker.cpp From 55912431bd720b8c728df03f72923c9b1d1b426e Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 11:41:23 -0400 Subject: [PATCH 11/40] Generate a CRT, connect the scan target. --- Machines/Thomson/MO5/MO5.cpp | 16 +++++++++++----- Machines/Thomson/MO5/Video.cpp | 18 ++++++++++++++++++ Machines/Thomson/MO5/Video.hpp | 28 ++++++++++++++++++++++++++++ Outputs/ScanTarget.hpp | 1 + 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 80cfe9554..d6065ddab 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -28,7 +28,8 @@ struct ConcreteMachine: ConcreteMachine(const Analyser::Static::Target &, const ROMMachine::ROMFetcher &rom_fetcher) : m6809_(*this), system_pia_port_handler_(*this), - system_pia_(system_pia_port_handler_) + system_pia_(system_pia_port_handler_), + video_(video_page(false), video_page(true)) { set_clock_rate(1'000'000); @@ -114,8 +115,12 @@ private: std::array rom_; uint8_t *start_pointer_ = nullptr; + uint8_t *video_page(const bool attributes) { + return &ram_[attributes ? 0 : 0x1'0000]; + } + void page_lower(const bool attributes) { - start_pointer_ = &ram_[attributes ? 0 : 0xf000]; + start_pointer_ = video_page(attributes); } friend struct SystemPIAPortHandler; @@ -188,11 +193,12 @@ private: // MARK: - ScanProducer. - void set_scan_target(Outputs::Display::ScanTarget *) final { + void set_scan_target(Outputs::Display::ScanTarget *const target) final { + video_->set_scan_target(target); } - Outputs::Display::ScanStatus get_scan_status() const final { - return Outputs::Display::ScanStatus(); + Outputs::Display::ScanStatus get_scaled_scan_status() const final { + return video_.last_valid()->get_scaled_scan_status(); } // MARK: - TimedMachine. diff --git a/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp index bf66ce5fd..bfc4edbd3 100644 --- a/Machines/Thomson/MO5/Video.cpp +++ b/Machines/Thomson/MO5/Video.cpp @@ -10,6 +10,8 @@ using namespace Thomson::MO5; +namespace { + // Video timing, as far as auto-translate lets me figure it out: // // 64 cycles/line; @@ -20,6 +22,22 @@ using namespace Thomson::MO5; // Within a line: ??? Who knows ??? // +static constexpr int CyclesPerLine = 64; +} + +Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) : + pixels_(pixels), attributes_(attributes), + crt_( + CyclesPerLine, + 1, + Outputs::Display::Type::SECAM, + Outputs::Display::InputDataType::Red4Green4Blue4 + ) { +// crt_.set_fixed_framing([&] { +// run_for(Cycles(10'000)); +// }); +} + void Video::run_for(const Cycles cycles) { (void)cycles; diff --git a/Machines/Thomson/MO5/Video.hpp b/Machines/Thomson/MO5/Video.hpp index f1c3ed8fe..a0fc01aa0 100644 --- a/Machines/Thomson/MO5/Video.hpp +++ b/Machines/Thomson/MO5/Video.hpp @@ -10,12 +10,40 @@ #define Video_hpp #include "ClockReceiver/ClockReceiver.hpp" +#include "Outputs/CRT/CRT.hpp" namespace Thomson::MO5 { struct Video { public: + Video(const uint8_t *pixels, const uint8_t *attributes); void run_for(Cycles); + + // MARK: - Standard boilerplate. + + void set_scan_target(Outputs::Display::ScanTarget *const target) { + crt_.set_scan_target(target); + } + + Outputs::Display::ScanStatus get_scaled_scan_status() const { + return crt_.get_scaled_scan_status(); + } + + void set_display_type(const Outputs::Display::DisplayType display_type) { + crt_.set_display_type(display_type); + } + + Outputs::Display::DisplayType get_display_type() const { + return crt_.get_display_type(); + } + +private: + const uint8_t *pixels_ = nullptr; + const uint8_t *attributes_ = nullptr; + Outputs::CRT::CRT crt_; + int line_ = 0; + int column_ = 0; + }; } diff --git a/Outputs/ScanTarget.hpp b/Outputs/ScanTarget.hpp index c7c838a44..1225edb10 100644 --- a/Outputs/ScanTarget.hpp +++ b/Outputs/ScanTarget.hpp @@ -19,6 +19,7 @@ namespace Outputs::Display { enum class Type { PAL50, + SECAM = PAL50, // Not correct as to composite colour, but correct as to timing. PAL60, NTSC60 }; From d04fd4e4298dc50c5a5debdf1efa5b03e737a515 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 12:03:50 -0400 Subject: [PATCH 12/40] Hack in some pixels. --- Machines/Thomson/MO5/MO5.cpp | 10 ++--- Machines/Thomson/MO5/Video.cpp | 71 ++++++++++++++++++++++++++++++++-- Machines/Thomson/MO5/Video.hpp | 5 ++- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index d6065ddab..9dc345f0f 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -29,7 +29,7 @@ struct ConcreteMachine: m6809_(*this), system_pia_port_handler_(*this), system_pia_(system_pia_port_handler_), - video_(video_page(false), video_page(true)) + video_(video_page(true), video_page(false)) { set_clock_rate(1'000'000); @@ -115,12 +115,12 @@ private: std::array rom_; uint8_t *start_pointer_ = nullptr; - uint8_t *video_page(const bool attributes) { - return &ram_[attributes ? 0 : 0x1'0000]; + uint8_t *video_page(const bool pixels) { + return &ram_[pixels ? 0 : 0x1'0000]; } - void page_lower(const bool attributes) { - start_pointer_ = video_page(attributes); + void page_lower(const bool pixels) { + start_pointer_ = video_page(pixels); } friend struct SystemPIAPortHandler; diff --git a/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp index bfc4edbd3..82c040585 100644 --- a/Machines/Thomson/MO5/Video.cpp +++ b/Machines/Thomson/MO5/Video.cpp @@ -23,6 +23,13 @@ namespace { // static constexpr int CyclesPerLine = 64; + +static constexpr int TotalPixelLines = 200; +static constexpr int TotalLines = 312; +static constexpr int VerticalSyncLine = 256; + +static constexpr int IRQCycle = 256 * CyclesPerLine; +static constexpr int FrameLength = TotalLines * CyclesPerLine; } Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) : @@ -33,12 +40,68 @@ Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) : Outputs::Display::Type::SECAM, Outputs::Display::InputDataType::Red4Green4Blue4 ) { -// crt_.set_fixed_framing([&] { -// run_for(Cycles(10'000)); -// }); + crt_.set_fixed_framing([&] { + run_for(Cycles(10'000)); + }); + crt_.set_display_type(Outputs::Display::DisplayType::RGB); } void Video::run_for(const Cycles cycles) { + int to_run = cycles.as(); - (void)cycles; + // HACK! Go line by line. + while(to_run) { + // HACK! Coarse counting block: work out number of lines to process. + const int start = position_ / CyclesPerLine; + const int end = std::min((position_ + to_run) / CyclesPerLine, TotalLines); + if(start == end) { + position_ += to_run; + break; + } + + for(int line = start; line < end; line++) { + if(line >= VerticalSyncLine && line < VerticalSyncLine * 3) { + crt_.output_sync(CyclesPerLine); + continue; + } + + if(line >= TotalPixelLines) { + crt_.output_sync(4); + crt_.output_blank(60); + continue; + } + + crt_.output_sync(4); + crt_.output_blank(10); + + output_ = reinterpret_cast(crt_.begin_data(320)); + if(output_) { + for(int x = 0; x < 40; x++) { + const uint8_t pixels = pixels_[source_address_++]; + + output_[0] = (pixels & 0x80) ? 0xffff : 0x0000; + output_[1] = (pixels & 0x40) ? 0xffff : 0x0000; + output_[2] = (pixels & 0x20) ? 0xffff : 0x0000; + output_[3] = (pixels & 0x10) ? 0xffff : 0x0000; + output_[4] = (pixels & 0x08) ? 0xffff : 0x0000; + output_[5] = (pixels & 0x04) ? 0xffff : 0x0000; + output_[6] = (pixels & 0x02) ? 0xffff : 0x0000; + output_[7] = (pixels & 0x01) ? 0xffff : 0x0000; + output_ += 8; + } + } + + crt_.output_data(40, 320); + crt_.output_blank(10); + } + + // HACK! Assuming the above proceeded in lines, adjust for number of cycles consumed. + const int did_run = (CyclesPerLine - (position_ % CyclesPerLine)) + ((end - start) - 1) * CyclesPerLine; + to_run -= did_run; + position_ += did_run; + if(position_ == FrameLength) { + position_ = 0; + source_address_ = 0; + } + } } diff --git a/Machines/Thomson/MO5/Video.hpp b/Machines/Thomson/MO5/Video.hpp index a0fc01aa0..ada7142ce 100644 --- a/Machines/Thomson/MO5/Video.hpp +++ b/Machines/Thomson/MO5/Video.hpp @@ -41,9 +41,10 @@ private: const uint8_t *pixels_ = nullptr; const uint8_t *attributes_ = nullptr; Outputs::CRT::CRT crt_; - int line_ = 0; - int column_ = 0; + int position_ = 0; + uint16_t source_address_ = 0; + uint16_t *output_ = nullptr; }; } From aa5692e844f8d9dbde5247026cb92477ad3b8a50 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 12:51:46 -0400 Subject: [PATCH 13/40] Communicate interrupts to the PIA. --- Components/6821/6821.hpp | 11 +++++ Machines/Thomson/MO5/MO5.cpp | 5 ++- Machines/Thomson/MO5/Video.cpp | 80 ++++++++++++++++++++++++++-------- Machines/Thomson/MO5/Video.hpp | 5 +++ 4 files changed, 81 insertions(+), 20 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index 0d7459b0e..2d55b33c2 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -21,6 +21,11 @@ enum class IRQ { B, }; +enum class Control { + CA1, CA2, + CB1, CB2, +}; + template class MC6821 { public: @@ -67,6 +72,12 @@ public: refresh(); } + template + void set(const bool value) { + // TODO. + (void)value; + } + private: PortHandlerT &port_handler_; diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 9dc345f0f..651d27e5a 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -68,7 +68,9 @@ struct ConcreteMachine: const AddressT address, CPU::M6809::data_t value ) { - video_ += m6809_.duration(bus_phase); + if(video_ += m6809_.duration(bus_phase)) { + system_pia_.set(video_.last_valid()->irq()); + } if constexpr (read_write == CPU::M6809::ReadWrite::NoData) { return Cycles(0); @@ -149,6 +151,7 @@ private: void output(const uint8_t value) { if constexpr (port == Motorola::MC6821::Port::A) { machine_.page_lower(value & 1); + machine_.video_->set_border_colour((value >> 1) & 0xf); // Port A outputs: // b0 = lower 8kb RAM paging; diff --git a/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp index 82c040585..8f12d7269 100644 --- a/Machines/Thomson/MO5/Video.cpp +++ b/Machines/Thomson/MO5/Video.cpp @@ -8,6 +8,8 @@ #include "Video.hpp" +#include + using namespace Thomson::MO5; namespace { @@ -22,14 +24,31 @@ namespace { // Within a line: ??? Who knows ??? // -static constexpr int CyclesPerLine = 64; +constexpr int CyclesPerLine = 64; -static constexpr int TotalPixelLines = 200; -static constexpr int TotalLines = 312; -static constexpr int VerticalSyncLine = 256; +constexpr int TotalPixelLines = 200; +constexpr int TotalLines = 312; +constexpr int VerticalSyncLine = 256; + +constexpr int IRQCycle = 256 * CyclesPerLine; +constexpr int IRQLength = 8; + +constexpr int FrameLength = TotalLines * CyclesPerLine; + +constexpr uint16_t rgb(const uint16_t code) { +#ifdef TARGET_RT_BIG_ENDIAN + return code; +#else + return std::rotl(code, 8); +#endif +} + +// Per https://pulkomandy.tk/wiki/doku.php?id=documentations:devices:gate.arrays#video_generation +constexpr uint16_t palette[] = { + rgb(0x000), rgb(0xf55), rgb(0x0f0), rgb(0xff0), rgb(0x55f), rgb(0xf0f), rgb(0x5ff), rgb(0xfff), + rgb(0xaaa), rgb(0xfaa), rgb(0xafa), rgb(0xffa), rgb(0x5af), rgb(0xfaf), rgb(0xaff), rgb(0xfa5), +}; -static constexpr int IRQCycle = 256 * CyclesPerLine; -static constexpr int FrameLength = TotalLines * CyclesPerLine; } Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) : @@ -60,39 +79,46 @@ void Video::run_for(const Cycles cycles) { } for(int line = start; line < end; line++) { - if(line >= VerticalSyncLine && line < VerticalSyncLine * 3) { + if(line >= VerticalSyncLine && line < VerticalSyncLine + 3) { crt_.output_sync(CyclesPerLine); continue; } if(line >= TotalPixelLines) { crt_.output_sync(4); - crt_.output_blank(60); + crt_.output_level(60, border_); continue; } crt_.output_sync(4); - crt_.output_blank(10); + crt_.output_level(10, border_); output_ = reinterpret_cast(crt_.begin_data(320)); if(output_) { for(int x = 0; x < 40; x++) { - const uint8_t pixels = pixels_[source_address_++]; + const uint8_t pixels = pixels_[source_address_]; + const uint8_t attributes = attributes_[source_address_]; + ++source_address_; - output_[0] = (pixels & 0x80) ? 0xffff : 0x0000; - output_[1] = (pixels & 0x40) ? 0xffff : 0x0000; - output_[2] = (pixels & 0x20) ? 0xffff : 0x0000; - output_[3] = (pixels & 0x10) ? 0xffff : 0x0000; - output_[4] = (pixels & 0x08) ? 0xffff : 0x0000; - output_[5] = (pixels & 0x04) ? 0xffff : 0x0000; - output_[6] = (pixels & 0x02) ? 0xffff : 0x0000; - output_[7] = (pixels & 0x01) ? 0xffff : 0x0000; + const uint16_t colours[] = { + palette[attributes & 0xf], + palette[attributes >> 4], + }; + + output_[0] = colours[(pixels >> 7) & 1]; + output_[1] = colours[(pixels >> 6) & 1]; + output_[2] = colours[(pixels >> 5) & 1]; + output_[3] = colours[(pixels >> 4) & 1]; + output_[4] = colours[(pixels >> 3) & 1]; + output_[5] = colours[(pixels >> 2) & 1]; + output_[6] = colours[(pixels >> 1) & 1]; + output_[7] = colours[(pixels >> 0) & 1]; output_ += 8; } } crt_.output_data(40, 320); - crt_.output_blank(10); + crt_.output_level(10, border_); } // HACK! Assuming the above proceeded in lines, adjust for number of cycles consumed. @@ -105,3 +131,19 @@ void Video::run_for(const Cycles cycles) { } } } + +Cycles Video::next_sequence_point() const { + // Pulse the interrupt output for 8 cycles, arbitrarily. TODO: what's the real number? + if(position_ < IRQCycle) return IRQCycle - position_; + if(position_ < IRQCycle + IRQLength) return IRQCycle + IRQLength - position_; + return FrameLength - position_ + IRQCycle; +} + +bool Video::irq() const { + return position_ >= IRQCycle && position_ < (IRQCycle + IRQLength); +} + +void Video::set_border_colour(const uint8_t colour) { + // TODO: bits possibly need a swizzle? + border_ = palette[colour & 0xf]; +} diff --git a/Machines/Thomson/MO5/Video.hpp b/Machines/Thomson/MO5/Video.hpp index ada7142ce..418479e34 100644 --- a/Machines/Thomson/MO5/Video.hpp +++ b/Machines/Thomson/MO5/Video.hpp @@ -18,6 +18,10 @@ struct Video { public: Video(const uint8_t *pixels, const uint8_t *attributes); void run_for(Cycles); + Cycles next_sequence_point() const; + + void set_border_colour(uint8_t); + bool irq() const; // MARK: - Standard boilerplate. @@ -44,6 +48,7 @@ private: int position_ = 0; uint16_t source_address_ = 0; + uint16_t border_ = 0; uint16_t *output_ = nullptr; }; From d2d635a73546ecec303681d2f46564a40f1b1ccb Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 12:58:16 -0400 Subject: [PATCH 14/40] Provide that some control lines might sometimes be outputs. --- Components/6821/6821.hpp | 9 +++++++-- Machines/Thomson/MO5/MO5.cpp | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index 2d55b33c2..7903036ad 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -74,8 +74,13 @@ public: template void set(const bool value) { - // TODO. - (void)value; + // TODO: + // + // (1) is this control an input (CA1 and CA2 always, CB1 and CB2 configurable)? + // (2) if so, does this change set one of the interrupt outputs? + + // Forward everything to the port handler. + port_handler_.template observe(value); } private: diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 651d27e5a..5efb5588e 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -168,7 +168,7 @@ private: } template - void set_interrupt(const bool active) { + void set(const bool active) { if constexpr (irq == Motorola::MC6821::IRQ::A) { machine_.m6809_.set(active); } @@ -178,6 +178,12 @@ private: } } + template + void observe(const bool value) { + // TODO: CA2 is drive motor control, so catch that. + (void)value; + } + // TODO: // // CA1: lightpen input From e098a5d08d405bd45d0c6fcd63855b9c724e20f5 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 13:24:05 -0400 Subject: [PATCH 15/40] Attempt wiring of interrupts. --- Components/6821/6821.hpp | 74 ++++++++++++++++++++++++++++++------ Machines/Thomson/MO5/MO5.cpp | 2 +- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index 7903036ad..35daf4889 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -37,8 +37,9 @@ public: if constexpr (address & RS0Mask) { ports_[port].control = value; + update_interrupts(); } else { - if(ports_[port].control & Control::DataVisible) { + if(ports_[port].control & Flag::DataVisible) { ports_[port].data = value; } else { ports_[port].direction = value; @@ -58,7 +59,9 @@ public: if constexpr(address & 1) { return ports_[port].control; } else { - if(ports_[port].control & Control::DataVisible) { + if(ports_[port].control & Flag::DataVisible) { + ports_[port].control &= ~(IRQA | IRQB); + update_interrupts(); return ports_[port].input(port_handler_.template input()); } else { return ports_[port].direction; @@ -74,24 +77,51 @@ public: template void set(const bool value) { - // TODO: - // - // (1) is this control an input (CA1 and CA2 always, CB1 and CB2 configurable)? - // (2) if so, does this change set one of the interrupt outputs? - - // Forward everything to the port handler. + // Reject anything that isn't a change. + static constexpr int input = int(control); + if(inputs_[input] == value) { + return; + } + inputs_[input] = value; port_handler_.template observe(value); + + static constexpr bool is_irqb = control == Control::CB2 || control == Control::CA2; + static constexpr int port = control == Control::CB2 || control == Control::CB1; + + // Reject any set to CB2 if it's in output mode. + if constexpr (is_irqb) { + if(ports_[port].control & Flag::C2IsOutput) { + return; + } + } + + // Test whether change was in the triggering direction; if so update control bit and check for change + // to interrupt output. + static constexpr int direction = is_irqb ? IRQBDirection : IRQADirection; + if(value != bool(ports_[port].control & direction)) { + ports_[port].control |= is_irqb ? Flag::IRQB : Flag::IRQA; + update_interrupts(); + } } private: PortHandlerT &port_handler_; + bool inputs_[4]{}; - enum Control: uint8_t { + enum Flag: uint8_t { IRQA = 0b1000'0000, IRQB = 0b0100'0000, - CA2 = 0b0011'1000, + C2 = 0b0011'1000, DataVisible = 0b0000'0100, - CA1 = 0b0000'0011, + C1 = 0b0000'0011, + + C2IsOutput = 0b0010'0000, + + IRQADirection = 0b0000'0010, + EnableIRQA = 0b0000'0001, + + IRQBDirection = 0b0001'0000, + EnableIRQB = 0b0000'1000, }; struct { @@ -99,6 +129,13 @@ private: uint8_t data = 0; uint8_t direction = 0; // Per bit: 0 = input; 1 = output. + bool irqa() const { + return (control & (IRQA | EnableIRQA)) == (IRQA | EnableIRQA); + } + bool irqb() const { + return (control & (IRQB | EnableIRQB | C2IsOutput)) == (IRQB | EnableIRQB); + } + uint8_t output() const { return data | ~direction; } @@ -116,6 +153,21 @@ private: ports_[index].previous_output = output; port_handler_.template output(output); } + + bool irqa_ = false, irqb_ = false; + void update_interrupts() { + const bool irqa = ports_[0].irqa() || ports_[1].irqa(); + const bool irqb = ports_[0].irqb() || ports_[1].irqb(); + + if(irqa_ != irqa) { + irqa_ = irqa; + port_handler_.template set(irqa); + } + if(irqb_ != irqb) { + irqb_ = irqb; + port_handler_.template set(irqb); + } + } }; }; diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 5efb5588e..a4a229f8f 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -69,7 +69,7 @@ struct ConcreteMachine: CPU::M6809::data_t value ) { if(video_ += m6809_.duration(bus_phase)) { - system_pia_.set(video_.last_valid()->irq()); + system_pia_.set(video_.last_valid()->irq()); } if constexpr (read_write == CPU::M6809::ReadWrite::NoData) { From 3913a2c9d3c29036bf149d33674de65e37d4f378 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 14:45:56 -0400 Subject: [PATCH 16/40] Switch comprehension of what combines to make IRQA and B. --- Components/6821/6821.hpp | 59 +++++++++++++++++++++--------------- Machines/Thomson/MO5/MO5.cpp | 1 + Processors/6809/6809.hpp | 2 ++ 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index 35daf4889..d1d1a224a 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -37,7 +37,6 @@ public: if constexpr (address & RS0Mask) { ports_[port].control = value; - update_interrupts(); } else { if(ports_[port].control & Flag::DataVisible) { ports_[port].data = value; @@ -60,9 +59,10 @@ public: return ports_[port].control; } else { if(ports_[port].control & Flag::DataVisible) { - ports_[port].control &= ~(IRQA | IRQB); + const uint8_t result = ports_[port].input(port_handler_.template input()); + ports_[port].clear_irq(); update_interrupts(); - return ports_[port].input(port_handler_.template input()); + return result; } else { return ports_[port].direction; } @@ -85,11 +85,11 @@ public: inputs_[input] = value; port_handler_.template observe(value); - static constexpr bool is_irqb = control == Control::CB2 || control == Control::CA2; - static constexpr int port = control == Control::CB2 || control == Control::CB1; + static constexpr bool is_irq2 = control == Control::CB2 || control == Control::CA2; // 0 = IRQ1; 1 = IRQ2. + static constexpr int port = control == Control::CB2 || control == Control::CB1; // 0 = port A; 1 = port B. // Reject any set to CB2 if it's in output mode. - if constexpr (is_irqb) { + if constexpr (is_irq2) { if(ports_[port].control & Flag::C2IsOutput) { return; } @@ -97,9 +97,9 @@ public: // Test whether change was in the triggering direction; if so update control bit and check for change // to interrupt output. - static constexpr int direction = is_irqb ? IRQBDirection : IRQADirection; + static constexpr int direction = is_irq2 ? IRQ2Direction : IRQ1Direction; if(value != bool(ports_[port].control & direction)) { - ports_[port].control |= is_irqb ? Flag::IRQB : Flag::IRQA; + ports_[port].template apply_irq(); update_interrupts(); } } @@ -109,19 +109,16 @@ private: bool inputs_[4]{}; enum Flag: uint8_t { - IRQA = 0b1000'0000, - IRQB = 0b0100'0000, - C2 = 0b0011'1000, DataVisible = 0b0000'0100, - C1 = 0b0000'0011, - C2IsOutput = 0b0010'0000, - IRQADirection = 0b0000'0010, - EnableIRQA = 0b0000'0001, + IRQ1 = 0b1000'0000, + IRQ1Direction = 0b0000'0010, + EnableIRQ1 = 0b0000'0001, - IRQBDirection = 0b0001'0000, - EnableIRQB = 0b0000'1000, + IRQ2 = 0b0100'0000, + IRQ2Direction = 0b0001'0000, + EnableIRQ2 = 0b0000'1000, }; struct { @@ -129,20 +126,32 @@ private: uint8_t data = 0; uint8_t direction = 0; // Per bit: 0 = input; 1 = output. - bool irqa() const { - return (control & (IRQA | EnableIRQA)) == (IRQA | EnableIRQA); - } - bool irqb() const { - return (control & (IRQB | EnableIRQB | C2IsOutput)) == (IRQB | EnableIRQB); + template + void apply_irq() { + control |= mask; + irq_ |= + (mask == IRQ1 && control & EnableIRQ1) || + (mask == IRQ2 && ((control & (EnableIRQ2 | C2IsOutput)) == EnableIRQ2)); } + void clear_irq() { + irq_ = false; + control &= ~(IRQ1 | IRQ2); + } + + bool irq() const { + return irq_; + } + + mutable uint8_t previous_output = 0x00; uint8_t output() const { return data | ~direction; } uint8_t input(const uint8_t bus) const { return (data & direction) | (bus & ~direction); } - mutable uint8_t previous_output = 0x00; + private: + bool irq_ = false; } ports_[2]; template @@ -156,8 +165,8 @@ private: bool irqa_ = false, irqb_ = false; void update_interrupts() { - const bool irqa = ports_[0].irqa() || ports_[1].irqa(); - const bool irqb = ports_[0].irqb() || ports_[1].irqb(); + const bool irqa = ports_[0].irq(); + const bool irqb = ports_[1].irq(); if(irqa_ != irqa) { irqa_ = irqa; diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index a4a229f8f..5a98a18ff 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -160,6 +160,7 @@ private: } if constexpr (port == Motorola::MC6821::Port::B) { + printf("Keyboard scan: %d\n", (value >> 1) & 0x3f); // Port B outputs: // b0 = 1-bit sound output; // b1–3 = keyboard column; diff --git a/Processors/6809/6809.hpp b/Processors/6809/6809.hpp index 3f5b068e2..262e074dc 100644 --- a/Processors/6809/6809.hpp +++ b/Processors/6809/6809.hpp @@ -861,6 +861,8 @@ struct Processor { case Operation::SWI2: return 0xfff4; case Operation::SWI3: return 0xfff2; case Operation::RESET: return 0xfffe; + + // TODO: below is incorrect assuming that spurious interrupts can occur. default: __builtin_unreachable(); } } (); From 2a1d9bc8e4f40d3eb960d1e4e8c210683d7fa131 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 20 Mar 2026 16:41:03 -0400 Subject: [PATCH 17/40] Try: storage is in control register, but is not writeable. --- Components/6821/6821.hpp | 35 ++++++++++++++++++----------------- Processors/6809/6809.hpp | 5 ++++- Processors/6809/Registers.hpp | 3 +-- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index d1d1a224a..f4a7f9fa5 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -36,7 +36,8 @@ public: static constexpr int port = bool(address & RS1Mask); if constexpr (address & RS0Mask) { - ports_[port].control = value; + static constexpr auto IRQMask = IRQ1 | IRQ2; + ports_[port].control = (ports_[port].control & IRQMask) | (value & ~IRQMask); } else { if(ports_[port].control & Flag::DataVisible) { ports_[port].data = value; @@ -56,18 +57,17 @@ public: uint8_t read() { static constexpr int port = (address >> 1) & 1; if constexpr(address & 1) { - return ports_[port].control; + const auto result = ports_[port].control; + ports_[port].clear_irq(); + update_interrupts(); + return result; } else { if(ports_[port].control & Flag::DataVisible) { - const uint8_t result = ports_[port].input(port_handler_.template input()); - ports_[port].clear_irq(); - update_interrupts(); - return result; + return ports_[port].input(port_handler_.template input()); } else { return ports_[port].direction; } } - return 0; } void refresh() const { @@ -99,7 +99,7 @@ public: // to interrupt output. static constexpr int direction = is_irq2 ? IRQ2Direction : IRQ1Direction; if(value != bool(ports_[port].control & direction)) { - ports_[port].template apply_irq(); + ports_[port].apply_irq(is_irq2 ? Flag::IRQ2 : Flag::IRQ1); update_interrupts(); } } @@ -126,21 +126,24 @@ private: uint8_t data = 0; uint8_t direction = 0; // Per bit: 0 = input; 1 = output. - template - void apply_irq() { + void apply_irq(const uint8_t mask) { control |= mask; - irq_ |= - (mask == IRQ1 && control & EnableIRQ1) || - (mask == IRQ2 && ((control & (EnableIRQ2 | C2IsOutput)) == EnableIRQ2)); } void clear_irq() { - irq_ = false; control &= ~(IRQ1 | IRQ2); } bool irq() const { - return irq_; + static constexpr auto IRQ1Value = EnableIRQ1 | IRQ1; + static constexpr auto IRQ1Mask = IRQ1Value; + + static constexpr auto IRQ2Value = EnableIRQ2 | IRQ2; + static constexpr auto IRQ2Mask = IRQ2Value | C2IsOutput; + + return + ((control & IRQ1Mask) == IRQ1Value) || + ((control & IRQ2Mask) == IRQ2Value); } mutable uint8_t previous_output = 0x00; @@ -150,8 +153,6 @@ private: uint8_t input(const uint8_t bus) const { return (data & direction) | (bus & ~direction); } - private: - bool irq_ = false; } ports_[2]; template diff --git a/Processors/6809/6809.hpp b/Processors/6809/6809.hpp index 262e074dc..e6846a92f 100644 --- a/Processors/6809/6809.hpp +++ b/Processors/6809/6809.hpp @@ -400,6 +400,9 @@ struct Processor { } if(exceptions_) { + // Avoid potential accidental detection of SWIs on the interrupt paths. + operation_.operation = Operation::None; + if(exceptions_ & Exception::Halt) { goto halt; } @@ -795,7 +798,7 @@ struct Processor { goto exception; nmi_irq: - internal_cycles(2); + internal_cycles(2); // TODO: is this when interrupts are acknowledged? goto exception; exception: diff --git a/Processors/6809/Registers.hpp b/Processors/6809/Registers.hpp index 644644cc4..dc7ae5ddd 100644 --- a/Processors/6809/Registers.hpp +++ b/Processors/6809/Registers.hpp @@ -347,6 +347,7 @@ struct IndexedAddressDecoder { } switch(suffix()) { using enum FormSuffix; + default: // For invalid suffixes. case NoOffset: return 0; case Offset8bit: return 1; @@ -366,8 +367,6 @@ struct IndexedAddressDecoder { case Offset16bitFromPC: return 4; case Extended: return 1; - - default:__builtin_unreachable(); } } From b88269a937cd211821e344ee6d027717d723fa2c Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 21 Mar 2026 10:23:13 -0400 Subject: [PATCH 18/40] Regularise condition order. --- Processors/6809/Registers.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Processors/6809/Registers.hpp b/Processors/6809/Registers.hpp index dc7ae5ddd..ca4527683 100644 --- a/Processors/6809/Registers.hpp +++ b/Processors/6809/Registers.hpp @@ -130,13 +130,13 @@ struct ConditionCodeRegister { case NE: return !get(); case EQ: return get(); + case LT: return get() != get(); case LE: return get() || test(); case LS: return get() || get(); - case LT: return get() != get(); - case HI: return !get() && !get(); case GE: return get() == get(); case GT: return !get() && test(); + case HI: return !get() && !get(); } return false; } From 0b1ca94aa5dfe10b18f91b34c10ba7c4fe05bc0b Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 21 Mar 2026 10:24:10 -0400 Subject: [PATCH 19/40] After an extra cycle of thought, RTI is like the tail of RTS. --- Processors/6809/6809.hpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Processors/6809/6809.hpp b/Processors/6809/6809.hpp index e6846a92f..1c5339309 100644 --- a/Processors/6809/6809.hpp +++ b/Processors/6809/6809.hpp @@ -744,11 +744,7 @@ struct Processor { rts: addressed_internal_cycle(Address::Literal(registers_.pc.full)); - read(BusState::Normal, Literal(registers_.reg()), address_.halves.high, ++registers_.reg()); - read(BusState::Normal, Literal(registers_.reg()), address_.halves.low, ++registers_.reg()); - registers_.reg() = address_.full; - internal_cycle(); - goto fetch_decode; + goto rti_not_entire; bsr: read(BusState::Normal, Literal(registers_.pc.full), operand_.halves.low, ++registers_.pc.full); From 957fbcfa644a32f1bc002681e07a2650f1ef124b Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 21 Mar 2026 10:37:50 -0400 Subject: [PATCH 20/40] Post interrupt acknowledge on vector fetches. --- Processors/6809/6809.hpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Processors/6809/6809.hpp b/Processors/6809/6809.hpp index 1c5339309..50aa4bf7c 100644 --- a/Processors/6809/6809.hpp +++ b/Processors/6809/6809.hpp @@ -198,7 +198,7 @@ struct Processor { time_ -= Cycles(1); \ time_ -= \ bus_handler_.template perform(addr, target_); \ - goto local_label(skipMRDY); \ + goto local_label(skipMRDY); \ } \ \ if constexpr (Traits::uses_mrdy) { \ @@ -794,7 +794,7 @@ struct Processor { goto exception; nmi_irq: - internal_cycles(2); // TODO: is this when interrupts are acknowledged? + internal_cycles(2); goto exception; exception: @@ -872,8 +872,18 @@ struct Processor { } internal_cycle(); - read(BusState::Normal, Literal(address_.full), registers_.pc.halves.high, ++address_.full); - read(BusState::Normal, Literal(address_.full), registers_.pc.halves.low, ++address_.full); + read( + BusState::InterruptOrResetAcknowledge, + Literal(address_.full), + registers_.pc.halves.high, + ++address_.full + ); + read( + BusState::InterruptOrResetAcknowledge, + Literal(address_.full), + registers_.pc.halves.low, + ++address_.full + ); internal_cycle(); goto fetch_decode; From 258afa077f9fe92d3fbaba5c0616c0186ea548d8 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 21 Mar 2026 11:29:08 -0400 Subject: [PATCH 21/40] Further simplify expression of conditions. --- Processors/6809/Registers.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Processors/6809/Registers.hpp b/Processors/6809/Registers.hpp index ca4527683..8dcf75180 100644 --- a/Processors/6809/Registers.hpp +++ b/Processors/6809/Registers.hpp @@ -130,13 +130,13 @@ struct ConditionCodeRegister { case NE: return !get(); case EQ: return get(); - case LT: return get() != get(); - case LE: return get() || test(); case LS: return get() || get(); + case LE: return get() || test(); + case LT: return get() != get(); - case GE: return get() == get(); - case GT: return !get() && test(); - case HI: return !get() && !get(); + case GE: return !test(); + case GT: return !test(); + case HI: return !test(); } return false; } From 9254ce8ffdf33528dd9bd1890c99da8b285c2374 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 21 Mar 2026 22:41:04 -0400 Subject: [PATCH 22/40] Update address even under back-pressure. --- Machines/Thomson/MO5/Video.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp index 8f12d7269..33f269952 100644 --- a/Machines/Thomson/MO5/Video.cpp +++ b/Machines/Thomson/MO5/Video.cpp @@ -115,6 +115,8 @@ void Video::run_for(const Cycles cycles) { output_[7] = colours[(pixels >> 0) & 1]; output_ += 8; } + } else { + source_address_ += 40; } crt_.output_data(40, 320); From 77f5b04c9566e00b1dd432d9f99e71932bc4b988 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 21 Mar 2026 23:34:53 -0400 Subject: [PATCH 23/40] Extract some value from [L]BLS tests. --- OSBindings/Mac/Clock SignalTests/6809Tests.mm | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/OSBindings/Mac/Clock SignalTests/6809Tests.mm b/OSBindings/Mac/Clock SignalTests/6809Tests.mm index 046ee3a28..6b4a5c8f0 100644 --- a/OSBindings/Mac/Clock SignalTests/6809Tests.mm +++ b/OSBindings/Mac/Clock SignalTests/6809Tests.mm @@ -122,16 +122,28 @@ struct M6809Traits { // Test set uses the test that either exactly one of NZC is set, or they all are. That's four possibilities. // - // Documented test is N != C, or Z. That's six possibilities (four with Z set; two more with Z unset but the + // Documented test is N != V, or Z. That's six possibilities (four with Z set; two more with Z unset but the // other two not matching). // - // Hence it's seems true that the test set isn't right on this. Or the documented test isn't. + // Not even both being dependent on the same flags, it's clear that the test set and documentation diverge. case BLE: - case LBLE: return; + case LBLE: + return; // The test set doesn't branch if both Z and C are set. The documented test is to branch. case BLS: - case LBLS: return; + case LBLS: + if( + m6809.registers().cc.get() && + m6809.registers().cc.get() + ) { + return; + } + break; + + // The test set implements CLR as a write operation. It's actually a modify. + case CLR: + return; case EXG: case TFR: { // The test suite supports only the operands listed below, treating the rest as NOPs. From 255508856fc9e896f7413d75163011ecd175190b Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 11:14:30 -0400 Subject: [PATCH 24/40] Transition to accurately-timed video collection. --- Machines/Thomson/MO5/MO5.cpp | 2 +- Machines/Thomson/MO5/Video.cpp | 154 ++++++++++++++++++++------------- Machines/Thomson/MO5/Video.hpp | 4 + 3 files changed, 101 insertions(+), 59 deletions(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 5a98a18ff..8f4b36260 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -160,7 +160,7 @@ private: } if constexpr (port == Motorola::MC6821::Port::B) { - printf("Keyboard scan: %d\n", (value >> 1) & 0x3f); +// printf("Keyboard scan: %d\n", (value >> 1) & 0x3f); // Port B outputs: // b0 = 1-bit sound output; // b1–3 = keyboard column; diff --git a/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp index 33f269952..52832316d 100644 --- a/Machines/Thomson/MO5/Video.cpp +++ b/Machines/Thomson/MO5/Video.cpp @@ -49,6 +49,16 @@ constexpr uint16_t palette[] = { rgb(0xaaa), rgb(0xfaa), rgb(0xafa), rgb(0xffa), rgb(0x5af), rgb(0xfaf), rgb(0xaff), rgb(0xfa5), }; +/// Calculates the overlap, if any, between [begin, end] and [RangeBegin, RangeEnd] and calls FuncT with a begin and end if the range is non-zero. +template +void clamp(const int begin, const int end, const FuncT &&function) { + const int range_begin = std::max(begin, RangeBegin); + const int range_end = std::min(end, RangeEnd); + if(range_end > range_begin) { + function(range_begin, range_end); + } +} + } Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) : @@ -68,74 +78,103 @@ Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) : void Video::run_for(const Cycles cycles) { int to_run = cycles.as(); - // HACK! Go line by line. while(to_run) { - // HACK! Coarse counting block: work out number of lines to process. - const int start = position_ / CyclesPerLine; - const int end = std::min((position_ + to_run) / CyclesPerLine, TotalLines); - if(start == end) { - position_ += to_run; - break; - } + const int line = position_ / CyclesPerLine; + const int start_column = position_ % CyclesPerLine; - for(int line = start; line < end; line++) { - if(line >= VerticalSyncLine && line < VerticalSyncLine + 3) { - crt_.output_sync(CyclesPerLine); - continue; - } + const int in_line = std::min(CyclesPerLine - start_column, to_run); + const int end_column = std::min(CyclesPerLine, start_column + in_line); + to_run -= in_line; - if(line >= TotalPixelLines) { - crt_.output_sync(4); - crt_.output_level(60, border_); - continue; - } - - crt_.output_sync(4); - crt_.output_level(10, border_); - - output_ = reinterpret_cast(crt_.begin_data(320)); - if(output_) { - for(int x = 0; x < 40; x++) { - const uint8_t pixels = pixels_[source_address_]; - const uint8_t attributes = attributes_[source_address_]; - ++source_address_; - - const uint16_t colours[] = { - palette[attributes & 0xf], - palette[attributes >> 4], - }; - - output_[0] = colours[(pixels >> 7) & 1]; - output_[1] = colours[(pixels >> 6) & 1]; - output_[2] = colours[(pixels >> 5) & 1]; - output_[3] = colours[(pixels >> 4) & 1]; - output_[4] = colours[(pixels >> 3) & 1]; - output_[5] = colours[(pixels >> 2) & 1]; - output_[6] = colours[(pixels >> 1) & 1]; - output_[7] = colours[(pixels >> 0) & 1]; - output_ += 8; - } - } else { - source_address_ += 40; - } - - crt_.output_data(40, 320); - crt_.output_level(10, border_); - } - - // HACK! Assuming the above proceeded in lines, adjust for number of cycles consumed. - const int did_run = (CyclesPerLine - (position_ % CyclesPerLine)) + ((end - start) - 1) * CyclesPerLine; - to_run -= did_run; - position_ += did_run; if(position_ == FrameLength) { position_ = 0; source_address_ = 0; } + position_ += in_line; + + if(line >= VerticalSyncLine && line < VerticalSyncLine + 3) { + vsync_line(start_column, end_column); + } else if(line >= TotalPixelLines) { + border_line(start_column, end_column); + } else { + pixel_line(start_column, end_column); + } } } +void Video::vsync_line(const int line_begin, const int line_end) { + crt_.output_sync(line_end - line_begin); +} + +void Video::border_line(const int line_begin, const int line_end) { + static constexpr int EndOfSync = 4; + clamp<0, EndOfSync>(line_begin, line_end, [&](const int begin, const int end) { + crt_.output_sync(end - begin); + }); + clamp(line_begin, line_end, [&](const int begin, const int end) { + crt_.output_level(end - begin, border_); + }); +} + +void Video::pixel_line(const int line_begin, const int line_end) { + // Layout: [sync][border][pixels][border]. + static constexpr int EndOfSync = 4; + static constexpr int EndOfLeftBorder = 14; + static constexpr int EndOfPixels = 54; + + clamp<0, EndOfSync>(line_begin, line_end, [&](const int begin, const int end) { + crt_.output_sync(end - begin); + }); + clamp(line_begin, line_end, [&](const int begin, const int end) { + crt_.output_level(end - begin, border_); + }); + clamp(line_begin, line_end, [&](const int begin, const int end) { + if(begin == EndOfLeftBorder) { + output_ = reinterpret_cast(crt_.begin_data(320)); + } + + if(output_) { + for(int c = begin; c < end; c++) { + const uint8_t pixels = pixels_[source_address_]; + const uint8_t attributes = attributes_[source_address_]; + ++source_address_; + + const uint16_t colours[] = { + palette[attributes & 0xf], + palette[attributes >> 4], + }; + + output_[0] = colours[(pixels >> 7) & 1]; + output_[1] = colours[(pixels >> 6) & 1]; + output_[2] = colours[(pixels >> 5) & 1]; + output_[3] = colours[(pixels >> 4) & 1]; + output_[4] = colours[(pixels >> 3) & 1]; + output_[5] = colours[(pixels >> 2) & 1]; + output_[6] = colours[(pixels >> 1) & 1]; + output_[7] = colours[(pixels >> 0) & 1]; + output_ += 8; + } + } else { + source_address_ += end - begin; + } + + if(end == EndOfPixels) { + crt_.output_data(40, 320); + output_ = nullptr; + } + }); + clamp(line_begin, line_end, [&](const int begin, const int end) { + crt_.output_level(end - begin, border_); + }); +} + Cycles Video::next_sequence_point() const { - // Pulse the interrupt output for 8 cycles, arbitrarily. TODO: what's the real number? + // Pulse the interrupt output for 8 cycles, arbitrarily. The real number seems to be undocumented, and it + // doesn't actually make much concrete difference in concrete terms because this is fed into edge-detection via + // the PIA. Knowing the real number would only fix the case where the detected transition is switched to + // trailing-edge during the pulse, which probably doesn't happen in any real software. + // + // Would be nice to know the real number though. if(position_ < IRQCycle) return IRQCycle - position_; if(position_ < IRQCycle + IRQLength) return IRQCycle + IRQLength - position_; return FrameLength - position_ + IRQCycle; @@ -146,6 +185,5 @@ bool Video::irq() const { } void Video::set_border_colour(const uint8_t colour) { - // TODO: bits possibly need a swizzle? border_ = palette[colour & 0xf]; } diff --git a/Machines/Thomson/MO5/Video.hpp b/Machines/Thomson/MO5/Video.hpp index 418479e34..bb1d67403 100644 --- a/Machines/Thomson/MO5/Video.hpp +++ b/Machines/Thomson/MO5/Video.hpp @@ -50,6 +50,10 @@ private: uint16_t source_address_ = 0; uint16_t border_ = 0; uint16_t *output_ = nullptr; + + void vsync_line(int, int); + void border_line(int, int); + void pixel_line(int, int); }; } From 49760c64b53393480a677e0380d4eac1bb77223c Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 11:19:21 -0400 Subject: [PATCH 25/40] Pull out magic constant, add commentary. --- Machines/Thomson/MO5/Video.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp index 52832316d..1a909283d 100644 --- a/Machines/Thomson/MO5/Video.cpp +++ b/Machines/Thomson/MO5/Video.cpp @@ -23,12 +23,17 @@ namespace { // // Within a line: ??? Who knows ??? // +// Have rationalised as 4 cycles of sync and the rest as appropriate colours. Via IRQCycle the interrupt can be placed +// arbitrarily within the frame so I think any implementation within a line is valid as long as I place the interrupt +// appropriately. TODO: where is the interrupt placed? +// constexpr int CyclesPerLine = 64; constexpr int TotalPixelLines = 200; constexpr int TotalLines = 312; constexpr int VerticalSyncLine = 256; +constexpr int VerticalSyncLength = 3; constexpr int IRQCycle = 256 * CyclesPerLine; constexpr int IRQLength = 8; @@ -92,7 +97,7 @@ void Video::run_for(const Cycles cycles) { } position_ += in_line; - if(line >= VerticalSyncLine && line < VerticalSyncLine + 3) { + if(line >= VerticalSyncLine && line < VerticalSyncLine + VerticalSyncLength) { vsync_line(start_column, end_column); } else if(line >= TotalPixelLines) { border_line(start_column, end_column); From 8163e70d0f1edbee85d1eeb08a3979d3c5cbb402 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 12:38:53 -0400 Subject: [PATCH 26/40] Factor out rangey stuff. --- Machines/Thomson/MO5/MO5.cpp | 8 +- Machines/Thomson/MO5/Video.cpp | 94 +++++-------------- Machines/Thomson/MO5/Video.hpp | 29 +++++- Numeric/SegmentCounter.hpp | 59 ++++++++++++ .../Clock Signal.xcodeproj/project.pbxproj | 2 + 5 files changed, 122 insertions(+), 70 deletions(-) create mode 100644 Numeric/SegmentCounter.hpp diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 8f4b36260..f94001c06 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -151,7 +151,13 @@ private: void output(const uint8_t value) { if constexpr (port == Motorola::MC6821::Port::A) { machine_.page_lower(value & 1); - machine_.video_->set_border_colour((value >> 1) & 0xf); + machine_.video_->set_border_colour(6); +// machine_.video_->set_border_colour((value >> 1) & 0xf); + printf("Border: %d\n", (value >> 1) & 0xf); + + if(((value >> 1) & 0xf) == 9) { + printf(""); + } // Port A outputs: // b0 = lower 8kb RAM paging; diff --git a/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp index 1a909283d..071abfd09 100644 --- a/Machines/Thomson/MO5/Video.cpp +++ b/Machines/Thomson/MO5/Video.cpp @@ -14,32 +14,6 @@ using namespace Thomson::MO5; namespace { -// Video timing, as far as auto-translate lets me figure it out: -// -// 64 cycles/line; -// 56 lines post signalled vsync, then 200 of video, then 56 more, for 312 total. -// -// Start of vsync is connected to CPU IRQ. -// -// Within a line: ??? Who knows ??? -// -// Have rationalised as 4 cycles of sync and the rest as appropriate colours. Via IRQCycle the interrupt can be placed -// arbitrarily within the frame so I think any implementation within a line is valid as long as I place the interrupt -// appropriately. TODO: where is the interrupt placed? -// - -constexpr int CyclesPerLine = 64; - -constexpr int TotalPixelLines = 200; -constexpr int TotalLines = 312; -constexpr int VerticalSyncLine = 256; -constexpr int VerticalSyncLength = 3; - -constexpr int IRQCycle = 256 * CyclesPerLine; -constexpr int IRQLength = 8; - -constexpr int FrameLength = TotalLines * CyclesPerLine; - constexpr uint16_t rgb(const uint16_t code) { #ifdef TARGET_RT_BIG_ENDIAN return code; @@ -54,16 +28,6 @@ constexpr uint16_t palette[] = { rgb(0xaaa), rgb(0xfaa), rgb(0xafa), rgb(0xffa), rgb(0x5af), rgb(0xfaf), rgb(0xaff), rgb(0xfa5), }; -/// Calculates the overlap, if any, between [begin, end] and [RangeBegin, RangeEnd] and calls FuncT with a begin and end if the range is non-zero. -template -void clamp(const int begin, const int end, const FuncT &&function) { - const int range_begin = std::max(begin, RangeBegin); - const int range_end = std::min(end, RangeEnd); - if(range_end > range_begin) { - function(range_begin, range_end); - } -} - } Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) : @@ -83,28 +47,21 @@ Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) : void Video::run_for(const Cycles cycles) { int to_run = cycles.as(); - while(to_run) { - const int line = position_ / CyclesPerLine; - const int start_column = position_ % CyclesPerLine; - - const int in_line = std::min(CyclesPerLine - start_column, to_run); - const int end_column = std::min(CyclesPerLine, start_column + in_line); - to_run -= in_line; - - if(position_ == FrameLength) { - position_ = 0; + position_.advance( + cycles.as(), + [&] (const int line, const int begin, const int end) { + if(line >= VerticalSyncLine && line < VerticalSyncLine + VerticalSyncLength) { + vsync_line(begin, end); + } else if(line >= TotalPixelLines) { + border_line(begin, end); + } else { + pixel_line(begin, end); + } + }, + [&] { source_address_ = 0; } - position_ += in_line; - - if(line >= VerticalSyncLine && line < VerticalSyncLine + VerticalSyncLength) { - vsync_line(start_column, end_column); - } else if(line >= TotalPixelLines) { - border_line(start_column, end_column); - } else { - pixel_line(start_column, end_column); - } - } + ); } void Video::vsync_line(const int line_begin, const int line_end) { @@ -113,10 +70,10 @@ void Video::vsync_line(const int line_begin, const int line_end) { void Video::border_line(const int line_begin, const int line_end) { static constexpr int EndOfSync = 4; - clamp<0, EndOfSync>(line_begin, line_end, [&](const int begin, const int end) { + Numeric::clamp<0, EndOfSync>(line_begin, line_end, [&](const int begin, const int end) { crt_.output_sync(end - begin); }); - clamp(line_begin, line_end, [&](const int begin, const int end) { + Numeric::clamp(line_begin, line_end, [&](const int begin, const int end) { crt_.output_level(end - begin, border_); }); } @@ -126,16 +83,17 @@ void Video::pixel_line(const int line_begin, const int line_end) { static constexpr int EndOfSync = 4; static constexpr int EndOfLeftBorder = 14; static constexpr int EndOfPixels = 54; + static constexpr int PixelsPerLine = 320; - clamp<0, EndOfSync>(line_begin, line_end, [&](const int begin, const int end) { + Numeric::clamp<0, EndOfSync>(line_begin, line_end, [&](const int begin, const int end) { crt_.output_sync(end - begin); }); - clamp(line_begin, line_end, [&](const int begin, const int end) { + Numeric::clamp(line_begin, line_end, [&](const int begin, const int end) { crt_.output_level(end - begin, border_); }); - clamp(line_begin, line_end, [&](const int begin, const int end) { + Numeric::clamp(line_begin, line_end, [&](const int begin, const int end) { if(begin == EndOfLeftBorder) { - output_ = reinterpret_cast(crt_.begin_data(320)); + output_ = reinterpret_cast(crt_.begin_data(PixelsPerLine)); } if(output_) { @@ -164,11 +122,11 @@ void Video::pixel_line(const int line_begin, const int line_end) { } if(end == EndOfPixels) { - crt_.output_data(40, 320); + crt_.output_data(EndOfPixels - EndOfLeftBorder, PixelsPerLine); output_ = nullptr; } }); - clamp(line_begin, line_end, [&](const int begin, const int end) { + Numeric::clamp(line_begin, line_end, [&](const int begin, const int end) { crt_.output_level(end - begin, border_); }); } @@ -180,13 +138,13 @@ Cycles Video::next_sequence_point() const { // trailing-edge during the pulse, which probably doesn't happen in any real software. // // Would be nice to know the real number though. - if(position_ < IRQCycle) return IRQCycle - position_; - if(position_ < IRQCycle + IRQLength) return IRQCycle + IRQLength - position_; - return FrameLength - position_ + IRQCycle; + if(position_.absolute() < IRQCycle) return IRQCycle - position_.absolute(); + if(position_.absolute() < IRQCycle + IRQLength) return IRQCycle + IRQLength - position_.absolute(); + return FrameLength - position_.absolute() + IRQCycle; } bool Video::irq() const { - return position_ >= IRQCycle && position_ < (IRQCycle + IRQLength); + return position_.absolute() >= IRQCycle && position_.absolute() < (IRQCycle + IRQLength); } void Video::set_border_colour(const uint8_t colour) { diff --git a/Machines/Thomson/MO5/Video.hpp b/Machines/Thomson/MO5/Video.hpp index bb1d67403..5d770bd81 100644 --- a/Machines/Thomson/MO5/Video.hpp +++ b/Machines/Thomson/MO5/Video.hpp @@ -10,6 +10,7 @@ #define Video_hpp #include "ClockReceiver/ClockReceiver.hpp" +#include "Numeric/SegmentCounter.hpp" #include "Outputs/CRT/CRT.hpp" namespace Thomson::MO5 { @@ -45,7 +46,6 @@ private: const uint8_t *pixels_ = nullptr; const uint8_t *attributes_ = nullptr; Outputs::CRT::CRT crt_; - int position_ = 0; uint16_t source_address_ = 0; uint16_t border_ = 0; @@ -54,6 +54,33 @@ private: void vsync_line(int, int); void border_line(int, int); void pixel_line(int, int); + + // Video timing, as far as auto-translate lets me figure it out: + // + // 64 cycles/line; + // 56 lines post signalled vsync, then 200 of video, then 56 more, for 312 total. + // + // Start of vsync is connected to CPU IRQ. + // + // Within a line: ??? Who knows ??? + // + // Have rationalised as 4 cycles of sync and the rest as appropriate colours. Via IRQCycle the interrupt can be placed + // arbitrarily within the frame so I think any implementation within a line is valid as long as I place the interrupt + // appropriately. TODO: where is the interrupt placed? + // + static constexpr int CyclesPerLine = 64; + static constexpr int TotalLines = 312; + + static constexpr int TotalPixelLines = 200; + static constexpr int VerticalSyncLine = 256; + static constexpr int VerticalSyncLength = 3; + + static constexpr int IRQCycle = 256 * CyclesPerLine; + static constexpr int IRQLength = 8; + + static constexpr int FrameLength = TotalLines * CyclesPerLine; + + Numeric::DividingAccumulator position_; }; } diff --git a/Numeric/SegmentCounter.hpp b/Numeric/SegmentCounter.hpp new file mode 100644 index 000000000..f21007329 --- /dev/null +++ b/Numeric/SegmentCounter.hpp @@ -0,0 +1,59 @@ +// +// SegmentCounter.hpp +// Clock Signal +// +// Created by Thomas Harte on 22/03/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +namespace Numeric { + +/*! + A counter that divides its time into n segments, each of a fixed length, then repeats. + + Lambdas receive: + (1) calls to advance through segment and regions within them; and + (2) optionally, an announcement each time the counter wraps around. +*/ +template +struct DividingAccumulator { + template + void advance(int count, const AdvanceFuncT &&advance, const ResetFuncT &&reset = []{}) { + while(count > 0) { + const int segment = position_ / SegmentLength; + const int begin = position_ % SegmentLength; + + const int length = std::min(SegmentLength - begin, count); + const int end = std::min(SegmentLength, begin + length); + count -= length; + + advance(segment, begin, end); + + position_ += length; + if(position_ == SegmentLength * Segments) { + position_ = 0; + reset(); + } + } + } + + /// @returns Position within the range [0, SegmentLength*Segments) + int absolute() const { + return position_; + } + +private: + int position_ = 0; +}; + +/// Calculates the overlap, if any, between [begin, end] and [RangeBegin, RangeEnd] and calls FuncT with a begin and end if the range is non-zero. +template +void clamp(const int begin, const int end, const FuncT &&function) { + const int range_begin = std::max(begin, RangeBegin); + const int range_end = std::min(end, RangeEnd); + if(range_end > range_begin) { + function(range_begin, range_end); + } +} + +} diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index a48525c23..76981edf9 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -2381,6 +2381,7 @@ 4BBB77DC2867EBB300D335A1 /* IIgs Memory Map */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "IIgs Memory Map"; sourceTree = ""; }; 4BBC7DBF2F6D94E500E71794 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = ""; }; 4BBC7DC02F6D94E500E71794 /* Video.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Video.cpp; sourceTree = ""; }; + 4BBC7DC42F70507B00E71794 /* SegmentCounter.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SegmentCounter.hpp; sourceTree = ""; }; 4BBC951C1F368D83008F4C34 /* i8272.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = i8272.cpp; sourceTree = ""; }; 4BBC951D1F368D83008F4C34 /* i8272.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = i8272.hpp; sourceTree = ""; }; 4BBF49AE1ED2880200AB3669 /* FUSETests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FUSETests.swift; sourceTree = ""; }; @@ -3886,6 +3887,7 @@ 4BFEA2F12682A90200EBF94C /* Sizes.hpp */, 4BD9713A2BFD7E7100C907AA /* StringSimilarity.hpp */, 4B0329202D0A78DC00C51EB5 /* UpperBound.hpp */, + 4BBC7DC42F70507B00E71794 /* SegmentCounter.hpp */, ); name = Numeric; path = ../../Numeric; From 2f7ab1419849b809df4989f92f8fabf477303dd3 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 12:39:52 -0400 Subject: [PATCH 27/40] Add header for min/max. --- Numeric/SegmentCounter.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Numeric/SegmentCounter.hpp b/Numeric/SegmentCounter.hpp index f21007329..b10640fae 100644 --- a/Numeric/SegmentCounter.hpp +++ b/Numeric/SegmentCounter.hpp @@ -6,6 +6,8 @@ // Copyright © 2026 Thomas Harte. All rights reserved. // +#include + namespace Numeric { /*! From 36873ec48d3cdf4f6d246fb72020aaf94af24ebf Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 12:43:33 -0400 Subject: [PATCH 28/40] Further simplify. --- Machines/Thomson/MO5/MO5.cpp | 8 +------- Numeric/SegmentCounter.hpp | 8 +++----- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index f94001c06..8f4b36260 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -151,13 +151,7 @@ private: void output(const uint8_t value) { if constexpr (port == Motorola::MC6821::Port::A) { machine_.page_lower(value & 1); - machine_.video_->set_border_colour(6); -// machine_.video_->set_border_colour((value >> 1) & 0xf); - printf("Border: %d\n", (value >> 1) & 0xf); - - if(((value >> 1) & 0xf) == 9) { - printf(""); - } + machine_.video_->set_border_colour((value >> 1) & 0xf); // Port A outputs: // b0 = lower 8kb RAM paging; diff --git a/Numeric/SegmentCounter.hpp b/Numeric/SegmentCounter.hpp index b10640fae..1e85e09e0 100644 --- a/Numeric/SegmentCounter.hpp +++ b/Numeric/SegmentCounter.hpp @@ -24,13 +24,11 @@ struct DividingAccumulator { while(count > 0) { const int segment = position_ / SegmentLength; const int begin = position_ % SegmentLength; - const int length = std::min(SegmentLength - begin, count); - const int end = std::min(SegmentLength, begin + length); + + advance(segment, begin, begin + length); + count -= length; - - advance(segment, begin, end); - position_ += length; if(position_ == SegmentLength * Segments) { position_ = 0; From 3ab1319fb4ee60ac5dbb53fbcf8631393b25b4b3 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 12:45:28 -0400 Subject: [PATCH 29/40] Eliminate dead state. --- Machines/Thomson/MO5/Video.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp index 071abfd09..b51b0046f 100644 --- a/Machines/Thomson/MO5/Video.cpp +++ b/Machines/Thomson/MO5/Video.cpp @@ -45,8 +45,6 @@ Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) : } void Video::run_for(const Cycles cycles) { - int to_run = cycles.as(); - position_.advance( cycles.as(), [&] (const int line, const int begin, const int end) { From 127a0730a6195a49ca058085cc7b9913af94763b Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 13:07:07 -0400 Subject: [PATCH 30/40] Log unhandled IO accesses. --- Machines/Thomson/MO5/MO5.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 8f4b36260..ba3334aab 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -81,6 +81,14 @@ struct ConcreteMachine: case 0xa7c1: access<0xa7c1, read_write>(system_pia_, value); break; case 0xa7c2: access<0xa7c2, read_write>(system_pia_, value); break; case 0xa7c3: access<0xa7c3, read_write>(system_pia_, value); break; + default: + if constexpr (CPU::M6809::is_read(read_write)) { + value = 0xff; + printf("Unhandled read at %04x\n", +address); + } else { + printf("Unhandled write: %02x -> %04x\n", +value, +address); + } + break; } } else { if constexpr (CPU::M6809::is_read(read_write)) { From f627ad2bf814ab5a3c9f9422a5404c315a6e7fb0 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 13:11:16 -0400 Subject: [PATCH 31/40] Fuzz memory, establishing that there is a jump into an uninitisalised area. --- Machines/Thomson/MO5/MO5.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index ba3334aab..ae2ab1348 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -11,11 +11,11 @@ #include "Video.hpp" #include "Machines/MachineTypes.hpp" +#include "Machines/Utility/MemoryFuzzer.hpp" #include "Processors/6809/6809.hpp" #include "Components/6821/6821.hpp" #include "ClockReceiver/JustInTime.hpp" - using namespace Thomson::MO5; namespace { @@ -42,6 +42,7 @@ struct ConcreteMachine: const auto &rom = roms.find(ROM::Name::ThomasonMO5v11)->second; std::copy_n(rom.begin(), rom.size(), rom_.begin()); + Memory::Fuzz(ram_); system_pia_.refresh(); } From 94552d7deed2808183c61b070797a3a3a64379e6 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 18:14:37 -0400 Subject: [PATCH 32/40] Lock in exception vector. --- Processors/6809/6809.hpp | 82 ++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/Processors/6809/6809.hpp b/Processors/6809/6809.hpp index 50aa4bf7c..d5a492e3f 100644 --- a/Processors/6809/6809.hpp +++ b/Processors/6809/6809.hpp @@ -79,6 +79,16 @@ enum class LIC { Inactive, }; +enum Vector: uint16_t { + SWI3 = 0xfff2, + SWI2 = 0xfff4, + FIRQ = 0xfff6, + IRQ = 0xfff8, + SWI = 0xfffa, + NMI = 0xfffc, + Reset = 0xfffe, +}; + // Missing outputs: // // AVMA — advanced VMA, i.e. bus will be accessed in the next cycle. @@ -355,17 +365,17 @@ struct Processor { resume_point_ = ResumePoint::ResetSpin; return; } - addressed_internal_cycle(Address::Fixed<0xfffe>()) + addressed_internal_cycle(Address::Fixed()) if(exceptions_ & (Exception::Halt | Exception::DMABusReq | Exception::Reset)) { goto reset_spin; } - addressed_internal_cycle(Address::Fixed<0xfffe>()) - addressed_internal_cycle(Address::Fixed<0xfffe>()) - addressed_internal_cycle(Address::Fixed<0xfffe>()) + addressed_internal_cycle(Address::Fixed()) + addressed_internal_cycle(Address::Fixed()) + addressed_internal_cycle(Address::Fixed()) - read(BusState::InterruptOrResetAcknowledge, Address::Fixed<0xfffe>(), registers_.pc.halves.high); - read(BusState::InterruptOrResetAcknowledge, Address::Fixed<0xffff>(), registers_.pc.halves.low); + read(BusState::InterruptOrResetAcknowledge, Address::Fixed(), registers_.pc.halves.high); + read(BusState::InterruptOrResetAcknowledge, Address::Fixed(), registers_.pc.halves.low); internal_cycle(); @@ -384,6 +394,7 @@ struct Processor { -- registers_.reg(); write(BusState::Normal, Literal(registers_.reg()), registers_.reg()); + address_.full = Vector::FIRQ; goto interrupt_dispatch; // MARK: - Fetch/decode. @@ -408,18 +419,23 @@ struct Processor { } if(exceptions_ & (Exception::PowerOnReset | Exception::Reset)) { + address_ = Vector::Reset; goto reset; } if(exceptions_ & Exception::NMI) { + address_ = Vector::NMI; + exceptions_ &= ~Exception::NMI; goto nmi_irq; } if(exceptions_ & Exception::FIRQ && !registers_.cc.get()) { + address_ = Vector::FIRQ; goto firq; } if(exceptions_ & Exception::IRQ && !registers_.cc.get()) { + address_ = Vector::IRQ; goto nmi_irq; } } @@ -569,10 +585,20 @@ struct Processor { case Operation::RTI: goto rti; case Operation::SWI: + address_ = Vector::SWI; + goto swi_reset; + case Operation::SWI2: + address_ = Vector::SWI2; + goto swi_reset; + case Operation::SWI3: + address_.full = Vector::SWI3; + goto swi_reset; + case Operation::RESET: - goto swi_reset; + address_.full = Vector::Reset; + goto swi_reset; default: __builtin_unreachable(); } @@ -766,11 +792,18 @@ struct Processor { return; } - if( - exceptions_ & Exception::NMI || - (exceptions_ & Exception::FIRQ && !registers_.cc.get()) || - (exceptions_ & Exception::IRQ && !registers_.cc.get()) - ) { + if(exceptions_ & Exception::NMI) { + address_ = Vector::NMI; + goto interrupt_dispatch; + } + + if(exceptions_ & Exception::FIRQ && !registers_.cc.get()) { + address_ = Vector::FIRQ; + goto interrupt_dispatch; + } + + if(exceptions_ & Exception::IRQ && !registers_.cc.get()) { + address_ = Vector::IRQ; goto interrupt_dispatch; } @@ -841,31 +874,6 @@ struct Processor { } interrupt_dispatch: - address_.full = [&]() -> uint16_t { - if(exceptions_ & Exception::NMI) { - exceptions_ &= ~Exception::NMI; - return 0xfffc; - } - - if(exceptions_ & Exception::FIRQ && !registers_.cc.get()) { - return 0xfff6; - } - - if(exceptions_ & Exception::IRQ && !registers_.cc.get()) { - return 0xfff8; - } - - switch(operation_.operation) { - case Operation::SWI: return 0xfffa; - case Operation::SWI2: return 0xfff4; - case Operation::SWI3: return 0xfff2; - case Operation::RESET: return 0xfffe; - - // TODO: below is incorrect assuming that spurious interrupts can occur. - default: __builtin_unreachable(); - } - } (); - if(operation_.operation != Operation::SWI2 && operation_.operation != Operation::SWI3) { registers_.cc.set(true); registers_.cc.set(true); From ec6b5fd66d46a7001c450b94bd19889c100552c1 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 18:28:05 -0400 Subject: [PATCH 33/40] Prove that keyboard scanning isn't progressing. --- Machines/Thomson/MO5/MO5.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index ae2ab1348..d56b051e3 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -150,7 +150,13 @@ private: if constexpr (port == Motorola::MC6821::Port::B) { // Port B inputs: // b7: status of key at that position. - return 0xff; +// printf("Key read\n"); + if(key_ == 27) { + return 0x80; + } else { + return 0x00; + } +// return (key_ == 23) ? 0x80 : 0x00; } __builtin_unreachable(); @@ -169,7 +175,8 @@ private: } if constexpr (port == Motorola::MC6821::Port::B) { -// printf("Keyboard scan: %d\n", (value >> 1) & 0x3f); + key_ = (value >> 1) & 0b111'111; +// printf("Key: %d\n", key_); // Port B outputs: // b0 = 1-bit sound output; // b1–3 = keyboard column; @@ -203,7 +210,7 @@ private: private: ConcreteMachine &machine_; - + uint8_t key_ = 0; }; SystemPIAPortHandler system_pia_port_handler_; Motorola::MC6821::MC6821 system_pia_; From 1be89a3dd5aa86c3f535a38d58d54e157e095edf Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 19:48:56 -0400 Subject: [PATCH 34/40] Fix 6821 read decoding; keyboard is now being scanned. --- Components/6821/6821.hpp | 4 ++-- Machines/Thomson/MO5/MO5.cpp | 11 ++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index f4a7f9fa5..fa1b7c010 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -55,8 +55,8 @@ public: template uint8_t read() { - static constexpr int port = (address >> 1) & 1; - if constexpr(address & 1) { + static constexpr int port = bool(address & RS1Mask); + if constexpr(address & RS0Mask) { const auto result = ports_[port].control; ports_[port].clear_irq(); update_interrupts(); diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index d56b051e3..70bb1c62c 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -149,14 +149,8 @@ private: if constexpr (port == Motorola::MC6821::Port::B) { // Port B inputs: - // b7: status of key at that position. -// printf("Key read\n"); - if(key_ == 27) { - return 0x80; - } else { - return 0x00; - } -// return (key_ == 23) ? 0x80 : 0x00; + // b7: status of key at that position. [0 = pressed?] + return 0x80; } __builtin_unreachable(); @@ -176,7 +170,6 @@ private: if constexpr (port == Motorola::MC6821::Port::B) { key_ = (value >> 1) & 0b111'111; -// printf("Key: %d\n", key_); // Port B outputs: // b0 = 1-bit sound output; // b1–3 = keyboard column; From ae1b13d1f07fc35c8856d255a9d18aaa030188bd Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 20:09:22 -0400 Subject: [PATCH 35/40] Add key state tracking. --- Machines/Thomson/MO5/Keyboard.hpp | 24 +++++++++++++++ Machines/Thomson/MO5/MO5.cpp | 29 ++++++++++++++++++- .../Clock Signal.xcodeproj/project.pbxproj | 2 ++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 Machines/Thomson/MO5/Keyboard.hpp diff --git a/Machines/Thomson/MO5/Keyboard.hpp b/Machines/Thomson/MO5/Keyboard.hpp new file mode 100644 index 000000000..17c159fbe --- /dev/null +++ b/Machines/Thomson/MO5/Keyboard.hpp @@ -0,0 +1,24 @@ +// +// Keyboard.hpp +// Clock Signal +// +// Created by Thomas Harte on 22/03/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "Machines/KeyboardMachine.hpp" + +namespace MO5 { + +enum Key: uint16_t { +}; + +struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper { + uint16_t mapped_key_for_key(const Inputs::Keyboard::Key key) const { + return 27; + } +}; + +} diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index 70bb1c62c..f49df7797 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -16,11 +16,14 @@ #include "Components/6821/6821.hpp" #include "ClockReceiver/JustInTime.hpp" +#include "Keyboard.hpp" + using namespace Thomson::MO5; namespace { struct ConcreteMachine: + public MachineTypes::MappedKeyboardMachine, public MachineTypes::TimedMachine, public MachineTypes::ScanProducer, public Machine @@ -150,7 +153,7 @@ private: if constexpr (port == Motorola::MC6821::Port::B) { // Port B inputs: // b7: status of key at that position. [0 = pressed?] - return 0x80; + return key_states_[key_] ? 0x00 : 0x80; } __builtin_unreachable(); @@ -201,9 +204,18 @@ private: // CB1: 50Hz interrupt // CB2: genlock enable, maybe? Video "encrustation". + void clear_all_keys() { + std::fill(std::begin(key_states_), std::end(key_states_), false); + } + + void set_key_state(const uint16_t key, const bool is_pressed) { + key_states_[key] = is_pressed; + } + private: ConcreteMachine &machine_; uint8_t key_ = 0; + bool key_states_[0x40]{}; }; SystemPIAPortHandler system_pia_port_handler_; Motorola::MC6821::MC6821 system_pia_; @@ -230,6 +242,21 @@ private: video_.flush(); } } + + // MARK: - MappedKeyboardMachine. + + MO5::KeyboardMapper keyboard_mapper_; + KeyboardMapper *get_keyboard_mapper() final { + return &keyboard_mapper_; + } + + void set_key_state(const uint16_t key, const bool is_pressed) final { + system_pia_port_handler_.set_key_state(key, is_pressed); + } + + void clear_all_keys() final { + system_pia_port_handler_.clear_all_keys(); + } }; } diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 76981edf9..c9e582838 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -2382,6 +2382,7 @@ 4BBC7DBF2F6D94E500E71794 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = ""; }; 4BBC7DC02F6D94E500E71794 /* Video.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Video.cpp; sourceTree = ""; }; 4BBC7DC42F70507B00E71794 /* SegmentCounter.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SegmentCounter.hpp; sourceTree = ""; }; + 4BBC7DC52F70BAFB00E71794 /* Keyboard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = ""; }; 4BBC951C1F368D83008F4C34 /* i8272.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = i8272.cpp; sourceTree = ""; }; 4BBC951D1F368D83008F4C34 /* i8272.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = i8272.hpp; sourceTree = ""; }; 4BBF49AE1ED2880200AB3669 /* FUSETests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FUSETests.swift; sourceTree = ""; }; @@ -5342,6 +5343,7 @@ children = ( 4BCA6FA92F589C0C000B9537 /* MO5.cpp */, 4BBC7DC02F6D94E500E71794 /* Video.cpp */, + 4BBC7DC52F70BAFB00E71794 /* Keyboard.hpp */, 4BCA6FA82F589C0C000B9537 /* MO5.hpp */, 4BBC7DBF2F6D94E500E71794 /* Video.hpp */, ); From 8a2116ce9fbe21d5300446b4f3d5974eabe80329 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 20:26:31 -0400 Subject: [PATCH 36/40] Start working out keycodes, empirically. --- Machines/Thomson/MO5/Keyboard.hpp | 46 ++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/Machines/Thomson/MO5/Keyboard.hpp b/Machines/Thomson/MO5/Keyboard.hpp index 17c159fbe..e9d984c57 100644 --- a/Machines/Thomson/MO5/Keyboard.hpp +++ b/Machines/Thomson/MO5/Keyboard.hpp @@ -17,7 +17,51 @@ enum Key: uint16_t { struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper { uint16_t mapped_key_for_key(const Inputs::Keyboard::Key key) const { - return 27; + switch(key) { + default: return 1; + + using enum Inputs::Keyboard::Key; + case k4: return 23; // 4 + case N: return 0; + case J: return 2; + case H: return 3; + case U: return 4; + case Y: return 5; + case k7: return 6; + case k6: return 7; + case Comma: return 9; + case K: return 10; + case G: return 11; + case I: return 12; + case T: return 13; + case k8: return 14; + case k5: return 15; + case FullStop: return 16; +// case k2: return 17; // [go to top left?] + case L: return 18; + + case F: return 19; + case O: return 20; + case R: return 21; + case k9: return 22; +// case k4: return 23; [duplicate?] +// case k3: return 24; // @ + + case Space: return 25; // [space] + case M: return 26; // M + case D: return 27; // D + case P: return 28; // P + case E: return 29; // E + + + case Z: return 30; // [space] + case X: return 31; // M + case C: return 32; // D + case V: return 33; // P + case B: return 34; // E +// case M: return 35; // 0 + } + } }; From 36c460838697c8c6626552379fe581b51bf77c48 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 22:14:07 -0400 Subject: [PATCH 37/40] Attempt a full keyboard mapping. --- Machines/Thomson/MO5/Keyboard.hpp | 83 +++++++++++++++++-------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/Machines/Thomson/MO5/Keyboard.hpp b/Machines/Thomson/MO5/Keyboard.hpp index e9d984c57..5e73b6e82 100644 --- a/Machines/Thomson/MO5/Keyboard.hpp +++ b/Machines/Thomson/MO5/Keyboard.hpp @@ -18,50 +18,59 @@ enum Key: uint16_t { struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper { uint16_t mapped_key_for_key(const Inputs::Keyboard::Key key) const { switch(key) { - default: return 1; + default: return MachineTypes::MappedKeyboardMachine::KeyNotMapped; using enum Inputs::Keyboard::Key; - case k4: return 23; // 4 - case N: return 0; - case J: return 2; - case H: return 3; - case U: return 4; - case Y: return 5; - case k7: return 6; - case k6: return 7; - case Comma: return 9; - case K: return 10; - case G: return 11; - case I: return 12; - case T: return 13; - case k8: return 14; - case k5: return 15; - case FullStop: return 16; -// case k2: return 17; // [go to top left?] - case L: return 18; + case k0: return 0x1e; case k1: return 0x2f; + case k2: return 0x27; case k3: return 0x1f; + case k4: return 0x17; case k5: return 0x0f; + case k6: return 0x07; case k7: return 0x06; + case k8: return 0x0e; case k9: return 0x16; - case F: return 19; - case O: return 20; - case R: return 21; - case k9: return 22; -// case k4: return 23; [duplicate?] -// case k3: return 24; // @ + case Q: return 0x2d; case W: return 0x25; + case E: return 0x1d; case R: return 0x15; + case T: return 0x0d; case Y: return 0x05; + case U: return 0x04; case I: return 0x0c; + case O: return 0x14; case P: return 0x1c; + case A: return 0x2b; case S: return 0x23; + case D: return 0x1b; case F: return 0x13; + case G: return 0x0b; case H: return 0x03; + case J: return 0x02; case K: return 0x0a; + case L: return 0x12; case Z: return 0x30; + case X: return 0x28; case C: return 0x32; + case V: return 0x2a; case B: return 0x22; + case N: return 0x00; case M: return 0x1a; - case Space: return 25; // [space] - case M: return 26; // M - case D: return 27; // D - case P: return 28; // P - case E: return 29; // E + case FullStop: return 0x10; + case Hyphen: return 0x26; + case Equals: return 0x2e; // + + case OpenSquareBracket: return 0x18; // @ + case Semicolon: return 0x1a; // M - case Z: return 30; // [space] - case X: return 31; // M - case C: return 32; // D - case V: return 33; // P - case B: return 34; // E -// case M: return 35; // 0 + case CloseSquareBracket: + case Quote: return 0x2c; // Asterisk. + + case Space: return 0x20; + case Enter: return 0x34; + case Backspace: return 0x36; + case Tab: return 0x37; + + case Up: return 0x31; case Down: return 0x21; + case Left: return 0x29; case Right: return 0x19; + + case LeftShift: + case RightShift: return 0x38; + case LeftControl: + case RightControl: return 0x35; + case LeftOption: + case RightOption: return 0x33; + case LeftMeta: + case RightMeta: return 0x39; + + // TODO: INS and EFF from the lower right, which I think are 0x09 and 0x11? + // And where the heck is forward slash? } - } }; From dd1c2a6c0139b1eae4c0f1b43b2d9730ef1efbcd Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 22:17:49 -0400 Subject: [PATCH 38/40] Add comma. --- Machines/Thomson/MO5/Keyboard.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Machines/Thomson/MO5/Keyboard.hpp b/Machines/Thomson/MO5/Keyboard.hpp index 5e73b6e82..84d88248f 100644 --- a/Machines/Thomson/MO5/Keyboard.hpp +++ b/Machines/Thomson/MO5/Keyboard.hpp @@ -42,6 +42,7 @@ struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMappe case N: return 0x00; case M: return 0x1a; case FullStop: return 0x10; + case Comma: return 0x08; case Hyphen: return 0x26; case Equals: return 0x2e; // + From 2564b2b8356f2c6c899bd715fc45eaf401477256 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 22:43:37 -0400 Subject: [PATCH 39/40] Add audio. --- Machines/Thomson/MO5/MO5.cpp | 44 ++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index f49df7797..8aa9726c4 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -16,6 +16,10 @@ #include "Components/6821/6821.hpp" #include "ClockReceiver/JustInTime.hpp" +#include "Components/AudioToggle/AudioToggle.hpp" +#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp" +#include "Outputs/Speaker/Implementation/BufferSource.hpp" + #include "Keyboard.hpp" using namespace Thomson::MO5; @@ -23,6 +27,7 @@ using namespace Thomson::MO5; namespace { struct ConcreteMachine: + public MachineTypes::AudioProducer, public MachineTypes::MappedKeyboardMachine, public MachineTypes::TimedMachine, public MachineTypes::ScanProducer, @@ -32,9 +37,12 @@ struct ConcreteMachine: m6809_(*this), system_pia_port_handler_(*this), system_pia_(system_pia_port_handler_), - video_(video_page(true), video_page(false)) + video_(video_page(true), video_page(false)), + audio_toggle_(audio_queue_), + speaker_(audio_toggle_) { set_clock_rate(1'000'000); + speaker_.set_input_rate(1'000'000.0f); const auto request = ROM::Request(ROM::Name::ThomasonMO5v11); auto roms = rom_fetcher(request); @@ -49,6 +57,10 @@ struct ConcreteMachine: system_pia_.refresh(); } + ~ConcreteMachine() { + audio_queue_.lock_flush(); + } + template < int address, CPU::M6809::ReadWrite read_write, @@ -75,6 +87,7 @@ struct ConcreteMachine: if(video_ += m6809_.duration(bus_phase)) { system_pia_.set(video_.last_valid()->irq()); } + time_since_audio_update_ += m6809_.duration(bus_phase); if constexpr (read_write == CPU::M6809::ReadWrite::NoData) { return Cycles(0); @@ -162,21 +175,22 @@ private: template void output(const uint8_t value) { if constexpr (port == Motorola::MC6821::Port::A) { - machine_.page_lower(value & 1); - machine_.video_->set_border_colour((value >> 1) & 0xf); - // Port A outputs: // b0 = lower 8kb RAM paging; // b1–4: border colour; // b6: tape output + machine_.page_lower(value & 1); + machine_.video_->set_border_colour((value >> 1) & 0xf); } if constexpr (port == Motorola::MC6821::Port::B) { - key_ = (value >> 1) & 0b111'111; // Port B outputs: // b0 = 1-bit sound output; // b1–3 = keyboard column; // b4–6: keyboard line; + key_ = (value >> 1) & 0b111'111; + machine_.update_audio(); + machine_.audio_toggle_.set_output(value & 1); } } @@ -222,6 +236,22 @@ private: JustInTimeActor video_; + // MARK: - AudioProducer. + + Concurrency::AsyncTaskQueue audio_queue_; + Audio::Toggle audio_toggle_; + Outputs::Speaker::PullLowpass speaker_; + + Cycles time_since_audio_update_; + void update_audio() { + const auto cycles = time_since_audio_update_.flush(); + speaker_.run_for(audio_queue_, cycles); + } + + Outputs::Speaker::Speaker *get_speaker() override { + return &speaker_; + } + // MARK: - ScanProducer. void set_scan_target(Outputs::Display::ScanTarget *const target) final { @@ -241,6 +271,10 @@ private: if(outputs & Output::Video) { video_.flush(); } + if(outputs & Output::Audio) { + update_audio(); + audio_queue_.perform(); + } } // MARK: - MappedKeyboardMachine. From 66af2c00a8ebd443a73ce38529553aa1f1782a48 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 22 Mar 2026 23:06:19 -0400 Subject: [PATCH 40/40] Add TODO. --- Components/6821/6821.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp index fa1b7c010..ae175f626 100644 --- a/Components/6821/6821.hpp +++ b/Components/6821/6821.hpp @@ -26,6 +26,8 @@ enum class Control { CB1, CB2, }; +// TODO: CB2/CA2 output. + template class MC6821 { public: