// // BBCMicro.cpp // Clock Signal // // Created by Thomas Harte on 14/09/2025. // Copyright © 2025 Thomas Harte. All rights reserved. // #include "BBCMicro.hpp" #include "Keyboard.hpp" #include "Machines/MachineTypes.hpp" #include "Machines/Utility/MemoryFuzzer.hpp" #include "Processors/6502/6502.hpp" #include "Components/6522/6522.hpp" #include "Components/6845/CRTC6845.hpp" #include "Components/SN76489/SN76489.hpp" #include "Components/6850/6850.hpp" #include "Analyser/Static/Acorn/Target.hpp" #include "Outputs/Log.hpp" #include "Outputs/CRT/CRT.hpp" #include "Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "Concurrency/AsyncTaskQueue.hpp" #include #include #include #include namespace BBCMicro { namespace { using Logger = Log::Logger; /*! Combines an SN76489 with an appropriate asynchronous queue and filtering speaker. */ struct Audio { Audio() : sn76489_(TI::SN76489::Personality::SN76489, audio_queue_), speaker_(sn76489_) { // I'm *VERY* unsure about this. speaker_.set_input_rate(2'000'000.0f); } ~Audio() { audio_queue_.flush(); } TI::SN76489 *operator ->() { flush(); return &sn76489_; } void operator +=(const HalfCycles duration) { speaker_.run_for(audio_queue_, time_since_update_.flush()); time_since_update_ += duration; } void flush() { audio_queue_.perform(); } Outputs::Speaker::Speaker *speaker() { return &speaker_; } private: Concurrency::AsyncTaskQueue audio_queue_; TI::SN76489 sn76489_; Outputs::Speaker::PullLowpass speaker_; HalfCycles time_since_update_; }; /*! Models the user-port VIA. */ struct UserVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler { }; using UserVIA = MOS::MOS6522::MOS6522; /*! Target for the video base address. */ struct VideoBaseAddress { void set_video_base(const uint8_t code) { switch(code) { case 0b00: video_base_ = 0x4000; break; case 0b01: video_base_ = 0x5800; break; case 0b10: video_base_ = 0x6000; break; case 0b11: video_base_ = 0x3000; break; } } protected: uint16_t video_base_ = 0; }; /*! Models the system VIA, which connects to the SN76489 and the keyboard. */ struct SystemVIAPortHandler; using SystemVIA = MOS::MOS6522::MOS6522; struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler { SystemVIAPortHandler(Audio &audio, VideoBaseAddress &video_base, SystemVIA &via) : audio_(audio), video_base_(video_base), via_(via) { // Set initial mode to mode 0. set_key(7, true); set_key(8, true); set_key(9, true); } // CA2: key pressed; // CA1: vertical sync; // CB2: lightpen strobe offscreen; // CB1: ADC conversion complete. template void set_port_output(const uint8_t value, uint8_t) { if(port == MOS::MOS6522::Port::A) { // Logger::info().append("Port A write: %02x", value); port_a_output_ = value; update_ca2(); return; } // The addressable latch. // // B0: enable writes to the sound generator; // B1, B2: read/write to the sound processor; // B3: keyboard scanning mode (?) // B4/B5: "hardware scrolling" (new base address > 32768?) // B6/B7: keyboard LEDs. const auto mask = uint8_t(1 << (value & 7)); const auto old_latch = latch_; latch_ = (latch_ & ~mask) | ((value & 8) ? mask : 0); if(mask == 0x8) { update_ca2(); } // Check for a strobe on the audio output. if((old_latch^latch_) & old_latch & 1) { audio_->write(port_a_output_); } video_base_.set_video_base((latch_ >> 4) & 3); // Update keyboard LEDs. if(mask >= 0x40) { Logger::info().append("CAPS: %d SHIFT: %d", bool(latch_ & 0x40), bool(latch_ & 0x40)); } } template uint8_t get_port_input() const { if(port == MOS::MOS6522::Port::B) { // TODO: // // b4/5: joystick fire buttons; // b6/7: speech interrupt/ready inputs. Logger::info().append("Port B read"); return 0x3f; // b6 = b7 = 0 => no speech hardware? } if(latch_ & 0b1000) { return 0xff; } // Read keyboard. Low six bits of output are key to check, state should be returned in high bit. const uint8_t key_state = key_states_[(port_a_output_ >> 4) & 7][port_a_output_ & 0xf] ? 0x80 : 0x00; Logger::info().append("Keyboard read from key %d = %d", port_a_output_, key_state); return key_state; } void set_key(const uint8_t key, const bool pressed) { key_states_[key >> 4][key & 0xf] = pressed; update_ca2(); } private: uint8_t latch_ = 0; uint8_t port_a_output_ = 0; Audio &audio_; VideoBaseAddress &video_base_; SystemVIA &via_; using KeyRow = std::bitset<16>; std::array key_states_{}; void update_ca2() { const bool state = [&]() -> bool { if(latch_ & 8) { return key_states_[1].any() | key_states_[2].any() | key_states_[3].any() | key_states_[4].any() | key_states_[5].any() | key_states_[6].any() | key_states_[7].any(); } else { return key_states_[(port_a_output_ >> 4) & 7].any(); } } (); // Logger::info().append("CA2 to %d in mode %d", state, bool(latch_ & 8)).append_if(!(latch_ & 8), " for key %02x", port_a_output_ & 0x7f); via_.set_control_line_input(state); } }; /*! Handles CRTC bus activity. */ class CRTCBusHandler: public VideoBaseAddress { public: CRTCBusHandler(const uint8_t *const ram, SystemVIA &system_via) : crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1), ram_(ram), system_via_(system_via) {} void set_palette(const uint8_t value) { const auto index = value >> 4; Logger::info().append("Palette entry %d set to %x", index, value & 0xf); } void set_control(const uint8_t value) { Logger::info().append("Video control set to %x", value); cycle_length_ = (value & 0x10) ? 8 : 16; Logger::info().append("TODO: video control => flash %d", bool(value & 0x01)); Logger::info().append("TODO: video control => teletext %d", bool(value & 0x02)); Logger::info().append("TODO: video control => columns %d", (value >> 2) & 0x03); Logger::info().append("TODO: video control => cursor segment %d%d%d", bool(value & 0x80), bool(value & 0x40), bool(value & 0x20)); } /*! 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. */ void perform_bus_cycle(const Motorola::CRTC::BusState &state) { system_via_.set_control_line_input(state.vsync); // bool print = false; // uint16_t start_address = 0x7c00; // int rows = 24; // if(print) { // for(int y = 0; y < rows; y++) { // for(int x = 0; x < 40; x++) { // printf("%c", ram_[start_address + y*40 + x]); // } // printf("\n"); // } // } // Count cycles since horizontal sync to insert a colour burst. if(state.hsync) { ++cycles_into_hsync_; } else { cycles_into_hsync_ = 0; } const bool is_colour_burst = (cycles_into_hsync_ >= 5 && cycles_into_hsync_ < 9); // Sync is taken to override pixels, and is combined as a simple OR. const bool is_sync = state.hsync || state.vsync; const bool is_blank = !is_sync && state.hsync; OutputMode output_mode; if(is_sync) { output_mode = OutputMode::Sync; } else if(is_colour_burst) { output_mode = OutputMode::ColourBurst; } else if(is_blank) { output_mode = OutputMode::Blank; } else if(state.display_enable) { output_mode = OutputMode::Pixels; } else { output_mode = OutputMode::Border; } // If a transition between sync/border/pixels just occurred, flush whatever was // in progress to the CRT and reset counting. if(output_mode != previous_output_mode_) { if(cycles_) { switch(previous_output_mode_) { default: case OutputMode::Blank: crt_.output_blank(cycles_); break; case OutputMode::Sync: crt_.output_sync(cycles_); break; case OutputMode::Border: crt_.output_blank(cycles_); break; case OutputMode::ColourBurst: crt_.output_default_colour_burst(cycles_); break; case OutputMode::Pixels: crt_.output_data(cycles_, pixels_); pixel_pointer_ = pixel_data_ = nullptr; pixels_ = 0; break; } } cycles_ = 0; previous_output_mode_ = output_mode; } // Increment cycles since state changed. cycles_ += cycle_length_; // Collect some more pixels if output is ongoing. if(previous_output_mode_ == OutputMode::Pixels) { if(!pixel_data_) { pixel_pointer_ = pixel_data_ = crt_.begin_data(320, 8); } if(pixel_pointer_) { // Hard coded for Mode 0! auto address = uint16_t( (state.refresh_address << 3) | state.row_address ); if(address & 0x8000) { address = (video_base_ + address) & 0x7fff; } const auto source = ram_[address]; pixel_pointer_[0] = (source & 0x80) ? 0xff : 0x00; pixel_pointer_[1] = (source & 0x40) ? 0xff : 0x00; pixel_pointer_[2] = (source & 0x20) ? 0xff : 0x00; pixel_pointer_[3] = (source & 0x10) ? 0xff : 0x00; pixel_pointer_[4] = (source & 0x08) ? 0xff : 0x00; pixel_pointer_[5] = (source & 0x04) ? 0xff : 0x00; pixel_pointer_[6] = (source & 0x02) ? 0xff : 0x00; pixel_pointer_[7] = (source & 0x01) ? 0xff : 0x00; pixel_pointer_ += 8; pixels_ += 8; // 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(pixels_ == 320) { crt_.output_data(cycles_, pixels_); pixel_pointer_ = pixel_data_ = nullptr; cycles_ = 0; pixels_ = 0; } } } } /// Sets the destination for output. void set_scan_target(Outputs::Display::ScanTarget *const scan_target) { crt_.set_scan_target(scan_target); } /// @returns The current scan status. Outputs::Display::ScanStatus get_scaled_scan_status() const { return crt_.get_scaled_scan_status(); } /// Sets the type of display. void set_display_type(const Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); } /// Gets the type of display. Outputs::Display::DisplayType get_display_type() const { return crt_.get_display_type(); } private: enum class OutputMode { Sync, Blank, ColourBurst, Border, Pixels } previous_output_mode_ = OutputMode::Sync; int cycles_ = 0; int cycles_into_hsync_ = 0; int cycle_length_ = 8; Outputs::CRT::CRT crt_; uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr; size_t pixels_; const uint8_t *const ram_ = nullptr; SystemVIA &system_via_; }; using CRTC = Motorola::CRTC::CRTC6845< CRTCBusHandler, Motorola::CRTC::Personality::HD6845S, Motorola::CRTC::CursorType::None>; } class ConcreteMachine: public Machine, public MachineTypes::AudioProducer, public MachineTypes::MappedKeyboardMachine, public MachineTypes::ScanProducer, public MachineTypes::TimedMachine, public MOS::MOS6522::IRQDelegatePortHandler::Delegate { public: ConcreteMachine( const Analyser::Static::Acorn::BBCMicroTarget &target, const ROMMachine::ROMFetcher &rom_fetcher ) : m6502_(*this), system_via_port_handler_(audio_, crtc_bus_handler_, system_via_), user_via_(user_via_port_handler_), system_via_(system_via_port_handler_), crtc_bus_handler_(ram_.data(), system_via_), crtc_(crtc_bus_handler_), acia_(HalfCycles(2'000'000)) // TODO: look up real ACIA clock rate. { set_clock_rate(2'000'000); system_via_port_handler_.set_interrupt_delegate(this); user_via_port_handler_.set_interrupt_delegate(this); // Grab ROMs. using Request = ::ROM::Request; using Name = ::ROM::Name; const auto request = Request(Name::AcornBASICII) && Request(Name::BBCMicroMOS12); auto roms = rom_fetcher(request); if(!request.validate(roms)) { throw ROMMachine::Error::MissingROMs; } const auto os_data = roms.find(Name::BBCMicroMOS12)->second; std::copy(os_data.begin(), os_data.end(), os_.begin()); install_sideways(15, roms.find(Name::AcornBASICII)->second, false); // Setup fixed parts of memory map. page(0, &ram_[0], true); page(1, &ram_[16384], true); page_sideways(15); page(3, os_.data(), false); Memory::Fuzz(ram_); (void)target; } // MARK: - 6502 bus. Cycles perform_bus_operation( const CPU::MOS6502::BusOperation operation, const uint16_t address, uint8_t *const value ) { // Returns @c true if @c address is a device on the 1Mhz bus; @c false otherwise. static constexpr auto is_1mhz = [](const uint16_t address) { // Fast exit if outside the IO space. if(address < 0xfc00) return false; if(address >= 0xff00) return false; // Pages FC ('Fred'), FD ('Jim'). if(address < 0xfe00) return true; // The 6845, 6850 and serial ULA. if(address < 0xfe18) return true; // The two VIAs. if(address >= 0xfe40 && address < 0xfe80) return true; // The ADC. if(address >= 0xfec0 && address < 0xfee0) return true; // Otherwise: in IO space, but not a 1Mhz device. return false; }; // Determine whether this access hits the 1Mhz bus; if so then apply appropriate penalty, and update phase. const auto duration = is_1mhz(address) ? Cycles(2 + (phase_&1)) : Cycles(1); phase_ += duration.as(); // // 1Mhz devices. // const auto half_cycles = HalfCycles(duration.as_integral()); audio_ += half_cycles; system_via_.run_for(half_cycles); user_via_.run_for(half_cycles); // // 2Mhz devices. // // TODO: if CRTC clock is 1Mhz, adapt. if(crtc_2mhz_) { crtc_.run_for(duration); } else { // TODO: possibly skip one cycle if clock speed just changed partway through a 1Mhz window? const auto cycles = (phase_ >> 1) - ((phase_ - duration.as()) >> 1); crtc_.run_for(Cycles(cycles)); } // // Questionably-clocked devices. // acia_.run_for(half_cycles); // // Check for an IO access; if found then perform that and exit. // // static bool log = false; if(address >= 0xfc00 && address < 0xff00) { if(address >= 0xfe40 && address < 0xfe60) { if(is_read(operation)) { *value = system_via_.read(address); } else { system_via_.write(address, *value); } } else if(address >= 0xfe60 && address < 0xfe80) { if(is_read(operation)) { *value = user_via_.read(address); } else { user_via_.write(address, *value); } } else if(address == 0xfe30) { if(is_read(operation)) { *value = 0xfe; } else { page_sideways(*value & 0xf); } } else if(address >= 0xfe00 && address < 0xfe08) { if(is_read(operation)) { if(address & 1) { *value = crtc_.get_register(); } else { *value = crtc_.get_status(); } } else { if(address & 1) { crtc_.set_register(*value); } else { crtc_.select_register(*value); } } } else if(address >= 0xfe20 && address < 0xfe30) { if(is_read(operation)) { *value = 0xfe; } else { switch(address) { case 0xfe20: crtc_bus_handler_.set_control(*value); crtc_2mhz_ = *value & 0x10; break; case 0xfe21: crtc_bus_handler_.set_palette(*value); break; } } } else if(address == 0xfee0) { if(is_read(operation)) { Logger::info().append("Read tube status: 0"); *value = 0; } else { Logger::info().append("Wrote tube: %02x", *value); } } else if(address >= 0xfe08 && address < 0xfe10) { if(is_read(operation)) { Logger::info().append("ACIA read"); *value = acia_.read(address); } else { Logger::info().append("ACIA write: %02x", *value); acia_.write(address, *value); } } else { Logger::error() .append("Unhandled IO %s at %04x", is_read(operation) ? "read" : "write", address) .append_if(!is_read(operation), ": %02x", *value); } return duration; } // // ROM or RAM access. // // if(operation == CPU::MOS6502Esque::BusOperation::ReadOpcode) { // log |= address == 0xc4c0; // // if(log) { // printf("%04x\n", address); // } // } if(is_read(operation)) { // TODO: probably don't do this with this condition? See how it compiles. If it's a CMOV somehow, no problem. if((address >> 14) == 2 && !sideways_read_mask_) { *value = 0xff; } else { *value = memory_[address >> 14][address]; } } else { if(memory_write_masks_[address >> 14]) { memory_[address >> 14][address] = *value; if(address >= 0x7c00 && *value) { Logger::info().append("Output character: %c", *value); } } } return duration; } private: // MARK: - AudioProducer. Outputs::Speaker::Speaker *get_speaker() override { return audio_.speaker(); } // MARK: - ScanProducer. void set_scan_target(Outputs::Display::ScanTarget *const target) override { crtc_bus_handler_.set_scan_target(target); } Outputs::Display::ScanStatus get_scaled_scan_status() const override { return crtc_bus_handler_.get_scaled_scan_status(); } // MARK: - KeyboardMachine. BBCMicro::KeyboardMapper mapper_; KeyboardMapper *get_keyboard_mapper() override { return &mapper_; } void set_key_state(const uint16_t key, const bool is_pressed) override { system_via_port_handler_.set_key(uint8_t(key), is_pressed); } // MARK: - TimedMachine. void run_for(const Cycles cycles) override { m6502_.run_for(cycles); } void flush_output(const int outputs) final { if(outputs & Output::Audio) { audio_.flush(); } } // MARK: - IRQDelegatePortHandler::Delegate. void mos6522_did_change_interrupt_status(void *) override { update_irq_line(); } // MARK: - Clock phase. int phase_ = 0; // MARK: - Memory. std::array ram_; using ROM = std::array; ROM os_; std::array roms_; std::bitset<16> rom_inserted_; std::bitset<16> rom_write_masks_; uint8_t *memory_[4]; std::bitset<4> memory_write_masks_; bool sideways_read_mask_ = false; void page(const size_t slot, uint8_t *const source, bool is_writeable) { memory_[slot] = source - (slot * 16384); memory_write_masks_[slot] = is_writeable; } void page_sideways(const size_t source) { sideways_read_mask_ = rom_inserted_[source]; page(2, roms_[source].data(), rom_write_masks_[source]); } void install_sideways(const size_t slot, const std::vector &source, bool is_writeable) { rom_write_masks_[slot] = is_writeable; rom_inserted_[slot] = true; assert(source.size() == roms_[slot].size()); std::copy(source.begin(), source.end(), roms_[slot].begin()); } // MARK: - Components. CPU::MOS6502::Processor m6502_; UserVIAPortHandler user_via_port_handler_; SystemVIAPortHandler system_via_port_handler_; UserVIA user_via_; SystemVIA system_via_; void update_irq_line() { m6502_.set_irq_line( user_via_.get_interrupt_line() || system_via_.get_interrupt_line() ); } Audio audio_; CRTCBusHandler crtc_bus_handler_; CRTC crtc_; bool crtc_2mhz_ = true; Motorola::ACIA::ACIA acia_; }; } using namespace BBCMicro; std::unique_ptr Machine::BBCMicro( const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher ) { using Target = Analyser::Static::Acorn::BBCMicroTarget; const Target *const acorn_target = dynamic_cast(target); return std::make_unique(*acorn_target, rom_fetcher); }