diff --git a/Components/6821/6821.hpp b/Components/6821/6821.hpp new file mode 100644 index 000000000..ae175f626 --- /dev/null +++ b/Components/6821/6821.hpp @@ -0,0 +1,187 @@ +// +// 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 + +namespace Motorola::MC6821 { + +enum class Port { + A = 0, + B = 1, +}; + +enum class IRQ { + A, + B, +}; + +enum class Control { + CA1, CA2, + CB1, CB2, +}; + +// TODO: CB2/CA2 output. + +template +class MC6821 { +public: + MC6821(PortHandlerT &port_handler) : port_handler_(port_handler) {} + + template + void write(const uint8_t value) { + static constexpr int port = bool(address & RS1Mask); + + if constexpr (address & RS0Mask) { + 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; + } else { + ports_[port].direction = value; + } + + const uint8_t output = ports_[port].output(); + if(output != ports_[port].previous_output) { + ports_[port].previous_output = output; + port_handler_.template output(output); + } + } + } + + template + uint8_t read() { + static constexpr int port = bool(address & RS1Mask); + if constexpr(address & RS0Mask) { + const auto result = ports_[port].control; + ports_[port].clear_irq(); + update_interrupts(); + return result; + } else { + if(ports_[port].control & Flag::DataVisible) { + return ports_[port].input(port_handler_.template input()); + } else { + return ports_[port].direction; + } + } + } + + void refresh() const { + refresh(); + refresh(); + } + + template + void set(const bool value) { + // 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_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_irq2) { + 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_irq2 ? IRQ2Direction : IRQ1Direction; + if(value != bool(ports_[port].control & direction)) { + ports_[port].apply_irq(is_irq2 ? Flag::IRQ2 : Flag::IRQ1); + update_interrupts(); + } + } + +private: + PortHandlerT &port_handler_; + bool inputs_[4]{}; + + enum Flag: uint8_t { + DataVisible = 0b0000'0100, + C2IsOutput = 0b0010'0000, + + IRQ1 = 0b1000'0000, + IRQ1Direction = 0b0000'0010, + EnableIRQ1 = 0b0000'0001, + + IRQ2 = 0b0100'0000, + IRQ2Direction = 0b0001'0000, + EnableIRQ2 = 0b0000'1000, + }; + + struct { + uint8_t control = 0; + uint8_t data = 0; + uint8_t direction = 0; // Per bit: 0 = input; 1 = output. + + void apply_irq(const uint8_t mask) { + control |= mask; + } + + void clear_irq() { + control &= ~(IRQ1 | IRQ2); + } + + bool irq() const { + 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; + uint8_t output() const { + return data | ~direction; + } + uint8_t input(const uint8_t bus) const { + return (data & direction) | (bus & ~direction); + } + } 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); + } + + bool irqa_ = false, irqb_ = false; + void update_interrupts() { + const bool irqa = ports_[0].irq(); + const bool irqb = ports_[1].irq(); + + if(irqa_ != irqa) { + irqa_ = irqa; + port_handler_.template set(irqa); + } + if(irqb_ != irqb) { + irqb_ = irqb; + port_handler_.template set(irqb); + } + } +}; + +}; + +#endif /* _821_hpp */ diff --git a/Machines/Thomson/MO5/Keyboard.hpp b/Machines/Thomson/MO5/Keyboard.hpp new file mode 100644 index 000000000..84d88248f --- /dev/null +++ b/Machines/Thomson/MO5/Keyboard.hpp @@ -0,0 +1,78 @@ +// +// 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 { + switch(key) { + default: return MachineTypes::MappedKeyboardMachine::KeyNotMapped; + + using enum Inputs::Keyboard::Key; + 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 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 FullStop: return 0x10; + case Comma: return 0x08; + case Hyphen: return 0x26; + case Equals: return 0x2e; // + + + case OpenSquareBracket: return 0x18; // @ + case Semicolon: return 0x1a; // M + + 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? + } + } +}; + +} diff --git a/Machines/Thomson/MO5/MO5.cpp b/Machines/Thomson/MO5/MO5.cpp index f4725f040..8aa9726c4 100644 --- a/Machines/Thomson/MO5/MO5.cpp +++ b/Machines/Thomson/MO5/MO5.cpp @@ -8,22 +8,41 @@ #include "MO5.hpp" +#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" + +#include "Components/AudioToggle/AudioToggle.hpp" +#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp" +#include "Outputs/Speaker/Implementation/BufferSource.hpp" + +#include "Keyboard.hpp" using namespace Thomson::MO5; namespace { struct ConcreteMachine: + public MachineTypes::AudioProducer, + public MachineTypes::MappedKeyboardMachine, public MachineTypes::TimedMachine, public MachineTypes::ScanProducer, 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_), + 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); @@ -33,10 +52,26 @@ 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(); } - void run_for(const Cycles cycles) final { - m6809_.run_for(cycles); + ~ConcreteMachine() { + audio_queue_.lock_flush(); + } + + 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 < @@ -49,21 +84,49 @@ 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(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); + } else { + if(address >= 0xa7c0 && address < 0xa800) { + switch(address) { + 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; + 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 { - 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) { + if(address < 40*200) video_.flush(); + 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,16 +138,158 @@ private: }; CPU::M6809::Processor m6809_; - std::array ram_; + std::array ram_; std::array rom_; + uint8_t *start_pointer_ = nullptr; + + uint8_t *video_page(const bool pixels) { + return &ram_[pixels ? 0 : 0x1'0000]; + } + + void page_lower(const bool pixels) { + start_pointer_ = video_page(pixels); + } + + friend struct SystemPIAPortHandler; + struct SystemPIAPortHandler { + SystemPIAPortHandler(ConcreteMachine &machine) : machine_(machine) {} + + template + 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; + } + + if constexpr (port == Motorola::MC6821::Port::B) { + // Port B inputs: + // b7: status of key at that position. [0 = pressed?] + return key_states_[key_] ? 0x00 : 0x80; + } + + __builtin_unreachable(); + } + + template + void output(const uint8_t value) { + if constexpr (port == Motorola::MC6821::Port::A) { + // 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) { + // 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); + } + } + + template + void set(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); + } + } + + template + void observe(const bool value) { + // TODO: CA2 is drive motor control, so catch that. + (void)value; + } + + // TODO: + // + // CA1: lightpen input + // CA2: drive motor control + // 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_; + + 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 *) 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. + + void run_for(const Cycles cycles) final { + m6809_.run_for(cycles); + } + void flush_output(int outputs) final { + if(outputs & Output::Video) { + video_.flush(); + } + if(outputs & Output::Audio) { + update_audio(); + audio_queue_.perform(); + } + } + + // 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/Machines/Thomson/MO5/Video.cpp b/Machines/Thomson/MO5/Video.cpp new file mode 100644 index 000000000..b51b0046f --- /dev/null +++ b/Machines/Thomson/MO5/Video.cpp @@ -0,0 +1,150 @@ +// +// Video.cpp +// Clock Signal +// +// Created by Thomas Harte on 20/03/2026. +// Copyright © 2026 Thomas Harte. All rights reserved. +// + +#include "Video.hpp" + +#include + +using namespace Thomson::MO5; + +namespace { + +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), +}; + +} + +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)); + }); + crt_.set_display_type(Outputs::Display::DisplayType::RGB); +} + +void Video::run_for(const Cycles cycles) { + 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; + } + ); +} + +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; + Numeric::clamp<0, EndOfSync>(line_begin, line_end, [&](const int begin, const int end) { + crt_.output_sync(end - begin); + }); + Numeric::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; + static constexpr int PixelsPerLine = 320; + + Numeric::clamp<0, EndOfSync>(line_begin, line_end, [&](const int begin, const int end) { + crt_.output_sync(end - begin); + }); + Numeric::clamp(line_begin, line_end, [&](const int begin, const int end) { + crt_.output_level(end - begin, border_); + }); + Numeric::clamp(line_begin, line_end, [&](const int begin, const int end) { + if(begin == EndOfLeftBorder) { + output_ = reinterpret_cast(crt_.begin_data(PixelsPerLine)); + } + + 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(EndOfPixels - EndOfLeftBorder, PixelsPerLine); + output_ = nullptr; + } + }); + Numeric::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. 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_.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_.absolute() >= IRQCycle && position_.absolute() < (IRQCycle + IRQLength); +} + +void Video::set_border_colour(const uint8_t colour) { + border_ = palette[colour & 0xf]; +} diff --git a/Machines/Thomson/MO5/Video.hpp b/Machines/Thomson/MO5/Video.hpp new file mode 100644 index 000000000..5d770bd81 --- /dev/null +++ b/Machines/Thomson/MO5/Video.hpp @@ -0,0 +1,88 @@ +// +// 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" +#include "Numeric/SegmentCounter.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); + Cycles next_sequence_point() const; + + void set_border_colour(uint8_t); + bool irq() const; + + // 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_; + + 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); + + // 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_; +}; + +} + +#endif /* Video_hpp */ diff --git a/Numeric/SegmentCounter.hpp b/Numeric/SegmentCounter.hpp new file mode 100644 index 000000000..1e85e09e0 --- /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. +// + +#include + +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); + + advance(segment, begin, begin + length); + + count -= length; + 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/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 { diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 53b1eb4ab..c9e582838 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,10 @@ 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 = ""; }; + 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 = ""; }; @@ -2531,6 +2538,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 = ""; }; @@ -3880,6 +3888,7 @@ 4BFEA2F12682A90200EBF94C /* Sizes.hpp */, 4BD9713A2BFD7E7100C907AA /* StringSimilarity.hpp */, 4B0329202D0A78DC00C51EB5 /* UpperBound.hpp */, + 4BBC7DC42F70507B00E71794 /* SegmentCounter.hpp */, ); name = Numeric; path = ../../Numeric; @@ -5276,6 +5285,7 @@ 4BC080DD26A481C100D03FD8 /* 6526 */, 4B1E85791D174DEC001EF87D /* 6532 */, 4BC9DF4C1D04691600F44158 /* 6560 */, + 4BDB73512F6CA1A200964D69 /* 6821 */, 4BE845221F2FF7F400A5EA22 /* 6845 */, 4BB307B8235001C300457D33 /* 6850 */, 4BD9137C1F3115AC009BCF85 /* 8255 */, @@ -5331,8 +5341,11 @@ 4BCA6FAA2F589C0C000B9537 /* MO5 */ = { isa = PBXGroup; children = ( - 4BCA6FA82F589C0C000B9537 /* MO5.hpp */, 4BCA6FA92F589C0C000B9537 /* MO5.cpp */, + 4BBC7DC02F6D94E500E71794 /* Video.cpp */, + 4BBC7DC52F70BAFB00E71794 /* Keyboard.hpp */, + 4BCA6FA82F589C0C000B9537 /* MO5.hpp */, + 4BBC7DBF2F6D94E500E71794 /* Video.hpp */, ); path = MO5; sourceTree = ""; @@ -5591,6 +5604,14 @@ path = 5380; sourceTree = ""; }; + 4BDB73512F6CA1A200964D69 /* 6821 */ = { + isa = PBXGroup; + children = ( + 4BDB734F2F6CA1A200964D69 /* 6821.hpp */, + ); + path = 6821; + sourceTree = ""; + }; 4BE211FB253FC80800435408 /* AppleIIgs */ = { isa = PBXGroup; children = ( @@ -6460,6 +6481,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 */, @@ -6818,6 +6840,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 */, @@ -6954,6 +6977,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/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. 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 }; diff --git a/Processors/6809/6809.hpp b/Processors/6809/6809.hpp index 980ee3f62..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. @@ -198,7 +208,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) { \ @@ -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. @@ -400,23 +411,31 @@ struct Processor { } if(exceptions_) { + // Avoid potential accidental detection of SWIs on the interrupt paths. + operation_.operation = Operation::None; + if(exceptions_ & Exception::Halt) { goto halt; } 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; } } @@ -566,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(); } @@ -741,11 +770,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); @@ -767,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; } @@ -842,37 +874,24 @@ 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; - default: __builtin_unreachable(); - } - } (); - if(operation_.operation != Operation::SWI2 && operation_.operation != Operation::SWI3) { registers_.cc.set(true); registers_.cc.set(true); } 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; @@ -950,11 +969,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() { diff --git a/Processors/6809/Registers.hpp b/Processors/6809/Registers.hpp index 644644cc4..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 LE: return get() || test(); case LS: return get() || get(); + case LE: return get() || test(); case LT: return get() != get(); - case HI: return !get() && !get(); - case GE: return get() == get(); - case GT: return !get() && test(); + case GE: return !test(); + case GT: return !test(); + case HI: return !test(); } return false; } @@ -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(); } } 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