mirror of
https://github.com/TomHarte/CLK.git
synced 2025-02-16 18:30:32 +00:00
Upped the documentation.
This commit is contained in:
parent
f5e2dd410e
commit
d3bf8fa53b
@ -18,17 +18,34 @@
|
||||
|
||||
using namespace AmstradCPC;
|
||||
|
||||
/*!
|
||||
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:
|
||||
InterruptTimer() : timer_(0), interrupt_request_(false) {}
|
||||
|
||||
inline void increment() {
|
||||
/*!
|
||||
Indicates that a new hsync pulse has been recognised. Per documentation
|
||||
difficulties, it is not presently clear to me whether this should be
|
||||
the leading or trailing edge of horizontal sync.
|
||||
*/
|
||||
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_) {
|
||||
@ -40,54 +57,68 @@ class InterruptTimer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates the leading edge of a new vertical sync.
|
||||
inline void signal_vsync() {
|
||||
reset_counter_ = 2;
|
||||
}
|
||||
|
||||
inline void reset_request() {
|
||||
/// 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 interrupt_request_;
|
||||
}
|
||||
|
||||
/// Resets the timer.
|
||||
inline void reset_count() {
|
||||
timer_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
int reset_counter_;
|
||||
bool interrupt_request_;
|
||||
int timer_;
|
||||
};
|
||||
|
||||
/*!
|
||||
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.
|
||||
inline void setup_output() {
|
||||
ay_.reset(new GI::AY38910);
|
||||
ay_->set_input_rate(1000000);
|
||||
}
|
||||
|
||||
/// Destructs the AY.
|
||||
inline void close_output() {
|
||||
ay_.reset();
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// Issues a request to the AY to perform all processing up to the current time.
|
||||
inline void flush() {
|
||||
ay_->run_for(cycles_since_update_.divide_cycles(Cycles(4)));
|
||||
ay_->flush();
|
||||
}
|
||||
|
||||
/// @returns the speaker the AY is using for output.
|
||||
std::shared_ptr<Outputs::Speaker> get_speaker() {
|
||||
return ay_;
|
||||
}
|
||||
|
||||
/// @returns the AY itself.
|
||||
GI::AY38910 *ay() {
|
||||
return ay_.get();
|
||||
}
|
||||
@ -97,6 +128,11 @@ class AYDeferrer {
|
||||
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) :
|
||||
@ -114,10 +150,16 @@ class CRTCBusHandler {
|
||||
build_mode_tables();
|
||||
}
|
||||
|
||||
/*!
|
||||
The CRTC entry function; takes the current bus state and determines what output
|
||||
to produce based on the current palette and mode.
|
||||
*/
|
||||
inline void perform_bus_cycle(const Motorola::CRTC::BusState &state) {
|
||||
// Sync is taken to override pixels, and is combined as a simple OR.
|
||||
bool is_sync = state.hsync || state.vsync;
|
||||
|
||||
// if a transition between sync/border/pixels just occurred, announce it
|
||||
// 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);
|
||||
@ -150,6 +192,7 @@ class CRTCBusHandler {
|
||||
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 =
|
||||
(uint16_t)(
|
||||
((state.refresh_address & 0x3ff) << 1) |
|
||||
@ -157,6 +200,7 @@ class CRTCBusHandler {
|
||||
((state.refresh_address & 0x3000) << 2)
|
||||
);
|
||||
|
||||
// fetch two bytes and translate into pixels
|
||||
switch(mode_) {
|
||||
case 0:
|
||||
((uint16_t *)pixel_pointer_)[0] = mode0_output_[ram_[address]];
|
||||
@ -184,7 +228,9 @@ class CRTCBusHandler {
|
||||
|
||||
}
|
||||
|
||||
// flush the current buffer if full
|
||||
// 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, pixel_divider_);
|
||||
pixel_pointer_ = pixel_data_ = nullptr;
|
||||
@ -193,7 +239,8 @@ class CRTCBusHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// check for a trailing hsync
|
||||
// check for a trailing 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_;
|
||||
@ -205,17 +252,20 @@ class CRTCBusHandler {
|
||||
}
|
||||
}
|
||||
|
||||
interrupt_timer_.increment();
|
||||
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(
|
||||
@ -227,26 +277,35 @@ class CRTCBusHandler {
|
||||
crt_->set_visible_area(Outputs::CRT::Rect(0.05f, 0.05f, 0.9f, 0.9f));
|
||||
}
|
||||
|
||||
/// Destructs the CRT.
|
||||
void close_output() {
|
||||
crt_.reset();
|
||||
}
|
||||
|
||||
/// @returns the CRT.
|
||||
std::shared_ptr<Outputs::CRT::CRT> get_crt() {
|
||||
return crt_;
|
||||
}
|
||||
|
||||
/*!
|
||||
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;
|
||||
}
|
||||
|
||||
/// @returns the current value of the CRTC's vertical sync output.
|
||||
bool get_vsync() const {
|
||||
return was_vsync_;
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
border_ = mapped_palette_value(colour);
|
||||
@ -329,35 +388,58 @@ class CRTCBusHandler {
|
||||
InterruptTimer &interrupt_timer_;
|
||||
};
|
||||
|
||||
/*!
|
||||
Passively holds the current keyboard state. Keyboard state is modified in response
|
||||
to external messages, so is handled by the machine, but is read by the i8255 port
|
||||
handler, so is factored out.
|
||||
*/
|
||||
struct KeyboardState {
|
||||
KeyboardState() : rows{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} {}
|
||||
uint8_t rows[10];
|
||||
};
|
||||
|
||||
/*!
|
||||
Provides the mechanism of receipt for input and output of the 8255's various ports.
|
||||
*/
|
||||
class i8255PortHandler : public Intel::i8255::PortHandler {
|
||||
public:
|
||||
i8255PortHandler(const KeyboardState &key_state, const CRTCBusHandler &crtc_bus_handler, AYDeferrer &ay, Storage::Tape::BinaryTapePlayer &tape_player) :
|
||||
key_state_(key_state), crtc_bus_handler_(crtc_bus_handler), ay_(ay), tape_player_(tape_player) {}
|
||||
i8255PortHandler(
|
||||
const KeyboardState &key_state,
|
||||
const CRTCBusHandler &crtc_bus_handler,
|
||||
AYDeferrer &ay,
|
||||
Storage::Tape::BinaryTapePlayer &tape_player) :
|
||||
key_state_(key_state),
|
||||
crtc_bus_handler_(crtc_bus_handler),
|
||||
ay_(ay),
|
||||
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:
|
||||
ay_.flush();
|
||||
// Port A is connected to the AY's data bus.
|
||||
ay_.ay()->set_data_input(value);
|
||||
break;
|
||||
case 1:
|
||||
// printf("Vsync, etc: %02x\n", value);
|
||||
// Port B is an input only. So output goes nowehere.
|
||||
break;
|
||||
case 2: {
|
||||
// TODO: the AY really should allow port communications to be active. Work needed.
|
||||
// The low four bits of the value sent to Port C select a keyboard line.
|
||||
// At least for now, do a static push of the keyboard state here. So this
|
||||
// is a capture. TODO: it should be a live connection.
|
||||
int key_row = value & 15;
|
||||
if(key_row < 10) {
|
||||
ay_.ay()->set_port_input(false, key_state_.rows[key_row]);
|
||||
} else {
|
||||
ay_.ay()->set_port_input(false, 0xff);
|
||||
}
|
||||
tape_player_.set_motor_control(!!((value >> 4) & 1));
|
||||
tape_player_.set_tape_output(!!((value >> 5) & 1));
|
||||
|
||||
// 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) |
|
||||
@ -368,18 +450,21 @@ class i8255PortHandler : public Intel::i8255::PortHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
case 0: return ay_.ay()->get_data_output(); // Port A is wired to the AY
|
||||
case 1: return
|
||||
(crtc_bus_handler_.get_vsync() ? 0x01 : 0x00) |
|
||||
(tape_player_.get_input() ? 0x80 : 0x00) |
|
||||
0x7e;
|
||||
case 2:
|
||||
// printf("[In] Key row, etc\n");
|
||||
break;
|
||||
(crtc_bus_handler_.get_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;
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
private:
|
||||
@ -389,6 +474,9 @@ class i8255PortHandler : public Intel::i8255::PortHandler {
|
||||
Storage::Tape::BinaryTapePlayer &tape_player_;
|
||||
};
|
||||
|
||||
/*!
|
||||
The actual Amstrad CPC implementation; tying the 8255, 6845 and AY to the Z80.
|
||||
*/
|
||||
class ConcreteMachine:
|
||||
public CPU::Z80::Processor<ConcreteMachine>,
|
||||
public Machine {
|
||||
@ -404,6 +492,7 @@ class ConcreteMachine:
|
||||
set_clock_rate(4000000);
|
||||
}
|
||||
|
||||
/// The entry point for performing a partial Z80 machine cycle.
|
||||
inline 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);
|
||||
@ -446,9 +535,14 @@ class ConcreteMachine:
|
||||
case 0: crtc_bus_handler_.select_pen(*cycle.value & 0x1f); break;
|
||||
case 1: crtc_bus_handler_.set_colour(*cycle.value & 0x1f); break;
|
||||
case 2:
|
||||
// Perform ROM paging.
|
||||
read_pointers_[0] = (*cycle.value & 4) ? &ram_[0] : os_.data();
|
||||
read_pointers_[3] = (*cycle.value & 8) ? &ram_[49152] : basic_.data();
|
||||
|
||||
// Reset the interrupt timer if requested.
|
||||
if(*cycle.value & 15) interrupt_timer_.reset_count();
|
||||
|
||||
// Post the next mode.
|
||||
crtc_bus_handler_.set_next_mode(*cycle.value & 3);
|
||||
break;
|
||||
case 3: printf("RAM paging?\n"); break;
|
||||
@ -464,12 +558,13 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a PIO access
|
||||
// Check for an 8255 PIO access
|
||||
if(!(address & 0x800)) {
|
||||
i8255_.set_register((address >> 8) & 3, *cycle.value);
|
||||
}
|
||||
break;
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
// Default to nothing answering
|
||||
*cycle.value = 0xff;
|
||||
|
||||
// Check for a CRTC access
|
||||
@ -488,42 +583,56 @@ class ConcreteMachine:
|
||||
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_.reset_request();
|
||||
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_.flush();
|
||||
}
|
||||
|
||||
/// A CRTMachine function; indicates that outputs should be created now.
|
||||
void setup_output(float aspect_ratio) {
|
||||
crtc_bus_handler_.setup_output(aspect_ratio);
|
||||
ay_.setup_output();
|
||||
}
|
||||
|
||||
/// A CRTMachine function; indicates that outputs should be destroyed now.
|
||||
void close_output() {
|
||||
crtc_bus_handler_.close_output();
|
||||
ay_.close_output();
|
||||
}
|
||||
|
||||
/// @returns the CRT in use.
|
||||
std::shared_ptr<Outputs::CRT::CRT> get_crt() {
|
||||
return crtc_bus_handler_.get_crt();
|
||||
}
|
||||
|
||||
/// @returns the speaker in use.
|
||||
std::shared_ptr<Outputs::Speaker> get_speaker() {
|
||||
return ay_.get_speaker();
|
||||
}
|
||||
|
||||
/// Wires virtual-dispatched CRTMachine run_for requests to the static Z80 method.
|
||||
void run_for(const Cycles cycles) {
|
||||
CPU::Z80::Processor<ConcreteMachine>::run_for(cycles);
|
||||
}
|
||||
|
||||
/// The ConfigurationTarget entry point; should configure this meachine as described by @c target.
|
||||
void configure_as_target(const StaticAnalyser::Target &target) {
|
||||
// Establish reset memory map as per machine model (or, for now, as a hard-wired 464)
|
||||
read_pointers_[0] = os_.data();
|
||||
@ -536,11 +645,13 @@ class ConcreteMachine:
|
||||
write_pointers_[2] = &ram_[32768];
|
||||
write_pointers_[3] = &ram_[49152];
|
||||
|
||||
// If there are any tapes supplied, use the first of them.
|
||||
if(!target.tapes.empty()) {
|
||||
tape_player_.set_tape(target.tapes.front());
|
||||
}
|
||||
}
|
||||
|
||||
// See header; provides the system ROMs.
|
||||
void set_rom(ROMType type, std::vector<uint8_t> data) {
|
||||
// Keep only the two ROMs that are currently of interest.
|
||||
switch(type) {
|
||||
@ -550,12 +661,14 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
// See header; sets a key as either pressed or released.
|
||||
void set_key_state(uint16_t key, bool isPressed) {
|
||||
int line = key >> 4;
|
||||
uint8_t mask = (uint8_t)(1 << (key & 7));
|
||||
if(isPressed) key_state_.rows[line] &= ~mask; else key_state_.rows[line] |= mask;
|
||||
}
|
||||
|
||||
// See header; sets all keys to released.
|
||||
void clear_all_keys() {
|
||||
memset(key_state_.rows, 0xff, 10);
|
||||
}
|
||||
@ -584,6 +697,7 @@ class ConcreteMachine:
|
||||
KeyboardState key_state_;
|
||||
};
|
||||
|
||||
// See header; constructs and returns an instance of the Amstrad CPC.
|
||||
Machine *Machine::AmstradCPC() {
|
||||
return new ConcreteMachine;
|
||||
}
|
||||
|
@ -45,14 +45,23 @@ enum Key: uint16_t {
|
||||
#undef Line
|
||||
};
|
||||
|
||||
/*!
|
||||
Models an Amstrad CPC, a CRT-outputting machine that can accept configuration targets.
|
||||
*/
|
||||
class Machine:
|
||||
public CRTMachine::Machine,
|
||||
public ConfigurationTarget::Machine {
|
||||
public:
|
||||
/// Creates an returns an Amstrad CPC on the heap.
|
||||
static Machine *AmstradCPC();
|
||||
|
||||
/// Sets the contents of rom @c type to @c data. Assumed to be a setup step; has no effect once a machine is running.
|
||||
virtual void set_rom(ROMType type, std::vector<uint8_t> data) = 0;
|
||||
virtual void set_key_state(uint16_t key, bool isPressed) = 0;
|
||||
|
||||
/// Indicates that @c key is either pressed or released, according to @c is_pressed.
|
||||
virtual void set_key_state(uint16_t key, bool is_pressed) = 0;
|
||||
|
||||
/// Indicates that all keys are now released.
|
||||
virtual void clear_all_keys() = 0;
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user