Files

763 lines
21 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// MO.cpp
// Clock Signal
//
// Created by Thomas Harte on 04/03/2026.
// Copyright © 2026 Thomas Harte. All rights reserved.
//
#include "MO.hpp"
#include "CD90-640.hpp"
#include "FastTape.hpp"
#include "Keyboard.hpp"
#include "MemoryMap.hpp"
#include "Video.hpp"
#include "Activity/Source.hpp"
#include "Machines/MachineTypes.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 "Outputs/Log.hpp"
#include "Analyser/Static/Thomson/Target.hpp"
using namespace Thomson::MO;
namespace {
using Log = Log::Logger<Log::Source::MO5>;
static constexpr int ClockRate = 1'000'000;
static constexpr uint8_t MusicExpansionMask = 63;
using Target = Analyser::Static::Thomson::MOTarget;
constexpr Keyboard::Machine machine(const Target::Model model) {
switch(model) {
case Target::Model::MO5v1:
case Target::Model::MO5v11:
return Keyboard::Machine::MO5;
case Target::Model::MO6v1:
case Target::Model::MO6v2:
case Target::Model::MO6v3:
return Keyboard::Machine::MO6;
case Target::Model::Prodest128:
return Keyboard::Machine::Prodest128;
default: __builtin_unreachable();
}
}
template <bool has_floppy, bool is_mo6>
struct ConcreteMachine:
public Activity::Source,
public Configurable::Device,
public MachineTypes::AudioProducer,
public MachineTypes::JoystickMachine,
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::MediaChangeObserver,
public MachineTypes::MediaTarget,
public MachineTypes::SoftResettable,
public MachineTypes::ScanProducer,
public MachineTypes::TimedMachine,
public Machine,
public Utility::TypeRecipient<Thomson::MO::Keyboard::CharacterMapper>
{
ConcreteMachine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
Utility::TypeRecipient<Thomson::MO::Keyboard::CharacterMapper>(machine(target.model)),
m6809_(*this),
system_pia_port_handler_(*this),
system_pia_(system_pia_port_handler_),
sound_and_game_pia_port_handler_(*this),
sound_and_game_pia_(sound_and_game_pia_port_handler_),
video_(memory_.video(true), memory_.video(false)),
tape_player_(ClockRate),
audio_(audio_queue_, MusicExpansionMask),
speaker_(audio_),
keyboard_mapper_(machine(target.model))
{
set_clock_rate(ClockRate);
speaker_.set_input_rate(ClockRate);
construct_joysticks();
const auto BasicROM = [](const Target::Model model) {
switch(model) {
using enum Target::Model;
default: __builtin_unreachable();
case MO5v1: return ROM::Name::ThomsonMO5v1;
case MO5v11: return ROM::Name::ThomsonMO5v11;
case MO6v1: return ROM::Name::ThomsonMO6v1;
case MO6v2: return ROM::Name::ThomsonMO6v2;
case MO6v3: return ROM::Name::ThomsonMO6v3;
case Prodest128: return ROM::Name::OlivettiProdest128;
}
} (target.model);
auto request = ROM::Request(BasicROM);
if(has_floppy) {
request = request && ROM::Request(ROM::Name::ThomsonCD90_640);
}
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
{
auto rom = roms.find(BasicROM)->second;
memory_.set_rom(rom);
}
if(has_floppy) {
memory_.set_floppy_rom(roms.find(ROM::Name::ThomsonCD90_640)->second);
}
system_pia_.refresh();
insert_media(target.media);
type_string(target.loading_command);
}
~ConcreteMachine() {
audio_queue_.lock_flush();
}
template <
CPU::M6809::BusPhase bus_phase,
CPU::M6809::LIC lic,
CPU::M6809::ReadWrite read_write,
CPU::M6809::BusState bus_state,
typename AddressT
>
Cycles perform(
const AddressT address,
CPU::M6809::data_t<read_write> value
) {
static constexpr auto duration = CPU::M6809::duration<Cycles>(bus_phase);
if(video_ += duration) {
system_pia_.template set<Motorola::MC6821::Control::CB1>(video_.get()->irq());
}
time_since_audio_update_ += duration;
tape_player_.run_for(duration);
if(typer_) {
typer_->run_for(duration);
}
if(has_floppy) {
fdc_.run_for(duration * 8); // My WD1770 has a nominal clock of 8Mhz.
}
//
// Complete MO5 memory map:
//
// 0x0000–: video RAM, either pixels or attributes paged.
// 0x2000–: RAM
// 0xa000–: DOS ROM, if installed
// 0xa7c0–: memory-mapped devices
// 0xb000–: cartridge, if installed
// 0xc000–: BASIC/ROM, if no cartridge installed
// 0xf000–: BASIC/ROM
//
if constexpr (read_write != CPU::M6809::ReadWrite::NoData) {
if(address >= 0xa7c0 && address < 0xa800) {
const auto unmapped = [&] {
if constexpr (CPU::M6809::is_read(read_write)) {
value = 0xff;
Log::info().append("Unhandled read at %04x", +address);
} else {
Log::info().append("Unhandled write: %02x -> %04x", +value, +address);
}
};
switch(address) {
using namespace CPU::M6809;
case 0xa7c0:
access<0xa7c0, read_write>(system_pia_, value);
if constexpr (CPU::M6809::is_read(read_write)) {
if(allow_fast_tape_loading_ && tape_player_.motor_control()) {
fast_tape_loader_.add_tape_read(m6809_.registers().pc.full, memory_);
}
}
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;
case 0xa7cb: access<0xa7cb, read_write>(memory_, value); break;
case 0xa7cc: access<0xa7cc, read_write>(sound_and_game_pia_, value); break;
case 0xa7cd: access<0xa7cd, read_write>(sound_and_game_pia_, value); break;
case 0xa7ce: access<0xa7ce, read_write>(sound_and_game_pia_, value); break;
case 0xa7cf: access<0xa7cf, read_write>(sound_and_game_pia_, value); break;
case 0xa7d0: if(has_floppy) access<0xa7d0, read_write>(fdc_, value); else unmapped(); break;
case 0xa7d1: if(has_floppy) access<0xa7d1, read_write>(fdc_, value); else unmapped(); break;
case 0xa7d2: if(has_floppy) access<0xa7d2, read_write>(fdc_, value); else unmapped(); break;
case 0xa7d3: if(has_floppy) access<0xa7d3, read_write>(fdc_, value); else unmapped(); break;
case 0xa7d8: if(has_floppy) access<0xa7d8, read_write>(fdc_, value); else unmapped(); break;
case 0xa7e4: if(is_mo6) access<0xa7e4, read_write>(memory_, value); else unmapped(); break;
case 0xa7e5:
if(is_mo6) memory_.template system_access<0xa7e5, read_write>(video_, value); else unmapped();
break;
case 0xa7e6:
if(is_mo6) memory_.template system_access<0xa7e6, read_write>(video_, value); else unmapped();
break;
case 0xa7e7:
if constexpr (CPU::M6809::is_read(read_write)) {
if constexpr (is_mo6) {
value = video_->vertical_state() & memory_.template read<0xa7e7>();
} else {
value = video_->vertical_state();
}
} else {
if constexpr (is_mo6) {
access<0xa7e7, read_write>(video_, value);
access<0xa7e7, read_write>(memory_, value);
} else {
unmapped();
}
}
break;
case 0xa7da: if(is_mo6) access<0xa7da, read_write>(video_, value); else unmapped(); break;
case 0xa7db: if(is_mo6) access<0xa7db, read_write>(video_, value); else unmapped(); break;
case 0xa7dc: if(is_mo6) access<0xa7dc, read_write>(video_, value); else unmapped(); break;
case 0xa7dd:
if(is_mo6) {
access<0xa7dd, read_write>(memory_, value);
access<0xa7dd, read_write>(video_, value);
} else {
unmapped();
}
break;
default:
unmapped();
break;
}
} else {
if constexpr (CPU::M6809::is_read(read_write)) {
value = memory_.read(address);
if constexpr (lic == CPU::M6809::LIC::InstructionFetch) {
if(
allow_fast_tape_loading_ &&
tape_player_.motor_control() &&
fast_tape_loader_.trap_address.has_value() &&
address == *fast_tape_loader_.trap_address
) [[unlikely]] {
auto *const serialiser = tape_player_.serialiser();
if(serialiser) {
const auto action = fast_tape_loader_.did_trap(
address,
memory_,
m6809_.registers(),
tape_player_,
*serialiser
);
switch(action) {
using enum Thomson::TrapAction;
case None: break;
case NOP: value = 0x12; break;
case RTS: value = 0x39; break;
default: __builtin_unreachable();
}
}
}
}
} else {
if(address < 40*200) {
video_.flush();
}
memory_.write(address, value);
}
}
}
return Cycles(0);
}
private:
struct M6809Traits {
static constexpr bool uses_mrdy = false;
static constexpr auto pause_precision = CPU::M6809::PausePrecision::BetweenInstructions;
using BusHandlerT = ConcreteMachine;
};
CPU::M6809::Processor<M6809Traits> m6809_;
MemoryMap<is_mo6> memory_;
friend struct SystemPIAPortHandler;
struct SystemPIAPortHandler {
SystemPIAPortHandler(ConcreteMachine &machine) : machine_(machine) {}
//
// Control lines:
//
// CA1: lightpen input
// CA2: drive motor control output
// CB1: 50Hz interrupt input
// CB2: "Video incrustation"?
//
//
// Port A:
//
// b7 [input]: tape data (high = tape attached but not running)
// b6 [output]: tape data
// b1–b5: ... machine specific ...
// b0 [output]: lower 8kb paging
//
// MO5:
//
// b5 [input]: light pen button
// b1–b3 [output]: border colour
//
// MO6:
//
// b5 [output]: ROM page selection
// b4 [output]: shift lock LED
// b3 [output]: additional bit for keyboard line
// b2 [output]: "mute souris" (mute mouse? Probably related to the mouse and 6-bit DAC sharing lines.)
// b1 [input]: light pen button (maybe?)
//
//
// Port B:
//
// b7 [input]: state of selected key
// b4–6 [output]: keyboard column
// b1–3 [output]: keyboard line
// b0 [output]: 1-bit sound
//
template <Motorola::MC6821::Port port>
uint8_t input() {
if constexpr (port == Motorola::MC6821::Port::A) {
return machine_.tape_player_.input() ? 0x00 : 0x80;
}
if constexpr (port == Motorola::MC6821::Port::B) {
// Cf. R118, R119 and T21 from the schematic; if PA3 (stored here as bit 6) is active then
// the output of PB1–3 (here bits 0 to 2) is ineffective.
const auto key = key_ & 0x40 ? key_ & ~7 : key_;
return key_states_[key] ? 0x00 : 0x80;
}
__builtin_unreachable();
}
template <Motorola::MC6821::Port port>
void output(const uint8_t value) {
if constexpr (port == Motorola::MC6821::Port::A) {
machine_.memory_.page_video(value & 0x01);
machine_.memory_.page_monitor(value & 0x20);
if constexpr (!is_mo6) {
machine_.video_->set_border_colour((value >> 1) & 0xf);
} else {
// TODO: determine why I'm having to invert this bit.
key_ = (key_ & 0b0'111'111) | ((~value << 3) & 0b1'000'000);
if(machine_.activity_observer_) {
machine_.activity_observer_->set_led_status(machine_.ShiftLED, value & 0x10);
}
}
}
if constexpr (port == Motorola::MC6821::Port::B) {
key_ = (key_ & 0b1'000'000) | ((value >> 1) & 0b0'111'111);
machine_.set_audio(value & 1, std::nullopt);
}
}
template <Motorola::MC6821::IRQ irq>
void set(const bool active) {
if constexpr (irq == Motorola::MC6821::IRQ::A) {
machine_.m6809_.template set<CPU::M6809::Line::FIRQ>(active);
}
if constexpr (irq == Motorola::MC6821::IRQ::B) {
machine_.m6809_.template set<CPU::M6809::Line::IRQ>(active);
}
}
template <Motorola::MC6821::Control control>
void observe(const bool value) {
if constexpr (control == Motorola::MC6821::Control::CA2) {
machine_.tape_player_.set_motor_control(!value);
}
if constexpr (control == Motorola::MC6821::Control::CB2) {
Log::info().append("Video encrustation (?) is %d", value);
}
}
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_[0x80]{}; // There are 64 key addresses on an MO5; 128 on an MO6.
};
SystemPIAPortHandler system_pia_port_handler_;
Motorola::MC6821::MC6821<SystemPIAPortHandler, 2, 1> system_pia_;
friend struct SoundAndGamePIAPortHandler;
struct SoundAndGamePIAPortHandler {
SoundAndGamePIAPortHandler(ConcreteMachine &machine) : machine_(machine) {}
template <Motorola::MC6821::Control control>
void observe(const bool) {}
template <Motorola::MC6821::Port port>
void output(const uint8_t value) {
// Port B:
//
// b0–b5: DAC output
if constexpr (port == Motorola::MC6821::Port::B) {
machine_.set_audio(std::nullopt, value & MusicExpansionMask);
}
}
template <Motorola::MC6821::IRQ irq>
void set(const bool) {}
template <Motorola::MC6821::Port port>
uint8_t input() {
// Port A:
//
// b0: joystick 0 up / left mouse click
// b1: joystick 0 down / right mouse click
// b2: joystick 0 left / XB
// b3: joystick 0 right / YB
// b4: joystick 1 up
// b5: joystick 1 down
// b6: joystick 1 left
// b7: joystick 1 right
if constexpr (port == Motorola::MC6821::Port::A) {
return port_a_;
}
// Port B:
//
// b0: common 0
// b1: common 1
// b2: joystick 0 button 2 / XA
// b3: joystick 1 button 2
// b4:
// b5:
// b6: joystick 0 button 1 / YA
// b7: joystick 1 button 1
if constexpr (port == Motorola::MC6821::Port::B) {
return port_b_;
}
return 0xff;
}
// Control ports: (CE/CF?)
//
// CA1: joystick 0 button 2
// CA2: joystick 0 button 1
// CB1: joystick 1 button 2
// CB2: joystick 1 button 1
void set_joystick_input(
const int index,
const Inputs::Joystick::Input &input,
bool is_active
) {
const auto apply = [&](uint8_t &port, const uint8_t mask) {
if(is_active) {
port &= ~mask;
} else {
port |= mask;
}
};
switch(input.type) {
using enum Inputs::Joystick::Input::Type;
case Up: apply(port_a_, index ? 0x10 : 0x01); break;
case Down: apply(port_a_, index ? 0x20 : 0x02); break;
case Left: apply(port_a_, index ? 0x40 : 0x04); break;
case Right: apply(port_a_, index ? 0x80 : 0x08); break;
case Fire:
if(input.info.control.index) {
apply(port_b_, index ? 0x08 : 0x04);
if(index) {
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CB1>(is_active);
} else {
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CA1>(is_active);
}
} else {
apply(port_b_, index ? 0x80 : 0x40);
if(index) {
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CB2>(is_active);
} else {
machine_.sound_and_game_pia_.template set<Motorola::MC6821::Control::CA2>(is_active);
}
}
break;
default: break;
}
}
private:
ConcreteMachine &machine_;
uint8_t port_a_ = 0xff;
uint8_t port_b_ = 0xff;
};
struct Joystick: public Inputs::ConcreteJoystick {
Joystick(SoundAndGamePIAPortHandler &pia_port_handler, const int index) :
ConcreteJoystick({
Input(Input::Up),
Input(Input::Down),
Input(Input::Left),
Input(Input::Right),
Input(Input::Fire, 0),
Input(Input::Fire, 1),
}),
pia_port_handler_(pia_port_handler),
index_(index) {}
void did_set_input(const Input &digital_input, bool is_active) final {
pia_port_handler_.set_joystick_input(index_, digital_input, is_active);
}
private:
SoundAndGamePIAPortHandler &pia_port_handler_;
int index_;
};
SoundAndGamePIAPortHandler sound_and_game_pia_port_handler_;
Motorola::MC6821::MC6821<SoundAndGamePIAPortHandler, 2, 1> sound_and_game_pia_;
JustInTimeActor<Thomson::Video, Cycles> video_;
// MARK: - Tape and disk.
Storage::Tape::BinaryTapePlayer tape_player_;
bool allow_fast_tape_loading_ = true;
Thomson::FastTapeLoader fast_tape_loader_;
Thomson::CD90_640 fdc_;
// MARK: - AudioProducer.
Concurrency::AsyncTaskQueue<false> audio_queue_;
Audio::DAC audio_;
Outputs::Speaker::PullLowpass<Audio::DAC> 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_;
}
bool audio_enabled_ = false;
uint8_t audio_level_ = MusicExpansionMask;
void set_audio(const std::optional<bool> enabled, const std::optional<uint8_t> level) {
const auto new_audio_enabled = enabled.value_or(audio_enabled_);
const auto new_audio_level = level.value_or(audio_level_);
if(new_audio_level == audio_level_ && new_audio_enabled == audio_enabled_) {
return;
}
audio_level_ = new_audio_level;
audio_enabled_ = new_audio_enabled;
update_audio();
audio_.set_output(audio_enabled_ ? audio_level_ : 0);
}
// MARK: - ScanProducer.
void set_scan_target(Outputs::Display::ScanTarget *const target) final {
video_.get()->set_scan_target(target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return video_.get()->get_scaled_scan_status();
}
// MARK: - TimedMachine.
void run_for(const Cycles cycles) final {
m6809_.run_for(cycles);
}
void flush_output(const int outputs) final {
if(outputs & Output::Video) {
video_.flush();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
// MARK: - Resettable.
void soft_reset() final {
m6809_.template set<CPU::M6809::Line::PowerOnReset>(true);
memory_.reset();
}
// MARK: - MappedKeyboardMachine.
Thomson::MO::Keyboard::KeyboardMapper keyboard_mapper_;
KeyboardMapper *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();
}
void type_string(const std::wstring &string) final {
Utility::TypeRecipient<Thomson::MO::Keyboard::CharacterMapper>::add_typer(string);
}
bool can_type(const wchar_t c) const final {
return Utility::TypeRecipient<Thomson::MO::Keyboard::CharacterMapper>::can_type(c);
}
HalfCycles typer_delay(const std::wstring &) const final {
if(m6809_.template get<CPU::M6809::Line::PowerOnReset>()) {
return is_mo6 ? Cycles(1'000'000) : Cycles(1'000'000);
} else {
return Cycles(0);
}
}
HalfCycles typer_frequency() const final {
return Cycles(30'000);
}
// MARK: - MediaTarget and MediaChangeObserver.
bool insert_media(const Analyser::Static::Media &media) override {
if(!media.tapes.empty()) {
tape_player_.set_tape(media.tapes.front(), TargetPlatform::ThomsonMO);
}
if(!media.cartridges.empty()) {
auto rom = media.cartridges.front()->segments().front().data;
if(rom.size() < 16384) {
rom.resize(16384);
}
memory_.set_cartridge(rom);
}
if(has_floppy) {
size_t index = 0;
for(auto &disk: media.disks) {
fdc_.set_disk(disk, index);
++ index;
}
}
return !media.tapes.empty() || (!media.disks.empty() && has_floppy) || !media.cartridges.empty();
}
ChangeEffect effect_for_file_did_change(const std::string &) const override {
// TODO: unless it's a disk that's already inserted.
return ChangeEffect::RestartMachine;
}
// MARK: - Activity Source.
void set_activity_observer(Activity::Observer *const observer) override {
if(is_mo6) {
activity_observer_ = observer;
activity_observer_->register_led(ShiftLED, Activity::Observer::LEDPresentation::Persistent);
}
tape_player_.set_activity_observer(observer);
if(has_floppy) {
fdc_.set_activity_observer(observer);
}
}
static constexpr char ShiftLED[] = "Shift Lock";
Activity::Observer *activity_observer_ = nullptr;
// MARK: - Configuration options.
std::unique_ptr<Reflection::Struct> get_options() const final {
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
options->quick_load = allow_fast_tape_loading_;
return options;
}
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
const auto options = dynamic_cast<Options *>(str.get());
allow_fast_tape_loading_ = options->quick_load;
}
// MARK: - Joystick Machine.
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
return joysticks_;
}
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
void construct_joysticks() {
joysticks_.push_back(std::make_unique<Joystick>(sound_and_game_pia_port_handler_, 0));
joysticks_.push_back(std::make_unique<Joystick>(sound_and_game_pia_port_handler_, 1));
}
};
}
namespace {
template <bool is_mo6>
std::unique_ptr<Machine> machine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) {
switch(target.floppy) {
using enum Target::Floppy;
case None: return std::make_unique<ConcreteMachine<false, is_mo6>>(target, rom_fetcher);
case CD90_640: return std::make_unique<ConcreteMachine<true, is_mo6>>(target, rom_fetcher);
default: __builtin_unreachable();
}
}
}
std::unique_ptr<Machine> Machine::create(
const Analyser::Static::Target &target,
const ROMMachine::ROMFetcher &rom_fetcher
) {
const auto &thomson_target = static_cast<const Target &>(target);
if(Analyser::Static::Thomson::is_mo6(thomson_target.model)) {
return machine<true>(thomson_target, rom_fetcher);
} else {
return machine<false>(thomson_target, rom_fetcher);
}
}