diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 3a32b6676..f8efa5c6a 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -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 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 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, 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 get_crt() { return crtc_bus_handler_.get_crt(); } + /// @returns the speaker in use. std::shared_ptr 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::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 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; } diff --git a/Machines/AmstradCPC/AmstradCPC.hpp b/Machines/AmstradCPC/AmstradCPC.hpp index 0abe34dd7..964d6b22a 100644 --- a/Machines/AmstradCPC/AmstradCPC.hpp +++ b/Machines/AmstradCPC/AmstradCPC.hpp @@ -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 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; };