// // CRTC6845.hpp // Clock Signal // // Created by Thomas Harte on 31/07/2017. // Copyright 2017 Thomas Harte. All rights reserved. // #pragma once #include "../../ClockReceiver/ClockReceiver.hpp" #include #include namespace Motorola::CRTC { struct BusState { bool display_enable = false; bool hsync = false; bool vsync = false; bool cursor = false; uint16_t refresh_address = 0; uint16_t row_address = 0; // Not strictly part of the bus state; provided because the partition between 6845 and bus handler // doesn't quite hold up in some emulated systems where the two are integrated and share more state. int field_count = 0; }; class BusHandler { public: /*! Performs the first phase of a 6845 bus cycle; this is the phase in which it is intended that systems using the 6845 respect the bus state and produce pixels, sync or whatever they require. */ void perform_bus_cycle_phase1(const BusState &) {} /*! Performs the second phase of a 6845 bus cycle. Some bus state, including sync, is updated directly after phase 1 and hence is visible to an observer during phase 2. Handlers may therefore implement @c perform_bus_cycle_phase2 to be notified of the availability of that state without having to wait until the next cycle has begun. */ void perform_bus_cycle_phase2(const BusState &) {} }; enum class Personality { HD6845S, // Type 0 in CPC parlance. Zero-width HSYNC available, no status, programmable VSYNC length. // Considered exactly identical to the UM6845, so this enum covers both. UM6845R, // Type 1 in CPC parlance. Status register, fixed-length VSYNC. MC6845, // Type 2. No status register, fixed-length VSYNC, no zero-length HSYNC. AMS40226, // Type 3. Status is get register, fixed-length VSYNC, no zero-length HSYNC. EGA, // Extended EGA-style CRTC; uses 16-bit addressing throughout. }; constexpr bool is_egavga(Personality p) { return p >= Personality::EGA; } // https://www.pcjs.org/blog/2018/03/20/ advises that "the behavior of bits 5 and 6 [of register 10, the cursor start // register is really card specific". // // This enum captures those specifics. enum class CursorType { /// No cursor signal is generated. None, /// MDA style: 00 => symmetric blinking; 01 or 10 => no blinking; 11 => short on, long off. MDA, /// EGA style: ignore the bits completely. EGA, }; // TODO UM6845R and R12/R13; see http://www.cpcwiki.eu/index.php/CRTC#CRTC_Differences template class CRTC6845 { public: CRTC6845(BusHandlerT &bus_handler) noexcept : bus_handler_(bus_handler), status_(0) {} void select_register(uint8_t r) { selected_register_ = r; } uint8_t get_status() { switch(personality) { case Personality::UM6845R: return status_ | (bus_state_.vsync ? 0x20 : 0x00); case Personality::AMS40226: return get_register(); default: return 0xff; } return 0xff; } uint8_t get_register() { if(selected_register_ == 31) status_ &= ~0x80; if(selected_register_ == 16 || selected_register_ == 17) status_ &= ~0x40; if(personality == Personality::UM6845R && selected_register_ == 31) return dummy_register_; if(selected_register_ < 12 || selected_register_ > 17) return 0xff; return registers_[selected_register_]; } void set_register(uint8_t value) { static constexpr bool is_ega = is_egavga(personality); auto load_low = [value](uint16_t &target) { target = (target & 0xff00) | value; }; auto load_high = [value](uint16_t &target) { constexpr uint8_t mask = RefreshMask >> 8; target = uint16_t((target & 0x00ff) | ((value & mask) << 8)); }; switch(selected_register_) { case 0: layout_.horizontal.total = value; break; case 1: layout_.horizontal.displayed = value; break; case 2: layout_.horizontal.start_sync = value; break; case 3: layout_.horizontal.sync_width = value & 0xf; layout_.vertical.sync_lines = value >> 4; // TODO: vertical sync lines: // "(0 means 16 on some CRTC. Not present on all CRTCs, fixed to 16 lines on these)" break; case 4: layout_.vertical.total = value & 0x7f; break; case 5: layout_.vertical.adjust = value & 0x1f; break; case 6: layout_.vertical.displayed = value & 0x7f; break; case 7: layout_.vertical.start_sync = value & 0x7f; break; case 8: switch(value & 3) { default: layout_.interlace_mode_ = InterlaceMode::Off; break; case 0b01: layout_.interlace_mode_ = InterlaceMode::InterlaceSync; break; case 0b11: layout_.interlace_mode_ = InterlaceMode::InterlaceSyncAndVideo; break; } // Per CPC documentation, skew doesn't work on a "type 1 or 2", i.e. an MC6845 or a UM6845R. if(personality != Personality::UM6845R && personality != Personality::MC6845) { switch((value >> 4)&3) { default: display_skew_mask_ = 1; break; case 1: display_skew_mask_ = 2; break; case 2: display_skew_mask_ = 4; break; } } break; case 9: layout_.vertical.end_row = value & 0x1f; break; case 10: layout_.vertical.start_cursor = value & 0x1f; layout_.cursor_flags = (value >> 5) & 3; break; case 11: layout_.vertical.end_cursor = value & 0x1f; break; case 12: load_high(layout_.start_address); break; case 13: load_low(layout_.start_address); break; case 14: load_high(layout_.cursor_address); break; case 15: load_low(layout_.cursor_address); break; } static constexpr uint8_t masks[] = { 0xff, // Horizontal total. 0xff, // Horizontal display end. 0xff, // Start horizontal blank. 0xff, // // EGA: b0–b4: end of horizontal blank; // b5–b6: "Number of character clocks to delay start of display after Horizontal Total has been reached." is_ega ? 0xff : 0x7f, // Start horizontal retrace. 0x1f, 0x7f, 0x7f, 0xff, 0x1f, 0x7f, 0x1f, uint8_t(RefreshMask >> 8), uint8_t(RefreshMask), uint8_t(RefreshMask >> 8), uint8_t(RefreshMask), }; if(selected_register_ < 16) { registers_[selected_register_] = value & masks[selected_register_]; } if(selected_register_ == 31 && personality == Personality::UM6845R) { dummy_register_ = value; } } void trigger_light_pen() { registers_[17] = bus_state_.refresh_address & 0xff; registers_[16] = bus_state_.refresh_address >> 8; status_ |= 0x40; } void run_for(Cycles cycles) { auto cyles_remaining = cycles.as_integral(); while(cyles_remaining--) { // Check for end of visible characters. if(character_counter_ == layout_.horizontal.displayed) { // TODO: consider skew in character_is_visible_. Or maybe defer until perform_bus_cycle? character_is_visible_ = false; end_of_line_address_ = bus_state_.refresh_address; } perform_bus_cycle_phase1(); bus_state_.refresh_address = (bus_state_.refresh_address + 1) & RefreshMask; bus_state_.cursor = is_cursor_line_ && bus_state_.refresh_address == layout_.cursor_address; // Check for end-of-line. if(character_counter_ == layout_.horizontal.total) { character_counter_ = 0; do_end_of_line(); character_is_visible_ = true; } else { // Increment counter. character_counter_++; } // Check for start of horizontal sync. if(character_counter_ == layout_.horizontal.start_sync) { hsync_counter_ = 0; bus_state_.hsync = true; } // Check for end of horizontal sync; note that a sync time of zero will result in an immediate // cancellation of the plan to perform sync if this is an HD6845S or UM6845R; otherwise zero // will end up counting as 16 as it won't be checked until after overflow. if(bus_state_.hsync) { switch(personality) { case Personality::HD6845S: case Personality::UM6845R: bus_state_.hsync = hsync_counter_ != layout_.horizontal.sync_width; hsync_counter_ = (hsync_counter_ + 1) & 15; break; default: hsync_counter_ = (hsync_counter_ + 1) & 15; bus_state_.hsync = hsync_counter_ != layout_.horizontal.sync_width; break; } } perform_bus_cycle_phase2(); } } const BusState &get_bus_state() const { return bus_state_; } private: static constexpr uint16_t RefreshMask = (personality >= Personality::EGA) ? 0xffff : 0x3fff; inline void perform_bus_cycle_phase1() { // Skew theory of operation: keep a history of the last three states, and apply whichever is selected. character_is_visible_shifter_ = (character_is_visible_shifter_ << 1) | unsigned(character_is_visible_); bus_state_.display_enable = (int(character_is_visible_shifter_) & display_skew_mask_) && line_is_visible_; bus_handler_.perform_bus_cycle_phase1(bus_state_); } inline void perform_bus_cycle_phase2() { bus_handler_.perform_bus_cycle_phase2(bus_state_); } inline void do_end_of_line() { if constexpr (cursor_type != CursorType::None) { // Check for cursor disable. // TODO: this is handled differently on the EGA, should I ever implement that. is_cursor_line_ &= bus_state_.row_address != layout_.vertical.end_cursor; } // Check for end of vertical sync. if(bus_state_.vsync) { vsync_counter_ = (vsync_counter_ + 1) & 15; // On the UM6845R and AMS40226, honour the programmed vertical sync time; on the other CRTCs // always use a vertical sync count of 16. switch(personality) { case Personality::HD6845S: case Personality::AMS40226: bus_state_.vsync = vsync_counter_ != layout_.vertical.sync_lines; break; default: bus_state_.vsync = vsync_counter_ != 0; break; } } if(is_in_adjustment_period_) { line_counter_++; if(line_counter_ == layout_.vertical.adjust) { is_in_adjustment_period_ = false; do_end_of_frame(); } } else { // Advance vertical counter. if(bus_state_.row_address == layout_.vertical.end_row) { bus_state_.row_address = 0; line_address_ = end_of_line_address_; // Check for entry into the overflow area. if(line_counter_ == layout_.vertical.total) { if(layout_.vertical.adjust) { line_counter_ = 0; is_in_adjustment_period_ = true; } else { do_end_of_frame(); } } else { line_counter_ = (line_counter_ + 1) & 0x7f; // Check for start of vertical sync. if(line_counter_ == layout_.vertical.start_sync) { bus_state_.vsync = true; vsync_counter_ = 0; } // Check for end of visible lines. if(line_counter_ == layout_.vertical.displayed) { line_is_visible_ = false; } } } else { bus_state_.row_address = (bus_state_.row_address + 1) & 0x1f; } } bus_state_.refresh_address = line_address_; character_counter_ = 0; character_is_visible_ = (layout_.horizontal.displayed != 0); if constexpr (cursor_type != CursorType::None) { // Check for cursor enable. is_cursor_line_ |= bus_state_.row_address == layout_.vertical.start_cursor; switch(cursor_type) { // MDA-style blinking. // https://retrocomputing.stackexchange.com/questions/27803/what-are-the-blinking-rates-of-the-caret-and-of-blinking-text-on-pc-graphics-car // gives an 8/8 pattern for regular blinking though mode 11 is then just a guess. case CursorType::MDA: switch(layout_.cursor_flags) { case 0b11: is_cursor_line_ &= (bus_state_.field_count & 8) < 3; break; case 0b00: is_cursor_line_ &= bool(bus_state_.field_count & 8); break; case 0b01: is_cursor_line_ = false; break; case 0b10: is_cursor_line_ = true; break; default: break; } break; } } } inline void do_end_of_frame() { line_counter_ = 0; line_is_visible_ = true; line_address_ = layout_.start_address; bus_state_.refresh_address = line_address_; ++bus_state_.field_count; } BusHandlerT &bus_handler_; BusState bus_state_; enum class InterlaceMode { Off, InterlaceSync, InterlaceSyncAndVideo, }; enum class BlinkMode { // TODO. }; struct { struct { uint8_t total; uint8_t displayed; uint8_t start_sync; uint8_t sync_width; } horizontal; struct { uint8_t total; uint8_t displayed; uint8_t start_sync; uint8_t sync_lines; uint8_t adjust; uint8_t end_row; uint8_t start_cursor; uint8_t end_cursor; } vertical; InterlaceMode interlace_mode_ = InterlaceMode::Off; uint16_t start_address; uint16_t cursor_address; uint16_t light_pen_address; uint8_t cursor_flags; } layout_; uint8_t registers_[18]{}; uint8_t dummy_register_ = 0; int selected_register_ = 0; uint8_t character_counter_ = 0; uint8_t line_counter_ = 0; bool character_is_visible_ = false, line_is_visible_ = false; int hsync_counter_ = 0; int vsync_counter_ = 0; bool is_in_adjustment_period_ = false; uint16_t line_address_ = 0; uint16_t end_of_line_address_ = 0; uint8_t status_ = 0; int display_skew_mask_ = 1; unsigned int character_is_visible_shifter_ = 0; bool is_cursor_line_ = false; }; }