diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45794c931..022f472e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,7 +53,7 @@ jobs: name: SDL UI / scons / ${{ matrix.os }} strategy: matrix: - os: [macos-11, macos-12, macos-13, macos-14, ubuntu-latest] + os: [macos-14, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout diff --git a/Components/9918/Implementation/9918.cpp b/Components/9918/Implementation/9918.cpp index 86f7639b9..34de778a1 100644 --- a/Components/9918/Implementation/9918.cpp +++ b/Components/9918/Implementation/9918.cpp @@ -163,7 +163,7 @@ void Base::posit_sprite(int sprite_number, int sprite_position, uin } const auto sprite_row = uint8_t(screen_row - sprite_position); - if(sprite_row < 0 || sprite_row >= sprite_height_) return; + if(sprite_row >= sprite_height_) return; // The less-than-zero case is dealt with by the cast to unsigned. if(fetch_sprite_buffer_->active_sprite_slot == mode_timing_.maximum_visible_sprites) { status_ |= StatusSpriteOverflow; diff --git a/Components/I2C/I2C.cpp b/Components/I2C/I2C.cpp new file mode 100644 index 000000000..82b49400e --- /dev/null +++ b/Components/I2C/I2C.cpp @@ -0,0 +1,226 @@ +// +// I2C.cpp +// Clock Signal +// +// Created by Thomas Harte on 16/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#include "I2C.hpp" + +#include "../../Outputs/Log.hpp" + +using namespace I2C; + +namespace { + +Log::Logger logger; + +} + +void Bus::set_data(bool pulled) { + set_clock_data(clock_, pulled); +} +bool Bus::data() { + bool result = data_; + if(peripheral_bits_) { + result |= !(peripheral_response_ & 0x80); + } + return result; +} + +void Bus::set_clock(bool pulled) { + set_clock_data(pulled, data_); +} +bool Bus::clock() { + return clock_; +} + +void Bus::set_clock_data(bool clock_pulled, bool data_pulled) { + // Proceed only if changes are evidenced. + if(clock_pulled == clock_ && data_pulled == data_) { + return; + } + + const bool prior_clock = clock_; + const bool prior_data = data_; + clock_ = clock_pulled; + data_ = data_pulled; + + // If currently serialising from a peripheral then shift onwards on + // every clock trailing edge. + if(peripheral_bits_) { + // Trailing edge of clock => bit has been consumed. + if(!prior_clock && clock_) { + logger.info().append("<< %d", (peripheral_response_ >> 7) & 1); + --peripheral_bits_; + peripheral_response_ <<= 1; + + if(!peripheral_bits_) { + signal(Event::FinishedOutput); + } + } + return; + } + + // Not currently serialising implies listening. + if(!clock_ && prior_data != data_) { + // A data transition outside of a clock cycle implies a start or stop. + in_bit_ = false; + if(data_) { + logger.info().append("S"); + signal(Event::Start); + } else { + logger.info().append("W"); + signal(Event::Stop); + } + } else if(clock_ != prior_clock) { + // Bits: wait until the falling edge of the cycle. + if(!clock_) { + // Rising edge: clock period begins. + in_bit_ = true; + } else if(in_bit_) { + // Falling edge: clock period ends (assuming it began; otherwise this is a preparatory + // clock transition only, immediately after a start bit). + in_bit_ = false; + + if(data_) { + logger.info().append("0"); + signal(Event::Zero); + } else { + logger.info().append("1"); + signal(Event::One); + } + } + } +} + +void Bus::signal(Event event) { + const auto capture_bit = [&]() { + input_ = uint16_t((input_ << 1) | (event == Event::Zero ? 0 : 1)); + ++input_count_; + }; + + const auto acknowledge = [&]() { + // Post an acknowledgement bit. + peripheral_response_ = 0; + peripheral_bits_ = 1; + }; + + const auto set_state = [&](State state) { + state_ = state; + input_count_ = 0; + input_ = 0; + }; + + const auto enqueue = [&](std::optional next) { + if(next) { + peripheral_response_ = static_cast(*next); + peripheral_bits_ = 8; + set_state(State::AwaitingByteAcknowledge); + } else { + set_state(State::AwaitingAddress); + } + }; + + const auto stop = [&]() { + set_state(State::AwaitingAddress); + active_peripheral_ = nullptr; + }; + + // Allow start and stop conditions at any time. + if(event == Event::Start) { + set_state(State::CollectingAddress); + active_peripheral_ = nullptr; + return; + } + + if(event == Event::Stop) { + if(active_peripheral_) { + active_peripheral_->stop(); + } + stop(); + return; + } + + switch(state_) { + // While waiting for an address, don't respond to anything other than a + // start bit, which is actually dealt with above. + case State::AwaitingAddress: break; + + // To collect an address: shift in eight bits, and if there's a device + // at that address then acknowledge the address and segue into a read + // or write loop. + case State::CollectingAddress: + capture_bit(); + if(input_count_ == 8) { + auto pair = peripherals_.find(uint8_t(input_) & 0xfe); + if(pair != peripherals_.end()) { + active_peripheral_ = pair->second; + active_peripheral_->start(input_ & 1); + + if(input_&1) { + acknowledge(); + set_state(State::CompletingReadAcknowledge); + } else { + acknowledge(); + set_state(State::ReceivingByte); + } + } else { + state_ = State::AwaitingAddress; + } + } + break; + + // Receiving byte: wait until a scheduled acknowledgment has + // happened, then collect eight bits, then see whether the + // active peripheral will accept them. If so, acknowledge and repeat. + // Otherwise fall silent. + case State::ReceivingByte: + if(event == Event::FinishedOutput) { + return; + } + capture_bit(); + if(input_count_ == 8) { + if(active_peripheral_->write(static_cast(input_))) { + acknowledge(); + set_state(State::ReceivingByte); + } else { + stop(); + } + } + break; + + // The initial state immediately after a peripheral has been started + // in read mode and the address-select acknowledgement is still + // being serialised. + // + // Once that is completed, enqueues the first byte from the peripheral. + case State::CompletingReadAcknowledge: + if(event != Event::FinishedOutput) { + break; + } + enqueue(active_peripheral_->read()); + break; + + // Repeating state during reading; waits until the previous byte has + // been fully serialised, and if the host acknowledged it then posts + // the next. If the host didn't acknowledge, stops the connection. + case State::AwaitingByteAcknowledge: + if(event == Event::FinishedOutput) { + break; + } + if(event != Event::Zero) { + stop(); + break; + } + + // Add a new byte if there is one. + enqueue(active_peripheral_->read()); + break; + } +} + +void Bus::add_peripheral(Peripheral *peripheral, int address) { + peripherals_[address] = peripheral; +} diff --git a/Components/I2C/I2C.hpp b/Components/I2C/I2C.hpp new file mode 100644 index 000000000..e8dc0c783 --- /dev/null +++ b/Components/I2C/I2C.hpp @@ -0,0 +1,82 @@ +// +// I2C.hpp +// Clock Signal +// +// Created by Thomas Harte on 16/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include +#include +#include + +namespace I2C { + +/// Provides the virtual interface for an I2C peripheral; attaching this to a bus +/// provides automatic protocol handling. +struct Peripheral { + /// Indicates that the host signalled the start condition and addressed this + /// peripheral, along with whether it indicated a read or write. + virtual void start([[maybe_unused]] bool is_read) {} + + /// Indicates that the host signalled a stop. + virtual void stop() {} + + /// Requests the next byte to serialise onto the I2C bus after this peripheral has + /// been started in read mode. + /// + /// @returns A byte to serialise or std::nullopt if the peripheral declines to + /// continue to communicate. + virtual std::optional read() { return std::nullopt; } + + /// Provides a byte received from the bus after this peripheral has been started + /// in write mode. + /// + /// @returns @c true if the write should be acknowledged; @c false otherwise. + virtual bool write(uint8_t) { return false; } +}; + +class Bus { +public: + void set_data(bool pulled); + bool data(); + + void set_clock(bool pulled); + bool clock(); + + void set_clock_data(bool clock_pulled, bool data_pulled); + + void add_peripheral(Peripheral *, int address); + +private: + bool data_ = false; + bool clock_ = false; + bool in_bit_ = false; + std::unordered_map peripherals_; + + uint16_t input_ = 0xffff; + int input_count_ = -1; + + Peripheral *active_peripheral_ = nullptr; + uint16_t peripheral_response_ = 0xffff; + int peripheral_bits_ = 0; + + enum class Event { + Zero, One, Start, Stop, FinishedOutput, + }; + void signal(Event); + + enum class State { + AwaitingAddress, + CollectingAddress, + + CompletingReadAcknowledge, + AwaitingByteAcknowledge, + + ReceivingByte, + } state_ = State::AwaitingAddress; +}; + +} diff --git a/Components/Serial/Line.cpp b/Components/Serial/Line.cpp index d6eb875eb..6caf71a7c 100644 --- a/Components/Serial/Line.cpp +++ b/Components/Serial/Line.cpp @@ -161,9 +161,7 @@ void Line::update_delegate(bool level) { // Forward as many bits as occur. Storage::Time time_left(cycles_to_forward, int(clock_rate_.as_integral())); const int bit = level ? 1 : 0; - int bits = 0; while(time_left >= time_left_in_bit_) { - ++bits; if(!read_delegate_->serial_line_did_produce_bit(this, bit)) { read_delegate_phase_ = ReadDelegatePhase::WaitingForZero; if(bit) return; diff --git a/InstructionSets/ARM/BarrelShifter.hpp b/InstructionSets/ARM/BarrelShifter.hpp index 9c45c3059..575388c7e 100644 --- a/InstructionSets/ARM/BarrelShifter.hpp +++ b/InstructionSets/ARM/BarrelShifter.hpp @@ -30,7 +30,7 @@ template <> struct Carry { /// receive the new value of the carry flag following the rotation — @c 0 for no carry, @c non-0 for carry. /// /// Shift amounts of 0 are given the meaning attributed to them for immediate shift counts. -template +template void shift(uint32_t &source, uint32_t amount, typename Carry::type carry) { switch(type) { case ShiftType::LogicalLeft: @@ -47,49 +47,61 @@ void shift(uint32_t &source, uint32_t amount, typename Carry::type ca break; case ShiftType::LogicalRight: + if(!amount && is_immediate_shift) { + // An immediate logical shift right by '0' is treated as a shift by 32; + // assemblers are supposed to map LSR #0 to LSL #0. + amount = 32; + } + if(amount > 32) { if constexpr (set_carry) carry = 0; source = 0; - } else if(amount == 32 || !amount) { - // A logical shift right by '0' is treated as a shift by 32; - // assemblers are supposed to map LSR #0 to LSL #0. + } else if(amount == 32) { if constexpr (set_carry) carry = source & 0x8000'0000; source = 0; - } else { + } else if(amount > 0) { if constexpr (set_carry) carry = source & (1 << (amount - 1)); source >>= amount; } break; case ShiftType::ArithmeticRight: { + if(!amount && is_immediate_shift) { + // An immediate arithmetic shift of '0' is treated as a shift by 32. + amount = 32; + } + const uint32_t sign = (source & 0x8000'0000) ? 0xffff'ffff : 0x0000'0000; if(amount >= 32) { - if constexpr (set_carry) carry = sign; + if constexpr (set_carry) carry = source & 0x8000'0000; source = sign; } else if(amount > 0) { if constexpr (set_carry) carry = source & (1 << (amount - 1)); source = (source >> amount) | (sign << (32 - amount)); - } else { - // As per logical right, an arithmetic shift of '0' is - // treated as a shift by 32. - if constexpr (set_carry) carry = source & 0x8000'0000; - source = sign; } } break; case ShiftType::RotateRight: { - if(amount == 32) { - if constexpr (set_carry) carry = source & 0x8000'0000; - } else if(amount == 0) { - // Rotate right by 0 is treated as a rotate right by 1 through carry. - const uint32_t high = carry << 31; - if constexpr (set_carry) carry = source & 1; - source = (source >> 1) | high; - } else { - amount &= 31; + if(!amount) { + if(is_immediate_shift) { + // Immediate rotate right by 0 is treated as a rotate right by 1 through carry. + const uint32_t high = carry << 31; + if constexpr (set_carry) carry = source & 1; + source = (source >> 1) | high; + } + break; + } + + // "ROR by 32 has result equal to Rm, carry out equal to bit 31 ... + // [for] ROR by n where n is greater than 32 ... repeatedly subtract 32 from n + // until the amount is in the range 1 to 32" + amount &= 31; + if(amount) { if constexpr (set_carry) carry = source & (1 << (amount - 1)); source = (source >> amount) | (source << (32 - amount)); + } else { + if constexpr (set_carry) carry = source & 0x8000'0000; } } break; @@ -100,20 +112,20 @@ void shift(uint32_t &source, uint32_t amount, typename Carry::type ca } /// Acts as per @c shift above, but applies runtime shift-type selection. -template +template void shift(ShiftType type, uint32_t &source, uint32_t amount, typename Carry::type carry) { switch(type) { case ShiftType::LogicalLeft: - shift(source, amount, carry); + shift(source, amount, carry); break; case ShiftType::LogicalRight: - shift(source, amount, carry); + shift(source, amount, carry); break; case ShiftType::ArithmeticRight: - shift(source, amount, carry); + shift(source, amount, carry); break; case ShiftType::RotateRight: - shift(source, amount, carry); + shift(source, amount, carry); break; } } diff --git a/InstructionSets/ARM/Disassembler.hpp b/InstructionSets/ARM/Disassembler.hpp new file mode 100644 index 000000000..6f744e426 --- /dev/null +++ b/InstructionSets/ARM/Disassembler.hpp @@ -0,0 +1,270 @@ +// +// Disassembler.hpp +// Clock Signal +// +// Created by Thomas Harte on 19/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "OperationMapper.hpp" + +#include +#include + +namespace InstructionSet::ARM { + +/// Holds a single ARM operand, whether a source/destination or immediate value, potentially including a shift. +struct Operand { + enum class Type { + Immediate, Register, RegisterList, None + } type = Type::None; + uint32_t value = 0; + + // TODO: encode shifting + + operator std::string() const { + switch(type) { + default: return ""; + case Type::Register: return std::string("r") + std::to_string(value); + case Type::RegisterList: { + std::stringstream stream; + stream << '['; + bool first = true; + for(int c = 0; c < 16; c++) { + if(value & (1 << c)) { + if(!first) stream << ", "; + first = false; + + stream << 'r' << c; + } + } + stream << ']'; + + return stream.str(); + } + } + } +}; + +/// Describes a single ARM instruction, suboptimally but such that all relevant detail has been extracted +/// by the OperationMapper and is now easy to inspect or to turn into a string. +struct Instruction { + Condition condition = Condition::AL; + enum class Operation { + AND, EOR, SUB, RSB, + ADD, ADC, SBC, RSC, + TST, TEQ, CMP, CMN, + ORR, MOV, BIC, MVN, + + LDR, STR, + LDM, STM, + + B, BL, + + SWI, + + MRC, MCR, + + Undefined, + } operation = Operation::Undefined; + + Operand destination, operand1, operand2; + bool sets_flags = false; + bool is_byte = false; + + std::string to_string(uint32_t address) const { + std::ostringstream result; + + // Treat all nevers as nops. + if(condition == Condition::NV) { + return "nop"; + } + + // Print operation. + switch(operation) { + case Operation::Undefined: return "undefined"; + case Operation::SWI: return "swi"; + + case Operation::B: result << "b"; break; + case Operation::BL: result << "bl"; break; + + case Operation::AND: result << "and"; break; + case Operation::EOR: result << "eor"; break; + case Operation::SUB: result << "sub"; break; + case Operation::RSB: result << "rsb"; break; + case Operation::ADD: result << "add"; break; + case Operation::ADC: result << "adc"; break; + case Operation::SBC: result << "sbc"; break; + case Operation::RSC: result << "rsc"; break; + case Operation::TST: result << "tst"; break; + case Operation::TEQ: result << "teq"; break; + case Operation::CMP: result << "cmp"; break; + case Operation::CMN: result << "cmn"; break; + case Operation::ORR: result << "orr"; break; + case Operation::MOV: result << "mov"; break; + case Operation::BIC: result << "bic"; break; + case Operation::MVN: result << "mvn"; break; + + case Operation::LDR: result << "ldr"; break; + case Operation::STR: result << "str"; break; + case Operation::LDM: result << "ldm"; break; + case Operation::STM: result << "stm"; break; + + case Operation::MRC: result << "mrc"; break; + case Operation::MCR: result << "mcr"; break; + } + + // Append the sets-flags modifier if applicable. + if(sets_flags) result << 's'; + + // Possibly a condition code. + switch(condition) { + case Condition::EQ: result << "eq"; break; + case Condition::NE: result << "ne"; break; + case Condition::CS: result << "cs"; break; + case Condition::CC: result << "cc"; break; + case Condition::MI: result << "mi"; break; + case Condition::PL: result << "pl"; break; + case Condition::VS: result << "vs"; break; + case Condition::VC: result << "vc"; break; + case Condition::HI: result << "hi"; break; + case Condition::LS: result << "ls"; break; + case Condition::GE: result << "ge"; break; + case Condition::LT: result << "lt"; break; + case Condition::GT: result << "gt"; break; + case Condition::LE: result << "le"; break; + default: break; + } + + // If this is a branch, append the target. + if(operation == Operation::B || operation == Operation::BL) { + result << " 0x" << std::hex << ((address + 8 + operand1.value) & 0x3fffffc); + } + + if( + operation == Operation::LDR || operation == Operation::STR || + operation == Operation::LDM || operation == Operation::STM + ) { + if(is_byte) result << 'b'; + result << ' ' << static_cast(destination); + result << ", [" << static_cast(operand1) << "]"; + // TODO: learn how ARM shifts/etc are normally presented. + } + + return result.str(); + } +}; + +/// A target for @c dispatch that merely captures a description of the decoded instruction, being +/// able to vend it later via @c last(). +template +struct Disassembler { + Instruction last() { + return instruction_; + } + + bool should_schedule(Condition condition) { + instruction_ = Instruction(); + instruction_.condition = condition; + return true; + } + + template void perform(DataProcessing fields) { + constexpr DataProcessingFlags flags(f); + + instruction_.operand1.type = Operand::Type::Register; + instruction_.operand1.value = fields.operand1(); + + instruction_.destination.type = Operand::Type::Register; + instruction_.destination.value = fields.destination(); + + if(flags.operand2_is_immediate()) { + instruction_.operand2.type = Operand::Type::Immediate; +// instruction_.operand2.value = fields.immediate(), fields.rotate(); + // TODO: decode immediate. + + } else { + instruction_.operand2.type = Operand::Type::Register; + instruction_.operand2.value = fields.operand2(); + // TODO: capture shift_type(), etc. + } + + instruction_.sets_flags = flags.set_condition_codes(); + + switch(flags.operation()) { + case DataProcessingOperation::AND: instruction_.operation = Instruction::Operation::AND; break; + case DataProcessingOperation::EOR: instruction_.operation = Instruction::Operation::EOR; break; + case DataProcessingOperation::ORR: instruction_.operation = Instruction::Operation::ORR; break; + case DataProcessingOperation::BIC: instruction_.operation = Instruction::Operation::BIC; break; + case DataProcessingOperation::MOV: instruction_.operation = Instruction::Operation::MOV; break; + case DataProcessingOperation::MVN: instruction_.operation = Instruction::Operation::MVN; break; + case DataProcessingOperation::TST: instruction_.operation = Instruction::Operation::TST; break; + case DataProcessingOperation::TEQ: instruction_.operation = Instruction::Operation::TEQ; break; + case DataProcessingOperation::ADD: instruction_.operation = Instruction::Operation::ADD; break; + case DataProcessingOperation::ADC: instruction_.operation = Instruction::Operation::ADC; break; + case DataProcessingOperation::CMN: instruction_.operation = Instruction::Operation::CMN; break; + case DataProcessingOperation::SUB: instruction_.operation = Instruction::Operation::SUB; break; + case DataProcessingOperation::SBC: instruction_.operation = Instruction::Operation::SBC; break; + case DataProcessingOperation::CMP: instruction_.operation = Instruction::Operation::CMP; break; + case DataProcessingOperation::RSB: instruction_.operation = Instruction::Operation::RSB; break; + case DataProcessingOperation::RSC: instruction_.operation = Instruction::Operation::RSC; break; + } + } + + template void perform(Multiply) {} + template void perform(SingleDataTransfer fields) { + constexpr SingleDataTransferFlags flags(f); + instruction_.operation = + (flags.operation() == SingleDataTransferFlags::Operation::STR) ? + Instruction::Operation::STR : Instruction::Operation::LDR; + + instruction_.destination.type = Operand::Type::Register; + instruction_.destination.value = fields.destination(); + + instruction_.operand1.type = Operand::Type::Register; + instruction_.operand1.value = fields.base(); + } + template void perform(BlockDataTransfer fields) { + constexpr BlockDataTransferFlags flags(f); + instruction_.operation = + (flags.operation() == BlockDataTransferFlags::Operation::STM) ? + Instruction::Operation::STM : Instruction::Operation::LDM; + + instruction_.destination.type = Operand::Type::Register; + instruction_.destination.value = fields.base(); + + instruction_.operand1.type = Operand::Type::RegisterList; + instruction_.operand1.value = fields.register_list(); + } + template void perform(Branch fields) { + constexpr BranchFlags flags(f); + instruction_.operation = + (flags.operation() == BranchFlags::Operation::BL) ? + Instruction::Operation::BL : Instruction::Operation::B; + instruction_.operand1.type = Operand::Type::Immediate; + instruction_.operand1.value = fields.offset(); + } + template void perform(CoprocessorRegisterTransfer) { + constexpr CoprocessorRegisterTransferFlags flags(f); + instruction_.operation = + (flags.operation() == CoprocessorRegisterTransferFlags::Operation::MRC) ? + Instruction::Operation::MRC : Instruction::Operation::MCR; + } + template void perform(CoprocessorDataOperation) {} + template void perform(CoprocessorDataTransfer) {} + + void software_interrupt() { + instruction_.operation = Instruction::Operation::SWI; + } + void unknown() { + instruction_.operation = Instruction::Operation::Undefined; + } + +private: + Instruction instruction_; + +}; + +} diff --git a/InstructionSets/ARM/Executor.hpp b/InstructionSets/ARM/Executor.hpp index e03c236b1..b867fe82c 100644 --- a/InstructionSets/ARM/Executor.hpp +++ b/InstructionSets/ARM/Executor.hpp @@ -15,11 +15,40 @@ namespace InstructionSet::ARM { +/// Maps from a semantic ARM read of type @c SourceT to either the 8- or 32-bit value observed +/// by watching the low 8 bits or all 32 bits of the data bus. +template +DestinationT read_bus(SourceT value) { + if constexpr (std::is_same_v) { + return value; + } + if constexpr (std::is_same_v) { + return uint8_t(value); + } else { + return value | (value << 8) | (value << 16) | (value << 24); + } +} + +struct NullStatusHandler { + void did_set_status() {} +}; + /// A class compatible with the @c OperationMapper definition of a scheduler which applies all actions /// immediately, updating either a set of @c Registers or using the templated @c MemoryT to access /// memory. No hooks are currently provided for applying realistic timing. -template +/// +/// If a StatusObserverT is specified, it'll receive calls to @c did_set_status() following every direct +/// write to the status bits — i.e. any change that can affect mode or interrupt flags. +template struct Executor { + template + Executor(StatusObserverT &observer, Args &&...args) : status_observer_(observer), bus(std::forward(args)...) {} + + template + Executor(Args &&...args) : bus(std::forward(args)...) {} + + /// @returns @c true if @c condition implies an appropriate perform call should be made for this instruction, + /// @c false otherwise. bool should_schedule(Condition condition) { return registers_.test(condition); } @@ -39,9 +68,11 @@ struct Executor { operand2 = registers_[fields.operand2()]; } - uint32_t shift_amount; + // TODO: in C++20, a quick `if constexpr (requires` can eliminate the `allow_register` parameter. if constexpr (allow_register) { if(fields.shift_count_is_register()) { + uint32_t shift_amount; + // "When R15 appears in either of the Rn or Rs positions it will give the value // of the PC alone, with the PSR bits replaced by zeroes. ... // @@ -52,19 +83,15 @@ struct Executor { registers_.pc(4) : registers_[fields.shift_register()]; - // A register shift amount of 0 has a different meaning than an in-instruction - // shift amount of 0. - if(!shift_amount) { - return operand2; - } - } else { - shift_amount = fields.shift_amount(); + // "The amount by which the register should be shifted may be contained in + // ... **the bottom byte** of another register". + shift_amount &= 0xff; + shift(fields.shift_type(), operand2, shift_amount, rotate_carry); + return operand2; } - } else { - shift_amount = fields.shift_amount(); } - shift(fields.shift_type(), operand2, shift_amount, rotate_carry); + shift(fields.shift_type(), operand2, fields.shift_amount(), rotate_carry); return operand2; } @@ -96,15 +123,32 @@ struct Executor { // Get operand 2. if constexpr (flags.operand2_is_immediate()) { operand2 = fields.immediate(); - if(fields.rotate()) { - shift(operand2, fields.rotate(), rotate_carry); - } + shift(operand2, fields.rotate(), rotate_carry); } else { operand2 = decode_shift(fields, rotate_carry, shift_by_register ? 8 : 4); } - // Perform the data processing operation. uint32_t conditions = 0; + const auto sub = [&](uint32_t lhs, uint32_t rhs) { + conditions = lhs - rhs; + + if constexpr (flags.operation() == DataProcessingOperation::SBC || flags.operation() == DataProcessingOperation::RSC) { + conditions += registers_.c() - 1; + } + + if constexpr (flags.set_condition_codes()) { + // "For a subtraction, including the comparison instruction CMP, C is set to 0 if + // the subtraction produced a borrow (that is, an unsigned underflow), and to 1 otherwise." + registers_.set_c(!Numeric::carried_out(lhs, rhs, conditions)); + registers_.set_v(Numeric::overflow(lhs, rhs, conditions)); + } + + if constexpr (!is_comparison(flags.operation())) { + destination = conditions; + } + }; + + // Perform the data processing operation. switch(flags.operation()) { // Logical operations. case DataProcessingOperation::AND: conditions = destination = operand1 & operand2; break; @@ -140,56 +184,26 @@ struct Executor { case DataProcessingOperation::SUB: case DataProcessingOperation::SBC: case DataProcessingOperation::CMP: - conditions = operand1 - operand2; - - if constexpr (flags.operation() == DataProcessingOperation::SBC) { - conditions -= registers_.c(); - } - - if constexpr (flags.set_condition_codes()) { - registers_.set_c(Numeric::carried_out(operand1, operand2, conditions)); - registers_.set_v(Numeric::overflow(operand1, operand2, conditions)); - } - - if constexpr (!is_comparison(flags.operation())) { - destination = conditions; - } + sub(operand1, operand2); break; case DataProcessingOperation::RSB: case DataProcessingOperation::RSC: - conditions = operand2 - operand1; - - if constexpr (flags.operation() == DataProcessingOperation::RSC) { - conditions -= registers_.c(); - } - - if constexpr (flags.set_condition_codes()) { - registers_.set_c(Numeric::carried_out(operand2, operand1, conditions)); - registers_.set_v(Numeric::overflow(operand2, operand1, conditions)); - } - - destination = conditions; + sub(operand2, operand1); break; } + if(!is_comparison(flags.operation()) && fields.destination() == 15) { + registers_.set_pc(pc_proxy); + } if constexpr (flags.set_condition_codes()) { - // "When Rd is a register other than R15, the condition code flags in the PSR may be - // updated from the ALU flags as described above. When Rd is R15 and the S flag in - // the instruction is set, the PSR is overwritten by the corresponding ALU result. - // - // ... if the instruction is of a type which does not normally produce a result - // (CMP, CMN, TST, TEQ) but Rd is R15 and the S bit is set, the result will be used in - // this case to update those PSR flags which are not protected by virtue of the - // processor mode." - + // "When Rd is R15 and the S flag in the instruction is set, the PSR is overwritten by the + // corresponding bits in the ALU result... [even] if the instruction is of a type that does not + // normally produce a result (CMP, CMN, TST, TEQ) ... the result will be used to update those + // PSR flags which are not protected by virtue of the processor mode" if(fields.destination() == 15) { - if constexpr (is_comparison(flags.operation())) { - registers_.set_status(pc_proxy); - } else { - registers_.set_status(pc_proxy); - registers_.set_pc(pc_proxy); - } + registers_.set_status(conditions); + status_observer_.did_set_status(); } else { // Set N and Z in a unified way. registers_.set_nz(conditions); @@ -199,11 +213,6 @@ struct Executor { registers_.set_c(rotate_carry); } } - } else { - // "If the S flag is clear when Rd is R15, only the 24 PC bits of R15 will be written." - if(fields.destination() == 15) { - registers_.set_pc(pc_proxy); - } } } @@ -238,7 +247,7 @@ struct Executor { constexpr BranchFlags flags(f); if constexpr (flags.operation() == BranchFlags::Operation::BL) { - registers_[14] = registers_.pc(0); + registers_[14] = registers_.pc_status(0); } registers_.set_pc(registers_.pc(4) + branch.offset()); } @@ -278,12 +287,26 @@ struct Executor { } // Check for an address exception. - if(address >= (1 << 26)) { + if(is_invalid_address(address)) { registers_.exception(); return; } - constexpr bool trans = !flags.pre_index() && flags.write_back_address(); + // Decide whether to write back — when either postindexing or else write back is requested. + // + // Note to future self on write-back: + // + // It's currently unclear what to do in the case of e.g. `str r13, [r13, #0x10]!`. Is the value + // written r13 as modified or the original r13? If it's as modified, does that imply that + // write back has occurred regardless of a data abort? + // + // TODO: resolve uncertainty. + constexpr bool should_write_back = !flags.pre_index() || flags.write_back_address(); + + // "... post-indexed data transfers always write back the modified base. The only use of the [write-back address] + // bit in a post-indexed data transfer is in non-user mode code, where setting the W bit forces the /TRANS pin + // to go LOW for the transfer" + const bool trans = (registers_.mode() == Mode::User) || (!flags.pre_index() && flags.write_back_address()); if constexpr (flags.operation() == SingleDataTransferFlags::Operation::STR) { const uint32_t source = transfer.source() == 15 ? @@ -313,13 +336,18 @@ struct Executor { } else { did_read = bus.template read(address, value, registers_.mode(), trans); - // "An address offset from a word boundary will cause the data to be rotated into the - // register so that the addressed byte occuplies bits 0 to 7." - switch(address & 3) { - case 0: break; - case 1: value = (value >> 8) | (value << 24); break; - case 2: value = (value >> 16) | (value << 16); break; - case 3: value = (value >> 24) | (value << 8); break; + if constexpr (model != Model::ARMv2with32bitAddressing) { + // "An address offset from a word boundary will cause the data to be rotated into the + // register so that the addressed byte occuplies bits 0 to 7." + // + // (though the test set that inspired 'ARMv2with32bitAddressing' appears not to honour this; + // test below assumes it went away by the version of ARM that set supports) + switch(address & 3) { + case 0: break; + case 1: value = (value >> 8) | (value << 24); break; + case 2: value = (value >> 16) | (value << 16); break; + case 3: value = (value >> 24) | (value << 8); break; + } } } @@ -335,96 +363,129 @@ struct Executor { } } - // If either postindexing or else with writeback, update base. - if constexpr (!flags.pre_index() || flags.write_back_address()) { - if(transfer.base() == 15) { - registers_.set_pc(offsetted_address); - } else { - registers_[transfer.base()] = offsetted_address; + if constexpr (should_write_back) { + // Empirically: I think order of operations for a load is: (i) write back; (ii) store value from bus. + // So if this is a load, don't allow write back to overwrite what was loaded. + if(flags.operation() == SingleDataTransferFlags::Operation::STR || transfer.base() != transfer.destination()) { + if(transfer.base() == 15) { + registers_.set_pc(offsetted_address); + } else { + registers_[transfer.base()] = offsetted_address; + } } } } template void perform(BlockDataTransfer transfer) { constexpr BlockDataTransferFlags flags(f); + constexpr bool is_ldm = flags.operation() == BlockDataTransferFlags::Operation::LDM; - // Grab a copy of the list of registers to transfer. - const uint16_t list = transfer.register_list(); + // Ensure that *base points to the base register if it can be written back; + // also set address to the base. + uint32_t *base = nullptr; + uint32_t address; + if(transfer.base() == 15) { + address = registers_.pc_status(4); + } else { + base = ®isters_[transfer.base()]; + address = *base; + } + + // For an LDM pc_proxy will receive any read R15 value; + // for an STM it'll hold the value to be written. + uint32_t pc_proxy = 0; // Read the base address and take a copy in case a data abort means that - // it has to be restored later, and to write that value rather than - // the final address if the base register is first in the write-out list. - uint32_t address = transfer.base() == 15 ? - registers_.pc_status(4) : - registers_[transfer.base()]; - const uint32_t initial_address = address; + // it has to be restored later. + uint32_t initial_address = address; - // Figure out what the final address will be, since that's what'll be - // in the output if the base register is second or beyond in the - // write-out list. + // Grab the register list and decide whether user registers are being used. + const uint16_t list = transfer.register_list(); + const bool adopt_user_mode = flags.load_psr() && (!is_ldm || !(list & (1 << 15))); + + // Write back will prima facie occur if: + // (i) the instruction asks for it; and + // (ii) the write-back register isn't R15. + bool write_back = base && flags.write_back_address(); + + // Collate a transfer list; this is a very long-winded way of implementing STM + // and LDM but right now the objective is merely correctness. + // + // If this is LDM and it turns out that base is also in the transfer list, + // disable write back. + uint32_t *transfer_sources[16]; + uint32_t total = 0; + for(uint32_t c = 0; c < 15; c++) { + if(list & (1 << c)) { + uint32_t *const next = ®isters_.reg(adopt_user_mode, c); + if(is_ldm && next == base) write_back = false; + transfer_sources[total++] = next; + } + } + + // If the last thing in the list is R15, redirect it to the PC proxy, + // possibly populating with a meaningful value. + if(list & (1 << 15)) { + if(!is_ldm) { + pc_proxy = registers_.pc_status(8); + } + transfer_sources[total++] = &pc_proxy; + } + + // If this is STM and the first thing in the list is the same as base, + // point it at initial_address instead. + if(!is_ldm && total && transfer_sources[0] == base) { + transfer_sources[0] = &initial_address; + } + + // Calculate final_address, which is what will be written back if required; + // update address to point to the low end of the transfer block. // // Writes are always ordered from lowest address to highest; adjust the // start address if this write is supposed to fill memory downward from // the base. - - // TODO: use std::popcount when adopting C++20. - uint32_t total = ((list & 0xa) >> 1) + (list & 0x5); - total = ((list & 0xc) >> 2) + (list & 0x3); - uint32_t final_address; if constexpr (!flags.add_offset()) { - final_address = address + total * 4; + // Decrementing mode; final_address is the value the base register should + // have after this operation if writeback is enabled, so it's below + // the original address. But also writes always occur from lowest address + // to highest, so push the current address to the bottom. + final_address = address - total * 4; address = final_address; } else { final_address = address + total * 4; } - // For loads, keep a record of the value replaced by the last load and - // where it came from. A data abort cancels both the current load and - // the one before it, so this is used by this implementation to undo - // the previous load in that case. - struct { - uint32_t *target = nullptr; - uint32_t value; - } last_replacement; - - // Check whether access is forced ot the user bank; if so then switch - // to it now. Also keep track of the original mode to switch back at - // the end. - const Mode original_mode = registers_.mode(); - const bool adopt_user_mode = - ( - flags.operation() == BlockDataTransferFlags::Operation::STM && - flags.load_psr() - ) || - ( - flags.operation() == BlockDataTransferFlags::Operation::LDM && - !(list & (1 << 15)) - ); - if(adopt_user_mode) { - registers_.set_mode(Mode::User); + // Write back if enabled. + if(write_back) { + *base = final_address; } - bool address_error = false; + // Update address in advance for: + // * pre-indexed upward stores; and + // * post-indxed downward stores. + if constexpr (flags.pre_index() == flags.add_offset()) { + address += 4; + } - // Keep track of whether all accesses succeeded in order potentially to - // throw a data abort later. + // Perform all memory accesses, tracking whether either kind of abort will be + // required. + const bool trans = registers_.mode() == Mode::User; + const bool address_error = is_invalid_address(address); bool accesses_succeeded = true; - const auto access = [&](uint32_t &value) { - // Update address in advance for: - // * pre-indexed upward stores; and - // * post-indxed downward stores. - if constexpr (flags.pre_index() == flags.add_offset()) { - address += 4; - } - if constexpr (flags.operation() == BlockDataTransferFlags::Operation::STM) { - if(!address_error) { - // "If the abort occurs during a store multiple instruction, ARM takes little action until - // the instruction completes, whereupon it enters the data abort trap. The memory manager is - // responsible for preventing erroneous writes to the memory." - accesses_succeeded &= bus.template write(address, value, registers_.mode(), false); - } - } else { + if constexpr (is_ldm) { + // Keep a record of the value replaced by the last load and + // where it came from. A data abort cancels both the current load and + // the one before it, so this might be used by this implementation to + // undo the previous load. + struct { + uint32_t *target = nullptr; + uint32_t value; + } last_replacement; + + for(uint32_t c = 0; c < total; c++) { + uint32_t &value = *transfer_sources[c]; + // When ARM detects a data abort during a load multiple instruction, it modifies the operation of // the instruction to ensure that recovery is possible. // @@ -433,96 +494,72 @@ struct Executor { // * The base register is restored, to its modified value if write-back was requested. if(accesses_succeeded) { const uint32_t replaced = value; - accesses_succeeded &= bus.template read(address, value, registers_.mode(), false); + accesses_succeeded &= bus.template read(address, value, registers_.mode(), trans); // Update the last-modified slot if the access succeeded; otherwise // undo the last modification if there was one, and undo the base // address change. if(accesses_succeeded) { last_replacement.value = replaced; - last_replacement.target = &value; + last_replacement.target = transfer_sources[c]; } else { if(last_replacement.target) { *last_replacement.target = last_replacement.value; } - // Also restore the base register. - if(transfer.base() != 15) { - if constexpr (flags.write_back_address()) { - registers_[transfer.base()] = final_address; + // Also restore the base register, including to its original value + // if write back was disabled. + if(base) { + if(write_back) { + *base = final_address; } else { - registers_[transfer.base()] = initial_address; + *base = initial_address; } } } } else { // Implicitly: do the access anyway, but don't store the value. I think. uint32_t throwaway; - bus.template read(address, throwaway, registers_.mode(), false); + bus.template read(address, throwaway, registers_.mode(), trans); } - } - // Update address after the fact for: - // * post-indexed upward stores; and - // * pre-indxed downward stores. - if constexpr (flags.pre_index() != flags.add_offset()) { + // Advance. address += 4; } - }; + } else { + for(uint32_t c = 0; c < total; c++) { + uint32_t &value = *transfer_sources[c]; - // Check for an address exception. - address_error = address >= (1 << 26); - - // Write out registers 1 to 14. - for(int c = 0; c < 15; c++) { - if(list & (1 << c)) { - access(registers_[c]); - - // Modify base register after each write if writeback is enabled. - // This'll ensure the unmodified value goes out if it was the - // first-selected register only. - if constexpr (flags.write_back_address()) { - if(transfer.base() != 15) { - registers_[transfer.base()] = final_address; - } + if(!address_error) { + // "If the abort occurs during a store multiple instruction, ARM takes little action until + // the instruction completes, whereupon it enters the data abort trap. The memory manager is + // responsible for preventing erroneous writes to the memory." + accesses_succeeded &= bus.template write(address, value, registers_.mode(), trans); + } else { + // Do a throwaway read. + uint32_t throwaway; + bus.template read(address, throwaway, registers_.mode(), trans); } - } - } - // Definitively write back, even if the earlier register list - // was empty. - if constexpr (flags.write_back_address()) { - if(transfer.base() != 15) { - registers_[transfer.base()] = final_address; + // Advance. + address += 4; } } - // Read or write the program counter as a special case if it was in the list. - if(list & (1 << 15)) { - uint32_t value; - if constexpr (flags.operation() == BlockDataTransferFlags::Operation::STM) { - value = registers_.pc_status(8); - access(value); - } else { - access(value); - registers_.set_pc(value); - if constexpr (flags.load_psr()) { - registers_.set_status(value); - } - } - } - - // If user mode was unnaturally forced, switch back to the actual - // current operating mode. - if(adopt_user_mode) { - registers_.set_mode(original_mode); - } - // Finally throw an exception if necessary. if(address_error) { registers_.exception(); } else if(!accesses_succeeded) { registers_.exception(); + } else { + // If this was an LDM to R15 then apply it appropriately. + if(is_ldm && list & (1 << 15)) { + registers_.set_pc(pc_proxy); + if constexpr (flags.load_psr()) { + registers_.set_status(pc_proxy); + status_observer_.did_set_status(); + } + } } } @@ -549,6 +586,17 @@ struct Executor { return registers_; } + // Included primarily for testing; my full opinion on this is still + // incompletely-formed. + Registers ®isters() { + return registers_; + } + + /// Indicates a prefetch abort exception. + void prefetch_abort() { + registers_.exception(); + } + /// Sets the expected address of the instruction after whichever is about to be executed. /// So it's PC+4 compared to most other systems. void set_pc(uint32_t pc) { @@ -565,13 +613,26 @@ struct Executor { MemoryT bus; private: + using StatusObserverTStorage = + typename std::conditional< + std::is_same_v, + StatusObserverT, + StatusObserverT &>::type; + StatusObserverTStorage status_observer_; Registers registers_; + + static bool is_invalid_address(uint32_t address) { + if constexpr (model == Model::ARMv2with32bitAddressing) { + return false; + } + return address >= 1 << 26; + } }; /// Executes the instruction @c instruction which should have been fetched from @c executor.pc(), /// modifying @c executor. -template -void execute(uint32_t instruction, Executor &executor) { +template +void execute(uint32_t instruction, Executor &executor) { executor.set_pc(executor.pc() + 4); dispatch(instruction, executor); } diff --git a/InstructionSets/ARM/OperationMapper.hpp b/InstructionSets/ARM/OperationMapper.hpp index 1898b14b7..08dbf411c 100644 --- a/InstructionSets/ARM/OperationMapper.hpp +++ b/InstructionSets/ARM/OperationMapper.hpp @@ -15,6 +15,10 @@ namespace InstructionSet::ARM { enum class Model { ARMv2, + + /// Like an ARMv2 but all non-PC addressing is 64-bit. Primarily useful for a particular set of test + /// cases that I want to apply retroactively; not a real iteration. + ARMv2with32bitAddressing, }; enum class Condition { @@ -43,15 +47,15 @@ struct WithShiftControlBits { constexpr WithShiftControlBits(uint32_t opcode) noexcept : opcode_(opcode) {} /// The operand 2 register index if @c operand2_is_immediate() is @c false; meaningless otherwise. - int operand2() const { return opcode_ & 0xf; } + uint32_t operand2() const { return opcode_ & 0xf; } /// The type of shift to apply to operand 2 if @c operand2_is_immediate() is @c false; meaningless otherwise. ShiftType shift_type() const { return ShiftType((opcode_ >> 5) & 3); } /// @returns @c true if the amount to shift by should be taken from a register; @c false if it is an immediate value. bool shift_count_is_register() const { return opcode_ & (1 << 4); } /// The shift amount register index if @c shift_count_is_register() is @c true; meaningless otherwise. - int shift_register() const { return (opcode_ >> 8) & 0xf; } + uint32_t shift_register() const { return (opcode_ >> 8) & 0xf; } /// The amount to shift by if @c shift_count_is_register() is @c false; meaningless otherwise. - int shift_amount() const { return (opcode_ >> 7) & 0x1f; } + uint32_t shift_amount() const { return (opcode_ >> 7) & 0x1f; } protected: uint32_t opcode_; @@ -81,7 +85,7 @@ struct Branch { constexpr Branch(uint32_t opcode) noexcept : opcode_(opcode) {} /// The 26-bit offset to add to the PC. - int offset() const { return (opcode_ & 0xff'ffff) << 2; } + uint32_t offset() const { return (opcode_ & 0xff'ffff) << 2; } private: uint32_t opcode_; @@ -160,19 +164,19 @@ struct DataProcessing: public WithShiftControlBits { using WithShiftControlBits::WithShiftControlBits; /// The destination register index. i.e. Rd. - int destination() const { return (opcode_ >> 12) & 0xf; } + uint32_t destination() const { return (opcode_ >> 12) & 0xf; } /// The operand 1 register index. i.e. Rn. - int operand1() const { return (opcode_ >> 16) & 0xf; } + uint32_t operand1() const { return (opcode_ >> 16) & 0xf; } // // Immediate values for operand 2. // /// An 8-bit value to rotate right @c rotate() places if @c operand2_is_immediate() is @c true; meaningless otherwise. - int immediate() const { return opcode_ & 0xff; } + uint32_t immediate() const { return opcode_ & 0xff; } /// The number of bits to rotate @c immediate() by to the right if @c operand2_is_immediate() is @c true; meaningless otherwise. - int rotate() const { return (opcode_ >> 7) & 0x1e; } + uint32_t rotate() const { return (opcode_ >> 7) & 0x1e; } }; // @@ -202,16 +206,16 @@ struct Multiply { constexpr Multiply(uint32_t opcode) noexcept : opcode_(opcode) {} /// The destination register index. i.e. 'Rd'. - int destination() const { return (opcode_ >> 16) & 0xf; } + uint32_t destination() const { return (opcode_ >> 16) & 0xf; } /// The accumulator register index for multiply-add. i.e. 'Rn'. - int accumulator() const { return (opcode_ >> 12) & 0xf; } + uint32_t accumulator() const { return (opcode_ >> 12) & 0xf; } /// The multiplicand register index. i.e. 'Rs'. - int multiplicand() const { return (opcode_ >> 8) & 0xf; } + uint32_t multiplicand() const { return (opcode_ >> 8) & 0xf; } /// The multiplier register index. i.e. 'Rm'. - int multiplier() const { return opcode_ & 0xf; } + uint32_t multiplier() const { return opcode_ & 0xf; } private: uint32_t opcode_; @@ -246,16 +250,16 @@ struct SingleDataTransfer: public WithShiftControlBits { using WithShiftControlBits::WithShiftControlBits; /// The destination register index. i.e. 'Rd' for LDR. - int destination() const { return (opcode_ >> 12) & 0xf; } + uint32_t destination() const { return (opcode_ >> 12) & 0xf; } /// The destination register index. i.e. 'Rd' for STR. - int source() const { return (opcode_ >> 12) & 0xf; } + uint32_t source() const { return (opcode_ >> 12) & 0xf; } /// The base register index. i.e. 'Rn'. - int base() const { return (opcode_ >> 16) & 0xf; } + uint32_t base() const { return (opcode_ >> 16) & 0xf; } /// The immediate offset, if @c offset_is_register() was @c false; meaningless otherwise. - int immediate() const { return opcode_ & 0xfff; } + uint32_t immediate() const { return opcode_ & 0xfff; } }; // @@ -286,10 +290,21 @@ struct BlockDataTransfer: public WithShiftControlBits { using WithShiftControlBits::WithShiftControlBits; /// The base register index. i.e. 'Rn'. - int base() const { return (opcode_ >> 16) & 0xf; } + uint32_t base() const { return (opcode_ >> 16) & 0xf; } /// A bitfield indicating which registers to load or store. - int register_list() const { return opcode_ & 0xffff; } + uint16_t register_list() const { return static_cast(opcode_); } + uint32_t popcount() const { + const uint16_t list = register_list(); + + // TODO: just use std::popcount when adopting C++20. + uint32_t total = ((list & 0xaaaa) >> 1) + (list & 0x5555); + total = ((total & 0xcccc) >> 2) + (total & 0x3333); + total = ((total & 0xf0f0) >> 4) + (total & 0x0f0f); + total = ((total & 0xff00) >> 8) + (total & 0x00ff); + + return total; + } }; // @@ -307,11 +322,11 @@ private: struct CoprocessorDataOperation { constexpr CoprocessorDataOperation(uint32_t opcode) noexcept : opcode_(opcode) {} - int operand1() const { return (opcode_ >> 16) & 0xf; } - int operand2() const { return opcode_ & 0xf; } - int destination() const { return (opcode_ >> 12) & 0xf; } - int coprocessor() const { return (opcode_ >> 8) & 0xf; } - int information() const { return (opcode_ >> 5) & 0x7; } + uint32_t operand1() const { return (opcode_ >> 16) & 0xf; } + uint32_t operand2() const { return opcode_ & 0xf; } + uint32_t destination() const { return (opcode_ >> 12) & 0xf; } + uint32_t coprocessor() const { return (opcode_ >> 8) & 0xf; } + uint32_t information() const { return (opcode_ >> 5) & 0x7; } private: uint32_t opcode_; @@ -340,11 +355,11 @@ private: struct CoprocessorRegisterTransfer { constexpr CoprocessorRegisterTransfer(uint32_t opcode) noexcept : opcode_(opcode) {} - int operand1() const { return (opcode_ >> 16) & 0xf; } - int operand2() const { return opcode_ & 0xf; } - int destination() const { return (opcode_ >> 12) & 0xf; } - int coprocessor() const { return (opcode_ >> 8) & 0xf; } - int information() const { return (opcode_ >> 5) & 0x7; } + uint32_t operand1() const { return (opcode_ >> 16) & 0xf; } + uint32_t operand2() const { return opcode_ & 0xf; } + uint32_t destination() const { return (opcode_ >> 12) & 0xf; } + uint32_t coprocessor() const { return (opcode_ >> 8) & 0xf; } + uint32_t information() const { return (opcode_ >> 5) & 0x7; } private: uint32_t opcode_; @@ -408,22 +423,24 @@ struct OperationMapper { // page references are provided were more detailed versions of the // decoding are depicted. + // Multiply and multiply-accumulate (MUL, MLA); cf. p.23. + // + // This usurps a potential data processing decoding, so needs priority. + if constexpr (((partial >> 22) & 0b111'111) == 0b000'000) { + // This implementation provides only eight bits baked into the template parameters so + // an additional dynamic test is required to check whether this is really, really MUL or MLA. + if((instruction & 0b1111'0000) == 0b1001'0000) { + scheduler.template perform(Multiply(instruction)); + return; + } + } + // Data processing; cf. p.17. if constexpr (((partial >> 26) & 0b11) == 0b00) { scheduler.template perform(DataProcessing(instruction)); return; } - // Multiply and multiply-accumulate (MUL, MLA); cf. p.23. - if constexpr (((partial >> 22) & 0b111'111) == 0b000'000) { - // This implementation provides only eight bits baked into the template parameters so - // an additional dynamic test is required to check whether this is really, really MUL or MLA. - if(((instruction >> 4) & 0b1111) == 0b1001) { - scheduler.template perform(Multiply(instruction)); - return; - } - } - // Single data transfer (LDR, STR); cf. p.25. if constexpr (((partial >> 26) & 0b11) == 0b01) { scheduler.template perform(SingleDataTransfer(instruction)); diff --git a/InstructionSets/ARM/Registers.hpp b/InstructionSets/ARM/Registers.hpp index a2c410268..d85bf6440 100644 --- a/InstructionSets/ARM/Registers.hpp +++ b/InstructionSets/ARM/Registers.hpp @@ -17,13 +17,13 @@ namespace InstructionSet::ARM { namespace ConditionCode { -static constexpr uint32_t Negative = 1 << 31; -static constexpr uint32_t Zero = 1 << 30; -static constexpr uint32_t Carry = 1 << 29; -static constexpr uint32_t Overflow = 1 << 28; -static constexpr uint32_t IRQDisable = 1 << 27; -static constexpr uint32_t FIQDisable = 1 << 26; -static constexpr uint32_t Mode = (1 << 1) | (1 << 0); +static constexpr uint32_t Negative = 1u << 31; +static constexpr uint32_t Zero = 1u << 30; +static constexpr uint32_t Carry = 1u << 29; +static constexpr uint32_t Overflow = 1u << 28; +static constexpr uint32_t IRQDisable = 1u << 27; +static constexpr uint32_t FIQDisable = 1u << 26; +static constexpr uint32_t Mode = (1u << 1) | (1u << 0); static constexpr uint32_t Address = FIQDisable - Mode - 1; @@ -46,6 +46,11 @@ enum class Mode { /// This is to try to keep this structure independent of a specific ARM implementation. struct Registers { public: + // Don't allow copying. + Registers(const Registers &) = delete; + Registers &operator =(const Registers &) = delete; + Registers() = default; + /// Sets the N and Z flags according to the value of @c result. void set_nz(uint32_t value) { zero_result_ = negative_flag_ = value; @@ -138,31 +143,90 @@ struct Registers { /// The FIQ went low at least one cycle ago and ConditionCode::FIQDisable was not set. FIQ = 0x1c, }; + static constexpr uint32_t pc_offset_during(Exception exception) { + // The below is somewhat convoluted by the assumed execution model: + // + // * exceptions occuring during execution of an instruction are taken + // to occur after R15 has already been incremented by 4; but + // * exceptions occurring instead of execution of an instruction are + // taken to occur with R15 pointing to an instruction that hasn't begun. + // + // i.e. in net R15 always refers to the next instruction + // that has not yet started. + switch(exception) { + // "To return normally from FIQ use SUBS PC, R14_fiq, #4". + case Exception::FIQ: return 4; + + // "To return normally from IRQ use SUBS PC, R14_irq, #4". + case Exception::IRQ: return 4; + + // "If a return is required from [address exception traps], use + // SUBS PC, R14_svc, #4. This will return to the instruction after + // the one causing the trap". + case Exception::Address: return 4; + + // "A Data Abort requires [work before a return], the return being + // done by SUBS PC, R14_svc, #8" (and returning to the instruction + // that aborted). + case Exception::DataAbort: return 4; + + // "To continue after a Prefetch Abort use SUBS PC, R14_svc #4. + // This will attempt to re-execute the aborting instruction." + case Exception::PrefetchAbort: return 4; + + // "To return from a SWI, use MOVS PC, R14_svc. This returns to the instruction + // following the SWI". + case Exception::SoftwareInterrupt: return 0; + + // "To return from [an undefined instruction trap] use MOVS PC, R14_svc. + // This returns to the instruction following the undefined instruction". + case Exception::UndefinedInstruction: return 0; + + // Unspecified; a guess. + case Exception::Reset: return 0; + } + } /// Updates the program counter, interupt flags and link register if applicable to begin @c exception. - template + template void exception() { + const auto r14 = pc_status(pc_offset_during(type)); + switch(type) { + case Exception::IRQ: set_mode(Mode::IRQ); break; + case Exception::FIQ: set_mode(Mode::FIQ); break; + default: set_mode(Mode::Supervisor); break; + } + active_[14] = r14; + interrupt_flags_ |= ConditionCode::IRQDisable; - if constexpr (exception == Exception::Reset || exception == Exception::FIQ) { + if constexpr (type == Exception::Reset || type == Exception::FIQ) { interrupt_flags_ |= ConditionCode::FIQDisable; } + set_pc(uint32_t(type)); + } - switch(exception) { + /// Applies an exception of @c type and returns @c true if: (i) it is IRQ or FIQ; (ii) the processor is currently accepting such interrupts. + /// Otherwise returns @c false. + template + bool interrupt() { + switch(type) { case Exception::IRQ: - set_mode(Mode::IRQ); - active_[14] = pc(8); + if(interrupt_flags_ & ConditionCode::IRQDisable) { + return false; + } break; + case Exception::FIQ: - set_mode(Mode::FIQ); - active_[14] = pc(8); - break; - default: - set_mode(Mode::Supervisor); - active_[14] = pc(4); + if(interrupt_flags_ & ConditionCode::FIQDisable) { + return false; + } break; + + default: return false; } - set_pc(uint32_t(exception)); + exception(); + return true; } // MARK: - Condition tests. @@ -221,10 +285,15 @@ struct Registers { // For outgoing modes other than FIQ, only save the final two registers for now; // if the incoming mode is FIQ then the other five will be saved in the next switch. + // For FIQ, save all seven up front. switch(mode_) { + // FIQ outgoing: save R8 to R14. case Mode::FIQ: std::copy(active_.begin() + 8, active_.begin() + 15, fiq_registers_.begin()); break; + + // Non-FIQ outgoing: save R13 and R14. If saving to the user registers, + // use only the final two slots. case Mode::User: std::copy(active_.begin() + 13, active_.begin() + 15, user_registers_.begin() + 5); break; @@ -240,7 +309,10 @@ struct Registers { // For FIQ: save an additional five, then overwrite seven. switch(target_mode) { case Mode::FIQ: + // FIQ is incoming, so save registers 8 to 12 to the first five slots of the user registers. std::copy(active_.begin() + 8, active_.begin() + 13, user_registers_.begin()); + + // Replace R8 to R14. std::copy(fiq_registers_.begin(), fiq_registers_.end(), active_.begin() + 8); break; case Mode::User: @@ -262,12 +334,38 @@ struct Registers { mode_ = target_mode; } - uint32_t &operator[](int offset) { - return active_[offset]; + uint32_t &operator[](uint32_t offset) { + return active_[static_cast(offset)]; } - const uint32_t operator[](int offset) const { - return active_[offset]; + uint32_t operator[](uint32_t offset) const { + return active_[static_cast(offset)]; + } + + /// @returns A reference to the register at @c offset. If @c force_user_mode is true, + /// this will the the user-mode register. Otherwise it'll be that for the current mode. These references + /// are guaranteed to remain valid only until the next mode change. + uint32_t ®(bool force_user_mode, uint32_t offset) { + if(!force_user_mode) { + return active_[offset]; + } + + switch(mode_) { + case Mode::User: return active_[offset]; + + case Mode::Supervisor: + case Mode::IRQ: + if(offset == 13 || offset == 14) { + return user_registers_[offset - 8]; + } + return active_[offset]; + + case Mode::FIQ: + if(offset >= 8 && offset < 15) { + return user_registers_[offset - 8]; + } + return active_[offset]; + } } private: diff --git a/Machines/Acorn/Archimedes/Archimedes.cpp b/Machines/Acorn/Archimedes/Archimedes.cpp new file mode 100644 index 000000000..e8e7f8895 --- /dev/null +++ b/Machines/Acorn/Archimedes/Archimedes.cpp @@ -0,0 +1,527 @@ +// +// Archimedes.cpp +// Clock Signal +// +// Created by Thomas Harte on 04/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#include "Archimedes.hpp" + +#include "HalfDuplexSerial.hpp" +#include "InputOutputController.hpp" +#include "Keyboard.hpp" +#include "KeyboardMapper.hpp" +#include "MemoryController.hpp" +#include "Sound.hpp" + +#include "../../AudioProducer.hpp" +#include "../../KeyboardMachine.hpp" +#include "../../MediaTarget.hpp" +#include "../../MouseMachine.hpp" +#include "../../ScanProducer.hpp" +#include "../../TimedMachine.hpp" + +#include "../../../Activity/Source.hpp" + +#include "../../../InstructionSets/ARM/Disassembler.hpp" +#include "../../../InstructionSets/ARM/Executor.hpp" +#include "../../../Outputs/Log.hpp" +#include "../../../Components/I2C/I2C.hpp" + +#include +#include +#include +#include + +namespace { + +Log::Logger logger; + +} + +namespace Archimedes { + +#ifndef NDEBUG +template +struct HackyDebugger { + void notify(uint32_t address, uint32_t instruction, Executor &executor) { + pc_history[pc_history_ptr] = address; + pc_history_ptr = (pc_history_ptr + 1) % pc_history.size(); + +// if( +// executor_.pc() > 0x038021d0 && +// last_r1 != executor_.registers()[1] +// || +// ( +// last_link != executor_.registers()[14] || +// last_r0 != executor_.registers()[0] || +// last_r10 != executor_.registers()[10] || +// last_r1 != executor_.registers()[1] +// ) +// ) { +// logger.info().append("%08x modified R14 to %08x; R0 to %08x; R10 to %08x; R1 to %08x", +// last_pc, +// executor_.registers()[14], +// executor_.registers()[0], +// executor_.registers()[10], +// executor_.registers()[1] +// ); +// logger.info().append("%08x modified R1 to %08x", +// last_pc, +// executor_.registers()[1] +// ); +// last_link = executor_.registers()[14]; +// last_r0 = executor_.registers()[0]; +// last_r10 = executor_.registers()[10]; +// last_r1 = executor_.registers()[1]; +// } + +// if(instruction == 0xe8fd7fff) { +// printf("At %08x [%d]; after last PC %08x and %zu ago was %08x\n", +// address, +// instr_count, +// pc_history[(pc_history_ptr - 2 + pc_history.size()) % pc_history.size()], +// pc_history.size(), +// pc_history[pc_history_ptr]); +// } +// last_r9 = executor_.registers()[9]; + +// log |= address == 0x038031c4; +// log |= instr_count == 53552731 - 30; +// log &= executor_.pc() != 0x000000a0; + +// log = (executor_.pc() == 0x038162afc) || (executor_.pc() == 0x03824b00); +// log |= instruction & ; + + // The following has the effect of logging all taken SWIs and their return codes. +/* if( + (instruction & 0x0f00'0000) == 0x0f00'0000 && + executor.registers().test(InstructionSet::ARM::Condition(instruction >> 28)) + ) { + if(instruction & 0x2'0000) { + swis.emplace_back(); + swis.back().count = swi_count++; + swis.back().opcode = instruction; + swis.back().address = executor.pc(); + swis.back().return_address = executor.registers().pc(4); + for(int c = 0; c < 10; c++) swis.back().regs[c] = executor.registers()[uint32_t(c)]; + + // Possibly capture more detail. + // + // Cf. http://productsdb.riscos.com/support/developers/prm_index/numswilist.html + uint32_t pointer = 0; + switch(instruction & 0xfd'ffff) { + case 0x41501: + swis.back().swi_name = "MessageTrans_OpenFile"; + + // R0: pointer to file descriptor; R1: pointer to filename; R2: pointer to hold file data. + // (R0 and R1 are in the RMA if R2 = 0) + pointer = executor.registers()[1]; + break; + case 0x41502: + swis.back().swi_name = "MessageTrans_Lookup"; + break; + case 0x41506: + swis.back().swi_name = "MessageTrans_ErrorLookup"; + break; + + case 0x4028a: + swis.back().swi_name = "Podule_EnumerateChunksWithInfo"; + break; + + case 0x4000a: + swis.back().swi_name = "Econet_ReadLocalStationAndNet"; + break; + case 0x4000e: + swis.back().swi_name = "Econet_SetProtection"; + break; + case 0x40015: + swis.back().swi_name = "Econet_ClaimPort"; + break; + + case 0x40541: + swis.back().swi_name = "FileCore_Create"; + break; + + case 0x80156: + case 0x8015b: + swis.back().swi_name = "PDriver_MiscOpForDriver"; + break; + + case 0x05: + swis.back().swi_name = "OS_CLI"; + pointer = executor.registers()[0]; + break; + case 0x0d: + swis.back().swi_name = "OS_Find"; + if(executor.registers()[0] >= 0x40) { + pointer = executor.registers()[1]; + } + break; + case 0x1d: + swis.back().swi_name = "OS_Heap"; + break; + case 0x1e: + swis.back().swi_name = "OS_Module"; + break; + + case 0x20: + swis.back().swi_name = "OS_Release"; + break; + case 0x21: + swis.back().swi_name = "OS_ReadUnsigned"; + break; + case 0x23: + swis.back().swi_name = "OS_ReadVarVal"; + + // R0: pointer to variable name. + pointer = executor.registers()[0]; + break; + case 0x24: + swis.back().swi_name = "OS_SetVarVal"; + + // R0: pointer to variable name. + pointer = executor.registers()[0]; + break; + case 0x26: + swis.back().swi_name = "OS_GSRead"; + break; + case 0x27: + swis.back().swi_name = "OS_GSTrans"; + pointer = executor.registers()[0]; + break; + case 0x29: + swis.back().swi_name = "OS_FSControl"; + break; + case 0x2a: + swis.back().swi_name = "OS_ChangeDynamicArea"; + break; + + case 0x4c: + swis.back().swi_name = "OS_ReleaseDeviceVector"; + break; + + case 0x43057: + swis.back().swi_name = "Territory_LowerCaseTable"; + break; + case 0x43058: + swis.back().swi_name = "Territory_UpperCaseTable"; + break; + + case 0x42fc0: + swis.back().swi_name = "Portable_Speed"; + break; + case 0x42fc1: + swis.back().swi_name = "Portable_Control"; + break; + } + + if(pointer) { + while(true) { + uint8_t next; + executor.bus.template read(pointer, next, InstructionSet::ARM::Mode::Supervisor, false); + ++pointer; + + if(next < 32) break; + swis.back().value_name.push_back(static_cast(next)); + } + } + + } + + if(executor.registers().pc_status(0) & InstructionSet::ARM::ConditionCode::Overflow) { + logger.error().append("SWI called with V set"); + } + } + if(!swis.empty() && executor.pc() == swis.back().return_address) { + // Overflow set => SWI failure. + auto &back = swis.back(); + if(executor.registers().pc_status(0) & InstructionSet::ARM::ConditionCode::Overflow) { + auto info = logger.info(); + + info.append("[%d] Failed swi ", back.count); + if(back.swi_name.empty()) { + info.append("&%x", back.opcode & 0xfd'ffff); + } else { + info.append("%s", back.swi_name.c_str()); + } + + if(!back.value_name.empty()) { + info.append(" %s", back.value_name.c_str()); + } + + info.append(" @ %08x ", back.address); + for(uint32_t c = 0; c < 10; c++) { + info.append("r%d:%08x ", c, back.regs[c]); + } + } + + swis.pop_back(); + }*/ + + if(log) { + InstructionSet::ARM::Disassembler disassembler; + InstructionSet::ARM::dispatch(instruction, disassembler); + + auto info = logger.info(); + info.append("[%d] %08x: %08x\t\t%s\t prior:[", + instr_count, + executor.pc(), + instruction, + disassembler.last().to_string(executor.pc()).c_str()); + for(uint32_t c = 0; c < 15; c++) { + info.append("r%d:%08x ", c, executor.registers()[c]); + } + info.append("]"); + } +// opcodes.insert(instruction); +// if(accumulate) { +// int c = 0; +// for(auto instr : opcodes) { +// printf("0x%08x, ", instr); +// ++c; +// if(!(c&15)) printf("\n"); +// } +// accumulate = false; +// } + + ++instr_count; + } + +private: + std::array pc_history; + std::size_t pc_history_ptr = 0; + uint32_t instr_count = 0; + uint32_t swi_count = 0; + + struct SWICall { + uint32_t count; + uint32_t opcode; + uint32_t address; + uint32_t regs[10]; + uint32_t return_address; + std::string value_name; + std::string swi_name; + }; + std::vector swis; + uint32_t last_pc = 0; +// uint32_t last_r9 = 0; + bool log = false; + bool accumulate = true; + + std::set opcodes; +}; +#else +template +struct HackyDebugger { + void notify(uint32_t, uint32_t, Executor &) {} +}; +#endif + +class ConcreteMachine: + public Machine, + public MachineTypes::AudioProducer, + public MachineTypes::MappedKeyboardMachine, + public MachineTypes::MediaTarget, + public MachineTypes::MouseMachine, + public MachineTypes::TimedMachine, + public MachineTypes::ScanProducer, + public Activity::Source +{ + private: + // TODO: pick a sensible clock rate; this is just code for '24 MIPS, please'. + static constexpr int ClockRate = 24'000'000; + + // Runs for 24 cycles, distributing calls to the various ticking subsystems + // 'correctly' (i.e. correctly for the approximation in use). + // + // The implementation of this is coupled to the ClockRate above, hence its + // appearance here. + template + void macro_tick() { + macro_counter_ -= 24; + + // This is a 24-cycle window, so at 24Mhz macro_tick() is called at 1Mhz. + // Hence, required ticks are: + // + // * CPU: 24; + // * video: 24 / video_divider; + // * floppy: 8; + // * timers: 2; + // * sound: 1. + + tick_cpu_video<0, video_divider>(); tick_cpu_video<1, video_divider>(); + tick_cpu_video<2, video_divider>(); tick_floppy(); + tick_cpu_video<3, video_divider>(); tick_cpu_video<4, video_divider>(); + tick_cpu_video<5, video_divider>(); tick_floppy(); + tick_cpu_video<6, video_divider>(); tick_cpu_video<7, video_divider>(); + tick_cpu_video<8, video_divider>(); tick_floppy(); + tick_cpu_video<9, video_divider>(); tick_cpu_video<10, video_divider>(); + tick_cpu_video<11, video_divider>(); tick_floppy(); + tick_timers(); + + tick_cpu_video<12, video_divider>(); tick_cpu_video<13, video_divider>(); + tick_cpu_video<14, video_divider>(); tick_floppy(); + tick_cpu_video<15, video_divider>(); tick_cpu_video<16, video_divider>(); + tick_cpu_video<17, video_divider>(); tick_floppy(); + tick_cpu_video<18, video_divider>(); tick_cpu_video<19, video_divider>(); + tick_cpu_video<20, video_divider>(); tick_floppy(); + tick_cpu_video<21, video_divider>(); tick_cpu_video<22, video_divider>(); + tick_cpu_video<23, video_divider>(); tick_floppy(); + tick_timers(); + tick_sound(); + } + int macro_counter_ = 0; + + template + void tick_cpu_video() { + if constexpr (!(offset % video_divider)) { + tick_video(); + } + +#ifndef NDEBUG + // Debug mode: run CPU a lot slower. Actually at close to original advertised MIPS speed. + if constexpr (offset & 7) return; +#endif + if constexpr (offset & 1) return; + tick_cpu(); + } + + public: + ConcreteMachine( + const Analyser::Static::Target &target, + const ROMMachine::ROMFetcher &rom_fetcher + ) : executor_(*this, *this, *this) { + set_clock_rate(ClockRate); + + constexpr ROM::Name risc_os = ROM::Name::AcornRISCOS311; + ROM::Request request(risc_os); + auto roms = rom_fetcher(request); + if(!request.validate(roms)) { + throw ROMMachine::Error::MissingROMs; + } + + executor_.bus.set_rom(roms.find(risc_os)->second); + insert_media(target.media); + } + + void update_interrupts() { + using Exception = InstructionSet::ARM::Registers::Exception; + + const int requests = executor_.bus.interrupt_mask(); + if((requests & InterruptRequests::FIQ) && executor_.registers().interrupt()) { + return; + } + if(requests & InterruptRequests::IRQ) { + executor_.registers().interrupt(); + } + } + + void did_set_status() { + update_interrupts(); + } + + void update_clock_rates() { + video_divider_ = executor_.bus.video().clock_divider(); + } + + private: + // MARK: - ScanProducer. + void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { + executor_.bus.video().crt().set_scan_target(scan_target); + } + Outputs::Display::ScanStatus get_scaled_scan_status() const override { + return executor_.bus.video().crt().get_scaled_scan_status() * video_divider_; + } + + // MARK: - TimedMachine. + void run_for(Cycles cycles) override { + macro_counter_ += cycles.as(); + + while(macro_counter_ > 0) { + switch(video_divider_) { + default: macro_tick<2>(); break; + case 3: macro_tick<3>(); break; + case 4: macro_tick<4>(); break; + case 6: macro_tick<6>(); break; + } + + } + } + int video_divider_ = 1; + + void tick_cpu() { + uint32_t instruction; + if(!executor_.bus.read(executor_.pc(), instruction, executor_.registers().mode(), false)) { +// logger.info().append("Prefetch abort at %08x; last good was at %08x", executor_.pc(), last_pc); + executor_.prefetch_abort(); + + // TODO: does a double abort cause a reset? + executor_.bus.read(executor_.pc(), instruction, executor_.registers().mode(), false); + } + // TODO: pipeline prefetch? + + debugger_.notify(executor_.pc(), instruction, executor_); + InstructionSet::ARM::execute(instruction, executor_); + } + + void tick_timers() { executor_.bus.tick_timers(); } + void tick_sound() { executor_.bus.sound().tick(); } + void tick_video() { executor_.bus.video().tick(); } + void tick_floppy() { executor_.bus.tick_floppy(); } + + // MARK: - MediaTarget + bool insert_media(const Analyser::Static::Media &media) override { + size_t c = 0; + for(auto &disk : media.disks) { + executor_.bus.set_disk(disk, c); + c++; + if(c == 4) break; + } + return true; + } + + // MARK: - AudioProducer + Outputs::Speaker::Speaker *get_speaker() override { + return executor_.bus.speaker(); + } + + // MARK: - Activity::Source. + void set_activity_observer(Activity::Observer *observer) final { + executor_.bus.set_activity_observer(observer); + } + + // MARK: - MappedKeyboardMachine. + MappedKeyboardMachine::KeyboardMapper *get_keyboard_mapper() override { + return &keyboard_mapper_; + } + Archimedes::KeyboardMapper keyboard_mapper_; + + void set_key_state(uint16_t key, bool is_pressed) override { + const int row = Archimedes::KeyboardMapper::row(key); + const int column = Archimedes::KeyboardMapper::column(key); + executor_.bus.keyboard().set_key_state(row, column, is_pressed); + } + + // MARK: - MouseMachine. + Inputs::Mouse &get_mouse() override { + return executor_.bus.keyboard().mouse(); + } + + // MARK: - ARM execution + static constexpr auto arm_model = InstructionSet::ARM::Model::ARMv2; + using Executor = InstructionSet::ARM::Executor, ConcreteMachine>; + Executor executor_; + + // MARK: - Yucky, temporary junk. + HackyDebugger debugger_; +}; + +} + +using namespace Archimedes; + +std::unique_ptr Machine::Archimedes(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { + return std::make_unique(*target, rom_fetcher); +} diff --git a/Machines/Acorn/Archimedes/Archimedes.hpp b/Machines/Acorn/Archimedes/Archimedes.hpp new file mode 100644 index 000000000..c967ad2eb --- /dev/null +++ b/Machines/Acorn/Archimedes/Archimedes.hpp @@ -0,0 +1,27 @@ +// +// Archimedes.hpp +// Clock Signal +// +// Created by Thomas Harte on 04/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../../../Analyser/Static/StaticAnalyser.hpp" +#include "../../ROMMachine.hpp" + +#include + +namespace Archimedes { + +class Machine { + public: + virtual ~Machine() = default; + static std::unique_ptr Archimedes( + const Analyser::Static::Target *target, + const ROMMachine::ROMFetcher &rom_fetcher + ); +}; + +} diff --git a/Machines/Acorn/Archimedes/CMOSRAM.hpp b/Machines/Acorn/Archimedes/CMOSRAM.hpp new file mode 100644 index 000000000..f97ef80bb --- /dev/null +++ b/Machines/Acorn/Archimedes/CMOSRAM.hpp @@ -0,0 +1,85 @@ +// +// CMOSRAM.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../../../Components/I2C/I2C.hpp" +#include "../../../Outputs/Log.hpp" + +#include + +namespace Archimedes { + +struct CMOSRAM: public I2C::Peripheral { + CMOSRAM() { + ram_ = default_ram; + } + + void start(bool is_read) override { + expecting_address_ = !is_read; + } + + // TODO: first 16 addresses are registers, not RAM. + + std::optional read() override { + if(address_ < 16) { + logger.error().append("TODO: read at %d", address_); + } + + const uint8_t result = ram_[address_]; + ++address_; + return result; + } + + bool write(uint8_t value) override { + if(expecting_address_) { + address_ = value; + expecting_address_ = false; + return true; + } + + if(address_ < 16) { + logger.error().append("TODO: write at %d", address_); + return true; + } + + ram_[address_] = value; + ++address_; + return true; + } + +private: + bool expecting_address_ = false; + uint8_t address_; + + std::array ram_{}; + + // This is the default contents of RAM as written by RISC OS 3.11. + static constexpr std::array default_ram = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x14, 0x00, 0x6f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, + 0x00, 0xfe, 0x00, 0xeb, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x10, 0x50, 0x20, 0x08, 0x0a, 0x2c, + 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x5c, 0x13, 0x00, 0x00, 0x04, 0xfd, 0x08, 0x01, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + }; + + Log::Logger logger; +}; + +} diff --git a/Machines/Acorn/Archimedes/FloppyDisc.hpp b/Machines/Acorn/Archimedes/FloppyDisc.hpp new file mode 100644 index 000000000..7a31076a7 --- /dev/null +++ b/Machines/Acorn/Archimedes/FloppyDisc.hpp @@ -0,0 +1,48 @@ +// +// FloppyDisc.hpp +// Clock Signal +// +// Created by Thomas Harte on 07/04/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../../../Components/1770/1770.hpp" + +namespace Archimedes { + +template +class FloppyDisc: public WD::WD1770, public WD::WD1770::Delegate { +public: + FloppyDisc(InterruptObserverT &observer) : WD::WD1770(P1772), observer_(observer) { + emplace_drives(4, 8000000, 300, 2); + set_delegate(this); + } + + void wd1770_did_change_output(WD::WD1770 *) override { + observer_.update_interrupts(); + } + + void set_control(uint8_t value) { + // TODO: + // b0, b1, b2, b3 = drive selects; + // b4 = side select; + // b5 = motor on/off + // b6 = floppy in use (i.e. LED?); + // b7 = disc eject/change reset. + set_drive((value & 0xf) ^ 0xf); + get_drive().set_head(1 ^ ((value >> 4) & 1)); + get_drive().set_motor_on(!(value & 0x20)); + } + void reset() {} + + void set_disk(std::shared_ptr disk, size_t drive) { + get_drive(drive).set_disk(disk); + } + +private: + InterruptObserverT &observer_; +}; + +} diff --git a/Machines/Acorn/Archimedes/HalfDuplexSerial.hpp b/Machines/Acorn/Archimedes/HalfDuplexSerial.hpp new file mode 100644 index 000000000..e47a0a17d --- /dev/null +++ b/Machines/Acorn/Archimedes/HalfDuplexSerial.hpp @@ -0,0 +1,88 @@ +// +// HalfDuplexSerial.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +namespace Archimedes { + +/// Models a half-duplex serial link between two parties, framing bytes with one start bit and two stop bits. +struct HalfDuplexSerial { + static constexpr uint16_t ShiftMask = 0b1111'1110'0000'0000; + + /// Enqueues @c value for output. + void output(int party, uint8_t value) { + parties_[party].output_count = 11; + parties_[party].input = 0x7ff; + parties_[party].output = uint16_t((value << 1) | ShiftMask); + } + + /// @returns The last observed input. + uint8_t input(int party) const { + return uint8_t(parties_[party].input >> 1); + } + + static constexpr uint8_t Receive = 1 << 0; + static constexpr uint8_t Transmit = 1 << 1; + + /// @returns A bitmask of events that occurred during the last shift. + uint8_t events(int party) { + const auto result = parties_[party].events; + parties_[party].events = 0; + return result; + } + + bool is_outputting(int party) const { + return parties_[party].output_count != 11; + } + + /// Updates the shifters on both sides of the serial link. + void shift() { + const uint16_t next = parties_[0].output & parties_[1].output & 1; + + for(int c = 0; c < 2; c++) { + if(parties_[c].output_count) { + --parties_[c].output_count; + if(!parties_[c].output_count) { + parties_[c].events |= Transmit; + parties_[c].input_count = -1; + } + parties_[c].output = (parties_[c].output >> 1) | ShiftMask; + } else { + // Check for a start bit. + if(parties_[c].input_count == -1 && !next) { + parties_[c].input_count = 0; + } + + // Shift in if currently observing. + if(parties_[c].input_count >= 0 && parties_[c].input_count < 11) { + parties_[c].input = uint16_t((parties_[c].input >> 1) | (next << 10)); + + ++parties_[c].input_count; + if(parties_[c].input_count == 11) { + parties_[c].events |= Receive; + parties_[c].input_count = -1; + } + } + } + } + } + +private: + struct Party { + int output_count = 0; + int input_count = -1; + uint16_t output = 0xffff; + uint16_t input = 0; + uint8_t events = 0; + } parties_[2]; +}; + +static constexpr int IOCParty = 0; +static constexpr int KeyboardParty = 1; + +} diff --git a/Machines/Acorn/Archimedes/InputOutputController.hpp b/Machines/Acorn/Archimedes/InputOutputController.hpp new file mode 100644 index 000000000..64c752638 --- /dev/null +++ b/Machines/Acorn/Archimedes/InputOutputController.hpp @@ -0,0 +1,564 @@ +// +// InputOutputController.h +// Clock Signal +// +// Created by Thomas Harte on 20/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "CMOSRAM.hpp" +#include "FloppyDisc.hpp" +#include "Keyboard.hpp" +#include "Sound.hpp" +#include "Video.hpp" + +#include "../../../Outputs/Log.hpp" +#include "../../../Activity/Observer.hpp" +#include "../../../ClockReceiver/ClockingHintSource.hpp" + +namespace Archimedes { + +// IRQ A flags +namespace IRQA { + static constexpr uint8_t PrinterBusy = 0x01; + static constexpr uint8_t SerialRinging = 0x02; + static constexpr uint8_t PrinterAcknowledge = 0x04; + static constexpr uint8_t VerticalFlyback = 0x08; + static constexpr uint8_t PowerOnReset = 0x10; + static constexpr uint8_t Timer0 = 0x20; + static constexpr uint8_t Timer1 = 0x40; + static constexpr uint8_t Force = 0x80; +} + +// IRQ B flags +namespace IRQB { + static constexpr uint8_t PoduleFIQRequest = 0x01; + static constexpr uint8_t SoundBufferPointerUsed = 0x02; + static constexpr uint8_t SerialLine = 0x04; + static constexpr uint8_t IDE = 0x08; + static constexpr uint8_t FloppyDiscChanged = 0x10; + static constexpr uint8_t PoduleIRQRequest = 0x20; + static constexpr uint8_t KeyboardTransmitEmpty = 0x40; + static constexpr uint8_t KeyboardReceiveFull = 0x80; +} + +// FIQ flags +namespace FIQ { + static constexpr uint8_t FloppyDiscData = 0x01; + static constexpr uint8_t FloppyDiscInterrupt = 0x02; + static constexpr uint8_t Econet = 0x04; + static constexpr uint8_t PoduleFIQRequest = 0x40; + static constexpr uint8_t Force = 0x80; +} + +namespace InterruptRequests { + static constexpr int IRQ = 0x01; + static constexpr int FIQ = 0x02; +}; + +template +struct InputOutputController: public ClockingHint::Observer { + InputOutputController(InterruptObserverT &observer, ClockRateObserverT &clock_observer, const uint8_t *ram) : + observer_(observer), + keyboard_(serial_), + floppy_(*this), + sound_(*this, ram), + video_(*this, clock_observer, sound_, ram) + { + irq_a_.status = IRQA::Force | IRQA::PowerOnReset; + irq_b_.status = 0x00; + fiq_.status = FIQ::Force; + + floppy_.set_clocking_hint_observer(this); + + i2c_.add_peripheral(&cmos_, 0xa0); + update_interrupts(); + } + + int interrupt_mask() const { + return + ((irq_a_.request() | irq_b_.request()) ? InterruptRequests::IRQ : 0) | + (fiq_.request() ? InterruptRequests::FIQ : 0); + } + + template + bool tick_timer() { + if(!counters_[c].value && !counters_[c].reload) { + return false; + } + + --counters_[c].value; + if(!counters_[c].value) { + counters_[c].value = counters_[c].reload; + + switch(c) { + case 0: return irq_a_.set(IRQA::Timer0); + case 1: return irq_a_.set(IRQA::Timer1); + case 3: { + serial_.shift(); + keyboard_.update(); + + const uint8_t events = serial_.events(IOCParty); + bool did_interrupt = false; + if(events & HalfDuplexSerial::Receive) { + did_interrupt |= irq_b_.set(IRQB::KeyboardReceiveFull); + } + if(events & HalfDuplexSerial::Transmit) { + did_interrupt |= irq_b_.set(IRQB::KeyboardTransmitEmpty); + } + + return did_interrupt; + } + default: break; + } + // TODO: events for timers 2 (baud). + } + + return false; + } + + void tick_timers() { + bool did_change_interrupts = false; + did_change_interrupts |= tick_timer<0>(); + did_change_interrupts |= tick_timer<1>(); + did_change_interrupts |= tick_timer<2>(); + did_change_interrupts |= tick_timer<3>(); + if(did_change_interrupts) { + observer_.update_interrupts(); + } + } + + void tick_floppy() { + if(floppy_clocking_ != ClockingHint::Preference::None) { + floppy_.run_for(Cycles(1)); + } + } + + void set_disk(std::shared_ptr disk, size_t drive) { + floppy_.set_disk(disk, drive); + } + + /// Decomposes an Archimedes bus address into bank, offset and type. + struct Address { + constexpr Address(uint32_t bus_address) noexcept { + bank = (bus_address >> 16) & 0b111; + type = Type((bus_address >> 19) & 0b11); + offset = bus_address & 0b1111100; + } + + /// A value from 0 to 7 indicating the device being addressed. + uint32_t bank; + /// A seven-bit value which is a multiple of 4, indicating the address within the bank. + uint32_t offset; + /// Access type. + enum class Type { + Sync = 0b00, + Fast = 0b10, + Medium = 0b01, + Slow = 0b11 + } type; + }; + + // Peripheral addresses on the A500: + // + // fast/1 = FDC + // sync/2 = econet + // sync/3 = serial line + // + // bank 4 = podules + // + // fast/5 + + template + bool read(uint32_t address, IntT &destination) { + const Address target(address); + + const auto set_byte = [&](uint8_t value) { + if constexpr (std::is_same_v) { + destination = static_cast(value << 16) | 0xff'00'ff'ff; + } else { + destination = value; + } + }; + + // TODO: flatten the switch below, and the equivalent in `write`. + + switch(target.bank) { + default: + logger.error().append("Unrecognised IOC read from %08x i.e. bank %d / type %d", address, target.bank, target.type); + destination = IntT(~0); + break; + + // Bank 0: internal registers. + case 0: + switch(target.offset) { + default: + logger.error().append("Unrecognised IOC bank 0 read; offset %02x", target.offset); + break; + + case 0x00: { + uint8_t value = control_ | 0xc0; + value &= ~(i2c_.clock() ? 0x02 : 0x00); + value &= ~(i2c_.data() ? 0x01 : 0x00); + value &= ~(video_.flyback_active() ? 0x00 : 0x80); // i.e. high during flyback. + set_byte(value); +// logger.error().append("IOC control read: C:%d D:%d", !(value & 2), !(value & 1)); + } break; + + case 0x04: + set_byte(serial_.input(IOCParty)); + irq_b_.clear(IRQB::KeyboardReceiveFull); + observer_.update_interrupts(); +// logger.error().append("IOC keyboard receive: %02x", value); + break; + + // IRQ A. + case 0x10: + set_byte(irq_a_.status); +// logger.error().append("IRQ A status is %02x", value); + break; + case 0x14: + set_byte(irq_a_.request()); +// logger.error().append("IRQ A request is %02x", value); + break; + case 0x18: + set_byte(irq_a_.mask); +// logger.error().append("IRQ A mask is %02x", value); + break; + + // IRQ B. + case 0x20: + set_byte(irq_b_.status); +// logger.error().append("IRQ B status is %02x", value); + break; + case 0x24: + set_byte(irq_b_.request()); +// logger.error().append("IRQ B request is %02x", value); + break; + case 0x28: + set_byte(irq_b_.mask); +// logger.error().append("IRQ B mask is %02x", value); + break; + + // FIQ. + case 0x30: + set_byte(fiq_.status); +// logger.error().append("FIQ status is %02x", fiq_.status); + break; + case 0x34: + set_byte(fiq_.request()); +// logger.error().append("FIQ request is %02x", fiq_.request()); + break; + case 0x38: + set_byte(fiq_.mask); +// logger.error().append("FIQ mask is %02x", fiq_.mask); + break; + + // Counters. + case 0x40: case 0x50: case 0x60: case 0x70: + set_byte(counters_[(target.offset >> 4) - 0x4].output & 0xff); +// logger.error().append("%02x: Counter %d low is %02x", target, (target >> 4) - 0x4, value); + break; + + case 0x44: case 0x54: case 0x64: case 0x74: + set_byte(counters_[(target.offset >> 4) - 0x4].output >> 8); +// logger.error().append("%02x: Counter %d high is %02x", target, (target >> 4) - 0x4, value); + break; + } + break; + + // Bank 1: the floppy disc controller. + case 1: + set_byte(floppy_.read(target.offset >> 2)); +// logger.error().append("Floppy read; offset %02x", target.offset); + break; + } + + return true; + } + + template + bool write(uint32_t address, IntT bus_value) { + const Address target(address); + + // Empirically, RISC OS 3.19: + // * at 03801e88 and 03801e8c loads R8 and R9 with 0xbe0000 and 0xff0000 respectively; and + // * subsequently uses 32-bit strs (e.g. at 03801eac) to write those values to latch A. + // + // Given that 8-bit ARM writes duplicate the 8-bit value four times across the data bus, + // my conclusion is that the IOC is probably connected to data lines 15–23. + // + // Hence: use @c byte to get a current 8-bit value. + const auto byte = [](IntT original) -> uint8_t { + if constexpr (std::is_same_v) { + return static_cast(original >> 16); + } else { + return original; + } + }; + + switch(target.bank) { + default: + logger.error().append("Unrecognised IOC write of %02x to %08x i.e. bank %d / type %d", bus_value, address, target.bank, target.type); + break; + + // Bank 0: internal registers. + case 0: + switch(target.offset) { + default: + logger.error().append("Unrecognised IOC bank 0 write; %02x to offset %02x", bus_value, target.offset); + break; + + case 0x00: + control_ = byte(bus_value); + i2c_.set_clock_data(!(bus_value & 2), !(bus_value & 1)); + + // Per the A500 documentation: + // b7: vertical sync/test input bit, so should be programmed high; + // b6: input for printer acknowledgement, so should be programmed high; + // b5: speaker mute; 1 = muted; + // b4: "Available on the auxiliary I/O connector" + // b3: "Programmed HIGH, unless Reset Mask is required." + // b2: Used as the floppy disk (READY) input and must be programmed high; + // b1 and b0: I2C connections as above. + break; + + case 0x04: + serial_.output(IOCParty, byte(bus_value)); + irq_b_.clear(IRQB::KeyboardTransmitEmpty); + observer_.update_interrupts(); + break; + + case 0x14: + // b2: clear IF. + // b3: clear IR. + // b4: clear POR. + // b5: clear TM[0]. + // b6: clear TM[1]. + irq_a_.clear(byte(bus_value) & 0x7c); + observer_.update_interrupts(); + break; + + // Interrupts. + case 0x18: + irq_a_.mask = byte(bus_value); +// logger.error().append("IRQ A mask set to %02x", byte(bus_value)); + break; + case 0x28: + irq_b_.mask = byte(bus_value); +// logger.error().append("IRQ B mask set to %02x", byte(bus_value)); + break; + case 0x38: + fiq_.mask = byte(bus_value); +// logger.error().append("FIQ mask set to %02x", byte(bus_value)); + break; + + // Counters. + case 0x40: case 0x50: case 0x60: case 0x70: + counters_[(target.offset >> 4) - 0x4].reload = uint16_t( + (counters_[(target.offset >> 4) - 0x4].reload & 0xff00) | byte(bus_value) + ); + break; + + case 0x44: case 0x54: case 0x64: case 0x74: + counters_[(target.offset >> 4) - 0x4].reload = uint16_t( + (counters_[(target.offset >> 4) - 0x4].reload & 0x00ff) | (byte(bus_value) << 8) + ); + break; + + case 0x48: case 0x58: case 0x68: case 0x78: + counters_[(target.offset >> 4) - 0x4].value = counters_[(target.offset >> 4) - 0x4].reload; + break; + + case 0x4c: case 0x5c: case 0x6c: case 0x7c: + counters_[(target.offset >> 4) - 0x4].output = counters_[(target.offset >> 4) - 0x4].value; + break; + } + break; + + // Bank 1: the floppy disc controller. + case 1: +// logger.error().append("Floppy write; %02x to offset %02x", bus_value, target.offset); + floppy_.write(target.offset >> 2, byte(bus_value)); +// set_byte(floppy_.read(target.offset >> 2)); + break; + + // Bank 5: both the hard disk and the latches, depending on type. + case 5: + switch(target.type) { + default: + logger.error().append("Unrecognised IOC bank 5 type %d write; %02x to offset %02x", target.type, bus_value, target.offset); + break; + + case Address::Type::Fast: + switch(target.offset) { + default: + logger.error().append("Unrecognised IOC fast bank 5 write; %02x to offset %02x", bus_value, target.offset); + break; + + case 0x00: + logger.error().append("TODO: printer data write; %02x", byte(bus_value)); + break; + + case 0x18: { + // TODO, per the A500 documentation: + // + // Latch B: + // b0: ? + // b1: double/single density; 0 = double. + // b2: ? + // b3: floppy drive reset; 0 = reset. + // b4: printer strobe + // b5: ? + // b6: ? + // b7: Head select 3? + const uint8_t value = byte(bus_value); + + floppy_.set_is_double_density(!(value & 0x2)); + if(value & 0x08) floppy_.reset(); +// logger.error().append("TODO: latch B write; %02x", byte(bus_value)); + } break; + + case 0x40: { + const uint8_t value = byte(bus_value); + floppy_.set_control(value); + + // Set the floppy indicator on if any drive is selected, + // because this emulator is compressing them all into a + // single LED, and the machine has indicated 'in use'. + if(activity_observer_) { + activity_observer_->set_led_status(FloppyActivityLED, + !(value & 0x40) && ((value & 0xf) != 0xf) + ); + } + } break; + + case 0x48: + // TODO, per the A500 documentation: + // + // Latch C: + // (probably not present on earlier machines?) + // b2/b3: sync polarity [b3 = V polarity, b2 = H?] + // b0/b1: VIDC master clock; 00 = 24Mhz, 01 = 25.175Mhz; 10 = 36Mhz; 11 = reserved. + + logger.error().append("TODO: latch C write; %02x", byte(bus_value)); + break; + } + break; + } + break; + } + +// case 0x327'0000 & AddressMask: // Bank 7 +// logger.error().append("TODO: exteded external podule space"); +// return true; +// +// case 0x336'0000 & AddressMask: +// logger.error().append("TODO: podule interrupt request"); +// return true; +// +// case 0x336'0004 & AddressMask: +// logger.error().append("TODO: podule interrupt mask"); +// return true; +// +// case 0x33a'0000 & AddressMask: +// logger.error().append("TODO: 6854 / econet write"); +// return true; +// +// case 0x33b'0000 & AddressMask: +// logger.error().append("TODO: 6551 / serial line write"); +// return true; + return true; + } + + auto &sound() { return sound_; } + const auto &sound() const { return sound_; } + auto &video() { return video_; } + const auto &video() const { return video_; } + auto &keyboard() { return keyboard_; } + const auto &keyboard() const { return keyboard_; } + + void update_interrupts() { + const auto set = [&](Interrupt &target, uint8_t flag, bool set) { + if(set) { + target.set(flag); + } else { + target.clear(flag); + } + }; + + set(irq_b_, IRQB::SoundBufferPointerUsed, sound_.interrupt()); + set(fiq_, FIQ::FloppyDiscInterrupt, floppy_.get_interrupt_request_line()); + set(fiq_, FIQ::FloppyDiscData, floppy_.get_data_request_line()); + + if(video_.interrupt()) { + irq_a_.set(IRQA::VerticalFlyback); + } + + observer_.update_interrupts(); + } + + void set_activity_observer(Activity::Observer *observer) { + activity_observer_ = observer; + if(activity_observer_) { + activity_observer_->register_led(FloppyActivityLED); + } + } + +private: + Log::Logger logger; + InterruptObserverT &observer_; + Activity::Observer *activity_observer_ = nullptr; + static inline const std::string FloppyActivityLED = "Drive"; + + // IRQA, IRQB and FIQ states. + struct Interrupt { + uint8_t status = 0x00, mask = 0x00; + uint8_t request() const { + return status & mask; + } + bool set(uint8_t value) { + status |= value; + return status & mask; + } + void clear(uint8_t bits) { + status &= ~bits; + } + }; + Interrupt irq_a_, irq_b_, fiq_; + + // The IOCs four counters. + struct Counter { + uint16_t value = 0; + uint16_t reload = 0; + uint16_t output = 0; + }; + Counter counters_[4]; + + // The KART and keyboard beyond it. + HalfDuplexSerial serial_; + Keyboard keyboard_; + + // The control register. + uint8_t control_ = 0xff; + + // The floppy disc interface. + FloppyDisc floppy_; + ClockingHint::Preference floppy_clocking_ = ClockingHint::Preference::None; + void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) override { + floppy_clocking_ = clocking; + } + + // The I2C bus. + I2C::Bus i2c_; + CMOSRAM cmos_; + + // Audio and video. + Sound sound_; + Video> video_; +}; + +} + diff --git a/Machines/Acorn/Archimedes/Keyboard.hpp b/Machines/Acorn/Archimedes/Keyboard.hpp new file mode 100644 index 000000000..bfbe0710b --- /dev/null +++ b/Machines/Acorn/Archimedes/Keyboard.hpp @@ -0,0 +1,240 @@ +// +// Keyboard.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "HalfDuplexSerial.hpp" + +#include "../../../Outputs/Log.hpp" +#include "../../../Inputs/Mouse.hpp" + +namespace Archimedes { + +// Resource for the keyboard protocol: https://github.com/tmk/tmk_keyboard/wiki/ACORN-ARCHIMEDES-Keyboard +struct Keyboard { + Keyboard(HalfDuplexSerial &serial) : serial_(serial), mouse_(*this) {} + + void set_key_state(int row, int column, bool is_pressed) { + if(!scan_keyboard_) { + logger_.info().append("Ignored key event as key scanning disabled"); + return; + } + + // Don't waste bandwidth on repeating facts. + if(states_[row][column] == is_pressed) return; + states_[row][column] = is_pressed; + + // Post new key event. + logger_.info().append("Posting row %d, column %d is now %s", row, column, is_pressed ? "pressed" : "released"); + const uint8_t prefix = is_pressed ? 0b1100'0000 : 0b1101'0000; + enqueue(static_cast(prefix | row), static_cast(prefix | column)); + consider_dequeue(); + } + + void update() { + if(serial_.events(KeyboardParty) & HalfDuplexSerial::Receive) { + const auto reset = [&]() { + serial_.output(KeyboardParty, HRST); + state_ = State::Idle; + }; + + const uint8_t input = serial_.input(KeyboardParty); + + // A reset command is always accepted, usurping any other state. + if(input == HRST) { + logger_.info().append("HRST; resetting"); + state_ = State::ExpectingRAK1; + event_queue_.clear(); + serial_.output(KeyboardParty, HRST); + return; + } + + switch(state_) { + case State::ExpectingACK: + if(input != NACK && input != SMAK && input != MACK && input != SACK) { + logger_.error().append("No ack; requesting reset"); + reset(); + break; + } + state_ = State::Idle; + [[fallthrough]]; + + case State::Idle: + switch(input) { + case RQID: // Post keyboard ID. + serial_.output(KeyboardParty, 0x81); // Declare this to be a UK keyboard. + logger_.info().append("RQID; responded with 0x81"); + break; + + case PRST: // "1-byte command, does nothing." + logger_.info().append("PRST; ignored"); + break; + + case RQMP: + logger_.error().append("RQMP; TODO: respond something other than 0, 0"); + enqueue(0, 0); + break; + + case NACK: case SMAK: case MACK: case SACK: + scan_keyboard_ = input & 1; + scan_mouse_ = input & 2; + logger_.info().append("ACK; keyboard:%d mouse:%d", scan_keyboard_, scan_mouse_); + break; + + default: + if((input & 0b1111'0000) == 0b0100'0000) { + // RQPD; request to echo the low nibble. + serial_.output(KeyboardParty, 0b1110'0000 | (input & 0b1111)); + logger_.info().append("RQPD; echoing %x", input & 0b1111); + } else if(!(input & 0b1111'1000)) { + // LEDS: should set LED outputs. + logger_.error().append("TODO: set LEDs %d%d%d", static_cast(input&4), static_cast(input&2), static_cast(input&1)); + } else { + logger_.info().append("Ignoring unrecognised command %02x received in idle state", input); + } + break; + } + break; + + case State::ExpectingRAK1: + if(input != RAK1) { + logger_.info().append("Didn't get RAK1; resetting"); + reset(); + break; + } + logger_.info().append("Got RAK1; echoing"); + serial_.output(KeyboardParty, input); + state_ = State::ExpectingRAK2; + break; + + case State::ExpectingRAK2: + if(input != RAK2) { + logger_.info().append("Didn't get RAK2; resetting"); + reset(); + break; + } + logger_.info().append("Got RAK2; echoing"); + serial_.output(KeyboardParty, input); + state_ = State::ExpectingACK; + break; + + case State::ExpectingBACK: + if(input != BACK) { + logger_.info().append("Didn't get BACK; resetting"); + reset(); + break; + } + logger_.info().append("Got BACK; posting next byte"); + dequeue_next(); + state_ = State::ExpectingACK; + break; + } + } + + consider_dequeue(); + } + + void consider_dequeue() { + if(state_ == State::Idle) { + // If the key event queue is empty, grab as much mouse motion + // as available. + if(event_queue_.empty()) { + const int x = std::clamp(mouse_x_, -0x3f, 0x3f); + const int y = std::clamp(mouse_y_, -0x3f, 0x3f); + mouse_x_ -= x; + mouse_y_ -= y; + + if(x || y) { + enqueue(static_cast(x) & 0x7f, static_cast(-y) & 0x7f); + } + } + + if(dequeue_next()) { + state_ = State::ExpectingBACK; + } + } + } + + Inputs::Mouse &mouse() { + return mouse_; + } + +private: + HalfDuplexSerial &serial_; + Log::Logger logger_; + + bool states_[16][16]{}; + + bool scan_keyboard_ = false; + bool scan_mouse_ = false; + enum class State { + ExpectingRAK1, // Post a RAK1 and proceed to ExpectingRAK2 if RAK1 is received; otherwise request a reset. + ExpectingRAK2, // Post a RAK2 and proceed to ExpectingACK if RAK2 is received; otherwise request a reset. + ExpectingACK, // Process NACK, SACK, MACK or SMAK if received; otherwise request a reset. + + Idle, // Process any of: NACK, SACK, MACK, SMAK, RQID, RQMP, RQPD or LEDS if received; also + // unilaterally begin post a byte pair enqueued but not yet sent if any are waiting. + + ExpectingBACK, // Dequeue and post one further byte if BACK is received; otherwise request a reset. + } state_ = State::Idle; + + std::vector event_queue_; + void enqueue(uint8_t first, uint8_t second) { + event_queue_.push_back(first); + event_queue_.push_back(second); + } + bool dequeue_next() { + // To consider: a cheaper approach to the queue than this; in practice events + // are 'rare' so it's not high priority. + if(event_queue_.empty()) return false; + serial_.output(KeyboardParty, event_queue_[0]); + event_queue_.erase(event_queue_.begin()); + return true; + } + + static constexpr uint8_t HRST = 0b1111'1111; // Keyboard reset. + static constexpr uint8_t RAK1 = 0b1111'1110; // Reset response #1. + static constexpr uint8_t RAK2 = 0b1111'1101; // Reset response #2. + + static constexpr uint8_t RQID = 0b0010'0000; // Request for keyboard ID. + static constexpr uint8_t RQMP = 0b0010'0010; // Request for mouse data. + + static constexpr uint8_t BACK = 0b0011'1111; // Acknowledge for first keyboard data byte pair. + static constexpr uint8_t NACK = 0b0011'0000; // Acknowledge for last keyboard data byte pair, disables both scanning and mouse. + static constexpr uint8_t SACK = 0b0011'0001; // Last data byte acknowledge, enabling scanning but disabling mouse. + static constexpr uint8_t MACK = 0b0011'0010; // Last data byte acknowledge, disabling scanning but enabling mouse. + static constexpr uint8_t SMAK = 0b0011'0011; // Last data byte acknowledge, enabling scanning and mouse. + static constexpr uint8_t PRST = 0b0010'0001; // Does nothing. + + + struct Mouse: public Inputs::Mouse { + Mouse(Keyboard &keyboard): keyboard_(keyboard) {} + + void move(int x, int y) override { + keyboard_.mouse_x_ += x; + keyboard_.mouse_y_ += y; + } + + int get_number_of_buttons() override { + return 3; + } + + virtual void set_button_pressed(int index, bool is_pressed) override { + keyboard_.set_key_state(7, index, is_pressed); + } + + private: + Keyboard &keyboard_; + }; + Mouse mouse_; + + int mouse_x_ = 0; + int mouse_y_ = 0; +}; + +} diff --git a/Machines/Acorn/Archimedes/KeyboardMapper.hpp b/Machines/Acorn/Archimedes/KeyboardMapper.hpp new file mode 100644 index 000000000..416ece362 --- /dev/null +++ b/Machines/Acorn/Archimedes/KeyboardMapper.hpp @@ -0,0 +1,148 @@ +// +// KeyboardMapper.hpp +// Clock Signal +// +// Created by Thomas Harte on 23/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../../KeyboardMachine.hpp" + +namespace Archimedes { + +class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper { + public: + static constexpr uint16_t map(int row, int column) { + return static_cast((row << 4) | column); + } + + static constexpr int row(uint16_t key) { + return key >> 4; + } + + static constexpr int column(uint16_t key) { + return key & 0xf; + } + + // Adapted from the A500 Series Technical Reference Manual. + uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const override { + using k = Inputs::Keyboard::Key; + switch(key) { + case k::Escape: return map(0, 0); + case k::F1: return map(0, 1); + case k::F2: return map(0, 2); + case k::F3: return map(0, 3); + case k::F4: return map(0, 4); + case k::F5: return map(0, 5); + case k::F6: return map(0, 6); + case k::F7: return map(0, 7); + case k::F8: return map(0, 8); + case k::F9: return map(0, 9); + case k::F10: return map(0, 10); + case k::F11: return map(0, 11); + case k::F12: return map(0, 12); + case k::PrintScreen: return map(0, 13); + case k::ScrollLock: return map(0, 14); + case k::Pause: return map(0, 15); + + case k::BackTick: return map(1, 0); + case k::k1: return map(1, 1); + case k::k2: return map(1, 2); + case k::k3: return map(1, 3); + case k::k4: return map(1, 4); + case k::k5: return map(1, 5); + case k::k6: return map(1, 6); + case k::k7: return map(1, 7); + case k::k8: return map(1, 8); + case k::k9: return map(1, 9); + case k::k0: return map(1, 10); + case k::Hyphen: return map(1, 11); + case k::Equals: return map(1, 12); + // TODO: pound key. + case k::Backspace: return map(1, 14); + case k::Insert: return map(1, 15); + + case k::Home: return map(2, 0); + case k::PageUp: return map(2, 1); + case k::NumLock: return map(2, 2); + case k::KeypadSlash: return map(2, 3); + case k::KeypadAsterisk: return map(2, 4); + // TODO: keypad hash key + case k::Tab: return map(2, 6); + case k::Q: return map(2, 7); + case k::W: return map(2, 8); + case k::E: return map(2, 9); + case k::R: return map(2, 10); + case k::T: return map(2, 11); + case k::Y: return map(2, 12); + case k::U: return map(2, 13); + case k::I: return map(2, 14); + case k::O: return map(2, 15); + + case k::P: return map(3, 0); + case k::OpenSquareBracket: return map(3, 1); + case k::CloseSquareBracket: return map(3, 2); + case k::Backslash: return map(3, 3); + case k::Delete: return map(3, 4); + case k::End: return map(3, 5); + case k::PageDown: return map(3, 6); + case k::Keypad7: return map(3, 7); + case k::Keypad8: return map(3, 8); + case k::Keypad9: return map(3, 9); + case k::KeypadMinus: return map(3, 10); + case k::LeftControl: return map(3, 11); + case k::A: return map(3, 12); + case k::S: return map(3, 13); + case k::D: return map(3, 14); + case k::F: return map(3, 15); + + case k::G: return map(4, 0); + case k::H: return map(4, 1); + case k::J: return map(4, 2); + case k::K: return map(4, 3); + case k::L: return map(4, 4); + case k::Semicolon: return map(4, 5); + case k::Quote: return map(4, 6); + case k::Enter: return map(4, 7); + case k::Keypad4: return map(4, 8); + case k::Keypad5: return map(4, 9); + case k::Keypad6: return map(4, 10); + case k::KeypadPlus: return map(4, 11); + case k::LeftShift: return map(4, 12); + case k::Z: return map(4, 14); + case k::X: return map(4, 15); + + case k::C: return map(5, 0); + case k::V: return map(5, 1); + case k::B: return map(5, 2); + case k::N: return map(5, 3); + case k::M: return map(5, 4); + case k::Comma: return map(5, 5); + case k::FullStop: return map(5, 6); + case k::ForwardSlash: return map(5, 7); + case k::RightShift: return map(5, 8); + case k::Up: return map(5, 9); + case k::Keypad1: return map(5, 10); + case k::Keypad2: return map(5, 11); + case k::Keypad3: return map(5, 12); + case k::CapsLock: return map(5, 13); + case k::LeftOption: return map(5, 14); + case k::Space: return map(5, 15); + + case k::RightOption: return map(6, 0); + case k::RightControl: return map(6, 1); + case k::Left: return map(6, 2); + case k::Down: return map(6, 3); + case k::Right: return map(6, 4); + case k::Keypad0: return map(6, 5); + case k::KeypadDecimalPoint: return map(6, 6); + case k::KeypadEnter: return map(6, 7); + + default: return MachineTypes::MappedKeyboardMachine::KeyNotMapped; + } + } +}; + +} diff --git a/Machines/Acorn/Archimedes/MemoryController.hpp b/Machines/Acorn/Archimedes/MemoryController.hpp new file mode 100644 index 000000000..f76b6c91c --- /dev/null +++ b/Machines/Acorn/Archimedes/MemoryController.hpp @@ -0,0 +1,490 @@ +// +// MemoryController.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "InputOutputController.hpp" +#include "Video.hpp" +#include "Sound.hpp" + +#include "../../../InstructionSets/ARM/Registers.hpp" +#include "../../../Outputs/Log.hpp" +#include "../../../Activity/Observer.hpp" + +namespace Archimedes { + +/// Provides the mask with all bits set in the range [start, end], where start must be >= end. +template struct BitMask { + static_assert(start >= end); + static constexpr uint32_t value = ((1 << (start + 1)) - 1) - ((1 << end) - 1); +}; +static_assert(BitMask<0, 0>::value == 1); +static_assert(BitMask<1, 1>::value == 2); +static_assert(BitMask<15, 15>::value == 32768); +static_assert(BitMask<15, 0>::value == 0xffff); +static_assert(BitMask<15, 14>::value == 49152); + + +/// Models the MEMC, making this the Archimedes bus. Owns various other chips on the bus as a result. +template +struct MemoryController { + MemoryController(InterruptObserverT &observer, ClockRateObserverT &clock_rate_observer) : + ioc_(observer, clock_rate_observer, ram_.data()) { + read_zones_[0] = Zone::HighROM; // Temporarily put high ROM at address 0. + // TODO: could I just copy it in? Or, at least, + // could I detect at ROM loading time whether I can? + } + + int interrupt_mask() const { + return ioc_.interrupt_mask(); + } + + void set_rom(const std::vector &rom) { + if(rom_.size() % rom.size() || rom.size() > rom_.size()) { + // TODO: throw. + return; + } + + // Copy in as many times as it'll fit. + std::size_t base = 0; + while(base < rom_.size()) { + std::copy( + rom.begin(), + rom.end(), + rom_.begin() + base); + base += rom.size(); + } + } + + template + uint32_t aligned(uint32_t address) { + if constexpr (std::is_same_v) { + return address & static_cast(~3); + } + return address; + } + + template + bool write(uint32_t address, IntT source, InstructionSet::ARM::Mode, bool trans) { + // User mode may only _write_ to logically-mapped RAM (subject to further testing below). + if(trans && address >= 0x200'0000) { + return false; + } + + switch(write_zones_[(address >> 21) & 31]) { + case Zone::DMAAndMEMC: { + const auto buffer_address = [](uint32_t source) -> uint32_t { + return (source & 0x1'fffc) << 2; + }; + + // The MEMC itself isn't on the data bus; all values below should be taken from `address`. + switch((address >> 17) & 0b111) { + case 0b000: ioc_.video().set_frame_start(buffer_address(address)); break; + case 0b001: ioc_.video().set_buffer_start(buffer_address(address)); break; + case 0b010: ioc_.video().set_buffer_end(buffer_address(address)); break; + case 0b011: ioc_.video().set_cursor_start(buffer_address(address)); break; + + case 0b100: ioc_.sound().set_next_start(buffer_address(address)); break; + case 0b101: ioc_.sound().set_next_end(buffer_address(address)); break; + case 0b110: ioc_.sound().swap(); break; + + case 0b111: + os_mode_ = address & (1 << 12); + sound_dma_enable_ = address & (1 << 11); + ioc_.sound().set_dma_enabled(sound_dma_enable_); + video_dma_enable_ = address & (1 << 10); + switch((address >> 8) & 3) { + default: + dynamic_ram_refresh_ = DynamicRAMRefresh::None; + break; + case 0b01: + case 0b11: + dynamic_ram_refresh_ = DynamicRAMRefresh((address >> 8) & 3); + break; + } + high_rom_access_time_ = ROMAccessTime((address >> 6) & 3); + low_rom_access_time_ = ROMAccessTime((address >> 4) & 3); + page_size_ = PageSize((address >> 2) & 3); + + logger.info().append("MEMC Control: %08x -> OS:%d sound:%d video:%d refresh:%d high:%d low:%d size:%d", address, os_mode_, sound_dma_enable_, video_dma_enable_, dynamic_ram_refresh_, high_rom_access_time_, low_rom_access_time_, page_size_); + map_dirty_ = true; + break; + } + } break; + + case Zone::LogicallyMappedRAM: { + const auto item = logical_ram(address, trans); + if(!item) { + return false; + } + *item = source; + } break; + + case Zone::IOControllers: + ioc_.template write(address, source); + break; + + case Zone::VideoController: + // TODO: handle byte writes correctly. + ioc_.video().write(source); + break; + + case Zone::PhysicallyMappedRAM: + physical_ram(address) = source; + break; + + case Zone::AddressTranslator: +// printf("Translator write at %08x; replaces %08x\n", address, pages_[address & 0x7f]); + pages_[address & 0x7f] = address; + map_dirty_ = true; + break; + + default: + printf("TODO: write of %08x to %08x [%lu]\n", source, address, sizeof(IntT)); + break; + } + + return true; + } + + template + bool read(uint32_t address, IntT &source, InstructionSet::ARM::Mode, bool trans) { + // User mode may only read logically-maped RAM and ROM. + if(trans && address >= 0x200'0000 && address < 0x380'0000) { + return false; + } + + switch (read_zones_[(address >> 21) & 31]) { + case Zone::PhysicallyMappedRAM: + source = physical_ram(address); + break; + + case Zone::LogicallyMappedRAM: { + const auto item = logical_ram(address, trans); + if(!item) { + return false; + } + source = *item; + } break; + + case Zone::LowROM: +// logger.error().append("TODO: Low ROM read from %08x", address); + source = IntT(~0); + break; + + case Zone::HighROM: + // Real test is: require A24=A25=0, then A25=1. + read_zones_[0] = Zone::LogicallyMappedRAM; + source = high_rom(address); + break; + + case Zone::IOControllers: + ioc_.template read(address, source); + break; + + default: + logger.error().append("TODO: read from %08x", address); + break; + } + + return true; + } + + // + // Expose various IOC-owned things. + // + void tick_timers() { ioc_.tick_timers(); } + void tick_floppy() { ioc_.tick_floppy(); } + void set_disk(std::shared_ptr disk, size_t drive) { + ioc_.set_disk(disk, drive); + } + + Outputs::Speaker::Speaker *speaker() { + return ioc_.sound().speaker(); + } + + auto &sound() { return ioc_.sound(); } + const auto &sound() const { return ioc_.sound(); } + auto &video() { return ioc_.video(); } + const auto &video() const { return ioc_.video(); } + auto &keyboard() { return ioc_.keyboard(); } + const auto &keyboard() const { return ioc_.keyboard(); } + + void set_activity_observer(Activity::Observer *observer) { + ioc_.set_activity_observer(observer); + } + + private: + Log::Logger logger; + + enum class Zone { + LogicallyMappedRAM, + PhysicallyMappedRAM, + IOControllers, + LowROM, + HighROM, + VideoController, + DMAAndMEMC, + AddressTranslator, + }; + static std::array zones(bool is_read) { + std::array zones{}; + for(size_t c = 0; c < zones.size(); c++) { + const auto address = c << 21; + if(address < 0x200'0000) { + zones[c] = Zone::LogicallyMappedRAM; + } else if(address < 0x300'0000) { + zones[c] = Zone::PhysicallyMappedRAM; + } else if(address < 0x340'0000) { + zones[c] = Zone::IOControllers; + } else if(address < 0x360'0000) { + zones[c] = is_read ? Zone::LowROM : Zone::VideoController; + } else if(address < 0x380'0000) { + zones[c] = is_read ? Zone::LowROM : Zone::DMAAndMEMC; + } else { + zones[c] = is_read ? Zone::HighROM : Zone::AddressTranslator; + } + } + return zones; + } + + bool has_moved_rom_ = false; + std::array ram_{}; + std::array rom_; + InputOutputController ioc_; + + template + IntT &physical_ram(uint32_t address) { + address = aligned(address); + address &= (ram_.size() - 1); + return *reinterpret_cast(&ram_[address]); + } + + template + IntT &high_rom(uint32_t address) { + address = aligned(address); + return *reinterpret_cast(&rom_[address & (rom_.size() - 1)]); + } + + std::array read_zones_ = zones(true); + const std::array write_zones_ = zones(false); + + // Control register values. + bool os_mode_ = false; + bool sound_dma_enable_ = false; + bool video_dma_enable_ = false; // "Unaffected" by reset, so here picked arbitrarily. + + enum class DynamicRAMRefresh { + None = 0b00, + DuringFlyback = 0b01, + Continuous = 0b11, + } dynamic_ram_refresh_ = DynamicRAMRefresh::None; // State at reset is undefined; constrain to a valid enum value. + + enum class ROMAccessTime { + ns450 = 0b00, + ns325 = 0b01, + ns200 = 0b10, + ns200with60nsNibble = 0b11, + } high_rom_access_time_ = ROMAccessTime::ns450, low_rom_access_time_ = ROMAccessTime::ns450; + + enum class PageSize { + kb4 = 0b00, + kb8 = 0b01, + kb16 = 0b10, + kb32 = 0b11, + } page_size_ = PageSize::kb4; + + // Address translator. + // + // MEMC contains one entry per a physical page number, indicating where it goes logically. + // Any logical access is tested against all 128 mappings. So that's backwards compared to + // the ideal for an emulator, which would map from logical to physical, even if a lot more + // compact — there are always 128 physical pages; there are up to 8192 logical pages. + // + // So captured here are both the physical -> logical map as representative of the real + // hardware, and the reverse logical -> physical map, which is built (and rebuilt, and rebuilt) + // from the other. + + // Physical to logical mapping. + std::array pages_{}; + + // Logical to physical mapping; this is divided by 'access mode' + // (i.e. the combination of read/write, trans and OS mode flags, + // as multipliexed by the @c mapping() function) because mapping + // varies by mode — not just in terms of restricting access, but + // actually presenting different memory. + using MapTarget = std::array; + std::array mapping_; + + template + MapTarget &mapping(bool trans, bool os_mode) { + const size_t index = (is_read ? 1 : 0) | (os_mode ? 2 : 0) | ((trans && !os_mode) ? 4 : 0); + return mapping_[index]; + } + + bool map_dirty_ = true; + + template + IntT *logical_ram(uint32_t address, bool trans) { + // Possibly TODO: this recompute-if-dirty flag is supposed to ameliorate for an expensive + // mapping process. It can be eliminated when the process is improved. + if(map_dirty_) { + update_mapping(); + map_dirty_ = false; + } + address = aligned(address); + address &= 0x1ff'ffff; + size_t page; + + // TODO: eliminate switch here. + switch(page_size_) { + default: + case PageSize::kb4: + page = address >> 12; + address &= 0x0fff; + break; + case PageSize::kb8: + page = address >> 13; + address &= 0x1fff; + break; + case PageSize::kb16: + page = address >> 14; + address &= 0x3fff; + break; + case PageSize::kb32: + page = address >> 15; + address &= 0x7fff; + break; + } + + const auto &map = mapping(trans, os_mode_); + if(!map[page]) { + return nullptr; + } + return reinterpret_cast(&map[page][address]); + } + + void update_mapping() { + // For each physical page, project it into logical space. + switch(page_size_) { + default: + case PageSize::kb4: update_mapping(); break; + case PageSize::kb8: update_mapping(); break; + case PageSize::kb16: update_mapping(); break; + case PageSize::kb32: update_mapping(); break; + } + } + + template + void update_mapping() { + // Clear all logical mappings. + for(auto &map: mapping_) { + std::fill(map.begin(), map.end(), nullptr); + } + + // For each physical page, project it into logical space + // and store it. + for(const auto page: pages_) { + uint32_t physical, logical; + + switch(size) { + case PageSize::kb4: + // 4kb: + // A[6:0] -> PPN[6:0] + // A[11:10] -> LPN[12:11]; A[22:12] -> LPN[10:0] i.e. 8192 logical pages + physical = page & BitMask<6, 0>::value; + + physical <<= 12; + + logical = (page & BitMask<11, 10>::value) << 1; + logical |= (page & BitMask<22, 12>::value) >> 12; + break; + + case PageSize::kb8: + // 8kb: + // A[0] -> PPN[6]; A[6:1] -> PPN[5:0] + // A[11:10] -> LPN[11:10]; A[22:13] -> LPN[9:0] i.e. 4096 logical pages + physical = (page & BitMask<0, 0>::value) << 6; + physical |= (page & BitMask<6, 1>::value) >> 1; + + physical <<= 13; + + logical = page & BitMask<11, 10>::value; + logical |= (page & BitMask<22, 13>::value) >> 13; + break; + + case PageSize::kb16: + // 16kb: + // A[1:0] -> PPN[6:5]; A[6:2] -> PPN[4:0] + // A[11:10] -> LPN[10:9]; A[22:14] -> LPN[8:0] i.e. 2048 logical pages + physical = (page & BitMask<1, 0>::value) << 5; + physical |= (page & BitMask<6, 2>::value) >> 2; + + physical <<= 14; + + logical = (page & BitMask<11, 10>::value) >> 1; + logical |= (page & BitMask<22, 14>::value) >> 14; + break; + + case PageSize::kb32: + // 32kb: + // A[1] -> PPN[6]; A[2] -> PPN[5]; A[0] -> PPN[4]; A[6:3] -> PPN[3:0] + // A[11:10] -> LPN[9:8]; A[22:15] -> LPN[7:0] i.e. 1024 logical pages + physical = (page & BitMask<1, 1>::value) << 5; + physical |= (page & BitMask<2, 2>::value) << 3; + physical |= (page & BitMask<0, 0>::value) << 4; + physical |= (page & BitMask<6, 3>::value) >> 3; + + physical <<= 15; + + logical = (page & BitMask<11, 10>::value) >> 2; + logical |= (page & BitMask<22, 15>::value) >> 15; + break; + } + +// printf("%08x => physical %d -> logical %d\n", page, (physical >> 15), logical); + + // TODO: consider clashes. + // TODO: what if there's less than 4mb present? + const auto target = &ram_[physical]; + + const auto set_supervisor = [&](bool read, bool write) { + if(read) mapping(false, false)[logical] = target; + if(write) mapping(false, false)[logical] = target; + }; + + const auto set_os = [&](bool read, bool write) { + if(read) mapping(true, true)[logical] = target; + if(write) mapping(true, true)[logical] = target; + }; + + const auto set_user = [&](bool read, bool write) { + if(read) mapping(true, false)[logical] = target; + if(write) mapping(true, false)[logical] = target; + }; + + set_supervisor(true, true); + switch((page >> 8) & 3) { + case 0b00: + set_os(true, true); + set_user(true, true); + break; + case 0b01: + set_os(true, true); + set_user(true, false); + break; + default: + set_os(true, false); + set_user(false, false); + break; + } + } + } +}; + +} diff --git a/Machines/Acorn/Archimedes/Sound.hpp b/Machines/Acorn/Archimedes/Sound.hpp new file mode 100644 index 000000000..77a28845c --- /dev/null +++ b/Machines/Acorn/Archimedes/Sound.hpp @@ -0,0 +1,211 @@ +// +// Audio.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../../../Concurrency/AsyncTaskQueue.hpp" +#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" + +#include +#include + +namespace Archimedes { + +// Generate lookup table for sound output levels, and hold it only once regardless +// of how many template instantiations there are of @c Sound. +static constexpr std::array generate_levels() { + std::array result{}; + + // There are 8 segments of 16 steps; each segment is a linear + // interpolation from its start level to its end level and + // each level is double the previous. + // + // Bit 7 provides a sign. + + for(size_t c = 0; c < 256; c++) { + // This is the VIDC1 rule. +// const bool is_negative = c & 128; +// const auto point = static_cast(c & 0xf); +// const auto chord = static_cast((c >> 4) & 7); + + // VIDC2 rule, which seems to be effective. I've yet to spot the rule by which + // VIDC1/2 is detected. + const bool is_negative = c & 1; + const auto point = static_cast((c >> 1) & 0xf); + const auto chord = static_cast((c >> 5) & 7); + + const int start = (1 << chord) - 1; + const int end = (chord == 7) ? 247 : ((start << 1) + 1); + + const int level = start * (16 - point) + end * point; + result[c] = static_cast((level * 32767) / 3832); + if(is_negative) result[c] = -result[c]; + } + + return result; +} +struct SoundLevels { + static constexpr auto levels = generate_levels(); +}; + +/// Models the Archimedes sound output; in a real machine this is a joint efort between the VIDC and the MEMC. +template +struct Sound: private SoundLevels { + Sound(InterruptObserverT &observer, const uint8_t *ram) : ram_(ram), observer_(observer) { + speaker_.set_input_rate(1'000'000); + speaker_.set_high_frequency_cutoff(2'200.0f); + } + + void set_next_end(uint32_t value) { + next_.end = value; + } + + void set_next_start(uint32_t value) { + next_.start = value; + set_buffer_valid(true); // My guess: this is triggered on next buffer start write. + + // Definitely wrong; testing. +// set_halted(false); + } + + bool interrupt() const { + return !next_buffer_valid_; + } + + void swap() { + current_.start = next_.start; + std::swap(current_.end, next_.end); + set_buffer_valid(false); + set_halted(false); + } + + void set_frequency(uint8_t frequency) { + divider_ = reload_ = frequency; + } + + void set_stereo_image(uint8_t channel, uint8_t value) { + if(!value) { + positions_[channel].left = + positions_[channel].right = 0; + return; + } + + positions_[channel].right = value - 1; + positions_[channel].left = 6 - positions_[channel].right; + } + + void set_dma_enabled(bool enabled) { + dma_enabled_ = enabled; + } + + void tick() { + // Write silence if not currently outputting. + if(halted_ || !dma_enabled_) { + post_sample(Outputs::Speaker::StereoSample()); + return; + } + + // Apply user-programmed clock divider. + --divider_; + if(!divider_) { + divider_ = reload_ + 2; + + // Grab a single byte from the FIFO. + const uint8_t raw = ram_[static_cast(current_.start) + static_cast(byte_)]; + sample_ = Outputs::Speaker::StereoSample( // TODO: pan, volume. + static_cast((levels[raw] * positions_[byte_ & 7].left) / 6), + static_cast((levels[raw] * positions_[byte_ & 7].right) / 6) + ); + ++byte_; + + // If the FIFO is exhausted, consider triggering a DMA request. + if(byte_ == 16) { + byte_ = 0; + + current_.start += 16; + if(current_.start == current_.end) { + if(next_buffer_valid_) { + swap(); + } else { + set_halted(true); + } + } + } + } + post_sample(sample_); + } + + Outputs::Speaker::Speaker *speaker() { + return &speaker_; + } + + ~Sound() { + while(is_posting_.test_and_set()); + } + +private: + const uint8_t *ram_ = nullptr; + + uint8_t divider_ = 0, reload_ = 0; + int byte_ = 0; + + void set_buffer_valid(bool valid) { + next_buffer_valid_ = valid; + observer_.update_interrupts(); + } + + void set_halted(bool halted) { + if(halted_ != halted && !halted) { + byte_ = 0; + divider_ = reload_; + } + halted_ = halted; + } + + bool next_buffer_valid_ = false; + bool halted_ = true; // This is a bit of a guess. + bool dma_enabled_ = false; + + struct Buffer { + uint32_t start = 0, end = 0; + }; + Buffer current_, next_; + + struct StereoPosition { + // These are maintained as sixths, i.e. a value of 6 means 100%. + int left, right; + } positions_[8]; + + InterruptObserverT &observer_; + Outputs::Speaker::PushLowpass speaker_; + Concurrency::AsyncTaskQueue queue_; + + void post_sample(Outputs::Speaker::StereoSample sample) { + samples_[sample_target_][sample_pointer_++] = sample; + if(sample_pointer_ == samples_[0].size()) { + while(is_posting_.test_and_set()); + + const auto post_source = sample_target_; + sample_target_ ^= 1; + sample_pointer_ = 0; + queue_.enqueue([this, post_source]() { + speaker_.push(reinterpret_cast(samples_[post_source].data()), samples_[post_source].size()); + is_posting_.clear(); + }); + } + } + std::size_t sample_pointer_ = 0; + std::size_t sample_target_ = 0; + Outputs::Speaker::StereoSample sample_; + + using SampleBuffer = std::array; + std::array samples_; + std::atomic_flag is_posting_ = ATOMIC_FLAG_INIT; +}; + +} diff --git a/Machines/Acorn/Archimedes/Video.hpp b/Machines/Acorn/Archimedes/Video.hpp new file mode 100644 index 000000000..87ab8cc08 --- /dev/null +++ b/Machines/Acorn/Archimedes/Video.hpp @@ -0,0 +1,500 @@ +// +// Video.hpp +// Clock Signal +// +// Created by Thomas Harte on 20/03/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../../../Outputs/Log.hpp" +#include "../../../Outputs/CRT/CRT.hpp" + +#include +#include +#include + +namespace Archimedes { + +template +struct Video { + Video(InterruptObserverT &interrupt_observer, ClockRateObserverT &clock_rate_observer, SoundT &sound, const uint8_t *ram) : + interrupt_observer_(interrupt_observer), + clock_rate_observer_(clock_rate_observer), + sound_(sound), + ram_(ram), + crt_(Outputs::Display::InputDataType::Red4Green4Blue4) { + set_clock_divider(3); + crt_.set_visible_area(Outputs::Display::Rect(0.06f, 0.07f, 0.9f, 0.9f)); + crt_.set_display_type(Outputs::Display::DisplayType::RGB); + } + + static constexpr uint16_t colour(uint32_t value) { + uint8_t packed[2]{}; + packed[0] = value & 0xf; + packed[1] = (value & 0xf0) | ((value & 0xf00) >> 8); + +#if TARGET_RT_BIG_ENDIAN + return static_cast(packed[1] | (packed[0] << 8)); +#else + return static_cast(packed[0] | (packed[1] << 8)); +#endif + }; + static constexpr uint16_t high_spread[] = { + colour(0b0000'0000'0000), colour(0b0000'0000'1000), colour(0b0000'0100'0000), colour(0b0000'0100'1000), + colour(0b0000'1000'0000), colour(0b0000'1000'1000), colour(0b0000'1100'0000), colour(0b0000'1100'1000), + colour(0b1000'0000'0000), colour(0b1000'0000'1000), colour(0b1000'0100'0000), colour(0b1000'0100'1000), + colour(0b1000'1000'0000), colour(0b1000'1000'1000), colour(0b1000'1100'0000), colour(0b1000'1100'1000), + }; + + void write(uint32_t value) { + const auto target = (value >> 24) & 0xfc; + const auto timing_value = [](uint32_t value) -> uint32_t { + return (value >> 14) & 0x3ff; + }; + + switch(target) { + case 0x00: case 0x04: case 0x08: case 0x0c: + case 0x10: case 0x14: case 0x18: case 0x1c: + case 0x20: case 0x24: case 0x28: case 0x2c: + case 0x30: case 0x34: case 0x38: case 0x3c: + colours_[target >> 2] = colour(value); + break; + + case 0x40: border_colour_ = colour(value); break; + + case 0x44: case 0x48: case 0x4c: + cursor_colours_[(target - 0x40) >> 2] = colour(value); + break; + + case 0x80: horizontal_timing_.period = timing_value(value); break; + case 0x84: horizontal_timing_.sync_width = timing_value(value); break; + case 0x88: horizontal_timing_.border_start = timing_value(value); break; + case 0x8c: horizontal_timing_.display_start = timing_value(value); break; + case 0x90: horizontal_timing_.display_end = timing_value(value); break; + case 0x94: horizontal_timing_.border_end = timing_value(value); break; + case 0x98: + horizontal_timing_.cursor_start = (value >> 13) & 0x7ff; + cursor_shift_ = (value >> 11) & 3; + break; + case 0x9c: + logger.error().append("TODO: Video horizontal interlace: %d", (value >> 14) & 0x3ff); + break; + + case 0xa0: vertical_timing_.period = timing_value(value); break; + case 0xa4: vertical_timing_.sync_width = timing_value(value); break; + case 0xa8: vertical_timing_.border_start = timing_value(value); break; + case 0xac: vertical_timing_.display_start = timing_value(value); break; + case 0xb0: vertical_timing_.display_end = timing_value(value); break; + case 0xb4: vertical_timing_.border_end = timing_value(value); break; + case 0xb8: vertical_timing_.cursor_start = timing_value(value); break; + case 0xbc: vertical_timing_.cursor_end = timing_value(value); break; + + case 0xe0: + logger.error().append("TODO: video control: %08x", value); + + // Set pixel rate. This is the value that a 24Mhz clock should be divided + // by to get half the pixel rate. + switch(value & 0b11) { + case 0b00: set_clock_divider(6); break; // i.e. pixel clock = 8Mhz. + case 0b01: set_clock_divider(4); break; // 12Mhz. + case 0b10: set_clock_divider(3); break; // 16Mhz. + case 0b11: set_clock_divider(2); break; // 24Mhz. + } + + // Set colour depth. + colour_depth_ = Depth((value >> 2) & 0b11); + break; + + // + // Sound parameters. + // + case 0x60: case 0x64: case 0x68: case 0x6c: + case 0x70: case 0x74: case 0x78: case 0x7c: { + const uint8_t channel = ((value >> 26) + 7) & 7; + sound_.set_stereo_image(channel, value & 7); + } break; + + case 0xc0: + sound_.set_frequency(value & 0x7f); + break; + + default: + logger.error().append("TODO: unrecognised VIDC write of %08x", value); + break; + } + } + + void tick() { + // Pick new horizontal state, possibly rolling over into the vertical. + horizontal_state_.increment_position(horizontal_timing_); + + if(horizontal_state_.did_restart()) { + const auto old_phase = vertical_state_.phase(); + vertical_state_.increment_position(vertical_timing_); + pixel_count_ = 0; + + const auto phase = vertical_state_.phase(); + if(phase != old_phase) { + // I don't have good information on this; first guess: copy frame and + // cursor start addresses into counters at the start of the first vertical + // display line. + if(phase == Phase::Display) { + address_ = frame_start_; + cursor_address_ = cursor_start_; + } + if(old_phase == Phase::Display) { + entered_flyback_ = true; + interrupt_observer_.update_interrupts(); + } + } + + // Determine which next 8 bytes will be the cursor image for this line. + // Pragmatically, updating cursor_address_ once per line avoids probable + // errors in getting it to appear appropriately over both pixels and border. + if(vertical_state_.cursor_active) { + uint8_t *cursor_pixel = cursor_image_.data(); + for(int byte = 0; byte < 8; byte ++) { + cursor_pixel[0] = (ram_[cursor_address_] >> 0) & 3; + cursor_pixel[1] = (ram_[cursor_address_] >> 2) & 3; + cursor_pixel[2] = (ram_[cursor_address_] >> 4) & 3; + cursor_pixel[3] = (ram_[cursor_address_] >> 6) & 3; + cursor_pixel += 4; + ++cursor_address_; + } + } + cursor_pixel_ = 32; + } + + // Accumulate total phase. + ++time_in_phase_; + + // Determine current output phase. + Phase new_phase; + switch(vertical_state_.phase()) { + case Phase::Sync: new_phase = Phase::Sync; break; + case Phase::Blank: new_phase = Phase::Blank; break; + case Phase::Border: + new_phase = horizontal_state_.phase() == Phase::Display ? Phase::Border : horizontal_state_.phase(); + break; + case Phase::Display: + new_phase = horizontal_state_.phase(); + break; + } + + const auto flush_pixels = [&]() { + const auto duration = static_cast(time_in_phase_); + crt_.output_data(duration, static_cast(time_in_phase_) * 2); + time_in_phase_ = 0; + pixels_ = nullptr; + }; + + // Possibly output something. + if(new_phase != phase_) { + if(time_in_phase_) { + const auto duration = static_cast(time_in_phase_); + + switch(phase_) { + case Phase::Sync: crt_.output_sync(duration); break; + case Phase::Blank: crt_.output_blank(duration); break; + case Phase::Display: flush_pixels(); break; + case Phase::Border: crt_.output_level(duration, border_colour_); break; + } + time_in_phase_ = 0; + } + phase_ = new_phase; + } + + // Update cursor pixel counter if applicable; this might mean triggering it + // and it might just mean advancing it if it has already been triggered. + if(vertical_state_.cursor_active) { + const auto pixel_position = horizontal_state_.position << 1; + if(pixel_position <= horizontal_timing_.cursor_start && (pixel_position + 2) > horizontal_timing_.cursor_start) { + cursor_pixel_ = int(horizontal_timing_.cursor_start) - int(pixel_position); + } + } + + // Grab some more pixels if appropriate. + if(vertical_state_.display_active() && horizontal_state_.display_active()) { + const auto next_byte = [&]() -> uint8_t { + const auto next = ram_[address_]; + ++address_; + + // `buffer_end_` is the final address that a 16-byte block will be fetched from; + // the +16 here papers over the fact that I'm not accurately implementing DMA. + if(address_ == buffer_end_ + 16) { + address_ = buffer_start_; + } + return next; + }; + + switch(colour_depth_) { + case Depth::EightBPP: + pixel_data_[0] = next_byte(); + pixel_data_[1] = next_byte(); + break; + case Depth::FourBPP: + pixel_data_[0] = next_byte(); + break; + case Depth::TwoBPP: + if(!(pixel_count_&1)) { + pixel_data_[0] = next_byte(); + } + break; + case Depth::OneBPP: + if(!(pixel_count_&3)) { + pixel_data_[0] = next_byte(); + } + break; + } + ++pixel_count_; + } + + if(phase_ == Phase::Display) { + if(pixels_ && time_in_phase_ == PixelBufferSize/2) { + flush_pixels(); + } + + if(!pixels_) { + if(time_in_phase_) { + flush_pixels(); + } + + pixels_ = reinterpret_cast(crt_.begin_data(PixelBufferSize)); + } + + if(pixels_) { + // Each tick in here is two ticks of the pixel clock, so: + // + // 8bpp mode: output two bytes; + // 4bpp mode: output one byte; + // 2bpp mode: output one byte every second tick; + // 1bpp mode: output one byte every fourth tick. + switch(colour_depth_) { + case Depth::EightBPP: + pixels_[0] = (colours_[pixel_data_[0] & 0xf] & colour(0b0111'0011'0111)) | high_spread[pixel_data_[0] >> 4]; + pixels_[1] = (colours_[pixel_data_[1] & 0xf] & colour(0b0111'0011'0111)) | high_spread[pixel_data_[1] >> 4]; + break; + + case Depth::FourBPP: + pixels_[0] = colours_[pixel_data_[0] & 0xf]; + pixels_[1] = colours_[pixel_data_[0] >> 4]; + break; + + case Depth::TwoBPP: + pixels_[0] = colours_[pixel_data_[0] & 3]; + pixels_[1] = colours_[(pixel_data_[0] >> 2) & 3]; + pixel_data_[0] >>= 4; + break; + + case Depth::OneBPP: + pixels_[0] = colours_[pixel_data_[0] & 1]; + pixels_[1] = colours_[(pixel_data_[0] >> 1) & 1]; + pixel_data_[0] >>= 2; + break; + } + + // Overlay cursor if applicable. + // TODO: pull this so far out that the cursor can display over the border, too. + if(cursor_pixel_ < 32) { + if(cursor_pixel_ >= 0) { + const auto pixel = cursor_image_[static_cast(cursor_pixel_)]; + if(pixel) { + pixels_[0] = cursor_colours_[pixel]; + } + } + if(cursor_pixel_ < 31) { + const auto pixel = cursor_image_[static_cast(cursor_pixel_ + 1)]; + if(pixel) { + pixels_[1] = cursor_colours_[pixel]; + } + } + } + + pixels_ += 2; + } + } + + // Advance cursor position. + if(cursor_pixel_ < 32) cursor_pixel_ += 2; + } + + /// @returns @c true if a vertical retrace interrupt has been signalled since the last call to @c interrupt(); @c false otherwise. + bool interrupt() { + // Guess: edge triggered? + const bool interrupt = entered_flyback_; + entered_flyback_ = false; + return interrupt; + } + + bool flyback_active() const { + return vertical_state_.phase() != Phase::Display; + } + + void set_frame_start(uint32_t address) { frame_start_ = address; } + void set_buffer_start(uint32_t address) { buffer_start_ = address; } + void set_buffer_end(uint32_t address) { buffer_end_ = address; } + void set_cursor_start(uint32_t address) { cursor_start_ = address; } + + Outputs::CRT::CRT &crt() { return crt_; } + const Outputs::CRT::CRT &crt() const { return crt_; } + + int clock_divider() const { + return static_cast(clock_divider_); + } + +private: + Log::Logger logger; + InterruptObserverT &interrupt_observer_; + ClockRateObserverT &clock_rate_observer_; + SoundT &sound_; + + // In the current version of this code, video DMA occurrs costlessly, + // being deferred to the component itself. + const uint8_t *ram_ = nullptr; + Outputs::CRT::CRT crt_; + + // Horizontal and vertical timing. + struct Timing { + uint32_t period = 0; + uint32_t sync_width = 0; + uint32_t border_start = 0; + uint32_t border_end = 0; + uint32_t display_start = 0; + uint32_t display_end = 0; + uint32_t cursor_start = 0; + uint32_t cursor_end = 0; + }; + uint32_t cursor_shift_ = 0; + Timing horizontal_timing_, vertical_timing_; + + // Current video state. + enum class Phase { + Sync, Blank, Border, Display, + }; + struct State { + uint32_t position = 0; + + void increment_position(const Timing &timing) { + ++position; + if(position == 1024) position = 0; + + if(position == timing.period) { + state = DidRestart; + position = 0; + } + + if(position == timing.sync_width) state |= SyncEnded; + if(position == timing.display_start) state |= DisplayStarted; + if(position == timing.display_end) state |= DisplayEnded; + if(position == timing.border_start) state |= BorderStarted; + if(position == timing.border_end) state |= BorderEnded; + + cursor_active |= position == timing.cursor_start; + cursor_active &= position != timing.cursor_end; + } + + static constexpr uint8_t SyncEnded = 0x1; + static constexpr uint8_t BorderStarted = 0x2; + static constexpr uint8_t BorderEnded = 0x4; + static constexpr uint8_t DisplayStarted = 0x8; + static constexpr uint8_t DisplayEnded = 0x10; + static constexpr uint8_t DidRestart = 0x20; + uint8_t state = 0; + + bool cursor_active = false; + + bool did_restart() { + const bool result = state & DidRestart; + state &= ~DidRestart; + return result; + } + + bool display_active() const { + return (state & DisplayStarted) && !(state & DisplayEnded); + } + + Phase phase() const { + // TODO: turn the following logic into a 32-entry lookup table. + if(!(state & SyncEnded)) { + return Phase::Sync; + } + if(!(state & BorderStarted) || (state & BorderEnded)) { + return Phase::Blank; + } + if(!(state & DisplayStarted) || (state & DisplayEnded)) { + return Phase::Border; + } + return Phase::Display; + } + }; + State horizontal_state_, vertical_state_; + Phase phase_ = Phase::Sync; + uint32_t time_in_phase_ = 0; + uint32_t pixel_count_ = 0; + uint16_t *pixels_ = nullptr; + + // It is elsewhere assumed that this size is a multiple of 8. + static constexpr size_t PixelBufferSize = 320; + + // Programmer-set addresses. + uint32_t buffer_start_ = 0; + uint32_t buffer_end_ = 0; + uint32_t frame_start_ = 0; + uint32_t cursor_start_ = 0; + + // Ephemeral address state. + uint32_t address_ = 0; + + // Horizontal cursor output state. + uint32_t cursor_address_ = 0; + int cursor_pixel_ = 0; + std::array cursor_image_; + + // Ephemeral graphics data. + uint8_t pixel_data_[2]{}; + + // Colour palette, converted to internal format. + uint16_t border_colour_; + std::array colours_{}; + std::array cursor_colours_{}; + + // An interrupt flag; more closely related to the interface by which + // my implementation of the IOC picks up an interrupt request than + // to hardware. + bool entered_flyback_ = false; + + // The divider that would need to be applied to a 24Mhz clock to + // get half the current pixel clock; counting is in units of half + // the pixel clock because that's the fidelity at which the programmer + // places horizontal events — display start, end, sync period, etc. + uint32_t clock_divider_ = 0; + + enum class Depth { + OneBPP = 0b00, + TwoBPP = 0b01, + FourBPP = 0b10, + EightBPP = 0b11, + } colour_depth_; + + void set_clock_divider(uint32_t divider) { + if(divider == clock_divider_) { + return; + } + + clock_divider_ = divider; + const auto cycles_per_line = static_cast(24'000'000 / (divider * 312 * 50)); + crt_.set_new_timing( + cycles_per_line, + 312, /* Height of display. */ + Outputs::CRT::PAL::ColourSpace, + Outputs::CRT::PAL::ColourCycleNumerator, + Outputs::CRT::PAL::ColourCycleDenominator, + Outputs::CRT::PAL::VerticalSyncLength, + Outputs::CRT::PAL::AlternatesPhase); + clock_rate_observer_.update_clock_rates(); + } +}; + +} diff --git a/Machines/PCCompatible/PCCompatible.cpp b/Machines/PCCompatible/PCCompatible.cpp index 09f0475af..c0f17eb3f 100644 --- a/Machines/PCCompatible/PCCompatible.cpp +++ b/Machines/PCCompatible/PCCompatible.cpp @@ -1198,7 +1198,6 @@ class ConcreteMachine: using namespace PCCompatible; -// See header; constructs and returns an instance of the Amstrad CPC. std::unique_ptr Machine::PCCompatible(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { const Target *const pc_target = dynamic_cast(target); diff --git a/Machines/Utility/MachineForTarget.cpp b/Machines/Utility/MachineForTarget.cpp index 55c3c347f..98851fd15 100644 --- a/Machines/Utility/MachineForTarget.cpp +++ b/Machines/Utility/MachineForTarget.cpp @@ -11,9 +11,10 @@ #include // Sources for runtime options and machines. +#include "../Acorn/Archimedes/Archimedes.hpp" +#include "../Acorn/Electron/Electron.hpp" #include "../Amiga/Amiga.hpp" #include "../AmstradCPC/AmstradCPC.hpp" -#include "../Acorn/Electron/Electron.hpp" #include "../Apple/AppleII/AppleII.hpp" #include "../Apple/AppleIIgs/AppleIIgs.hpp" #include "../Apple/Macintosh/Macintosh.hpp" @@ -55,12 +56,12 @@ std::unique_ptr Machine::MachineForTarget(const Analyse std::unique_ptr machine; try { - // TODO: add Archimedes below. #define BindD(name, m) case Analyser::Machine::m: machine = std::make_unique>(name::Machine::m(target, rom_fetcher)); break; #define Bind(m) BindD(m, m) switch(target->machine) { Bind(Amiga) Bind(AmstradCPC) + Bind(Archimedes) BindD(Apple::II, AppleII) BindD(Apple::IIgs, AppleIIgs) BindD(Apple::Macintosh, Macintosh) diff --git a/Machines/Utility/ROMCatalogue.cpp b/Machines/Utility/ROMCatalogue.cpp index 410ac798c..9e02cbf1c 100644 --- a/Machines/Utility/ROMCatalogue.cpp +++ b/Machines/Utility/ROMCatalogue.cpp @@ -540,6 +540,15 @@ Description::Description(Name name) { *this = Description(name, "Electron", "the Electron MOS ROM v1.00", "os.rom", 16*1024, 0xbf63fb1fu); break; + case Name::AcornArthur030: + *this = Description(name, "Archimedes", "Arthur v0.30", "ROM030", 512*1024, 0x5df8ed42u); + break; + case Name::AcornRISCOS200: + *this = Description(name, "Archimedes", "RISC OS v2.00", "ROM200", 512*1024, 0x89c4ad36u); + break; + case Name::AcornRISCOS311: + *this = Description(name, "Archimedes", "RISC OS v3.11", "ROM311", 2*1024*1024, 0x54c0c963u); + break; case Name::AcornRISCOS319: *this = Description(name, "Archimedes", "RISC OS v3.19", "ROM319", 2*1024*1024, 0x00c7a3d3u); break; diff --git a/Machines/Utility/ROMCatalogue.hpp b/Machines/Utility/ROMCatalogue.hpp index a672e9522..44d9022bc 100644 --- a/Machines/Utility/ROMCatalogue.hpp +++ b/Machines/Utility/ROMCatalogue.hpp @@ -32,6 +32,9 @@ enum Name { Acorn1770DFS, // Acorn Archimedes. + AcornArthur030, + AcornRISCOS200, + AcornRISCOS311, AcornRISCOS319, // Amiga. diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index dc09a4580..4309b3eb1 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -215,6 +215,8 @@ 4B228CDB24DA41890077EF25 /* ScanTarget.metal in Sources */ = {isa = PBXBuildFile; fileRef = 4B228CDA24DA41880077EF25 /* ScanTarget.metal */; }; 4B2530F4244E6774007980BF /* fm.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B2530F3244E6773007980BF /* fm.json */; }; 4B25B5F925BD083C00362C84 /* DiskIIDrive.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B80CD6D2568A82600176FCC /* DiskIIDrive.cpp */; }; + 4B2A1CDC2BA775C5004496CE /* I2C.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A1CD92BA775C5004496CE /* I2C.cpp */; }; + 4B2A1CDD2BA775C5004496CE /* I2C.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A1CD92BA775C5004496CE /* I2C.cpp */; }; 4B2A332D1DB86821002876E3 /* OricOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B2A332B1DB86821002876E3 /* OricOptions.xib */; }; 4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; }; 4B2B3A4B1F9B8FA70062DABF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; }; @@ -966,6 +968,9 @@ 4BB505812B962DDF0031C43C /* Plus3.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB505752B962DDF0031C43C /* Plus3.cpp */; }; 4BB505822B962DDF0031C43C /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB505762B962DDF0031C43C /* Keyboard.cpp */; }; 4BB505832B962DDF0031C43C /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB505762B962DDF0031C43C /* Keyboard.cpp */; }; + 4BB505862B9634F30031C43C /* Archimedes.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB505842B9634F30031C43C /* Archimedes.cpp */; }; + 4BB505872B9634F30031C43C /* Archimedes.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB505842B9634F30031C43C /* Archimedes.cpp */; }; + 4BB505892B9C0E6F0031C43C /* Messy ARM in Resources */ = {isa = PBXBuildFile; fileRef = 4BB505882B9C0E6F0031C43C /* Messy ARM */; }; 4BB697CB1D4B6D3E00248BDF /* TimedEventLoop.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB697C91D4B6D3E00248BDF /* TimedEventLoop.cpp */; }; 4BB697CE1D4BA44400248BDF /* CommodoreGCR.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB697CC1D4BA44400248BDF /* CommodoreGCR.cpp */; }; 4BB73EA21B587A5100552FC2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EA11B587A5100552FC2 /* AppDelegate.swift */; }; @@ -1348,6 +1353,8 @@ 4B24095A1C45DF85004DA684 /* Stepper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Stepper.hpp; sourceTree = ""; }; 4B2530F3244E6773007980BF /* fm.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fm.json; sourceTree = ""; }; 4B262BFF29691F55002EC0F7 /* PersonalityTraits.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PersonalityTraits.hpp; sourceTree = ""; }; + 4B2A1CD92BA775C5004496CE /* I2C.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = I2C.cpp; sourceTree = ""; }; + 4B2A1CDA2BA775C5004496CE /* I2C.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = I2C.hpp; sourceTree = ""; }; 4B2A332C1DB86821002876E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/OricOptions.xib"; sourceTree = SOURCE_ROOT; }; 4B2A3B5A29993DFA007CE366 /* Storage.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Storage.hpp; sourceTree = ""; }; 4B2A3B5B29995FF6007CE366 /* LineBuffer.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LineBuffer.hpp; sourceTree = ""; }; @@ -1800,6 +1807,16 @@ 4BA91E1C216D85BA00F79557 /* MasterSystemVDPTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MasterSystemVDPTests.mm; sourceTree = ""; }; 4BA9C3CF1D8164A9002DDB61 /* MediaTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MediaTarget.hpp; sourceTree = ""; }; 4BAA167B21582B1D008A3276 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = ""; }; + 4BAB1E522BA9D9950002C9B9 /* Disassembler.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Disassembler.hpp; sourceTree = ""; }; + 4BAB1E532BAB5B040002C9B9 /* Sound.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Sound.hpp; sourceTree = ""; }; + 4BAB1E542BAB5B3F0002C9B9 /* Keyboard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = ""; }; + 4BAB1E552BAB5B6D0002C9B9 /* HalfDuplexSerial.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = HalfDuplexSerial.hpp; sourceTree = ""; }; + 4BAB1E562BAB5BC60002C9B9 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = ""; }; + 4BAB1E582BAB5C210002C9B9 /* MemoryController.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MemoryController.hpp; sourceTree = ""; }; + 4BAB1E592BAB5CB90002C9B9 /* CMOSRAM.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CMOSRAM.hpp; sourceTree = ""; }; + 4BAB1E5A2BAB5F400002C9B9 /* InputOutputController.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = InputOutputController.hpp; sourceTree = ""; }; + 4BAB1E5B2BAF59CB0002C9B9 /* KeyboardMapper.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = KeyboardMapper.hpp; sourceTree = ""; }; + 4BAB1E5C2BC3727C0002C9B9 /* FloppyDisc.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FloppyDisc.hpp; sourceTree = ""; }; 4BAB62AC1D3272D200DF5BA0 /* Disk.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Disk.hpp; sourceTree = ""; }; 4BAB62AE1D32730D00DF5BA0 /* Storage.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Storage.hpp; sourceTree = ""; }; 4BAF2B4C2004580C00480230 /* DMK.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DMK.cpp; sourceTree = ""; }; @@ -2102,6 +2119,9 @@ 4BB505752B962DDF0031C43C /* Plus3.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Plus3.cpp; sourceTree = ""; }; 4BB505762B962DDF0031C43C /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Keyboard.cpp; sourceTree = ""; }; 4BB505772B962DDF0031C43C /* Electron.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Electron.hpp; sourceTree = ""; }; + 4BB505842B9634F30031C43C /* Archimedes.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Archimedes.cpp; sourceTree = ""; }; + 4BB505852B9634F30031C43C /* Archimedes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Archimedes.hpp; sourceTree = ""; }; + 4BB505882B9C0E6F0031C43C /* Messy ARM */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Messy ARM"; sourceTree = ""; }; 4BB5B995281B1D3E00522DA9 /* RegisterSizes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RegisterSizes.hpp; sourceTree = ""; }; 4BB5B996281B1E3F00522DA9 /* Perform.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Perform.hpp; sourceTree = ""; }; 4BB5B997281B1F7B00522DA9 /* Status.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Status.hpp; sourceTree = ""; }; @@ -2679,6 +2699,7 @@ 4B1414631B588A1100E04248 /* Test Binaries */ = { isa = PBXGroup; children = ( + 4BB505882B9C0E6F0031C43C /* Messy ARM */, 4B680CE323A555CA00451D43 /* 68000 Comparative Tests */, 4B75F97A280D7C7700121055 /* 68000 Decoding */, 4B683B002727BE6F0043E541 /* Amiga Blitter Tests */, @@ -2765,6 +2786,7 @@ 4B2005482B92697500420C5C /* Executor.hpp */, 4B2005402B804AA300420C5C /* OperationMapper.hpp */, 4B2005462B8BD7A500420C5C /* Registers.hpp */, + 4BAB1E522BA9D9950002C9B9 /* Disassembler.hpp */, ); path = ARM; sourceTree = ""; @@ -2799,6 +2821,15 @@ path = "FM Synthesis"; sourceTree = ""; }; + 4B2A1CDB2BA775C5004496CE /* I2C */ = { + isa = PBXGroup; + children = ( + 4B2A1CD92BA775C5004496CE /* I2C.cpp */, + 4B2A1CDA2BA775C5004496CE /* I2C.hpp */, + ); + path = I2C; + sourceTree = ""; + }; 4B2A538F1D117D36003C6002 /* Audio */ = { isa = PBXGroup; children = ( @@ -4370,6 +4401,17 @@ 4BB505692B962DDF0031C43C /* Archimedes */ = { isa = PBXGroup; children = ( + 4BB505842B9634F30031C43C /* Archimedes.cpp */, + 4BB505852B9634F30031C43C /* Archimedes.hpp */, + 4BAB1E592BAB5CB90002C9B9 /* CMOSRAM.hpp */, + 4BAB1E5C2BC3727C0002C9B9 /* FloppyDisc.hpp */, + 4BAB1E552BAB5B6D0002C9B9 /* HalfDuplexSerial.hpp */, + 4BAB1E5A2BAB5F400002C9B9 /* InputOutputController.hpp */, + 4BAB1E542BAB5B3F0002C9B9 /* Keyboard.hpp */, + 4BAB1E5B2BAF59CB0002C9B9 /* KeyboardMapper.hpp */, + 4BAB1E582BAB5C210002C9B9 /* MemoryController.hpp */, + 4BAB1E532BAB5B040002C9B9 /* Sound.hpp */, + 4BAB1E562BAB5BC60002C9B9 /* Video.hpp */, ); path = Archimedes; sourceTree = ""; @@ -4377,19 +4419,19 @@ 4BB5056A2B962DDF0031C43C /* Electron */ = { isa = PBXGroup; children = ( - 4BB5056B2B962DDF0031C43C /* SoundGenerator.cpp */, - 4BB5056C2B962DDF0031C43C /* Plus3.hpp */, - 4BB5056D2B962DDF0031C43C /* Keyboard.hpp */, 4BB5056E2B962DDF0031C43C /* Electron.cpp */, - 4BB5056F2B962DDF0031C43C /* Video.cpp */, - 4BB505702B962DDF0031C43C /* Tape.hpp */, - 4BB505712B962DDF0031C43C /* Interrupts.hpp */, - 4BB505722B962DDF0031C43C /* Video.hpp */, - 4BB505732B962DDF0031C43C /* Tape.cpp */, - 4BB505742B962DDF0031C43C /* SoundGenerator.hpp */, - 4BB505752B962DDF0031C43C /* Plus3.cpp */, 4BB505762B962DDF0031C43C /* Keyboard.cpp */, + 4BB505752B962DDF0031C43C /* Plus3.cpp */, + 4BB5056B2B962DDF0031C43C /* SoundGenerator.cpp */, + 4BB505732B962DDF0031C43C /* Tape.cpp */, + 4BB5056F2B962DDF0031C43C /* Video.cpp */, 4BB505772B962DDF0031C43C /* Electron.hpp */, + 4BB505712B962DDF0031C43C /* Interrupts.hpp */, + 4BB5056D2B962DDF0031C43C /* Keyboard.hpp */, + 4BB5056C2B962DDF0031C43C /* Plus3.hpp */, + 4BB505742B962DDF0031C43C /* SoundGenerator.hpp */, + 4BB505702B962DDF0031C43C /* Tape.hpp */, + 4BB505722B962DDF0031C43C /* Video.hpp */, ); path = Electron; sourceTree = ""; @@ -4812,6 +4854,7 @@ 4B595FAA2086DFBA0083CAA8 /* AudioToggle */, 4B4A762D1DB1A35C007AAE2E /* AY38910 */, 4B302181208A550100773308 /* DiskII */, + 4B2A1CDB2BA775C5004496CE /* I2C */, 4B4B1A39200198C900A0F866 /* KonamiSCC */, 4BC23A212467600E001A6030 /* OPx */, 4BF0BC6E2973318E00CCA2B5 /* RP5C01 */, @@ -5566,6 +5609,7 @@ 4BB299B51B587D8400A49093 /* rolzx in Resources */, 4BB299DD1B587D8400A49093 /* stxa in Resources */, 4BB299051B587D8400A49093 /* arrb in Resources */, + 4BB505892B9C0E6F0031C43C /* Messy ARM in Resources */, 4BB299DC1B587D8400A49093 /* stazx in Resources */, 4B670A9D2401CB8400D4E002 /* z80ccf.tap in Resources */, 4B4F47652533EA64004245B8 /* suite-a.prg in Resources */, @@ -5864,6 +5908,7 @@ 4B055A9E1FAE85DA0060FFFF /* G64.cpp in Sources */, 4B055AB81FAE860F0060FFFF /* ZX80O81P.cpp in Sources */, 4B055AB01FAE86070060FFFF /* PulseQueuedTape.cpp in Sources */, + 4B2A1CDD2BA775C5004496CE /* I2C.cpp in Sources */, 4B0F1C1D2604EA1000B85C66 /* Keyboard.cpp in Sources */, 4B055AAC1FAE85FD0060FFFF /* PCMSegment.cpp in Sources */, 4BB307BC235001C300457D33 /* 6850.cpp in Sources */, @@ -5953,6 +5998,7 @@ 4BEDA40E25B2844B000C2DBD /* Decoder.cpp in Sources */, 4B1B88BD202E3D3D00B67DFF /* MultiMachine.cpp in Sources */, 4B055A971FAE85BB0060FFFF /* ZX8081.cpp in Sources */, + 4BB505872B9634F30031C43C /* Archimedes.cpp in Sources */, 4B055AAD1FAE85FD0060FFFF /* PCMTrack.cpp in Sources */, 4B2130E3273A7A0A008A77B4 /* Audio.cpp in Sources */, 4BD67DD1209BF27B00AB2146 /* Encoder.cpp in Sources */, @@ -6058,6 +6104,7 @@ 4B4518A11F75FD1C00926311 /* D64.cpp in Sources */, 4BCE0052227CE8CA000CA200 /* DiskIICard.cpp in Sources */, 4BF0BC68297108D600CCA2B5 /* MemorySlotHandler.cpp in Sources */, + 4B2A1CDC2BA775C5004496CE /* I2C.cpp in Sources */, 4BCF1FA41DADC3DD0039D2E7 /* Oric.cpp in Sources */, 4BD67DCB209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */, 4BB4BFB922A4372F0069048D /* StaticAnalyser.cpp in Sources */, @@ -6143,6 +6190,7 @@ 4B4518831F75E91A00926311 /* PCMTrack.cpp in Sources */, 4B8DF4F9254E36AE00F3433C /* Video.cpp in Sources */, 4B0ACC3223775819008902D0 /* Atari2600.cpp in Sources */, + 4BB505862B9634F30031C43C /* Archimedes.cpp in Sources */, 4B7C681E2751A104001671EC /* Bitplanes.cpp in Sources */, 4B45189F1F75FD1C00926311 /* AcornADF.cpp in Sources */, 4B7BA03023C2B19C00B98D9E /* Jasmin.cpp in Sources */, diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme index 474fa352e..19b54b90e 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme +++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme @@ -62,7 +62,7 @@ +#include "../../../InstructionSets/ARM/Disassembler.hpp" #include "../../../InstructionSets/ARM/Executor.hpp" -#include "CSROMFetcher.hpp" + +#include "NSData+dataWithContentsOfGZippedFile.h" + +#include +#include using namespace InstructionSet::ARM; namespace { -struct Memory { - std::vector rom; - +struct MemoryLedger { template - bool write(uint32_t address, IntT source, Mode mode, bool trans) { - (void)mode; - (void)trans; + bool write(uint32_t address, IntT source, Mode, bool) { + const auto is_faulty = [&](uint32_t address) -> bool { + return + write_pointer == writes.size() || + writes[write_pointer].size != sizeof(IntT) || + writes[write_pointer].address != address || + writes[write_pointer].value != source; + }; - printf("W of %08x to %08x [%lu]\n", source, address, sizeof(IntT)); - - if(has_moved_rom_ && address < ram_.size()) { - *reinterpret_cast(&ram_[address]) = source; + // The test set sometimes worries about write alignment, sometimes not... + if(is_faulty(address) && is_faulty(address & static_cast(~3))) { + return false; } - + ++write_pointer; return true; } template - bool read(uint32_t address, IntT &source, Mode mode, bool trans) { - (void)mode; - (void)trans; + bool read(uint32_t address, IntT &source, Mode, bool) { + const auto is_faulty = [&](uint32_t address) -> bool { + return + read_pointer == reads.size() || + reads[read_pointer].size != sizeof(IntT) || + reads[read_pointer].address != address; + }; - if(address >= 0x3800000) { - has_moved_rom_ = true; - source = *reinterpret_cast(&rom[address - 0x3800000]); - } else if(!has_moved_rom_) { - // TODO: this is true only very transiently. - source = *reinterpret_cast(&rom[address]); - } else if(address < ram_.size()) { - source = *reinterpret_cast(&ram_[address]); - } else { - source = 0; - printf("Unknown read from %08x [%lu]\n", address, sizeof(IntT)); + // As per writes; the test set sometimes forces alignment on the record, sometimes not... + if(is_faulty(address) && is_faulty(address & static_cast(~3))) { + return false; } - + source = reads[read_pointer].value; + ++read_pointer; return true; } - private: - bool has_moved_rom_ = false; - std::array ram_{}; + struct Access { + size_t size; + uint32_t address; + uint32_t value; + }; + + template + void add_access(bool is_read, uint32_t address, IntT value) { + auto &read = is_read ? reads.emplace_back() : writes.emplace_back(); + read.address = address; + read.value = value; + read.size = sizeof(IntT); + } + + std::vector reads; + std::vector writes; + size_t read_pointer = 0; + size_t write_pointer = 0; }; } @@ -71,19 +90,19 @@ struct Memory { // Test a shift by 1 into carry. value = 0x8000'0000; - shift(value, 1, carry); + shift(value, 1, carry); XCTAssertEqual(value, 0); XCTAssertNotEqual(carry, 0); // Test a shift by 18 into carry. value = 0x0000'4001; - shift(value, 18, carry); + shift(value, 18, carry); XCTAssertEqual(value, 0x4'0000); XCTAssertNotEqual(carry, 0); // Test a shift by 17, not generating carry. value = 0x0000'4001; - shift(value, 17, carry); + shift(value, 17, carry); XCTAssertEqual(value, 0x8002'0000); XCTAssertEqual(carry, 0); } @@ -94,40 +113,40 @@ struct Memory { // Test successive shifts by 4; one generating carry and one not. value = 0x12345678; - shift(value, 4, carry); + shift(value, 4, carry); XCTAssertEqual(value, 0x1234567); XCTAssertNotEqual(carry, 0); - shift(value, 4, carry); + shift(value, 4, carry); XCTAssertEqual(value, 0x123456); XCTAssertEqual(carry, 0); // Test shift by 1. value = 0x8003001; - shift(value, 1, carry); + shift(value, 1, carry); XCTAssertEqual(value, 0x4001800); XCTAssertNotEqual(carry, 0); // Test a shift by greater than 32. value = 0xffff'ffff; - shift(value, 33, carry); + shift(value, 33, carry); XCTAssertEqual(value, 0); XCTAssertEqual(carry, 0); // Test shifts by 32: result is always 0, carry is whatever was in bit 31. value = 0xffff'ffff; - shift(value, 32, carry); + shift(value, 32, carry); XCTAssertEqual(value, 0); XCTAssertNotEqual(carry, 0); value = 0x7fff'ffff; - shift(value, 32, carry); + shift(value, 32, carry); XCTAssertEqual(value, 0); XCTAssertEqual(carry, 0); // Test that a logical right shift by 0 is the same as a shift by 32. value = 0xffff'ffff; - shift(value, 0, carry); + shift(value, 0, carry); XCTAssertEqual(value, 0); XCTAssertNotEqual(carry, 0); } @@ -138,31 +157,31 @@ struct Memory { // Test a short negative shift. value = 0x8000'0030; - shift(value, 1, carry); + shift(value, 1, carry); XCTAssertEqual(value, 0xc000'0018); XCTAssertEqual(carry, 0); // Test a medium negative shift without carry. value = 0xffff'0000; - shift(value, 11, carry); + shift(value, 11, carry); XCTAssertEqual(value, 0xffff'ffe0); XCTAssertEqual(carry, 0); // Test a medium negative shift with carry. value = 0xffc0'0000; - shift(value, 23, carry); + shift(value, 23, carry); XCTAssertEqual(value, 0xffff'ffff); XCTAssertNotEqual(carry, 0); // Test a long negative shift. value = 0x8000'0000; - shift(value, 32, carry); + shift(value, 32, carry); XCTAssertEqual(value, 0xffff'ffff); XCTAssertNotEqual(carry, 0); // Test a positive shift. value = 0x0123'0031; - shift(value, 3, carry); + shift(value, 3, carry); XCTAssertEqual(value, 0x24'6006); XCTAssertEqual(carry, 0); } @@ -173,63 +192,355 @@ struct Memory { // Test a short rotate by one hex digit. value = 0xabcd'1234; - shift(value, 4, carry); + shift(value, 4, carry); XCTAssertEqual(value, 0x4abc'd123); XCTAssertEqual(carry, 0); // Test a longer rotate, with carry. value = 0xa5f9'6342; - shift(value, 17, carry); + shift(value, 17, carry); XCTAssertEqual(value, 0xb1a1'52fc); XCTAssertNotEqual(carry, 0); // Test a rotate by 32 without carry. value = 0x385f'7dce; - shift(value, 32, carry); + shift(value, 32, carry); XCTAssertEqual(value, 0x385f'7dce); XCTAssertEqual(carry, 0); // Test a rotate by 32 with carry. value = 0xfecd'ba12; - shift(value, 32, carry); + shift(value, 32, carry); XCTAssertEqual(value, 0xfecd'ba12); XCTAssertNotEqual(carry, 0); // Test a rotate through carry, carry not set. value = 0x123f'abcf; carry = 0; - shift(value, 0, carry); + shift(value, 0, carry); XCTAssertEqual(value, 0x091f'd5e7); XCTAssertNotEqual(carry, 0); // Test a rotate through carry, carry set. value = 0x123f'abce; carry = 1; - shift(value, 0, carry); + shift(value, 0, carry); XCTAssertEqual(value, 0x891f'd5e7); XCTAssertEqual(carry, 0); } -// TODO: turn the below into a trace-driven test case. -//- (void)testROM319 { -// constexpr ROM::Name rom_name = ROM::Name::AcornRISCOS319; -// ROM::Request request(rom_name); -// const auto roms = CSROMFetcher()(request); -// -// auto executor = std::make_unique>(); -// executor->bus.rom = roms.find(rom_name)->second; -// -// for(int c = 0; c < 1000; c++) { -// uint32_t instruction; -// executor->bus.read(executor->pc(), instruction, executor->registers().mode(), false); -// -// printf("%08x: %08x [", executor->pc(), instruction); -// for(int c = 0; c < 15; c++) { -// printf("r%d:%08x ", c, executor->registers()[c]); -// } -// printf("psr:%08x]\n", executor->registers().status()); -// execute(instruction, *executor); -// } -//} +- (void)testRegisterModes { + Registers r; + + // Set all user mode registers to their indices. + r.set_mode(Mode::User); + for(int c = 0; c < 15; c++) { + r[c] = c; + } + + // Set FIQ registers. + r.set_mode(Mode::FIQ); + for(int c = 8; c < 15; c++) { + r[c] = c | 0x100; + } + + // Set IRQ registers. + r.set_mode(Mode::IRQ); + for(int c = 13; c < 15; c++) { + r[c] = c | 0x200; + } + + // Set supervisor registers. + r.set_mode(Mode::FIQ); + r.set_mode(Mode::User); + r.set_mode(Mode::Supervisor); + for(int c = 13; c < 15; c++) { + r[c] = c | 0x300; + } + + // Check all results. + r.set_mode(Mode::User); + r.set_mode(Mode::FIQ); + for(int c = 0; c < 8; c++) { + XCTAssertEqual(r[c], c); + } + for(int c = 8; c < 15; c++) { + XCTAssertEqual(r[c], c | 0x100); + } + + r.set_mode(Mode::FIQ); + r.set_mode(Mode::IRQ); + r.set_mode(Mode::User); + r.set_mode(Mode::FIQ); + r.set_mode(Mode::Supervisor); + for(int c = 0; c < 13; c++) { + XCTAssertEqual(r[c], c); + } + for(int c = 13; c < 15; c++) { + XCTAssertEqual(r[c], c | 0x300); + } + + r.set_mode(Mode::FIQ); + r.set_mode(Mode::User); + for(int c = 0; c < 15; c++) { + XCTAssertEqual(r[c], c); + } + + r.set_mode(Mode::Supervisor); + r.set_mode(Mode::IRQ); + for(int c = 0; c < 13; c++) { + XCTAssertEqual(r[c], c); + } + for(int c = 13; c < 15; c++) { + XCTAssertEqual(r[c], c | 0x200); + } +} + +- (void)testFlags { + Registers regs; + + for(int c = 0; c < 256; c++) { + regs.set_mode(Mode::Supervisor); + + const uint32_t status = ((c & 0xfc) << 26) | (c & 0x03); + regs.set_status(status); + XCTAssertEqual(status, regs.status()); + } +} + +- (void)testMessy { + NSData *const tests = + [NSData dataWithContentsOfGZippedFile: + [[NSBundle bundleForClass:[self class]] + pathForResource:@"test" + ofType:@"txt.gz" + inDirectory:@"Messy ARM"] + ]; + const std::string text((char *)tests.bytes); + std::istringstream input(text); + + input >> std::hex; + + static constexpr auto model = Model::ARMv2with32bitAddressing; + using Exec = Executor; + std::unique_ptr test; + + struct FailureRecord { + int count = 0; + int first = 0; + NSString *sample; + }; + std::map failures; + + uint32_t opcode = 0; + bool ignore_opcode = false; + uint32_t masks[16]; + uint32_t test_pc_offset = 8; + + int test_count = 0; + while(!input.eof()) { + std::string label; + input >> label; + + if(label == "**") { + memset(masks, 0xff, sizeof(masks)); + ignore_opcode = false; + test_pc_offset = 8; + + input >> opcode; + test_count = 0; + + InstructionSet::ARM::Disassembler disassembler; + InstructionSet::ARM::dispatch(opcode, disassembler); + static constexpr uint32_t pc_address_mask = 0x03ff'fffc; + + const auto instruction = disassembler.last(); + switch(instruction.operation) { + case Instruction::Operation::BL: + // Tests don't multiplex flags into PC for storage to R14. + masks[14] = pc_address_mask; + break; + + case Instruction::Operation::SWI: + // Tested CPU either doesn't switch into supervisor mode, or + // is sufficiently accurate in its pipeline that register + // changes haven't happened yet. + ignore_opcode = true; + break; + + case Instruction::Operation::MOV: + // MOV from PC will pick up the address only in the test cases. + if( + instruction.operand2.type == Operand::Type::Register && + instruction.operand2.value == 15 + ) { + masks[instruction.destination.value] = pc_address_mask; + } + + // MOV to PC; there are both pipeline capture errors in the test + // set and its ARM won't change privilege level on a write to R15. + // Similarly, if the PC is operand 2 then it'll also contain the + // PSR on an ARM2 but not in the test set. + if(instruction.destination.value == 15 || instruction.operand2.value == 15) { + ignore_opcode = true; + } + break; + + case Instruction::Operation::TEQ: + case Instruction::Operation::TST: + case Instruction::Operation::ORR: + case Instruction::Operation::BIC: + case Instruction::Operation::SUB: + case Instruction::Operation::ADD: + // Routinely used to change privilege level on an ARM2 but + // doesn't seem to have that effect on the ARM used to generate + // the test set. + if(instruction.destination.value == 15 || instruction.operand2.value == 15) { + ignore_opcode = true; + } + break; + + case Instruction::Operation::STR: + case Instruction::Operation::LDR: + // Neither loads nor stores with R15 are matched to ARM2 behaviour by the test source. + ignore_opcode = instruction.destination.value == 15; + break; + + case Instruction::Operation::STM: + case Instruction::Operation::LDM: + // If the PC is involved, just skip the test; PC/PSR differences abound. + ignore_opcode = instruction.operand1.value & (1 << 15); + break; + + case Instruction::Operation::MCR: + case Instruction::Operation::MRC: + // The test case doesn't seem to throw on a missing coprocessor. + ignore_opcode = true; + break; + + default: break; + } + + continue; + } + + if(ignore_opcode) continue; + + if(label == "Before:" || label == "After:") { + // Read register state. + uint32_t regs[16]; + for(int c = 0; c < 16; c++) { + input >> regs[c]; + } + + if(!test) test = std::make_unique(); + auto ®isters = test->registers(); + if(label == "Before:") { + // This is the start of a new test. + + // Establish implicit register values. + for(uint32_t c = 0; c < 15; c++) { + registers.set_mode(Mode::FIQ); + registers[c] = 0x200 | c; + + registers.set_mode(Mode::Supervisor); + registers[c] = 0x300 | c; + + registers.set_mode(Mode::IRQ); + registers[c] = 0x400 | c; + + registers.set_mode(Mode::User); + registers[c] = 0x100 | c; + } + + // Apply provided state. + registers.set_mode(Mode::Supervisor); // To make sure the actual mode is applied. + registers.set_pc(regs[15] - 8); + registers.set_status(regs[15]); + for(uint32_t c = 0; c < 15; c++) { + registers[c] = regs[c]; + } + } else { + // Execute test and compare. + ++test_count; + + if(opcode == 0xe1a0ae2f && test_count == 2) { + printf(""); + } + + execute(opcode, *test); + NSMutableString *error = nil; + + for(uint32_t c = 0; c < 15; c++) { + if((regs[c] & masks[c]) != (registers[c] & masks[c])) { + if(!error) error = [[NSMutableString alloc] init]; else [error appendString:@"; "]; + [error appendFormat:@"R%d %08x v %08x", c, regs[c], registers[c]]; + } + } + if((regs[15] & masks[15]) != (registers.pc_status(test_pc_offset) & masks[15])) { + if(!error) error = [[NSMutableString alloc] init]; else [error appendString:@"; "]; + [error appendFormat:@"; PC/PSR %08x/%08x v %08x/%08x", + regs[15] & 0x03ff'fffc, regs[15] & ~0x03ff'fffc, + registers.pc(test_pc_offset), registers.status()]; + } + + if(error) { + ++failures[opcode].count; + if(failures[opcode].count == 1) { + failures[opcode].first = test_count; + failures[opcode].sample = error; + } + } + + test.reset(); + } + continue; + } + + // TODO: supply information below to ledger, and then use and test it. + + uint32_t address; + uint32_t value; + input >> address >> value; + + if(label == "r.b") { + // Capture a byte read for provision. + test->bus.add_access(true, address, value); + continue; + } + + if(label == "r.w") { + // Capture a word read for provision. + test->bus.add_access(true, address, value); + continue; + } + + if(label == "w.b") { + // Capture a byte write for comparison. + test->bus.add_access(false, address, value); + continue; + } + + if(label == "w.w") { + // Capture a word write for comparison. + test->bus.add_access(false, address, value); + continue; + } + } + + XCTAssertTrue(failures.empty()); + + if(!failures.empty()) { + NSLog(@"Failed %zu instructions; examples below", failures.size()); + for(const auto &pair: failures) { + NSLog(@"%08x, %d total, test %d: %@", pair.first, pair.second.count, pair.second.first, pair.second.sample); + } + } + + + for(const auto &pair: failures) { + printf("%08x ", pair.first); + } +} @end diff --git a/OSBindings/Mac/Clock SignalTests/Messy ARM/test.txt.gz b/OSBindings/Mac/Clock SignalTests/Messy ARM/test.txt.gz new file mode 100644 index 000000000..d1aeabcbd Binary files /dev/null and b/OSBindings/Mac/Clock SignalTests/Messy ARM/test.txt.gz differ diff --git a/OSBindings/Qt/clksignal.pro b/OSBindings/Qt/clksignal.pro index e88990d12..93ed5b54d 100644 --- a/OSBindings/Qt/clksignal.pro +++ b/OSBindings/Qt/clksignal.pro @@ -71,11 +71,12 @@ SOURCES += \ $$SRC/Components/AudioToggle/*.cpp \ $$SRC/Components/AY38910/*.cpp \ $$SRC/Components/DiskII/*.cpp \ + $$SRC/Components/I2C/*.cpp \ $$SRC/Components/KonamiSCC/*.cpp \ $$SRC/Components/OPx/*.cpp \ - $$SRC/Components/SN76489/*.cpp \ - $$SRC/Components/Serial/*.cpp \ $$SRC/Components/RP5C01/*.cpp \ + $$SRC/Components/Serial/*.cpp \ + $$SRC/Components/SN76489/*.cpp \ \ $$SRC/Inputs/*.cpp \ \ @@ -85,6 +86,7 @@ SOURCES += \ $$SRC/InstructionSets/x86/*.cpp \ \ $$SRC/Machines/*.cpp \ + $$SRC/Machines/Acorn/Archimedes/*.cpp \ $$SRC/Machines/Acorn/Electron/*.cpp \ $$SRC/Machines/Amiga/*.cpp \ $$SRC/Machines/AmstradCPC/*.cpp \ @@ -201,12 +203,13 @@ HEADERS += \ $$SRC/Components/AudioToggle/*.hpp \ $$SRC/Components/AY38910/*.hpp \ $$SRC/Components/DiskII/*.hpp \ + $$SRC/Components/I2C/*.hpp \ $$SRC/Components/KonamiSCC/*.hpp \ $$SRC/Components/OPx/*.hpp \ $$SRC/Components/OPx/Implementation/*.hpp \ + $$SRC/Components/RP5C01/*.hpp \ $$SRC/Components/Serial/*.hpp \ $$SRC/Components/SN76489/*.hpp \ - $$SRC/Components/RP5C01/*.hpp \ \ $$SRC/Concurrency/*.hpp \ \ diff --git a/OSBindings/SDL/SConstruct b/OSBindings/SDL/SConstruct index 73d5efc7e..3e4cb0f9b 100644 --- a/OSBindings/SDL/SConstruct +++ b/OSBindings/SDL/SConstruct @@ -56,6 +56,7 @@ SOURCES += glob.glob('../../Components/9918/Implementation/*.cpp') SOURCES += glob.glob('../../Components/AudioToggle/*.cpp') SOURCES += glob.glob('../../Components/AY38910/*.cpp') SOURCES += glob.glob('../../Components/DiskII/*.cpp') +SOURCES += glob.glob('../../Components/I2C/*.cpp') SOURCES += glob.glob('../../Components/KonamiSCC/*.cpp') SOURCES += glob.glob('../../Components/OPx/*.cpp') SOURCES += glob.glob('../../Components/RP5C01/*.cpp') @@ -72,6 +73,7 @@ SOURCES += glob.glob('../../InstructionSets/PowerPC/*.cpp') SOURCES += glob.glob('../../InstructionSets/x86/*.cpp') SOURCES += glob.glob('../../Machines/*.cpp') +SOURCES += glob.glob('../../Machines/Acorn/Archimedes/*.cpp') SOURCES += glob.glob('../../Machines/Acorn/Electron/*.cpp') SOURCES += glob.glob('../../Machines/Amiga/*.cpp') SOURCES += glob.glob('../../Machines/AmstradCPC/*.cpp') diff --git a/Outputs/CRT/CRT.cpp b/Outputs/CRT/CRT.cpp index 85e812bbb..ad77f5e2f 100644 --- a/Outputs/CRT/CRT.cpp +++ b/Outputs/CRT/CRT.cpp @@ -58,6 +58,7 @@ void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Di vertical_flywheel_output_divider_ = (real_clock_scan_period + 65534) / 65535; // Communicate relevant fields to the scan target. + scan_target_modals_.cycles_per_line = cycles_per_line; scan_target_modals_.output_scale.x = uint16_t(horizontal_flywheel_->get_scan_period()); scan_target_modals_.output_scale.y = uint16_t(real_clock_scan_period / vertical_flywheel_output_divider_); scan_target_modals_.expected_vertical_lines = height_of_display; @@ -117,13 +118,26 @@ void CRT::set_new_display_type(int cycles_per_line, Outputs::Display::Type displ case Outputs::Display::Type::PAL50: case Outputs::Display::Type::PAL60: scan_target_modals_.intended_gamma = 2.8f; - set_new_timing(cycles_per_line, (displayType == Outputs::Display::Type::PAL50) ? 312 : 262, Outputs::Display::ColourSpace::YUV, 709379, 2500, 5, true); - // i.e. 283.7516 colour cycles per line; 2.5 lines = vertical sync. + set_new_timing( + cycles_per_line, + (displayType == Outputs::Display::Type::PAL50) ? 312 : 262, + PAL::ColourSpace, + PAL::ColourCycleNumerator, + PAL::ColourCycleDenominator, + PAL::VerticalSyncLength, + PAL::AlternatesPhase); break; case Outputs::Display::Type::NTSC60: scan_target_modals_.intended_gamma = 2.2f; - set_new_timing(cycles_per_line, 262, Outputs::Display::ColourSpace::YIQ, 455, 2, 6, false); // i.e. 227.5 colour cycles per line, 3 lines = vertical sync. + set_new_timing( + cycles_per_line, + 262, + NTSC::ColourSpace, + NTSC::ColourCycleNumerator, + NTSC::ColourCycleDenominator, + NTSC::VerticalSyncLength, + NTSC::AlternatesPhase); break; } } @@ -150,7 +164,6 @@ CRT::CRT( int cycles_per_line, bool should_alternate, Outputs::Display::InputDataType data_type) { scan_target_modals_.input_data_type = data_type; - scan_target_modals_.cycles_per_line = cycles_per_line; scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor; set_new_timing(cycles_per_line, height_of_display, colour_space, colour_cycle_numerator, colour_cycle_denominator, vertical_sync_half_lines, should_alternate); } @@ -160,7 +173,6 @@ CRT::CRT( int cycles_per_line, Outputs::Display::Type display_type, Outputs::Display::InputDataType data_type) { scan_target_modals_.input_data_type = data_type; - scan_target_modals_.cycles_per_line = cycles_per_line; scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor; set_new_display_type(cycles_per_line, display_type); } @@ -171,11 +183,13 @@ CRT::CRT(int cycles_per_line, int vertical_sync_half_lines, Outputs::Display::InputDataType data_type) { scan_target_modals_.input_data_type = data_type; - scan_target_modals_.cycles_per_line = cycles_per_line; scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor; set_new_timing(cycles_per_line, height_of_display, Outputs::Display::ColourSpace::YIQ, 1, 1, vertical_sync_half_lines, false); } +// Use some from-thin-air arbitrary constants for default timing, otherwise passing +// construction off to one of the other constructors. +CRT::CRT(Outputs::Display::InputDataType data_type) : CRT(100, 1, 100, 1, data_type) {} // MARK: - Sync loop diff --git a/Outputs/CRT/CRT.hpp b/Outputs/CRT/CRT.hpp index d0d8ac7fc..eb69fa3ff 100644 --- a/Outputs/CRT/CRT.hpp +++ b/Outputs/CRT/CRT.hpp @@ -18,6 +18,30 @@ namespace Outputs::CRT { +namespace PAL { + +// 283.7516 colour cycles per line; 2.5 lines of vertical sync. +static constexpr int ColourCycleNumerator = 709379; +static constexpr int ColourCycleDenominator = 2500; +static constexpr int VerticalSyncLength = 5; + +static constexpr auto ColourSpace = Outputs::Display::ColourSpace::YUV; +static constexpr bool AlternatesPhase = true; + +} + +namespace NTSC { + +// 227.5 colour cycles per line; 3 lines of vertical sync. +static constexpr int ColourCycleNumerator = 455; +static constexpr int ColourCycleDenominator = 2; +static constexpr int VerticalSyncLength = 6; + +static constexpr auto ColourSpace = Outputs::Display::ColourSpace::YIQ; +static constexpr bool AlternatesPhase = false; + +} + class CRT; class Delegate { @@ -136,6 +160,11 @@ class CRT { Outputs::Display::Type display_type, Outputs::Display::InputDataType data_type); + /*! Constructs a CRT with no guaranteed expectations as to input signal other than data type; + this allows for callers that intend to rely on @c set_new_timing. + */ + CRT(Outputs::Display::InputDataType data_type); + /*! Resets the CRT with new timing information. The CRT then continues as though the new timing had been provided at construction. */ void set_new_timing( diff --git a/Outputs/Log.hpp b/Outputs/Log.hpp index 7ccef5756..5832b696f 100644 --- a/Outputs/Log.hpp +++ b/Outputs/Log.hpp @@ -24,14 +24,21 @@ enum class Source { AmigaChipset, AmigaBlitter, AppleIISCSICard, + Archimedes, + ARMIOC, + ARMMEMC, + ARMVIDC, AtariST, AtariSTDMAController, CommodoreStaticAnalyser, + CMOSRTC, DirectAccessDevice, Enterprise, i8272, - IntelligentKeyboard, + I2C, + IntelligentKeyboard, // Could probably be subsumed into 'Keyboard'? IWM, + Keyboard, M50740, Macintosh, MasterSystem, @@ -72,6 +79,8 @@ constexpr bool is_enabled(Source source) { case Source::NCR5380: case Source::SCC: case Source::SCSI: + case Source::I2C: + case Source::Keyboard: return false; } } @@ -87,14 +96,21 @@ constexpr const char *prefix(Source source) { case Source::AmigaCopper: return "Copper"; case Source::AmigaDisk: return "Disk"; case Source::AppleIISCSICard: return "SCSI card"; + case Source::Archimedes: return "Archimedes"; + case Source::ARMIOC: return "IOC"; + case Source::ARMMEMC: return "MEMC"; + case Source::ARMVIDC: return "VIDC"; case Source::AtariST: return "AtariST"; case Source::AtariSTDMAController: return "DMA"; case Source::CommodoreStaticAnalyser: return "Commodore Static Analyser"; + case Source::CMOSRTC: return "CMOSRTC"; case Source::DirectAccessDevice: return "Direct Access Device"; case Source::Enterprise: return "Enterprise"; case Source::i8272: return "i8272"; + case Source::I2C: return "I2C"; case Source::IntelligentKeyboard: return "IKYB"; case Source::IWM: return "IWM"; + case Source::Keyboard: return "Keyboard"; case Source::M50740: return "M50740"; case Source::Macintosh: return "Macintosh"; case Source::MasterSystem: return "SMS"; diff --git a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp index 4b3d5d16e..c38df212a 100644 --- a/Outputs/Speaker/Implementation/LowpassSpeaker.hpp +++ b/Outputs/Speaker/Implementation/LowpassSpeaker.hpp @@ -300,8 +300,8 @@ template class PushLowpass: public LowpassBase scale_ = 65536; - int get_scale() { - return scale_; + int get_scale() const { + return scale_.load(std::memory_order::memory_order_relaxed); } const int16_t *buffer_ = nullptr; diff --git a/Outputs/Speaker/Speaker.hpp b/Outputs/Speaker/Speaker.hpp index 1cb780d85..c2592c547 100644 --- a/Outputs/Speaker/Speaker.hpp +++ b/Outputs/Speaker/Speaker.hpp @@ -30,6 +30,13 @@ struct StereoSample { StereoSample(MonoSample value) { left = right = value; } + StereoSample(int16_t left, int16_t right) : left(left), right(right) {} + + StereoSample &operator =(const StereoSample &rhs) { + left = rhs.left; + right = rhs.right; + return *this; + } StereoSample &operator +=(const StereoSample &rhs) { left += rhs.left; diff --git a/Processors/Z80/Implementation/Z80Implementation.hpp b/Processors/Z80/Implementation/Z80Implementation.hpp index 463304e72..0afd1cd3f 100644 --- a/Processors/Z80/Implementation/Z80Implementation.hpp +++ b/Processors/Z80/Implementation/Z80Implementation.hpp @@ -947,7 +947,6 @@ template < class T, target.instructions.resize(256, nullptr); // Copy in all programs, recording where they go. - std::size_t destination = 0; for(std::size_t c = 0; c < 256; c++) { operation_indices.push_back(target.all_operations.size()); for(std::size_t t = 0; t < lengths[c];) { @@ -974,7 +973,6 @@ template < class T, } } target.all_operations.emplace_back(table[c][t]); - destination++; t++; } } diff --git a/README.md b/README.md index e9664d214..a7e87071e 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ It currently contains emulations of the: Also present but very much upcoming are the: +* Acorn Archimedes; * Commodore Amiga; and * early PC compatible. @@ -59,19 +60,48 @@ Similar effort is put into audio generation. If the real machine normally genera ### Samples -| 1:1 Pixel Copying | Composite Decoded | -|---|---| -|![The Electron start screen, with a classic 1:1 pixel emulation](READMEImages/NaiveElectron.png)|![The Electron start screen, decoded from an interlaced composite feed](READMEImages/CompositeElectron.png)| -|![Repton 3 in game, with a classic 1:1 pixel emulation](READMEImages/NaiveRepton3.png)|![Repton 3 in game, decoded from an interlaced composite feed](READMEImages/CompositeRepton3.png)| -|![Stormlord with a classic 1:1 pixel emulation](READMEImages/NaiveStormlord.png)|![Stormlord decoded from a composite feed](READMEImages/CompositeStormlord.png)| -|![Road Fighter with a classic 1:1 pixel emulation](READMEImages/NaiveRoadFighter.png)|![Road Fighter decoded from a composite feed](READMEImages/CompositeRoadFighter.png)| -|![A segment of the ColecoVision Donkey Kong title screen with a classic 1:1 pixel emulation](READMEImages/NaivePresentsDonkeyKong.png)|![A segment of the ColecoVision Donkey Kong title screen decoded from a composite feed](READMEImages/CompositePresentsDonkeyKong.png)| -|![Sonic the Hedgehog with a classic 1:1 pixel emulation](READMEImages/NaiveSonic.jpeg)|![Sonic the Hedgehog screen PAL decoded from a composite feed](READMEImages/CompositeSonic.png)| + + + + + + + + + + + + + + + + + + + + + + + + + +
1:1 Pixel CopyingComposite Decoded
The Electron start screen, with a classic 1:1 pixel emulationThe Electron start screen, decoded from an interlaced composite feed
Repton 3 in game, with a classic 1:1 pixel emulationRepton 3 in game, decoded from an interlaced composite feed
Stormlord with a classic 1:1 pixel emulationStormlord decoded from a composite feed
Road Fighter with a classic 1:1 pixel emulationRoad Fighter decoded from a composite feed
A segment of the ColecoVision Donkey Kong title screen with a classic 1:1 pixel emulationA segment of the ColecoVision Donkey Kong title screen decoded from a composite feed
Sonic the Hedgehog with a classic 1:1 pixel emulationSonic the Hedgehog screen PAL decoded from a composite feed
+ + + + + + + + + + + + + + +
1:1 Pixel CopyingCorrect Aspect Ratio, Filtered
Amstrad text, with a classic 1:1 pixel emulationAmstrad text, with correct aspect ratio and subject to a lowpass filter
The Amstrad CPC version of Stormlord, with a classic 1:1 pixel emulationThe Amstrad CPC version of Stormlord, with correct aspect ratio and subject to a lowpass filter
-| 1:1 Pixel Copying | Correct Aspect Ratio, Filtered | -|---|---| -|![Amstrad text, with a classic 1:1 pixel emulation](READMEImages/NaiveCPC.png)|![Amstrad text, with correct aspect ratio and subject to a lowpass filter](READMEImages/FilteredCPC.png)| -|![The Amstrad CPC version of Stormlord, with a classic 1:1 pixel emulation](READMEImages/NaiveCPCStormlord.png)|![The Amstrad CPC version of Stormlord, with correct aspect ratio and subject to a lowpass filter](READMEImages/CPCStormlord.png)| ## Low Latency @@ -86,18 +116,45 @@ Accuracy is a user-experience issue; the more accurate an emulator, the more lik This emulator attempts cycle-accurate emulation of all supported machines. In some cases it succeeds. ## Additional Screenshots -| | | -|---|---| -|![Apple IIe Prince of Persia](READMEImages/AppleIIPrinceOfPersia.png) | ![Apple Macintosh MusicWorks](READMEImages/MusicWorks.png) -|![Amiga Indianapolis 500](READMEImages/AmigaIndy500.png) | ![Atari ST Stunt Car Racer](READMEImages/STStuntCarRacer.png) -|![Amstrad CPC Chase HQ](READMEImages/CPCChaseHQ.png) | ![MSX 2 Space Manbow](READMEImages/MSX2SpaceManbow.png) -|![Amiga James Pond II](READMEImages/AmigaJamesPondII.png) | ![Atari 2600 Solaris](READMEImages/Atari2600Solaris.png) -|![Acorn Electron Chuckie Egg](READMEImages/ElectronChuckieEgg.png) | ![Enterprise HERO](READMEImages/EnterpriseHERO.png) -|![ColecoVision Galaxian](READMEImages/ColecoVisionGalaxian.png) | ![SG1000 Chack'n'Pop](READMEImages/SGChackNPop.png) -|![ZX81 3D Monster Maze](READMEImages/ZX81MonsterMaze.png) | ![Microsoft Flight Simulator](READMEImages/PCFlightSimulator.png) -|![Vic-20 Gridrunner](READMEImages/Vic20Gridrunner.png) | ![ZX Spectrum Chromatrons Attack](READMEImages/ZXSpectrumChromatronsAttack.png) -|![ZX Spectrum Menu](READMEImages/ZXSpectrumMenu.png) | ![VIC-20 BASIC](READMEImages/Vic20BASIC.png) -|![MS-DOS Prompt](READMEImages/MSDOSPrompt.png) | ![ZX80 Kong](READMEImages/ZX80Kong.png) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Apple IIe Prince of PersiaArchimedes Star Fighter 3000
Apple Macintosh MusicWorksAtari ST Stunt Car Racer
Amiga Indianapolis 500Acorn Electron Chuckie Egg
Amstrad CPC Chase HQMSX 2 Space Manbow
Amiga James Pond IIAtari 2600 Solaris
Enterprise HEROMicrosoft Flight Simulator
ColecoVision GalaxianSG1000 Chack'n'Pop
ZX81 3D Monster MazeZX80 Kong
Vic-20 GridrunnerZX Spectrum Chromatrons Attack
ZX Spectrum MenuVIC-20 BASIC
MS-DOS PromptRISC OS
+ ![macOS Version](READMEImages/MultipleSystems.png) ![Qt Version](READMEImages/MultipleSystems-Ubuntu.png) diff --git a/READMEImages/RISCOS.png b/READMEImages/RISCOS.png new file mode 100644 index 000000000..52f781879 Binary files /dev/null and b/READMEImages/RISCOS.png differ diff --git a/READMEImages/StarFighter3000.png b/READMEImages/StarFighter3000.png new file mode 100644 index 000000000..585267049 Binary files /dev/null and b/READMEImages/StarFighter3000.png differ diff --git a/cmake/CLK_SOURCES.cmake b/cmake/CLK_SOURCES.cmake index b96c05dca..4a621669f 100644 --- a/cmake/CLK_SOURCES.cmake +++ b/cmake/CLK_SOURCES.cmake @@ -55,6 +55,7 @@ set(CLK_SOURCES Components/DiskII/DiskIIDrive.cpp Components/DiskII/IWM.cpp Components/DiskII/MacintoshDoubleDensityDrive.cpp + Components/I2C/I2C.cpp Components/KonamiSCC/KonamiSCC.cpp Components/OPx/OPLL.cpp Components/RP5C01/RP5C01.cpp @@ -71,6 +72,7 @@ set(CLK_SOURCES InstructionSets/x86/Decoder.cpp InstructionSets/x86/Instruction.cpp + Machines/Acorn/Archimedes/Archimedes.cpp Machines/Acorn/Electron/Electron.cpp Machines/Acorn/Electron/Keyboard.cpp Machines/Acorn/Electron/Plus3.cpp