mirror of
https://github.com/TomHarte/CLK.git
synced 2024-08-16 16:28:59 +00:00
1090 lines
33 KiB
C++
1090 lines
33 KiB
C++
//
|
||
// AmstradCPC.cpp
|
||
// Clock Signal
|
||
//
|
||
// Created by Thomas Harte on 30/07/2017.
|
||
// Copyright © 2017 Thomas Harte. All rights reserved.
|
||
//
|
||
|
||
#include "AmstradCPC.hpp"
|
||
|
||
#include "Keyboard.hpp"
|
||
|
||
#include "../../Processors/Z80/Z80.hpp"
|
||
|
||
#include "../../Components/6845/CRTC6845.hpp"
|
||
#include "../../Components/8255/i8255.hpp"
|
||
#include "../../Components/8272/i8272.hpp"
|
||
#include "../../Components/AY38910/AY38910.hpp"
|
||
|
||
#include "../Utility/MemoryFuzzer.hpp"
|
||
#include "../Utility/Typer.hpp"
|
||
|
||
#include "../ConfigurationTarget.hpp"
|
||
#include "../CRTMachine.hpp"
|
||
#include "../KeyboardMachine.hpp"
|
||
|
||
#include "../../Storage/Tape/Tape.hpp"
|
||
|
||
#include "../../ClockReceiver/ForceInline.hpp"
|
||
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
|
||
|
||
#include "../../Analyser/Static/AmstradCPC/Target.hpp"
|
||
|
||
#include <cstdint>
|
||
#include <vector>
|
||
|
||
namespace AmstradCPC {
|
||
|
||
enum ROMType: int {
|
||
OS464 = 0, BASIC464,
|
||
OS664, BASIC664,
|
||
OS6128, BASIC6128,
|
||
AMSDOS
|
||
};
|
||
|
||
/*!
|
||
Models the CPC's interrupt timer. Inputs are vsync, hsync, interrupt acknowledge and reset, and its output
|
||
is simply yes or no on whether an interupt is currently requested. Internally it uses a counter with a period
|
||
of 52 and occasionally adjusts or makes decisions based on bit 4.
|
||
|
||
Hsync and vsync signals are expected to come directly from the CRTC; they are not decoded from a composite stream.
|
||
*/
|
||
class InterruptTimer {
|
||
public:
|
||
/*!
|
||
Indicates that a new hsync pulse has been recognised. This should be
|
||
supplied on the falling edge of the CRTC HSYNC signal, which is the
|
||
trailing edge because it is active high.
|
||
*/
|
||
inline void signal_hsync() {
|
||
// Increment the timer and if it has hit 52 then reset it and
|
||
// set the interrupt request line to true.
|
||
timer_++;
|
||
if(timer_ == 52) {
|
||
timer_ = 0;
|
||
interrupt_request_ = true;
|
||
}
|
||
|
||
// If a vertical sync has previously been indicated then after two
|
||
// further horizontal syncs the timer should either (i) set the interrupt
|
||
// line, if bit 4 is clear; or (ii) reset the timer.
|
||
if(reset_counter_) {
|
||
reset_counter_--;
|
||
if(!reset_counter_) {
|
||
if(timer_ & 32) {
|
||
interrupt_request_ = true;
|
||
}
|
||
timer_ = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Indicates the leading edge of a new vertical sync.
|
||
inline void signal_vsync() {
|
||
reset_counter_ = 2;
|
||
}
|
||
|
||
/// Indicates that an interrupt acknowledge has been received from the Z80.
|
||
inline void signal_interrupt_acknowledge() {
|
||
interrupt_request_ = false;
|
||
timer_ &= ~32;
|
||
}
|
||
|
||
/// @returns @c true if an interrupt is currently requested; @c false otherwise.
|
||
inline bool get_request() {
|
||
return last_interrupt_request_ = interrupt_request_;
|
||
}
|
||
|
||
/// Asks whether the interrupt status has changed.
|
||
inline bool request_has_changed() {
|
||
return last_interrupt_request_ != interrupt_request_;
|
||
}
|
||
|
||
/// Resets the timer.
|
||
inline void reset_count() {
|
||
timer_ = 0;
|
||
interrupt_request_ = false;
|
||
}
|
||
|
||
private:
|
||
int reset_counter_ = 0;
|
||
bool interrupt_request_ = false;
|
||
bool last_interrupt_request_ = false;
|
||
int timer_ = 0;
|
||
};
|
||
|
||
/*!
|
||
Provides a holder for an AY-3-8910 and its current cycles-since-updated count.
|
||
Therefore acts both to store an AY and to bookkeep this emulator's idiomatic
|
||
deferred clocking for this component.
|
||
*/
|
||
class AYDeferrer {
|
||
public:
|
||
/// Constructs a new AY instance and sets its clock rate.
|
||
AYDeferrer() : ay_(audio_queue_), speaker_(ay_) {
|
||
speaker_.set_input_rate(1000000);
|
||
}
|
||
|
||
~AYDeferrer() {
|
||
audio_queue_.flush();
|
||
}
|
||
|
||
/// Adds @c half_cycles half cycles to the amount of time that has passed.
|
||
inline void run_for(HalfCycles half_cycles) {
|
||
cycles_since_update_ += half_cycles;
|
||
}
|
||
|
||
/// Enqueues an update-to-now into the AY's deferred queue.
|
||
inline void update() {
|
||
speaker_.run_for(audio_queue_, cycles_since_update_.divide_cycles(Cycles(4)));
|
||
}
|
||
|
||
/// Issues a request to the AY to perform all processing up to the current time.
|
||
inline void flush() {
|
||
audio_queue_.perform();
|
||
}
|
||
|
||
/// @returns the speaker the AY is using for output.
|
||
Outputs::Speaker::Speaker *get_speaker() {
|
||
return &speaker_;
|
||
}
|
||
|
||
/// @returns the AY itself.
|
||
GI::AY38910::AY38910 &ay() {
|
||
return ay_;
|
||
}
|
||
|
||
private:
|
||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||
GI::AY38910::AY38910 ay_;
|
||
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
|
||
HalfCycles cycles_since_update_;
|
||
};
|
||
|
||
/*!
|
||
Provides the mechanism of receipt for the CRTC outputs. In practice has the gate array's
|
||
video fetching and serialisation logic built in. So this is responsible for all video
|
||
generation and therefore owns details such as the current palette.
|
||
*/
|
||
class CRTCBusHandler {
|
||
public:
|
||
CRTCBusHandler(uint8_t *ram, InterruptTimer &interrupt_timer) :
|
||
ram_(ram),
|
||
interrupt_timer_(interrupt_timer) {
|
||
establish_palette_hits();
|
||
build_mode_table();
|
||
}
|
||
|
||
/*!
|
||
The CRTC entry function for the main part of each clock cycle; takes the current
|
||
bus state and determines what output to produce based on the current palette and mode.
|
||
*/
|
||
forceinline void perform_bus_cycle_phase1(const Motorola::CRTC::BusState &state) {
|
||
// The gate array waits 2µs to react to the CRTC's vsync signal, and then
|
||
// caps output at 4µs. Since the clock rate is 1Mhz, that's 2 and 4 cycles,
|
||
// respectively.
|
||
if(state.hsync) {
|
||
cycles_into_hsync_++;
|
||
} else {
|
||
cycles_into_hsync_ = 0;
|
||
}
|
||
|
||
bool is_hsync = (cycles_into_hsync_ >= 2 && cycles_into_hsync_ < 6);
|
||
|
||
// Sync is taken to override pixels, and is combined as a simple OR.
|
||
bool is_sync = is_hsync || state.vsync;
|
||
|
||
// If a transition between sync/border/pixels just occurred, flush whatever was
|
||
// in progress to the CRT and reset counting.
|
||
if(state.display_enable != was_enabled_ || is_sync != was_sync_) {
|
||
if(was_sync_) {
|
||
crt_->output_sync(cycles_ * 16);
|
||
} else {
|
||
if(was_enabled_) {
|
||
if(cycles_) {
|
||
crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_);
|
||
pixel_pointer_ = pixel_data_ = nullptr;
|
||
}
|
||
} else {
|
||
output_border(cycles_);
|
||
}
|
||
}
|
||
|
||
cycles_ = 0;
|
||
was_sync_ = is_sync;
|
||
was_enabled_ = state.display_enable;
|
||
}
|
||
|
||
// increment cycles since state changed
|
||
cycles_++;
|
||
|
||
// collect some more pixels if output is ongoing
|
||
if(!is_sync && state.display_enable) {
|
||
if(!pixel_data_) {
|
||
pixel_pointer_ = pixel_data_ = crt_->allocate_write_area(320, 8);
|
||
}
|
||
if(pixel_pointer_) {
|
||
// the CPC shuffles output lines as:
|
||
// MA13 MA12 RA2 RA1 RA0 MA9 MA8 MA7 MA6 MA5 MA4 MA3 MA2 MA1 MA0 CCLK
|
||
// ... so form the real access address.
|
||
uint16_t address =
|
||
static_cast<uint16_t>(
|
||
((state.refresh_address & 0x3ff) << 1) |
|
||
((state.row_address & 0x7) << 11) |
|
||
((state.refresh_address & 0x3000) << 2)
|
||
);
|
||
|
||
// fetch two bytes and translate into pixels
|
||
switch(mode_) {
|
||
case 0:
|
||
reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode0_output_[ram_[address]];
|
||
reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode0_output_[ram_[address+1]];
|
||
pixel_pointer_ += 4;
|
||
break;
|
||
|
||
case 1:
|
||
reinterpret_cast<uint32_t *>(pixel_pointer_)[0] = mode1_output_[ram_[address]];
|
||
reinterpret_cast<uint32_t *>(pixel_pointer_)[1] = mode1_output_[ram_[address+1]];
|
||
pixel_pointer_ += 8;
|
||
break;
|
||
|
||
case 2:
|
||
reinterpret_cast<uint64_t *>(pixel_pointer_)[0] = mode2_output_[ram_[address]];
|
||
reinterpret_cast<uint64_t *>(pixel_pointer_)[1] = mode2_output_[ram_[address+1]];
|
||
pixel_pointer_ += 16;
|
||
break;
|
||
|
||
case 3:
|
||
reinterpret_cast<uint16_t *>(pixel_pointer_)[0] = mode3_output_[ram_[address]];
|
||
reinterpret_cast<uint16_t *>(pixel_pointer_)[1] = mode3_output_[ram_[address+1]];
|
||
pixel_pointer_ += 4;
|
||
break;
|
||
|
||
}
|
||
|
||
// flush the current buffer pixel if full; the CRTC allows many different display
|
||
// widths so it's not necessarily possible to predict the correct number in advance
|
||
// and using the upper bound could lead to inefficient behaviour
|
||
if(pixel_pointer_ == pixel_data_ + 320) {
|
||
crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_);
|
||
pixel_pointer_ = pixel_data_ = nullptr;
|
||
cycles_ = 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/*!
|
||
The CRTC entry function for phase 2 of each bus cycle — in which the next sync line state becomes
|
||
visible early. The CPC uses changes in sync to clock the interrupt timer.
|
||
*/
|
||
void perform_bus_cycle_phase2(const Motorola::CRTC::BusState &state) {
|
||
// check for a trailing CRTC hsync; if one occurred then that's the trigger potentially to change
|
||
// modes, and should also be sent on to the interrupt timer
|
||
if(was_hsync_ && !state.hsync) {
|
||
if(mode_ != next_mode_) {
|
||
mode_ = next_mode_;
|
||
switch(mode_) {
|
||
default:
|
||
case 0: pixel_divider_ = 4; break;
|
||
case 1: pixel_divider_ = 2; break;
|
||
case 2: pixel_divider_ = 1; break;
|
||
}
|
||
build_mode_table();
|
||
}
|
||
|
||
interrupt_timer_.signal_hsync();
|
||
}
|
||
|
||
// check for a leading vsync; that also needs to be communicated to the interrupt timer
|
||
if(!was_vsync_ && state.vsync) {
|
||
interrupt_timer_.signal_vsync();
|
||
}
|
||
|
||
// update current state for edge detection next time around
|
||
was_vsync_ = state.vsync;
|
||
was_hsync_ = state.hsync;
|
||
}
|
||
|
||
/// Constructs an appropriate CRT for video output.
|
||
void setup_output(float aspect_ratio) {
|
||
crt_.reset(new Outputs::CRT::CRT(1024, 16, Outputs::CRT::DisplayType::PAL50, 1));
|
||
crt_->set_rgb_sampling_function(
|
||
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)"
|
||
"{"
|
||
"uint sample = texture(texID, coordinate).r;"
|
||
"return vec3(float((sample >> 4) & 3u), float((sample >> 2) & 3u), float(sample & 3u)) / 2.0;"
|
||
"}");
|
||
crt_->set_visible_area(Outputs::CRT::Rect(0.075f, 0.05f, 0.9f, 0.9f));
|
||
crt_->set_video_signal(Outputs::CRT::VideoSignal::RGB);
|
||
}
|
||
|
||
/// Destructs the CRT.
|
||
void close_output() {
|
||
crt_.reset();
|
||
}
|
||
|
||
/// @returns the CRT.
|
||
Outputs::CRT::CRT *get_crt() {
|
||
return crt_.get();
|
||
}
|
||
|
||
/*!
|
||
Sets the next video mode. Per the documentation, mode changes take effect only at the end of line,
|
||
not immediately. So next means "as of the end of this line".
|
||
*/
|
||
void set_next_mode(int mode) {
|
||
next_mode_ = mode;
|
||
}
|
||
|
||
/// Palette management: selects a pen to modify.
|
||
void select_pen(int pen) {
|
||
pen_ = pen;
|
||
}
|
||
|
||
/// Palette management: sets the colour of the selected pen.
|
||
void set_colour(uint8_t colour) {
|
||
if(pen_ & 16) {
|
||
// If border is[/was] currently being output, flush what should have been
|
||
// drawn in the old colour.
|
||
if(!was_sync_ && !was_enabled_) {
|
||
output_border(cycles_);
|
||
cycles_ = 0;
|
||
}
|
||
border_ = mapped_palette_value(colour);
|
||
} else {
|
||
palette_[pen_] = mapped_palette_value(colour);
|
||
patch_mode_table(pen_);
|
||
}
|
||
}
|
||
|
||
private:
|
||
void output_border(unsigned int length) {
|
||
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_->allocate_write_area(1));
|
||
if(colour_pointer) *colour_pointer = border_;
|
||
crt_->output_level(length * 16);
|
||
}
|
||
|
||
#define Mode0Colour0(c) ((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2)
|
||
#define Mode0Colour1(c) ((c & 0x40) >> 6) | ((c & 0x10) >> 2) | ((c & 0x04) >> 1) | ((c & 0x01) << 3)
|
||
|
||
#define Mode1Colour0(c) ((c & 0x80) >> 7) | ((c & 0x08) >> 2)
|
||
#define Mode1Colour1(c) ((c & 0x40) >> 6) | ((c & 0x04) >> 1)
|
||
#define Mode1Colour2(c) ((c & 0x20) >> 5) | ((c & 0x02) >> 0)
|
||
#define Mode1Colour3(c) ((c & 0x10) >> 4) | ((c & 0x01) << 1)
|
||
|
||
#define Mode3Colour0(c) ((c & 0x80) >> 7) | ((c & 0x08) >> 2)
|
||
#define Mode3Colour1(c) ((c & 0x40) >> 6) | ((c & 0x04) >> 1)
|
||
|
||
void establish_palette_hits() {
|
||
for(int c = 0; c < 256; c++) {
|
||
mode0_palette_hits_[Mode0Colour0(c)].push_back(static_cast<uint8_t>(c));
|
||
mode0_palette_hits_[Mode0Colour1(c)].push_back(static_cast<uint8_t>(c));
|
||
|
||
mode1_palette_hits_[Mode1Colour0(c)].push_back(static_cast<uint8_t>(c));
|
||
mode1_palette_hits_[Mode1Colour1(c)].push_back(static_cast<uint8_t>(c));
|
||
mode1_palette_hits_[Mode1Colour2(c)].push_back(static_cast<uint8_t>(c));
|
||
mode1_palette_hits_[Mode1Colour3(c)].push_back(static_cast<uint8_t>(c));
|
||
|
||
mode3_palette_hits_[Mode3Colour0(c)].push_back(static_cast<uint8_t>(c));
|
||
mode3_palette_hits_[Mode3Colour1(c)].push_back(static_cast<uint8_t>(c));
|
||
}
|
||
}
|
||
|
||
void build_mode_table() {
|
||
switch(mode_) {
|
||
case 0:
|
||
// Mode 0: abcdefgh -> [gcea] [hdfb]
|
||
for(int c = 0; c < 256; c++) {
|
||
// prepare mode 0
|
||
uint8_t *mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]);
|
||
mode0_pixels[0] = palette_[Mode0Colour0(c)];
|
||
mode0_pixels[1] = palette_[Mode0Colour1(c)];
|
||
}
|
||
break;
|
||
|
||
case 1:
|
||
for(int c = 0; c < 256; c++) {
|
||
// prepare mode 1
|
||
uint8_t *mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]);
|
||
mode1_pixels[0] = palette_[Mode1Colour0(c)];
|
||
mode1_pixels[1] = palette_[Mode1Colour1(c)];
|
||
mode1_pixels[2] = palette_[Mode1Colour2(c)];
|
||
mode1_pixels[3] = palette_[Mode1Colour3(c)];
|
||
}
|
||
break;
|
||
|
||
case 2:
|
||
for(int c = 0; c < 256; c++) {
|
||
// prepare mode 2
|
||
uint8_t *mode2_pixels = reinterpret_cast<uint8_t *>(&mode2_output_[c]);
|
||
mode2_pixels[0] = palette_[((c & 0x80) >> 7)];
|
||
mode2_pixels[1] = palette_[((c & 0x40) >> 6)];
|
||
mode2_pixels[2] = palette_[((c & 0x20) >> 5)];
|
||
mode2_pixels[3] = palette_[((c & 0x10) >> 4)];
|
||
mode2_pixels[4] = palette_[((c & 0x08) >> 3)];
|
||
mode2_pixels[5] = palette_[((c & 0x04) >> 2)];
|
||
mode2_pixels[6] = palette_[((c & 0x03) >> 1)];
|
||
mode2_pixels[7] = palette_[((c & 0x01) >> 0)];
|
||
}
|
||
break;
|
||
|
||
case 3:
|
||
for(int c = 0; c < 256; c++) {
|
||
// prepare mode 3
|
||
uint8_t *mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]);
|
||
mode3_pixels[0] = palette_[Mode3Colour0(c)];
|
||
mode3_pixels[1] = palette_[Mode3Colour1(c)];
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
void patch_mode_table(int pen) {
|
||
switch(mode_) {
|
||
case 0: {
|
||
for(uint8_t c : mode0_palette_hits_[pen]) {
|
||
uint8_t *mode0_pixels = reinterpret_cast<uint8_t *>(&mode0_output_[c]);
|
||
mode0_pixels[0] = palette_[Mode0Colour0(c)];
|
||
mode0_pixels[1] = palette_[Mode0Colour1(c)];
|
||
}
|
||
} break;
|
||
case 1:
|
||
if(pen > 3) return;
|
||
for(uint8_t c : mode1_palette_hits_[pen]) {
|
||
uint8_t *mode1_pixels = reinterpret_cast<uint8_t *>(&mode1_output_[c]);
|
||
mode1_pixels[0] = palette_[Mode1Colour0(c)];
|
||
mode1_pixels[1] = palette_[Mode1Colour1(c)];
|
||
mode1_pixels[2] = palette_[Mode1Colour2(c)];
|
||
mode1_pixels[3] = palette_[Mode1Colour3(c)];
|
||
}
|
||
break;
|
||
case 2:
|
||
if(pen > 1) return;
|
||
// Whichever pen this is, there's only one table entry it doesn't touch, so just
|
||
// rebuild the whole thing.
|
||
build_mode_table();
|
||
break;
|
||
case 3:
|
||
if(pen > 3) return;
|
||
// Same argument applies here as to case 1, as the unused bits aren't masked out.
|
||
for(uint8_t c : mode3_palette_hits_[pen]) {
|
||
uint8_t *mode3_pixels = reinterpret_cast<uint8_t *>(&mode3_output_[c]);
|
||
mode3_pixels[0] = palette_[Mode3Colour0(c)];
|
||
mode3_pixels[1] = palette_[Mode3Colour1(c)];
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
#undef Mode0Colour0
|
||
#undef Mode0Colour1
|
||
|
||
#undef Mode1Colour0
|
||
#undef Mode1Colour1
|
||
#undef Mode1Colour2
|
||
#undef Mode1Colour3
|
||
|
||
#undef Mode3Colour0
|
||
#undef Mode3Colour1
|
||
|
||
uint8_t mapped_palette_value(uint8_t colour) {
|
||
#define COL(r, g, b) (r << 4) | (g << 2) | b
|
||
static const uint8_t mapping[32] = {
|
||
COL(1, 1, 1), COL(1, 1, 1), COL(0, 2, 1), COL(2, 2, 1),
|
||
COL(0, 0, 1), COL(2, 0, 1), COL(0, 1, 1), COL(2, 1, 1),
|
||
COL(2, 0, 1), COL(2, 2, 1), COL(2, 2, 0), COL(2, 2, 2),
|
||
COL(2, 0, 0), COL(2, 0, 2), COL(2, 1, 0), COL(2, 1, 2),
|
||
COL(0, 0, 1), COL(0, 2, 1), COL(0, 2, 0), COL(0, 2, 2),
|
||
COL(0, 0, 0), COL(0, 0, 2), COL(0, 1, 0), COL(0, 1, 2),
|
||
COL(1, 0, 1), COL(1, 2, 1), COL(1, 2, 0), COL(1, 2, 2),
|
||
COL(1, 0, 0), COL(1, 0, 2), COL(1, 1, 0), COL(1, 1, 2),
|
||
};
|
||
#undef COL
|
||
return mapping[colour];
|
||
}
|
||
|
||
unsigned int cycles_ = 0;
|
||
|
||
bool was_enabled_ = false, was_sync_ = false, was_hsync_ = false, was_vsync_ = false;
|
||
int cycles_into_hsync_ = 0;
|
||
|
||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
|
||
|
||
uint8_t *ram_ = nullptr;
|
||
|
||
int next_mode_ = 2, mode_ = 2;
|
||
|
||
unsigned int pixel_divider_ = 1;
|
||
uint16_t mode0_output_[256];
|
||
uint32_t mode1_output_[256];
|
||
uint64_t mode2_output_[256];
|
||
uint16_t mode3_output_[256];
|
||
|
||
std::vector<uint8_t> mode0_palette_hits_[16];
|
||
std::vector<uint8_t> mode1_palette_hits_[4];
|
||
std::vector<uint8_t> mode3_palette_hits_[4];
|
||
|
||
int pen_ = 0;
|
||
uint8_t palette_[16];
|
||
uint8_t border_ = 0;
|
||
|
||
InterruptTimer &interrupt_timer_;
|
||
};
|
||
|
||
/*!
|
||
Holds and vends the current keyboard state, acting as the AY's port handler.
|
||
*/
|
||
class KeyboardState: public GI::AY38910::PortHandler {
|
||
public:
|
||
KeyboardState() : rows_{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} {}
|
||
|
||
/*!
|
||
Sets the row currently being reported to the AY.
|
||
*/
|
||
void set_row(int row) {
|
||
row_ = row;
|
||
}
|
||
|
||
/*!
|
||
Reports the state of the currently-selected row as Port A to the AY.
|
||
*/
|
||
uint8_t get_port_input(bool port_b) {
|
||
if(!port_b && row_ < 10) {
|
||
return rows_[row_];
|
||
}
|
||
|
||
return 0xff;
|
||
}
|
||
|
||
/*!
|
||
Sets whether @c key on line @c line is currently pressed.
|
||
*/
|
||
void set_is_pressed(bool is_pressed, int line, int key) {
|
||
int mask = 1 << key;
|
||
assert(line < 10);
|
||
if(is_pressed) rows_[line] &= ~mask; else rows_[line] |= mask;
|
||
}
|
||
|
||
/*!
|
||
Sets all keys as currently unpressed.
|
||
*/
|
||
void clear_all_keys() {
|
||
memset(rows_, 0xff, 10);
|
||
}
|
||
|
||
private:
|
||
uint8_t rows_[10];
|
||
int row_;
|
||
};
|
||
|
||
/*!
|
||
Wraps the 8272 so as to provide proper clocking and RPM counts, and just directly
|
||
exposes motor control, applying the same value to all drives.
|
||
*/
|
||
class FDC: public Intel::i8272::i8272 {
|
||
private:
|
||
Intel::i8272::BusHandler bus_handler_;
|
||
std::shared_ptr<Storage::Disk::Drive> drive_;
|
||
|
||
public:
|
||
FDC() :
|
||
i8272(bus_handler_, Cycles(8000000)),
|
||
drive_(new Storage::Disk::Drive(8000000, 300, 1)) {
|
||
set_drive(drive_);
|
||
}
|
||
|
||
void set_motor_on(bool on) {
|
||
drive_->set_motor_on(on);
|
||
}
|
||
|
||
void select_drive(int c) {
|
||
// TODO: support more than one drive.
|
||
}
|
||
|
||
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
|
||
drive_->set_disk(disk);
|
||
}
|
||
};
|
||
|
||
/*!
|
||
Provides the mechanism of receipt for input and output of the 8255's various ports.
|
||
*/
|
||
class i8255PortHandler : public Intel::i8255::PortHandler {
|
||
public:
|
||
i8255PortHandler(
|
||
KeyboardState &key_state,
|
||
const Motorola::CRTC::CRTC6845<CRTCBusHandler> &crtc,
|
||
AYDeferrer &ay,
|
||
Storage::Tape::BinaryTapePlayer &tape_player) :
|
||
ay_(ay),
|
||
crtc_(crtc),
|
||
key_state_(key_state),
|
||
tape_player_(tape_player) {}
|
||
|
||
/// The i8255 will call this to set a new output value of @c value for @c port.
|
||
void set_value(int port, uint8_t value) {
|
||
switch(port) {
|
||
case 0:
|
||
// Port A is connected to the AY's data bus.
|
||
ay_.update();
|
||
ay_.ay().set_data_input(value);
|
||
break;
|
||
case 1:
|
||
// Port B is an input only. So output goes nowehere.
|
||
break;
|
||
case 2: {
|
||
// The low four bits of the value sent to Port C select a keyboard line.
|
||
int key_row = value & 15;
|
||
key_state_.set_row(key_row);
|
||
|
||
// Bit 4 sets the tape motor on or off.
|
||
tape_player_.set_motor_control((value & 0x10) ? true : false);
|
||
// Bit 5 sets the current tape output level
|
||
tape_player_.set_tape_output((value & 0x20) ? true : false);
|
||
|
||
// Bits 6 and 7 set BDIR and BC1 for the AY.
|
||
ay_.ay().set_control_lines(
|
||
(GI::AY38910::ControlLines)(
|
||
((value & 0x80) ? GI::AY38910::BDIR : 0) |
|
||
((value & 0x40) ? GI::AY38910::BC1 : 0) |
|
||
GI::AY38910::BC2
|
||
));
|
||
} break;
|
||
}
|
||
}
|
||
|
||
/// The i8255 will call this to obtain a new input for @c port.
|
||
uint8_t get_value(int port) {
|
||
switch(port) {
|
||
case 0: return ay_.ay().get_data_output(); // Port A is wired to the AY
|
||
case 1: return
|
||
(crtc_.get_bus_state().vsync ? 0x01 : 0x00) | // Bit 0 returns CRTC vsync.
|
||
(tape_player_.get_input() ? 0x80 : 0x00) | // Bit 7 returns cassette input.
|
||
0x7e; // Bits unimplemented:
|
||
//
|
||
// Bit 6: printer ready (1 = not)
|
||
// Bit 5: the expansion port /EXP pin, so depends on connected hardware
|
||
// Bit 4: 50/60Hz switch (1 = 50Hz)
|
||
// Bits 1–3: distributor ID (111 = Amstrad)
|
||
default: return 0xff;
|
||
}
|
||
}
|
||
|
||
private:
|
||
AYDeferrer &ay_;
|
||
const Motorola::CRTC::CRTC6845<CRTCBusHandler> &crtc_;
|
||
KeyboardState &key_state_;
|
||
Storage::Tape::BinaryTapePlayer &tape_player_;
|
||
};
|
||
|
||
/*!
|
||
The actual Amstrad CPC implementation; tying the 8255, 6845 and AY to the Z80.
|
||
*/
|
||
class ConcreteMachine:
|
||
public CRTMachine::Machine,
|
||
public ConfigurationTarget::Machine,
|
||
public KeyboardMachine::Machine,
|
||
public Utility::TypeRecipient,
|
||
public CPU::Z80::BusHandler,
|
||
public Sleeper::SleepObserver,
|
||
public Machine {
|
||
public:
|
||
ConcreteMachine() :
|
||
z80_(*this),
|
||
crtc_bus_handler_(ram_, interrupt_timer_),
|
||
crtc_(Motorola::CRTC::HD6845S, crtc_bus_handler_),
|
||
i8255_port_handler_(key_state_, crtc_, ay_, tape_player_),
|
||
i8255_(i8255_port_handler_),
|
||
tape_player_(8000000),
|
||
crtc_counter_(HalfCycles(4)) // This starts the CRTC exactly out of phase with the CPU's memory accesses
|
||
{
|
||
// primary clock is 4Mhz
|
||
set_clock_rate(4000000);
|
||
|
||
// ensure memory starts in a random state
|
||
Memory::Fuzz(ram_, sizeof(ram_));
|
||
|
||
// register this class as the sleep observer for the FDC and tape
|
||
fdc_.set_sleep_observer(this);
|
||
fdc_is_sleeping_ = fdc_.is_sleeping();
|
||
|
||
tape_player_.set_sleep_observer(this);
|
||
tape_player_is_sleeping_ = tape_player_.is_sleeping();
|
||
|
||
ay_.ay().set_port_handler(&key_state_);
|
||
}
|
||
|
||
/// The entry point for performing a partial Z80 machine cycle.
|
||
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
|
||
// Amstrad CPC timing scheme: assert WAIT for three out of four cycles
|
||
clock_offset_ = (clock_offset_ + cycle.length) & HalfCycles(7);
|
||
z80_.set_wait_line(clock_offset_ >= HalfCycles(2));
|
||
|
||
// Update the CRTC once every eight half cycles; aiming for half-cycle 4 as
|
||
// per the initial seed to the crtc_counter_, but any time in the final four
|
||
// will do as it's safe to conclude that nobody else has touched video RAM
|
||
// during that whole window
|
||
crtc_counter_ += cycle.length;
|
||
Cycles crtc_cycles = crtc_counter_.divide_cycles(Cycles(4));
|
||
if(crtc_cycles > Cycles(0)) crtc_.run_for(crtc_cycles);
|
||
|
||
// Check whether that prompted a change in the interrupt line. If so then date
|
||
// it to whenever the cycle was triggered.
|
||
if(interrupt_timer_.request_has_changed()) z80_.set_interrupt_line(interrupt_timer_.get_request(), -crtc_counter_);
|
||
|
||
// TODO (in the player, not here): adapt it to accept an input clock rate and
|
||
// run_for as HalfCycles
|
||
if(!tape_player_is_sleeping_) tape_player_.run_for(cycle.length.as_int());
|
||
|
||
// Pump the AY
|
||
ay_.run_for(cycle.length);
|
||
|
||
// Clock the FDC, if connected, using a lazy scale by two
|
||
if(has_fdc_ && !fdc_is_sleeping_) fdc_.run_for(Cycles(cycle.length.as_int()));
|
||
|
||
// Update typing activity
|
||
if(typer_) typer_->run_for(cycle.length);
|
||
|
||
// Stop now if no action is strictly required.
|
||
if(!cycle.is_terminal()) return HalfCycles(0);
|
||
|
||
uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||
switch(cycle.operation) {
|
||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||
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::Output:
|
||
// Check for a gate array access.
|
||
if((address & 0xc000) == 0x4000) {
|
||
write_to_gate_array(*cycle.value);
|
||
}
|
||
|
||
// Check for an upper ROM selection
|
||
if(has_fdc_ && !(address&0x2000)) {
|
||
upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : rom_model_ + 1;
|
||
if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data();
|
||
}
|
||
|
||
// Check for a CRTC access
|
||
if(!(address & 0x4000)) {
|
||
switch((address >> 8) & 3) {
|
||
case 0: crtc_.select_register(*cycle.value); break;
|
||
case 1: crtc_.set_register(*cycle.value); break;
|
||
default: break;
|
||
}
|
||
}
|
||
|
||
// Check for an 8255 PIO access
|
||
if(!(address & 0x800)) {
|
||
i8255_.set_register((address >> 8) & 3, *cycle.value);
|
||
}
|
||
|
||
// Check for an FDC access
|
||
if(has_fdc_ && (address & 0x580) == 0x100) {
|
||
fdc_.set_register(address & 1, *cycle.value);
|
||
}
|
||
|
||
// Check for a disk motor access
|
||
if(has_fdc_ && !(address & 0x580)) {
|
||
fdc_.set_motor_on(!!(*cycle.value));
|
||
}
|
||
break;
|
||
case CPU::Z80::PartialMachineCycle::Input:
|
||
// Default to nothing answering
|
||
*cycle.value = 0xff;
|
||
|
||
// Check for a PIO access
|
||
if(!(address & 0x800)) {
|
||
*cycle.value &= i8255_.get_register((address >> 8) & 3);
|
||
}
|
||
|
||
// Check for an FDC access
|
||
if(has_fdc_ && (address & 0x580) == 0x100) {
|
||
*cycle.value &= fdc_.get_register(address & 1);
|
||
}
|
||
|
||
// Check for a CRTC access; the below is not a typo — the CRTC can be selected
|
||
// for writing via an input, and will sample whatever happens to be available
|
||
if(!(address & 0x4000)) {
|
||
switch((address >> 8) & 3) {
|
||
case 0: crtc_.select_register(*cycle.value); break;
|
||
case 1: crtc_.set_register(*cycle.value); break;
|
||
case 2: *cycle.value &= crtc_.get_status(); break;
|
||
case 3: *cycle.value &= crtc_.get_register(); break;
|
||
}
|
||
}
|
||
|
||
// As with the CRTC, the gate array will sample the bus if the address decoding
|
||
// implies that it should, unaware of data direction
|
||
if((address & 0xc000) == 0x4000) {
|
||
write_to_gate_array(*cycle.value);
|
||
}
|
||
break;
|
||
|
||
case CPU::Z80::PartialMachineCycle::Interrupt:
|
||
// Nothing is loaded onto the bus during an interrupt acknowledge, but
|
||
// the fact of the acknowledge needs to be posted on to the interrupt timer.
|
||
*cycle.value = 0xff;
|
||
interrupt_timer_.signal_interrupt_acknowledge();
|
||
break;
|
||
|
||
default: break;
|
||
}
|
||
|
||
// This implementation doesn't use time-stuffing; once in-phase waits won't be longer
|
||
// than a single cycle so there's no real performance benefit to trying to find the
|
||
// next non-wait when a wait cycle comes in, and there'd be no benefit to reproducing
|
||
// the Z80's knowledge of where wait cycles occur here.
|
||
return HalfCycles(0);
|
||
}
|
||
|
||
/// Another Z80 entry point; indicates that a partcular run request has concluded.
|
||
void flush() {
|
||
// Just flush the AY.
|
||
ay_.update();
|
||
ay_.flush();
|
||
}
|
||
|
||
/// A CRTMachine function; indicates that outputs should be created now.
|
||
void setup_output(float aspect_ratio) override final {
|
||
crtc_bus_handler_.setup_output(aspect_ratio);
|
||
}
|
||
|
||
/// A CRTMachine function; indicates that outputs should be destroyed now.
|
||
void close_output() override final {
|
||
crtc_bus_handler_.close_output();
|
||
}
|
||
|
||
/// @returns the CRT in use.
|
||
Outputs::CRT::CRT *get_crt() override final {
|
||
return crtc_bus_handler_.get_crt();
|
||
}
|
||
|
||
/// @returns the speaker in use.
|
||
Outputs::Speaker::Speaker *get_speaker() override final {
|
||
return ay_.get_speaker();
|
||
}
|
||
|
||
/// Wires virtual-dispatched CRTMachine run_for requests to the static Z80 method.
|
||
void run_for(const Cycles cycles) override final {
|
||
z80_.run_for(cycles);
|
||
}
|
||
|
||
/// The ConfigurationTarget entry point; should configure this meachine as described by @c target.
|
||
void configure_as_target(const Analyser::Static::Target *target) override final {
|
||
auto *const cpc_target = dynamic_cast<const Analyser::Static::AmstradCPC::Target *>(target);
|
||
|
||
switch(cpc_target->model) {
|
||
case Analyser::Static::AmstradCPC::Target::Model::CPC464:
|
||
rom_model_ = ROMType::OS464;
|
||
has_128k_ = false;
|
||
has_fdc_ = false;
|
||
break;
|
||
case Analyser::Static::AmstradCPC::Target::Model::CPC664:
|
||
rom_model_ = ROMType::OS664;
|
||
has_128k_ = false;
|
||
has_fdc_ = true;
|
||
break;
|
||
case Analyser::Static::AmstradCPC::Target::Model::CPC6128:
|
||
rom_model_ = ROMType::OS6128;
|
||
has_128k_ = true;
|
||
has_fdc_ = true;
|
||
break;
|
||
}
|
||
|
||
// Establish default memory map
|
||
upper_rom_is_paged_ = true;
|
||
upper_rom_ = rom_model_ + 1;
|
||
|
||
write_pointers_[0] = &ram_[0];
|
||
write_pointers_[1] = &ram_[16384];
|
||
write_pointers_[2] = &ram_[32768];
|
||
write_pointers_[3] = &ram_[49152];
|
||
|
||
read_pointers_[0] = roms_[rom_model_].data();
|
||
read_pointers_[1] = write_pointers_[1];
|
||
read_pointers_[2] = write_pointers_[2];
|
||
read_pointers_[3] = roms_[upper_rom_].data();
|
||
|
||
// Type whatever is required.
|
||
if(!cpc_target->loading_command.empty()) {
|
||
type_string(cpc_target->loading_command);
|
||
}
|
||
|
||
insert_media(target->media);
|
||
}
|
||
|
||
bool insert_media(const Analyser::Static::Media &media) override final {
|
||
// If there are any tapes supplied, use the first of them.
|
||
if(!media.tapes.empty()) {
|
||
tape_player_.set_tape(media.tapes.front());
|
||
}
|
||
|
||
// Insert up to four disks.
|
||
int c = 0;
|
||
for(auto &disk : media.disks) {
|
||
fdc_.set_disk(disk, c);
|
||
c++;
|
||
if(c == 4) break;
|
||
}
|
||
|
||
return !media.tapes.empty() || (!media.disks.empty() && has_fdc_);
|
||
}
|
||
|
||
// 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(
|
||
"AmstradCPC",
|
||
{
|
||
"os464.rom", "basic464.rom",
|
||
"os664.rom", "basic664.rom",
|
||
"os6128.rom", "basic6128.rom",
|
||
"amsdos.rom"
|
||
});
|
||
|
||
for(std::size_t index = 0; index < roms.size(); ++index) {
|
||
auto &data = roms[index];
|
||
if(!data) return false;
|
||
roms_[static_cast<int>(index)] = std::move(*data);
|
||
roms_[static_cast<int>(index)].resize(16384);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
void set_component_is_sleeping(void *component, bool is_sleeping) override final {
|
||
fdc_is_sleeping_ = fdc_.is_sleeping();
|
||
tape_player_is_sleeping_ = tape_player_.is_sleeping();
|
||
}
|
||
|
||
// MARK: - Keyboard
|
||
|
||
void type_string(const std::string &string) override final {
|
||
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper());
|
||
Utility::TypeRecipient::add_typer(string, std::move(mapper));
|
||
}
|
||
|
||
HalfCycles get_typer_delay() override final {
|
||
return Cycles(4000000); // Wait 1 second before typing.
|
||
}
|
||
|
||
HalfCycles get_typer_frequency() override final {
|
||
return Cycles(160000); // Type one character per frame.
|
||
}
|
||
|
||
// See header; sets a key as either pressed or released.
|
||
void set_key_state(uint16_t key, bool isPressed) override final {
|
||
key_state_.set_is_pressed(isPressed, key >> 4, key & 7);
|
||
}
|
||
|
||
// See header; sets all keys to released.
|
||
void clear_all_keys() override final {
|
||
key_state_.clear_all_keys();
|
||
}
|
||
|
||
KeyboardMapper *get_keyboard_mapper() override {
|
||
return &keyboard_mapper_;
|
||
}
|
||
|
||
private:
|
||
inline void write_to_gate_array(uint8_t value) {
|
||
switch(value >> 6) {
|
||
case 0: crtc_bus_handler_.select_pen(value & 0x1f); break;
|
||
case 1: crtc_bus_handler_.set_colour(value & 0x1f); break;
|
||
case 2:
|
||
// Perform ROM paging.
|
||
read_pointers_[0] = (value & 4) ? write_pointers_[0] : roms_[rom_model_].data();
|
||
|
||
upper_rom_is_paged_ = !(value & 8);
|
||
read_pointers_[3] = upper_rom_is_paged_ ? roms_[upper_rom_].data() : write_pointers_[3];
|
||
|
||
// Reset the interrupt timer if requested.
|
||
if(value & 0x10) interrupt_timer_.reset_count();
|
||
|
||
// Post the next mode.
|
||
crtc_bus_handler_.set_next_mode(value & 3);
|
||
break;
|
||
case 3:
|
||
// Perform RAM paging, if 128kb is permitted.
|
||
if(has_128k_) {
|
||
bool adjust_low_read_pointer = read_pointers_[0] == write_pointers_[0];
|
||
bool adjust_high_read_pointer = read_pointers_[3] == write_pointers_[3];
|
||
#define RAM_BANK(x) &ram_[x * 16384]
|
||
#define RAM_CONFIG(a, b, c, d) write_pointers_[0] = RAM_BANK(a); write_pointers_[1] = RAM_BANK(b); write_pointers_[2] = RAM_BANK(c); write_pointers_[3] = RAM_BANK(d);
|
||
switch(value & 7) {
|
||
case 0: RAM_CONFIG(0, 1, 2, 3); break;
|
||
case 1: RAM_CONFIG(0, 1, 2, 7); break;
|
||
case 2: RAM_CONFIG(4, 5, 6, 7); break;
|
||
case 3: RAM_CONFIG(0, 3, 2, 7); break;
|
||
case 4: RAM_CONFIG(0, 4, 2, 3); break;
|
||
case 5: RAM_CONFIG(0, 5, 2, 3); break;
|
||
case 6: RAM_CONFIG(0, 6, 2, 3); break;
|
||
case 7: RAM_CONFIG(0, 7, 2, 3); break;
|
||
}
|
||
#undef RAM_CONFIG
|
||
#undef RAM_BANK
|
||
if(adjust_low_read_pointer) read_pointers_[0] = write_pointers_[0];
|
||
read_pointers_[1] = write_pointers_[1];
|
||
read_pointers_[2] = write_pointers_[2];
|
||
if(adjust_high_read_pointer) read_pointers_[3] = write_pointers_[3];
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
CPU::Z80::Processor<ConcreteMachine, false, true> z80_;
|
||
|
||
CRTCBusHandler crtc_bus_handler_;
|
||
Motorola::CRTC::CRTC6845<CRTCBusHandler> crtc_;
|
||
|
||
AYDeferrer ay_;
|
||
i8255PortHandler i8255_port_handler_;
|
||
Intel::i8255::i8255<i8255PortHandler> i8255_;
|
||
|
||
FDC fdc_;
|
||
|
||
InterruptTimer interrupt_timer_;
|
||
Storage::Tape::BinaryTapePlayer tape_player_;
|
||
|
||
HalfCycles clock_offset_;
|
||
HalfCycles crtc_counter_;
|
||
HalfCycles half_cycles_since_ay_update_;
|
||
|
||
uint8_t ram_[128 * 1024];
|
||
|
||
std::vector<uint8_t> roms_[7];
|
||
int rom_model_;
|
||
bool has_fdc_, fdc_is_sleeping_;
|
||
bool tape_player_is_sleeping_;
|
||
bool has_128k_;
|
||
bool upper_rom_is_paged_;
|
||
int upper_rom_;
|
||
|
||
uint8_t *ram_pages_[4];
|
||
uint8_t *read_pointers_[4];
|
||
uint8_t *write_pointers_[4];
|
||
|
||
KeyboardState key_state_;
|
||
AmstradCPC::KeyboardMapper keyboard_mapper_;
|
||
};
|
||
|
||
}
|
||
|
||
using namespace AmstradCPC;
|
||
|
||
// See header; constructs and returns an instance of the Amstrad CPC.
|
||
Machine *Machine::AmstradCPC() {
|
||
return new AmstradCPC::ConcreteMachine;
|
||
}
|
||
|
||
Machine::~Machine() {}
|