Files

1235 lines
37 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.
//
// CoCo.cpp
// Clock Signal
//
// Created by Thomas Harte on 01/05/2026.
// Copyright © 2026 Thomas Harte. All rights reserved.
//
#include "CoCo.hpp"
#include "DiskController.hpp"
#include "Keyboard.hpp"
#include "Processors/6809/6809.hpp"
#include "Components/6821/6821.hpp"
#include "Components/6847/6847.hpp"
#include "Activity/Source.hpp"
#include "Analyser/Static/TandyCoCo/Target.hpp"
#include "ClockReceiver/JustInTime.hpp"
#include "Machines/MachineTypes.hpp"
#include "Machines/Utility/MemoryFuzzer.hpp"
#include "Components/AudioToggle/AudioToggle.hpp"
#include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "Outputs/Speaker/Implementation/BufferSource.hpp"
#include <cassert>
using namespace Tandy::CoCo;
namespace {
struct MemoryMap {
uint8_t read(const uint16_t address) const {
return read_[address >> 13] ? read_[address >> 13][address] : 0xff;
}
void write(const uint16_t address, uint8_t value) {
if(!write_[address >> 13]) return;
write_[address >> 13][address] = value;
}
void set_read(const size_t begin, const size_t end, const uint8_t *data) {
assert(!(begin & 0x1fff));
assert(begin >= 0 && begin <= 0x10000);
assert(!(end & 0x1fff));
assert(end >= 0 && end <= 0x10000);
for(auto page = begin >> 13; page < end >> 13; page++) {
read_[page] = data - begin;
}
}
void set_readwrite(const size_t begin, const size_t end, uint8_t *data) {
assert(!(begin & 0x1fff));
assert(begin >= 0 && begin <= 0x10000);
assert(!(end & 0x1fff));
assert(end >= 0 && end <= 0x10000);
for(auto page = begin >> 13; page < end >> 13; page++) {
read_[page] = write_[page] = data - begin;
}
}
void reset() {
std::fill(std::begin(write_), std::end(write_), nullptr);
std::fill(std::begin(read_), std::end(read_), nullptr);
}
private:
uint8_t *write_[8]{};
const uint8_t *read_[8]{};
};
struct Joystick: public Inputs::ConcreteJoystick {
Joystick() :
ConcreteJoystick({
Input(Input::Horizontal),
Input(Input::Vertical),
Input(Input::Fire, 0),
Input(Input::Fire, 1)
}) {}
void did_set_input(const Input &input, const float value) final {
const size_t index = input.type == Input::Type::Vertical;
axes[index] = std::clamp<uint8_t>(uint8_t(value * 63.0), 0, 63);
}
void did_set_input(const Input &input, const bool value) final {
if(input.type != Input::Type::Fire) {
return;
}
buttons[input.info.control.index] = value;
}
float digital_minimum() const { return 0.0f; }
float digital_maximum() const { return 1.0f; }
uint8_t axes[2]{32, 32};
bool buttons[2]{};
};
// On the CoCo the most significant VDG data bit is connected to the VDG's '!alpha/semi_g' input.
// This allows it to switch automatically between alphanumeric mode and semi-graphics mode based
// on the most significant data bit, allowing mixed text and block-graphics on the same screen.
//
// Cf. https://web.archive.org/web/20210214054301/http://www.cs.unc.edu/~yakowenk/coco/text/semigraphics.html
struct ModeMapper {
Motorola::MC6847::Mode::IntT operator()(const uint8_t data, const Motorola::MC6847::Mode::IntT mode) {
return mode | (data & 0x80 ? Motorola::MC6847::Mode::Semigraphics : 0);
}
};
struct SAM {
SAM(MemoryMap &memory, const bool is_64kb) : memory_(memory), is_64kb_(is_64kb) {
Memory::Fuzz(ram_);
update_memory();
}
void reset() {
speed_ = ClockSpeed::Half;
set_all_ram(false);
page_ram(0);
}
uint8_t operator[](const uint16_t address) {
b1b3_ += ((previous_6847_address_ ^ address) & previous_6847_address_ & 1) << 1;
previous_6847_address_ = address;
x_ += b1b3_ >> 4;
b1b3_ &= 0b0'1110;
if(x_ == x_divider_) {
x_ = 0;
b4_ += 0b1'0000;
y_ += b4_ >> 5;
b4_ &= 0b1'0000;
if(y_ == y_divider_) {
y_ = 0;
b5b15_ += 0b10'0000;
}
}
// TODO: determine whether this should be able to read ROM.
return memory_.read((address & 1) | b1b3_ | b4_ | b5b15_);
}
template <bool active> void set_hsync() {
if(active) {
b1b3_ &= clear_mask_;
b4_ &= clear_mask_;
previous_6847_address_ = 0;
}
}
template <bool active> void set_field_sync() {
if(!active) {
b5b15_ = graphics_address_;
b4_ = 0;
b1b3_ = 0;
previous_6847_address_ = 0;
x_ = y_ = 0;
}
}
template <bool active> void set_row_preset() {}
template <uint16_t address, CPU::M6809::ReadWrite read_write>
void access() {
if(!is_write(read_write)) {
return;
}
const auto set = [&](auto &target, auto bit) {
target = target & ~bit;
target |= address & 1 ? bit : 0;
};
const auto configure_counter = [&] {
switch(graphics_mode_) {
case 0: x_divider_ = 1; y_divider_ = 12; clear_mask_ = 0b1111'1111'1110'0000; break;
case 1: x_divider_ = 3; y_divider_ = 1; clear_mask_ = 0b1111'1111'1111'0000; break;
case 2: x_divider_ = 1; y_divider_ = 3; clear_mask_ = 0b1111'1111'1110'0000; break;
case 3: x_divider_ = 2; y_divider_ = 1; clear_mask_ = 0b1111'1111'1111'0000; break;
case 4: x_divider_ = 1; y_divider_ = 2; clear_mask_ = 0b1111'1111'1110'0000; break;
case 5: x_divider_ = 1; y_divider_ = 1; clear_mask_ = 0b1111'1111'1111'0000; break;
case 6: x_divider_ = 1; y_divider_ = 1; clear_mask_ = 0b1111'1111'1110'0000; break;
case 7: x_divider_ = 1; y_divider_ = 1; clear_mask_ = 0b1111'1111'1111'1111; break;
default: __builtin_unreachable();
}
};
switch((address >> 1) & 0xf) {
default:
printf("Unhandled SAM access %04x\n", address);
break;
case 0: set(graphics_mode_, 1); configure_counter(); break;
case 1: set(graphics_mode_, 2); configure_counter(); break;
case 2: set(graphics_mode_, 4); configure_counter(); break;
case 3: set(graphics_address_, 0x0200); break;
case 4: set(graphics_address_, 0x0400); break;
case 5: set(graphics_address_, 0x0800); break;
case 6: set(graphics_address_, 0x1000); break;
case 7: set(graphics_address_, 0x2000); break;
case 8: set(graphics_address_, 0x4000); break;
case 9: set(graphics_address_, 0x8000); break;
case 11: { int tc = int(speed_); set(tc, 1); speed_ = ClockSpeed(tc); } break;
case 12: { int tc = int(speed_); set(tc, 2); speed_ = ClockSpeed(tc); } break;
// Likely these are to effect refresh; TODO: look into this.
case 13: set(ram_size_, 1); break;
case 14: set(ram_size_, 2); break;
case 10: page_ram(address & 1); break;
case 15: set_all_ram(address & 1); break;
}
}
template <
CPU::M6809::BusPhase bus_phase,
CPU::M6809::ReadWrite read_write,
typename AddressT
> Cycles cycle_cost(const AddressT address, const Cycles phase) {
//
// Without documentation I've made a guess here that the half-speed bus has an alignment requirement,
// so switching from full-speed to half-speed might result in a two-cycle access but might result in
// a three-cycle access (followed by two-cycle accesses).
//
// If that proves to be untrue, `phase` can be eliminated as an argument.
//
switch(speed_) {
case ClockSpeed::Full1:
case ClockSpeed::Full2:
return Cycles(0);
case ClockSpeed::Half:
return duration<Cycles>(bus_phase) + phase;
case ClockSpeed::HalfInRAM:
if constexpr (read_write == CPU::M6809::ReadWrite::NoData) {
return Cycles(0);
} else {
if(address < 0x8000 || all_ram_) {
return duration<Cycles>(bus_phase) + phase;
} else {
return Cycles(0);
}
}
break;
default: break;
}
__builtin_unreachable();
}
uint16_t graphics_address() const {
return graphics_address_;
}
bool insert_cartridge(const std::vector<uint8_t> &contents) {
if(contents.size() > 16 * 1024) {
return false;
}
// My memory map requires things to be a multiple of 0x2000 bytes; pad to that.
cartridge_ = contents;
if(cartridge_.size() & 0x1fff) {
const auto new_size = (cartridge_.size() + 0x2000) & ~size_t(0x1fff);
cartridge_.resize(new_size, 0xff);
}
update_memory();
return true;
}
bool has_cartridge() const {
return !cartridge_.empty();
}
void set_basic(const std::vector<uint8_t> rom) {
std::fill(basic_.begin(), basic_.end(), 0xff);
std::copy_n(rom.begin(), rom.size(), basic_.end() - rom.size());
}
void set_extended_basic(const std::vector<uint8_t> rom) {
std::copy_n(rom.begin(), rom.size(), basic_.begin());
}
void set_alternate_basic(const std::vector<uint8_t> rom) {
std::fill(alternate_basic_.begin(), alternate_basic_.end(), 0xff);
std::copy_n(rom.begin(), rom.size(), alternate_basic_.end() - rom.size());
}
void set_no_alternate_basic() {
std::copy(basic_.begin(), basic_.end(), alternate_basic_.begin());
}
private:
MemoryMap &memory_;
enum class ClockSpeed {
Half,
HalfInRAM,
Full1,
Full2,
};
int graphics_mode_ = 0;
uint16_t graphics_address_ = 0;
ClockSpeed speed_ = ClockSpeed::Half;
int ram_size_ = 0;
// 'X' and 'Y' are the data sheet names for these values, which is unfortunate given that they're involved
// in data fetch but don't correlate with dimensions.
//
// Requirements:
//
// The low bit of the address coming from the 6847 acts as the low bit of the address fetched.
// It also clocks counting.
//
// Above that is a three-bit counter, which it clocks.
//
// That three bit counter feeds into the 'x' divider. So if the x divider is 3 then that counter must overflow
// three times in order to propagate carry upwards.
//
// Carry from that counter is fed into a one-bit counter.
//
// That one bit counter then clocks the 'y' divider, carry from which clocks the remaining 11 bits of output.
//
// At field sync the top seven bits of the counter are reloaded from the programmed base address.
// At horizontal sync the programmed mask is applied to the counters.
// Upon row preset, both x and y dividers are reset to 0.
//
// Observations: the hsync clearing mask never affects the top 11 bits — the portiona after the y divider.
uint16_t b1b3_, x_, b4_, y_, b5b15_;
uint16_t x_divider_ = 1, y_divider_ = 1;
uint16_t clear_mask_ = 0xffff;
uint16_t previous_6847_address_ = 0;
std::vector<uint8_t> cartridge_;
std::array<uint8_t, 16 * 1024> basic_;
std::array<uint8_t, 16 * 1024> alternate_basic_;
std::array<uint8_t, 64 * 1024> ram_;
int ram_page_ = 0;
bool all_ram_ = false;
bool is_64kb_ = true;
void page_ram(const int page) {
ram_page_ = page;
update_memory();
}
void set_all_ram(const bool all_ram) {
all_ram_ = all_ram;
update_memory();
}
void update_memory() {
memory_.reset();
if(all_ram_ && is_64kb_) {
memory_.set_readwrite(0x0000, 0x1'0000, ram_.data());
} else {
memory_.set_readwrite(0x0000, 0x8000, ram_.data() + (is_64kb_ ? ram_page_ : 0) * 32768);
if(ram_page_) {
memory_.set_read(0x8000, 0xc000, alternate_basic_.data());
} else {
memory_.set_read(0x8000, 0xc000, basic_.data());
}
memory_.set_read(0xc000, 0xc000 + cartridge_.size(), cartridge_.data());
}
}
};
constexpr bool is_64kb(const Analyser::Static::TandyCoCo::Target::MemorySize size) {
return size == Analyser::Static::TandyCoCo::Target::MemorySize::SixtyFourKB;
}
static constexpr uint8_t MaxDACLevel = 0b111'111;
static constexpr double ClockRate = 1'789'772.5;
static constexpr auto AudioDivider = Cycles(1);
}
namespace TandyCoCo {
template <bool is_pal, bool has_disk_drive>
class ConcreteMachine:
public Activity::Source,
public Configurable::Device,
public Machine,
public MachineTypes::AudioProducer,
public MachineTypes::JoystickMachine,
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::MediaChangeObserver,
public MachineTypes::MediaTarget,
public MachineTypes::ScanProducer,
public MachineTypes::SoftResettable,
public MachineTypes::TimedMachine,
public Utility::TypeRecipient<Tandy::CoCo::Keyboard::CharacterMapper>
{
public:
ConcreteMachine(const Analyser::Static::TandyCoCo::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
m6809_(*this),
pia0_handler_(*this),
pia0_(pia0_handler_),
pia1_handler_(*this),
pia1_(pia1_handler_),
sam_(memory_, is_64kb(target.memory_size)),
m6847_(sam_, sam_),
tape_player_(int(ClockRate)),
audio_(audio_queue_, MaxDACLevel),
speaker_(audio_)
{
set_clock_rate(ClockRate);
speaker_.set_input_rate(ClockRate / AudioDivider.as<double>());
construct_joysticks();
is_dragon_ = Analyser::Static::TandyCoCo::is_dragon(target.model);
auto request = [&]() {
if(is_dragon_) {
if(is_64kb(target.memory_size)) {
return ROM::Request(ROM::Name::Dragon64ROM1) && ROM::Request(ROM::Name::Dragon64ROM2);
} else {
return ROM::Request(ROM::Name::Dragon32);
}
}
auto request = ROM::Request(ROM::Name::TandyExtendedBASIC10) || ROM::Request(ROM::Name::TandyExtendedBASIC11);
if(!has_disk_drive) request = request.optional();
if(has_disk_drive) {
auto disk_request = ROM::Request(ROM::Name::TandyCoCoDiskBASIC10) || ROM::Request(ROM::Name::TandyCoCoDiskBASIC11) || ROM::Request(ROM::Name::TandyCoCoDiskBASIC21);
request = request && disk_request;
}
// BASIC 1.3 is not used unless explicitly requested for now, as it doesn't 'just work' with disks
// as the rest of this code proceeds. I don't know whether its fussy about other ROMs or whether I've
// got a blocking implementation issue elsewhere.
switch(target.rom_version) {
using enum Analyser::Static::TandyCoCo::Target::ROMVersion;
case V10: request = request && ROM::Request(ROM::Name::TandyCoCoColourBasic10); break;
case V11: request = request && ROM::Request(ROM::Name::TandyCoCoColourBasic11); break;
case V12: request = request && ROM::Request(ROM::Name::TandyCoCoColourBasic12); break;
case V13: request = request && ROM::Request(ROM::Name::TandyCoCoColourBasic13); break;
default: {
auto basic_request =
ROM::Request(ROM::Name::TandyCoCoColourBasic11) ||
ROM::Request(ROM::Name::TandyCoCoColourBasic12);
if(target.rom_version != V11OrAbove) {
basic_request = basic_request || ROM::Request(ROM::Name::TandyCoCoColourBasic10);
}
request = request && basic_request;
} break;
}
return request;
} ();
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
const auto any_of = [&](const std::initializer_list<ROM::Name> &options) -> std::optional<std::vector<uint8_t>> {
for(auto &key: options) {
const auto rom = roms.find(key);
if(rom != roms.end()) {
return rom->second;
}
}
return std::nullopt;
};
const auto basic = any_of({
ROM::Name::Dragon64ROM1,
ROM::Name::Dragon32,
ROM::Name::TandyCoCoColourBasic13,
ROM::Name::TandyCoCoColourBasic12,
ROM::Name::TandyCoCoColourBasic11,
ROM::Name::TandyCoCoColourBasic10,
});
sam_.set_basic(*basic);
const auto extended_basic = any_of({
ROM::Name::TandyExtendedBASIC11,
ROM::Name::TandyExtendedBASIC10,
});
if(extended_basic.has_value()) {
sam_.set_extended_basic(*extended_basic);
}
const auto alternate_basic = any_of({
ROM::Name::Dragon64ROM2,
});
if(alternate_basic.has_value()) {
sam_.set_alternate_basic(*alternate_basic);
} else {
sam_.set_no_alternate_basic();
}
const auto disk_rom = any_of({
ROM::Name::TandyCoCoDiskBASIC21,
ROM::Name::TandyCoCoDiskBASIC11,
ROM::Name::TandyCoCoDiskBASIC10,
});
if(disk_rom.has_value()) {
sam_.insert_cartridge(*disk_rom);
}
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
) {
// TODO, maybe: pull the switch inside this SAM call outside the loop?
const auto delay = sam_.cycle_cost<bus_phase, read_write>(address, bus_phase_);
const auto duration = delay + CPU::M6809::duration<Cycles>(bus_phase);
bus_phase_ += duration;
bus_phase_ &= 1;
if constexpr (has_disk_drive) {
// Update after the fact due to the internal mechanics of WD177x posting; these signals from the WD should
// occur when it observes that the access cycle is over. It is now over because the next has begun.
m6809_.template set<CPU::M6809::Line::NMI>(disk_controller_.nmi());
m6809_.template set<CPU::M6809::Line::Halt>(disk_controller_.halt());
// Multiply by 4.5 to get very close to 8Mhz for the controller.
cycles_16mhz_ += duration * 9;
disk_controller_.run_for(cycles_16mhz_.divide(2));
}
if(m6847_ += duration) {
pia0_.template set<Motorola::MC6821::Control::CA1>(m6847_.get()->hsync());
pia0_.template set<Motorola::MC6821::Control::CB1>(m6847_.get()->fsync());
}
tape_player_.run_for(duration);
if(typer_) {
typer_->run_for(duration);
}
time_since_audio_update_ += duration;
if(!has_disk_drive && sam_.has_cartridge()) {
// When a cartridge is inserted: "the clock signal (Q) is shorted to the cartridge interrupt pin."
pia1_.template set<Motorola::MC6821::Control::CB1>(true);
pia1_.template set<Motorola::MC6821::Control::CB1>(false);
}
using namespace CPU::M6809;
if(address >> 8 == 0xff) {
if constexpr (read_write != ReadWrite::NoData) {
switch(address) {
default: printf("Unhandled at %04x\n", +address); break;
case 0xff00: case 0xff04: case 0xff08: case 0xff0c:
case 0xff10: case 0xff14: case 0xff18: case 0xff1c:
access<0xff00, read_write>(pia0_, value);
break;
case 0xff01: case 0xff05: case 0xff09: case 0xff0d:
case 0xff11: case 0xff15: case 0xff19: case 0xff1d:
access<0xff01, read_write>(pia0_, value);
break;
case 0xff02: case 0xff06: case 0xff0a: case 0xff0e:
case 0xff12: case 0xff16: case 0xff1a: case 0xff1e:
access<0xff02, read_write>(pia0_, value);
break;
case 0xff03: case 0xff07: case 0xff0b: case 0xff0f:
case 0xff13: case 0xff17: case 0xff1b: case 0xff1f:
access<0xff03, read_write>(pia0_, value);
break;
case 0xff20: case 0xff24: case 0xff28: case 0xff2c:
case 0xff30: case 0xff34: case 0xff38: case 0xff3c:
access<0xff20, read_write>(pia1_, value);
break;
case 0xff21: case 0xff25: case 0xff29: case 0xff2d:
case 0xff31: case 0xff35: case 0xff39: case 0xff3d:
access<0xff21, read_write>(pia1_, value);
break;
case 0xff22: case 0xff26: case 0xff2a: case 0xff2e:
case 0xff32: case 0xff36: case 0xff3a: case 0xff3e:
access<0xff22, read_write>(pia1_, value);
break;
case 0xff23: case 0xff27: case 0xff2b: case 0xff2f:
case 0xff33: case 0xff37: case 0xff3b: case 0xff3f:
access<0xff23, read_write>(pia1_, value);
break;
case 0xff40: if(has_disk_drive) access<0xff40, read_write>(disk_controller_, value); break;
case 0xff48: if(has_disk_drive) access<0xff48, read_write>(disk_controller_, value); break;
case 0xff49: if(has_disk_drive) access<0xff49, read_write>(disk_controller_, value); break;
case 0xff4a: if(has_disk_drive) access<0xff4a, read_write>(disk_controller_, value); break;
case 0xff4b: if(has_disk_drive) access<0xff4b, read_write>(disk_controller_, value); break;
case 0xffc0: sam_.access<0xffc0, read_write>(); break; case 0xffc1: sam_.access<0xffc1, read_write>(); break;
case 0xffc2: sam_.access<0xffc2, read_write>(); break; case 0xffc3: sam_.access<0xffc3, read_write>(); break;
case 0xffc4: sam_.access<0xffc4, read_write>(); break; case 0xffc5: sam_.access<0xffc5, read_write>(); break;
case 0xffc6: sam_.access<0xffc6, read_write>(); break; case 0xffc7: sam_.access<0xffc7, read_write>(); break;
case 0xffc8: sam_.access<0xffc8, read_write>(); break; case 0xffc9: sam_.access<0xffc9, read_write>(); break;
case 0xffca: sam_.access<0xffca, read_write>(); break; case 0xffcb: sam_.access<0xffcb, read_write>(); break;
case 0xffcc: sam_.access<0xffcc, read_write>(); break; case 0xffcd: sam_.access<0xffcd, read_write>(); break;
case 0xffce: sam_.access<0xffce, read_write>(); break; case 0xffcf: sam_.access<0xffcf, read_write>(); break;
case 0xffd0: sam_.access<0xffd0, read_write>(); break; case 0xffd1: sam_.access<0xffd1, read_write>(); break;
case 0xffd2: sam_.access<0xffd2, read_write>(); break; case 0xffd3: sam_.access<0xffd3, read_write>(); break;
case 0xffd4: sam_.access<0xffd4, read_write>(); break; case 0xffd5: sam_.access<0xffd5, read_write>(); break;
case 0xffd6: sam_.access<0xffd6, read_write>(); break; case 0xffd7: sam_.access<0xffd7, read_write>(); break;
case 0xffd8: sam_.access<0xffd8, read_write>(); break; case 0xffd9: sam_.access<0xffd9, read_write>(); break;
case 0xffda: sam_.access<0xffda, read_write>(); break; case 0xffdb: sam_.access<0xffdb, read_write>(); break;
case 0xffdc: sam_.access<0xffdc, read_write>(); break; case 0xffdd: sam_.access<0xffdd, read_write>(); break;
case 0xffde: sam_.access<0xffde, read_write>(); break; case 0xffdf: sam_.access<0xffdf, read_write>(); break;
case 0xfff2: case 0xfff3: case 0xfff4: case 0xfff5:
case 0xfff6: case 0xfff7: case 0xfff8: case 0xfff9:
case 0xfffa: case 0xfffb: case 0xfffc: case 0xfffd:
case 0xfffe: case 0xffff:
if constexpr (is_read(read_write)) {
value = memory_.read(address - 0x4000);
}
break;
}
}
} else {
if constexpr (is_read(read_write)) {
value = memory_.read(address);
if constexpr (lic == CPU::M6809::LIC::InstructionFetch) {
if(allow_fast_tape_loading_ && tape_player_.motor_control() && address == 0xa75d) {
// To reproduce:
//
// LA75D CLR CPERTM ; RESET PERIOD TIMER
// TST CBTPHA ; CHECK TO SEE IF SYNC’ED ON THE HI-LO TRANSITION OR LO-HI
// BNE LA773 ; BRANCH ON HI-LO TRANSITION
//
// * LO - HI TRANSITION
// LA763 BSR LA76C ; READ CASSETTE INPUT BIT 8D 07; 7 cycles
// BCS LA763 ; LOOP UNTIL IT IS LO 25 FC; 3 cycles
// LA767 BSR LA76C ; READ CASSETTE INPUT DATA
// BCC LA767 ; WAIT UNTIL IT GOES HI
// RTS
//
// * READ CASSETTE INPUT BIT OF THE PIA
// LA76C INC CPERTM ; INCREMENT PERIOD TIMER 0C 83; 6 cycles
// LDB PIA1 ; GET CASSETTE INPUT BIT F6 FF 20; 5 cycles
// RORB ; PUT CASSETTE BIT INTO THE CARRY FLAG 56; 2 cycles
// RTS 39; 5 cycles
//
// * WAIT FOR HI - LO TRANSITION
// LA773 BSR LA76C ; READ CASSETTE INPUT DATA
// BCC LA773 ; LOOP UNTIL IT IS HI
// LA777 BSR LA76C ; READ CASSETTE INPUT
// BCS LA777 ; LOOP UNTIL IT IS LO
// RTS
//
// where: CPERTM = $0083; CBTPHA = $0084.
//
// The value of B is immediately discared by the caller so all that's needed is to set
// CPERTM to the amount of time taken to observe the two transitions required, divided
// by 28 cycles = 28/894886.25 s.
//
bool polarity = memory_.read(0x0084);
Cycles::IntType total = 0;
for(int c = 0; c < 2; c++) {
while(tape_player_.input() != polarity) {
total += tape_player_.get_cycles_until_next_event();
tape_player_.run_for_input_pulse();
}
polarity ^= true;
}
memory_.write(0x0083, uint8_t(total / 56));
value = 0x39;
}
}
}
if constexpr (is_write(read_write)) {
// TODO: could do better with the test below by having SAM calculate a range?
const uint16_t video_start = sam_.graphics_address();
if(address >= video_start && address < video_start + 6144) {
m6847_.flush();
}
memory_.write(address, value);
}
}
return delay;
}
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 memory_;
// MARK: - TimedMachine.
void run_for(const Cycles cycles) final {
m6809_.run_for(cycles);
}
void flush_output(const int outputs) final {
if(outputs & Output::Video) {
m6847_.flush();
}
if(outputs & Output::Audio) {
update_audio();
audio_queue_.perform();
}
}
// MARK: - Resettable.
void soft_reset() final {
m6847_->toggle_colour_phase();
sam_.reset();
m6809_.template set<CPU::M6809::Line::PowerOnReset>(true);
}
// MARK: - PIAs.
friend struct PIA0Handler;
struct PIA0Handler {
PIA0Handler(ConcreteMachine &machine) : machine_(machine) {
clear_all_keys();
}
//
// Port A:
//
// b7: joystick comparison input
// b0–b6: keyboard row [input?]
// b0–b1, b2–b3: joystick button inputs;
// 0 and 2 = right joystick; 1 and 3 = left joystick;
// 0 and 1 = switch 1; 2 and 3 = switch 2.
//
// CA1: horizontal sync
// CA2: select line LSB of joystick MUX
//
// Port B:
//
// b0-b7: keyboard column [output?]
//
// CB1: field sync
// CB2: select line MSB of joystick MUX
// Interrupt outputs both connected to IRQ.
template <Motorola::MC6821::Port port>
uint8_t input() {
if constexpr (port == Motorola::MC6821::Port::A) {
return
(keyboard_column_ & 0x80 ? 0xff : key_columns_[7]) &
(keyboard_column_ & 0x40 ? 0xff : key_columns_[6]) &
(keyboard_column_ & 0x20 ? 0xff : key_columns_[5]) &
(keyboard_column_ & 0x10 ? 0xff : key_columns_[4]) &
(keyboard_column_ & 0x08 ? 0xff : key_columns_[3]) &
(keyboard_column_ & 0x04 ? 0xff : key_columns_[2]) &
(keyboard_column_ & 0x02 ? 0xff : key_columns_[1]) &
(keyboard_column_ & 0x01 ? 0xff : key_columns_[0]) &
(
machine_.dac_level_ > machine_.joystick(joystick_).axes[axis_]
? 0x7f : 0xff
) &
(machine_.joystick(1).buttons[1] ? 0xf7 : 0xff) &
(machine_.joystick(0).buttons[1] ? 0xfb : 0xff) &
(machine_.joystick(1).buttons[0] ? 0xfd : 0xff) &
(machine_.joystick(0).buttons[0] ? 0xfe : 0xff);
}
if constexpr (port == Motorola::MC6821::Port::B) {
return
(keyboard_row_ & 0x80 ? 0xff : key_rows_[7]) &
(keyboard_row_ & 0x40 ? 0xff : key_rows_[6]) &
(keyboard_row_ & 0x20 ? 0xff : key_rows_[5]) &
(keyboard_row_ & 0x10 ? 0xff : key_rows_[4]) &
(keyboard_row_ & 0x08 ? 0xff : key_rows_[3]) &
(keyboard_row_ & 0x04 ? 0xff : key_rows_[2]) &
(keyboard_row_ & 0x02 ? 0xff : key_rows_[1]) &
(keyboard_row_ & 0x01 ? 0xff : key_rows_[0]);
}
__builtin_unreachable();
}
template <Motorola::MC6821::Port port>
void output(const uint8_t value) {
if constexpr (port == Motorola::MC6821::Port::A) {
keyboard_row_ = value;
}
if constexpr (port == Motorola::MC6821::Port::B) {
keyboard_column_ = value;
}
}
template <Motorola::MC6821::IRQ irq>
void set(const bool active) {
if constexpr (irq == Motorola::MC6821::IRQ::A) {
machine_.set_irq<0>(active);
}
if constexpr (irq == Motorola::MC6821::IRQ::B) {
machine_.set_irq<1>(active);
}
}
template <Motorola::MC6821::Control control>
void observe(const bool value) {
if constexpr (control == Motorola::MC6821::Control::CA2) {
axis_ = value;
machine_.mux_ = (machine_.mux_ & 1) | (value ? 2 : 0);
}
if constexpr (control == Motorola::MC6821::Control::CB2) {
joystick_ = value;
machine_.mux_ = (machine_.mux_ & 2) | (value ? 1 : 0);
}
// Changes to the MUX might affect audio output, so give it a bump.
machine_.set_audio(std::nullopt, std::nullopt);
}
void set_key_pressed(const int column, const int row, bool is_pressed) {
const auto column_mask = uint8_t(1 << column);
const auto row_mask = uint8_t(1 << row);
key_columns_[row] &= ~column_mask;
key_rows_[column] &= ~row_mask;
if(!is_pressed) {
key_columns_[row] |= column_mask;
key_rows_[column] |= row_mask;
}
}
void clear_all_keys() {
std::fill(std::begin(key_columns_), std::end(key_columns_), 0xff);
std::fill(std::begin(key_rows_), std::end(key_rows_), 0xff);
}
private:
ConcreteMachine &machine_;
uint8_t keyboard_column_ = 0xff, keyboard_row_ = 0xff;
uint8_t key_columns_[8]{}, key_rows_[8]{};
uint8_t joystick_ = 0;
uint8_t axis_ = 0;
};
PIA0Handler pia0_handler_;
Motorola::MC6821::MC6821<PIA0Handler> pia0_;
bool irqs_[2]{};
template <int slot>
void set_irq(const bool active) {
irqs_[slot] = active;
m6809_.template set<CPU::M6809::Line::IRQ>(irqs_[0] || irqs_[1]);
}
bool firqs_[2]{};
template <int slot>
void set_firq(const bool active) {
firqs_[slot] = active;
m6809_.template set<CPU::M6809::Line::FIRQ>(firqs_[0] || firqs_[1]);
}
friend struct PIA1Handler;
struct PIA1Handler {
PIA1Handler(ConcreteMachine &machine) : machine_(machine) {}
//
// Port A:
//
// b2-b7: 6-bit DAC output
// b1: RS232 data output
// b0: tape input
//
// CA1: RS232 carrier detect
// CA2: tape motor control
//
// Port B:
//
// b7: 6847 alpha/graphics select (0 = alphanumeric)
// b4–b6: VDG GM inputs; also b5 = 6847 invert; b4 = 6847 shift toggle
// b3: colour set select (and RGB monitor detecting input? Probably CoCo3)
// b2: ram size input
// b1: 1-bit audio output
// b0: RS232 data input
//
// Note to self: semigraphic 6847 line is connected to b7 of the data bus; it is set or unset depending on
// the data fetched for that column.
//
// CB1: cartridge interrupt input
// CB2: sound enable
// Interrupt output connected to FIRQ.
template <Motorola::MC6821::Port port>
uint8_t input() {
if constexpr (port == Motorola::MC6821::Port::A) {
return
0xfe |
(machine_.tape_player_.input() ? 0x01 : 0x00);
}
if constexpr (port == Motorola::MC6821::Port::B) {
return 0xff;
}
__builtin_unreachable();
}
template <Motorola::MC6821::Port port>
void output(const uint8_t value) {
if constexpr (port == Motorola::MC6821::Port::A) {
machine_.dac_level_ = value >> 2;
// The DAC might be the output, so allow it to inspect.
machine_.set_audio(std::nullopt, std::nullopt);
}
if constexpr (port == Motorola::MC6821::Port::B) {
machine_.set_audio(std::nullopt, value & 0x2);
machine_.m6847_->set_mode(
value & 0x80, // Alpha/graphics.
false, // Graphics/semigraphics.
value & 0x10, // External ROM.
false, // Invert.
(value >> 4) & 7, // Graphics mode.
value & 0x08 // Colour select.
);
}
}
template <Motorola::MC6821::IRQ irq>
void set(const bool active) {
if constexpr (irq == Motorola::MC6821::IRQ::A) {
machine_.set_firq<0>(active);
}
if constexpr (irq == Motorola::MC6821::IRQ::B) {
machine_.set_firq<1>(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) {
machine_.set_audio(value, std::nullopt);
}
}
private:
ConcreteMachine &machine_;
};
PIA1Handler pia1_handler_;
Motorola::MC6821::MC6821<PIA1Handler> pia1_;
// MARK: - SAM.
Cycles bus_phase_;
SAM sam_;
// MARK: - Video and ScanProducer.
JustInTimeActor<
Motorola::MC6847::MC6847<
is_pal ? Motorola::MC6847::FrameTiming::PALPaddedVsync : Motorola::MC6847::FrameTiming::NTSC,
SAM,
SAM,
ModeMapper
>,
Cycles,
4
> m6847_;
void set_scan_target(Outputs::Display::ScanTarget *const target) final {
m6847_.get()->set_scan_target(target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return m6847_.get()->get_scaled_scan_status() / 4.0;
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
m6847_.get()->set_display_type(display_type);
}
Outputs::Display::DisplayType get_display_type() const final {
return m6847_.get()->get_display_type();
}
// MARK: - MappedKeyboardMachine.
bool is_dragon_ = false;
Tandy::CoCo::Keyboard::KeyboardMapper keyboard_mapper_;
KeyboardMapper *keyboard_mapper() final {
return &keyboard_mapper_;
}
void set_key_state(const uint16_t key, const bool is_pressed) final {
// The CoCo and Dragon differ in columns; a mapping from CoCo to Dragon column is:
//
// 0 -> 2
// 1 -> 3
// 2 -> 4
// 3 -> 5
// 4 -> 0
// 5 -> 1
// 6 -> 6
// 7 -> 7
const auto mapped_column = [&](const int column) {
if(!is_dragon_ || column >= 6) {
return column;
}
return (column + 2) % 6;
};
pia0_handler_.set_key_pressed(
mapped_column(Keyboard::column(key)),
Keyboard::row(key),
is_pressed
);
}
void clear_all_keys() final {
pia0_handler_.clear_all_keys();
}
void type_string(const std::wstring &string) final {
Utility::TypeRecipient<Tandy::CoCo::Keyboard::CharacterMapper>::add_typer(string);
}
bool can_type(const wchar_t c) const final {
return Utility::TypeRecipient<Tandy::CoCo::Keyboard::CharacterMapper>::can_type(c);
}
HalfCycles typer_delay(const std::wstring &) const final {
if(m6809_.template get<CPU::M6809::Line::PowerOnReset>()) {
return Cycles(2'750'000);
} else {
return Cycles(0);
}
}
HalfCycles typer_frequency() const final {
return Cycles(40'000);
}
// MARK: - MediaTarget and MediaChangeObserver.
Storage::Tape::BinaryTapePlayer tape_player_;
bool allow_fast_tape_loading_ = true;
bool insert_media(const Analyser::Static::Media &media) override {
if(!media.tapes.empty()) {
tape_player_.set_tape(media.tapes.front(), TargetPlatform::ThomsonMO);
}
if(has_disk_drive && !media.disks.empty()) {
for(size_t c = 0; c < media.disks.size() && c < 4; c++) {
disk_controller_.set_disk(media.disks[c], c);
}
}
const bool had_cartridge =
!has_disk_drive &&
!media.cartridges.empty() &&
sam_.insert_cartridge(media.cartridges.front()->segments().front().data);
return !media.tapes.empty() || had_cartridge || (has_disk_drive && !media.disks.empty());
}
ChangeEffect effect_for_file_did_change(const std::string &) const override {
return ChangeEffect::RestartMachine;
}
// 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_;
options->output = get_video_signal_configurable();
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;
set_video_signal_configurable(options->output);
}
// MARK: - Activity Source.
void set_activity_observer(Activity::Observer *const observer) override {
tape_player_.set_activity_observer(observer);
if(has_disk_drive) {
disk_controller_.set_activity_observer(observer);
}
}
// MARK: - Joysticks.
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
void construct_joysticks() {
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}
Joystick &joystick(const size_t index) {
return *static_cast<Joystick *>(joysticks_[index].get());
}
// 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_.divide(AudioDivider);
speaker_.run_for(audio_queue_, cycles);
}
Outputs::Speaker::Speaker *get_speaker() override {
return &speaker_;
}
uint8_t dac_level_ = 0;
uint8_t mux_ = 0;
bool audio_enabled_ = false;
bool audio_toggle_ = false;
uint8_t audio_output_ = 0;
void set_audio(const std::optional<bool> enabled, std::optional<bool> toggle) {
const auto new_audio_enabled = enabled.value_or(audio_enabled_);
audio_enabled_ = new_audio_enabled;
const auto new_audio_toggle = toggle.value_or(audio_toggle_);
audio_toggle_ = new_audio_toggle;
// When audio is enabled, the MUX determines the audio source. Source 0 is the DAC.
const auto new_audio_output = [&]() -> uint8_t {
if(!audio_enabled_) {
return audio_toggle_ ? 48 : 0;
}
switch(mux_) {
default: return 0;
case 0: return dac_level_;
// Omitted at present (with a possibility that I've read the two mux lines the wrong way around):
//
// 1 -> cassette;
// 2 -> cartridge;
// 3 -> not used.
}
} ();
if(new_audio_output == audio_output_) {
return;
}
audio_output_ = new_audio_output;
update_audio();
audio_.set_output(new_audio_output);
}
// MARK: - Disk.
DiskController disk_controller_;
Cycles cycles_16mhz_;
};
}
namespace {
template <bool has_disk_drive>
std::unique_ptr<Machine> create(
const Analyser::Static::TandyCoCo::Target &target,
const ROMMachine::ROMFetcher &rom_fetcher
) {
if(Analyser::Static::TandyCoCo::is_pal(target.model)) {
return std::make_unique<TandyCoCo::ConcreteMachine<true, has_disk_drive>>(target, rom_fetcher);
} else {
return std::make_unique<TandyCoCo::ConcreteMachine<false, has_disk_drive>>(target, rom_fetcher);
}
}
}
std::unique_ptr<Machine> Machine::create(
const Analyser::Static::Target &target,
const ROMMachine::ROMFetcher &rom_fetcher
) {
const auto &coco_target = static_cast<const Analyser::Static::TandyCoCo::Target &>(target);
if(coco_target.has_disk_drive) {
return ::create<true>(coco_target, rom_fetcher);
} else {
return ::create<false>(coco_target, rom_fetcher);
}
}