1
0
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:
Thomas Harte 2017-08-02 20:37:26 -04:00
parent f5e2dd410e
commit d3bf8fa53b
2 changed files with 148 additions and 25 deletions

View File

@ -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 13: 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;
}

View File

@ -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;
};