1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-07 23:29:06 +00:00
CLK/Machines/MSX/MSX.cpp

446 lines
12 KiB
C++
Raw Normal View History

2017-11-25 01:43:26 +00:00
//
// MSX.cpp
// Clock Signal
//
// Created by Thomas Harte on 24/11/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#include "MSX.hpp"
#include "Keyboard.hpp"
#include "../../Processors/Z80/Z80.hpp"
#include "../../Components/1770/1770.hpp"
#include "../../Components/9918/9918.hpp"
#include "../../Components/8255/i8255.hpp"
#include "../../Components/AY38910/AY38910.hpp"
#include "../../Storage/Tape/Parsers/MSX.hpp"
#include "../../Storage/Tape/Tape.hpp"
2017-11-25 18:33:51 +00:00
#include "../CRTMachine.hpp"
#include "../ConfigurationTarget.hpp"
#include "../KeyboardMachine.hpp"
2017-11-25 18:33:51 +00:00
#include "../../Outputs/Speaker/Implementation/CompoundSource.hpp"
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
2017-11-25 01:43:26 +00:00
namespace MSX {
/*!
Provides a sample source that can programmatically be set to one of two values.
*/
class AudioToggle: public Outputs::Speaker::SampleSource {
public:
AudioToggle(Concurrency::DeferringAsyncTaskQueue &audio_queue) :
audio_queue_(audio_queue) {}
void get_samples(std::size_t number_of_samples, std::int16_t *target) {
for(std::size_t sample = 0; sample < number_of_samples; ++sample) {
target[sample] = level_;
}
}
void skip_samples(const std::size_t number_of_samples) {}
void set_output(bool enabled) {
if(is_enabled_ == enabled) return;
is_enabled_ = enabled;
audio_queue_.defer([=] {
level_ = enabled ? 4096 : 0;
});
}
bool get_output() {
return is_enabled_;
}
private:
bool is_enabled_ = false;
int16_t level_ = 0;
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
};
class AYPortHandler: public GI::AY38910::PortHandler {
public:
AYPortHandler(Storage::Tape::BinaryTapePlayer &tape_player) : tape_player_(tape_player) {}
void set_port_output(bool port_b, uint8_t value) {
if(port_b) {
// Bits 03: touchpad handshaking (?)
// Bit 4—5: monostable timer pulses
// Bit 6: joystick select
// Bit 7: code LED, if any
}
}
uint8_t get_port_input(bool port_b) {
if(!port_b) {
// Bits 05: Joystick (up, down, left, right, A, B)
// Bit 6: keyboard switch (not universal)
// Bit 7: tape input
return 0x7f | (tape_player_.get_input() ? 0x00 : 0x80);
}
return 0xff;
}
2017-11-30 01:31:55 +00:00
private:
Storage::Tape::BinaryTapePlayer &tape_player_;
};
2017-11-25 01:43:26 +00:00
class ConcreteMachine:
2017-11-25 18:33:51 +00:00
public Machine,
public CPU::Z80::BusHandler,
public CRTMachine::Machine,
public ConfigurationTarget::Machine,
public KeyboardMachine::Machine {
public:
ConcreteMachine():
z80_(*this),
i8255_(i8255_port_handler_),
ay_(audio_queue_),
audio_toggle_(audio_queue_),
mixer_(ay_, audio_toggle_),
speaker_(mixer_),
tape_player_(3579545 * 2),
i8255_port_handler_(*this, audio_toggle_, tape_player_),
ay_port_handler_(tape_player_) {
2017-11-25 18:33:51 +00:00
set_clock_rate(3579545);
std::memset(unpopulated_, 0xff, sizeof(unpopulated_));
clear_all_keys();
ay_.set_port_handler(&ay_port_handler_);
speaker_.set_input_rate(3579545.0f / 2.0f);
2017-11-25 18:33:51 +00:00
}
void setup_output(float aspect_ratio) override {
vdp_.reset(new TI::TMS9918(TI::TMS9918::TMS9918A));
2017-11-25 18:33:51 +00:00
}
void close_output() override {
2017-12-02 21:30:43 +00:00
vdp_.reset();
2017-11-25 18:33:51 +00:00
}
Outputs::CRT::CRT *get_crt() override {
return vdp_->get_crt();
2017-11-25 18:33:51 +00:00
}
Outputs::Speaker::Speaker *get_speaker() override {
return &speaker_;
2017-11-25 18:33:51 +00:00
}
void run_for(const Cycles cycles) override {
z80_.run_for(cycles);
}
void configure_as_target(const StaticAnalyser::Target &target) override {
insert_media(target.media);
}
2017-12-03 19:52:42 +00:00
bool insert_media(const StaticAnalyser::Media &media) override {
if(!media.cartridges.empty()) {
const auto &segment = media.cartridges.front()->get_segments().front();
cartridge_ = segment.data;
// TODO: should clear other page 1 pointers, should allow for paging cartridges, etc.
size_t base = segment.start_address >> 14;
for(size_t c = 0; c < cartridge_.size(); c += 16384) {
memory_slots_[1].read_pointers[(c >> 14) + base] = cartridge_.data() + c;
}
}
if(!media.tapes.empty()) {
tape_player_.set_tape(media.tapes.front());
}
return true;
}
void page_memory(uint8_t value) {
2017-11-30 01:49:02 +00:00
for(size_t c = 0; c < 4; ++c) {
read_pointers_[c] = memory_slots_[value & 3].read_pointers[c];
write_pointers_[c] = memory_slots_[value & 3].write_pointers[c];
2017-11-30 01:49:02 +00:00
value >>= 2;
}
}
HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
2017-11-30 01:31:55 +00:00
if(time_until_interrupt_ > 0) {
time_until_interrupt_ -= cycle.length;
if(time_until_interrupt_ <= HalfCycles(0)) {
z80_.set_interrupt_line(true, time_until_interrupt_);
}
}
uint16_t address = cycle.address ? *cycle.address : 0x0000;
switch(cycle.operation) {
case CPU::Z80::PartialMachineCycle::ReadOpcode:
if(address == 0x1a63) {
// TAPION
tape_player_.set_motor_control(true);
using Parser = Storage::Tape::MSX::Parser;
std::unique_ptr<Parser::FileSpeed> new_speed = Parser::find_header(tape_player_);
ram_[0xFCA4] = new_speed->minimum_start_bit_duration;
ram_[0xFCA5] = new_speed->low_high_disrimination_duration;
*cycle.value = 0xc9;
break;
}
if(address == 0x1abc) {
// TAPIN
using Parser = Storage::Tape::MSX::Parser;
Parser::FileSpeed tape_speed;
tape_speed.minimum_start_bit_duration = ram_[0xFCA4];
tape_speed.low_high_disrimination_duration = ram_[0xFCA5];
// printf("Low lim: %02x / win wid: %02x\n", ram_[0xFCA4], ram_[0xFCA5]);
int next_byte = Parser::get_byte(tape_speed, tape_player_);
z80_.set_value_of_register(CPU::Z80::Register::A, static_cast<uint16_t>(next_byte));
*cycle.value = 0xc9;
break;
}
case CPU::Z80::PartialMachineCycle::Read:
*cycle.value = read_pointers_[address >> 14][address & 16383];
break;
case CPU::Z80::PartialMachineCycle::Write:
write_pointers_[address >> 14][address & 16383] = *cycle.value;
break;
case CPU::Z80::PartialMachineCycle::Input:
switch(address & 0xff) {
case 0x98: case 0x99:
2017-12-02 21:30:43 +00:00
vdp_->run_for(time_since_vdp_update_.flush());
*cycle.value = vdp_->get_register(address);
2017-11-30 01:31:55 +00:00
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 0xa2:
update_audio();
ay_.set_control_lines(static_cast<GI::AY38910::ControlLines>(GI::AY38910::BC2 | GI::AY38910::BC1));
*cycle.value = ay_.get_data_output();
ay_.set_control_lines(static_cast<GI::AY38910::ControlLines>(0));
break;
case 0xa8: case 0xa9:
case 0xaa: case 0xab:
*cycle.value = i8255_.get_register(address);
break;
default:
*cycle.value = 0xff;
break;
}
break;
case CPU::Z80::PartialMachineCycle::Output: {
const int port = address & 0xff;
switch(port) {
case 0x98: case 0x99:
2017-12-02 21:30:43 +00:00
vdp_->run_for(time_since_vdp_update_.flush());
vdp_->set_register(address, *cycle.value);
2017-11-30 01:31:55 +00:00
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 0xa0: case 0xa1:
update_audio();
ay_.set_control_lines(static_cast<GI::AY38910::ControlLines>(GI::AY38910::BDIR | GI::AY38910::BC2 | ((port == 0xa0) ? GI::AY38910::BC1 : 0)));
ay_.set_data_input(*cycle.value);
ay_.set_control_lines(static_cast<GI::AY38910::ControlLines>(0));
break;
case 0xa8: case 0xa9:
case 0xaa: case 0xab:
i8255_.set_register(address, *cycle.value);
break;
case 0xfc: case 0xfd: case 0xfe: case 0xff:
// printf("RAM banking %02x: %02x\n", port, *cycle.value);
break;
}
} break;
case CPU::Z80::PartialMachineCycle::Interrupt:
*cycle.value = 0xff;
break;
default: break;
}
// Update the tape. (TODO: allow for sleeping)
tape_player_.run_for(cycle.length.as_int());
// Per the best information I currently have, the MSX inserts an extra cycle into each opcode read,
// but otherwise runs without pause.
HalfCycles addition((cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode) ? 2 : 0);
time_since_vdp_update_ += cycle.length + addition;
2017-12-02 21:30:43 +00:00
time_since_ay_update_ += cycle.length + addition;
return addition;
}
void flush() {
2017-12-02 21:30:43 +00:00
vdp_->run_for(time_since_vdp_update_.flush());
update_audio();
audio_queue_.perform();
}
// Obtains the system ROMs.
bool set_rom_fetcher(const std::function<std::vector<std::unique_ptr<std::vector<uint8_t>>>(const std::string &machine, const std::vector<std::string> &names)> &roms_with_names) override {
auto roms = roms_with_names(
"MSX",
{
"msx.rom"
});
if(!roms[0]) return false;
rom_ = std::move(*roms[0]);
rom_.resize(32768);
for(size_t c = 0; c < 4; ++c) {
2017-11-30 01:49:02 +00:00
for(size_t slot = 0; slot < 3; ++slot) {
memory_slots_[slot].read_pointers[c] = unpopulated_;
memory_slots_[slot].write_pointers[c] = scratch_;
2017-11-30 01:49:02 +00:00
}
memory_slots_[3].read_pointers[c] =
memory_slots_[3].write_pointers[c] = &ram_[c * 16384];
2017-11-30 01:49:02 +00:00
}
memory_slots_[0].read_pointers[0] = rom_.data();
memory_slots_[0].read_pointers[1] = &rom_[16384];
2017-11-30 01:49:02 +00:00
for(size_t c = 0; c < 4; ++c) {
read_pointers_[c] = memory_slots_[0].read_pointers[c];
write_pointers_[c] = memory_slots_[0].write_pointers[c];
}
return true;
}
void set_keyboard_line(int line) {
selected_key_line_ = line;
}
uint8_t read_keyboard() {
return key_states_[selected_key_line_];
}
void clear_all_keys() override {
std::memset(key_states_, 0xff, sizeof(key_states_));
}
void set_key_state(uint16_t key, bool is_pressed) override {
int mask = 1 << (key & 7);
int line = key >> 4;
if(is_pressed) key_states_[line] &= ~mask; else key_states_[line] |= mask;
}
KeyboardMapper &get_keyboard_mapper() override {
return keyboard_mapper_;
}
private:
void update_audio() {
speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2)));
}
class i8255PortHandler: public Intel::i8255::PortHandler {
public:
i8255PortHandler(ConcreteMachine &machine, AudioToggle &audio_toggle, Storage::Tape::BinaryTapePlayer &tape_player) :
machine_(machine), audio_toggle_(audio_toggle), tape_player_(tape_player) {}
void set_value(int port, uint8_t value) {
switch(port) {
case 0: machine_.page_memory(value); break;
case 2: {
// TODO:
// b6 caps lock LED
// b5 audio output
// b4: cassette motor relay
tape_player_.set_motor_control(!(value & 0x10));
// b7: keyboard click
bool new_audio_level = !!(value & 0x80);
if(audio_toggle_.get_output() != new_audio_level) {
machine_.update_audio();
audio_toggle_.set_output(new_audio_level);
}
// b0b3: keyboard line
machine_.set_keyboard_line(value & 0xf);
} break;
default: printf("What what what what?\n"); break;
}
}
uint8_t get_value(int port) {
if(port == 1) {
return machine_.read_keyboard();
2017-11-30 01:31:55 +00:00
} else printf("What what?\n");
return 0xff;
}
private:
ConcreteMachine &machine_;
AudioToggle &audio_toggle_;
Storage::Tape::BinaryTapePlayer &tape_player_;
};
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
std::unique_ptr<TI::TMS9918> vdp_;
Intel::i8255::i8255<i8255PortHandler> i8255_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
GI::AY38910::AY38910 ay_;
AudioToggle audio_toggle_;
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910, AudioToggle> mixer_;
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910, AudioToggle>> speaker_;
Storage::Tape::BinaryTapePlayer tape_player_;
i8255PortHandler i8255_port_handler_;
AYPortHandler ay_port_handler_;
uint8_t *read_pointers_[4];
uint8_t *write_pointers_[4];
2017-11-30 01:49:02 +00:00
struct MemorySlots {
uint8_t *read_pointers[4];
uint8_t *write_pointers[4];
} memory_slots_[4];
2017-11-30 01:49:02 +00:00
uint8_t ram_[65536];
uint8_t scratch_[16384];
uint8_t unpopulated_[16384];
std::vector<uint8_t> rom_;
std::vector<uint8_t> cartridge_;
HalfCycles time_since_vdp_update_;
2017-12-02 21:30:43 +00:00
HalfCycles time_since_ay_update_;
2017-11-30 01:31:55 +00:00
HalfCycles time_until_interrupt_;
uint8_t key_states_[16];
int selected_key_line_ = 0;
MSX::KeyboardMapper keyboard_mapper_;
2017-11-25 01:43:26 +00:00
};
}
using namespace MSX;
Machine *Machine::MSX() {
return new ConcreteMachine;
}
Machine::~Machine() {}