Merge pull request #1355 from TomHarte/Archimedes

Add an inaccurate, basic Archimedes.
This commit is contained in:
Thomas Harte 2024-04-16 22:45:42 -04:00 committed by GitHub
commit 87d1a476a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 4601 additions and 410 deletions

View File

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

View File

@ -163,7 +163,7 @@ void Base<personality>::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;

226
Components/I2C/I2C.cpp Normal file
View File

@ -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<Log::Source::I2C> 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<uint8_t> next) {
if(next) {
peripheral_response_ = static_cast<uint16_t>(*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<uint8_t>(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;
}

82
Components/I2C/I2C.hpp Normal file
View File

@ -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 <cstdint>
#include <optional>
#include <unordered_map>
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<uint8_t> 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<int, Peripheral *> 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;
};
}

View File

@ -161,9 +161,7 @@ void Line<include_clock>::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;

View File

@ -30,7 +30,7 @@ template <> struct Carry<false> {
/// 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 <ShiftType type, bool set_carry>
template <ShiftType type, bool set_carry, bool is_immediate_shift>
void shift(uint32_t &source, uint32_t amount, typename Carry<set_carry>::type carry) {
switch(type) {
case ShiftType::LogicalLeft:
@ -47,49 +47,61 @@ void shift(uint32_t &source, uint32_t amount, typename Carry<set_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<set_carry>::type ca
}
/// Acts as per @c shift above, but applies runtime shift-type selection.
template <bool set_carry>
template <bool set_carry, bool is_immediate_shift>
void shift(ShiftType type, uint32_t &source, uint32_t amount, typename Carry<set_carry>::type carry) {
switch(type) {
case ShiftType::LogicalLeft:
shift<ShiftType::LogicalLeft, set_carry>(source, amount, carry);
shift<ShiftType::LogicalLeft, set_carry, is_immediate_shift>(source, amount, carry);
break;
case ShiftType::LogicalRight:
shift<ShiftType::LogicalRight, set_carry>(source, amount, carry);
shift<ShiftType::LogicalRight, set_carry, is_immediate_shift>(source, amount, carry);
break;
case ShiftType::ArithmeticRight:
shift<ShiftType::ArithmeticRight, set_carry>(source, amount, carry);
shift<ShiftType::ArithmeticRight, set_carry, is_immediate_shift>(source, amount, carry);
break;
case ShiftType::RotateRight:
shift<ShiftType::RotateRight, set_carry>(source, amount, carry);
shift<ShiftType::RotateRight, set_carry, is_immediate_shift>(source, amount, carry);
break;
}
}

View File

@ -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 <string>
#include <sstream>
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<std::string>(destination);
result << ", [" << static_cast<std::string>(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 <Model model>
struct Disassembler {
Instruction last() {
return instruction_;
}
bool should_schedule(Condition condition) {
instruction_ = Instruction();
instruction_.condition = condition;
return true;
}
template <Flags f> 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 <Flags> void perform(Multiply) {}
template <Flags f> 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 <Flags f> 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 <Flags f> 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 <Flags f> void perform(CoprocessorRegisterTransfer) {
constexpr CoprocessorRegisterTransferFlags flags(f);
instruction_.operation =
(flags.operation() == CoprocessorRegisterTransferFlags::Operation::MRC) ?
Instruction::Operation::MRC : Instruction::Operation::MCR;
}
template <Flags> void perform(CoprocessorDataOperation) {}
template <Flags> void perform(CoprocessorDataTransfer) {}
void software_interrupt() {
instruction_.operation = Instruction::Operation::SWI;
}
void unknown() {
instruction_.operation = Instruction::Operation::Undefined;
}
private:
Instruction instruction_;
};
}

View File

@ -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 <typename DestinationT, typename SourceT>
DestinationT read_bus(SourceT value) {
if constexpr (std::is_same_v<DestinationT, SourceT>) {
return value;
}
if constexpr (std::is_same_v<DestinationT, uint8_t>) {
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 <Model model, typename MemoryT>
///
/// 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 <Model model, typename MemoryT, typename StatusObserverT = NullStatusHandler>
struct Executor {
template <typename... Args>
Executor(StatusObserverT &observer, Args &&...args) : status_observer_(observer), bus(std::forward<Args>(args)...) {}
template <typename... Args>
Executor(Args &&...args) : bus(std::forward<Args>(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<set_carry, false>(fields.shift_type(), operand2, shift_amount, rotate_carry);
return operand2;
}
} else {
shift_amount = fields.shift_amount();
}
shift<set_carry>(fields.shift_type(), operand2, shift_amount, rotate_carry);
shift<set_carry, true>(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<ShiftType::RotateRight, shift_sets_carry>(operand2, fields.rotate(), rotate_carry);
}
shift<ShiftType::RotateRight, shift_sets_carry, false>(operand2, fields.rotate(), rotate_carry);
} else {
operand2 = decode_shift<true, shift_sets_carry>(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<false, 31>(lhs, rhs, conditions));
registers_.set_v(Numeric::overflow<false>(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<false, 31>(operand1, operand2, conditions));
registers_.set_v(Numeric::overflow<false>(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<false, 31>(operand2, operand1, conditions));
registers_.set_v(Numeric::overflow<false>(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<Registers::Exception::Address>();
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<uint32_t>(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 <Flags f> 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 = &registers_[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 = &registers_.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<uint32_t>(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<uint32_t>(address, value, registers_.mode(), false);
accesses_succeeded &= bus.template read<uint32_t>(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<uint32_t>(address, throwaway, registers_.mode(), false);
bus.template read<uint32_t>(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<uint32_t>(address, value, registers_.mode(), trans);
} else {
// Do a throwaway read.
uint32_t throwaway;
bus.template read<uint32_t>(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<Registers::Exception::Address>();
} else if(!accesses_succeeded) {
registers_.exception<Registers::Exception::DataAbort>();
} 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 &registers() {
return registers_;
}
/// Indicates a prefetch abort exception.
void prefetch_abort() {
registers_.exception<Registers::Exception::PrefetchAbort>();
}
/// 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, NullStatusHandler>,
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 <Model model, typename MemoryT>
void execute(uint32_t instruction, Executor<model, MemoryT> &executor) {
template <Model model, typename MemoryT, typename StatusObserverT>
void execute(uint32_t instruction, Executor<model, MemoryT, StatusObserverT> &executor) {
executor.set_pc(executor.pc() + 4);
dispatch<model>(instruction, executor);
}

View File

@ -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<uint16_t>(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<i>(Multiply(instruction));
return;
}
}
// Data processing; cf. p.17.
if constexpr (((partial >> 26) & 0b11) == 0b00) {
scheduler.template perform<i>(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<i>(Multiply(instruction));
return;
}
}
// Single data transfer (LDR, STR); cf. p.25.
if constexpr (((partial >> 26) & 0b11) == 0b01) {
scheduler.template perform<i>(SingleDataTransfer(instruction));

View File

@ -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 <Exception exception>
template <Exception type>
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 <Exception type>
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<type>();
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<size_t>(offset)];
}
const uint32_t operator[](int offset) const {
return active_[offset];
uint32_t operator[](uint32_t offset) const {
return active_[static_cast<size_t>(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 &reg(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:

View File

@ -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 <algorithm>
#include <array>
#include <set>
#include <vector>
namespace {
Log::Logger<Log::Source::Archimedes> logger;
}
namespace Archimedes {
#ifndef NDEBUG
template <InstructionSet::ARM::Model model, typename Executor>
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<uint8_t>(pointer, next, InstructionSet::ARM::Mode::Supervisor, false);
++pointer;
if(next < 32) break;
swis.back().value_name.push_back(static_cast<char>(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<model> disassembler;
InstructionSet::ARM::dispatch<model>(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<uint32_t, 75> 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<SWICall> swis;
uint32_t last_pc = 0;
// uint32_t last_r9 = 0;
bool log = false;
bool accumulate = true;
std::set<uint32_t> opcodes;
};
#else
template <InstructionSet::ARM::Model model, typename Executor>
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 <int video_divider>
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 <int offset, int video_divider>
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<Exception::FIQ>()) {
return;
}
if(requests & InterruptRequests::IRQ) {
executor_.registers().interrupt<Exception::IRQ>();
}
}
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<int>();
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<arm_model, MemoryController<ConcreteMachine, ConcreteMachine>, ConcreteMachine>;
Executor executor_;
// MARK: - Yucky, temporary junk.
HackyDebugger<arm_model, Executor> debugger_;
};
}
using namespace Archimedes;
std::unique_ptr<Machine> Machine::Archimedes(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
return std::make_unique<ConcreteMachine>(*target, rom_fetcher);
}

View File

@ -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 <memory>
namespace Archimedes {
class Machine {
public:
virtual ~Machine() = default;
static std::unique_ptr<Machine> Archimedes(
const Analyser::Static::Target *target,
const ROMMachine::ROMFetcher &rom_fetcher
);
};
}

View File

@ -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 <array>
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<uint8_t> 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<uint8_t, 256> ram_{};
// This is the default contents of RAM as written by RISC OS 3.11.
static constexpr std::array<uint8_t, 256> 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<Log::Source::CMOSRTC> logger;
};
}

View File

@ -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 <typename InterruptObserverT>
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<Storage::Disk::Disk> disk, size_t drive) {
get_drive(drive).set_disk(disk);
}
private:
InterruptObserverT &observer_;
};
}

View File

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

View File

@ -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 <typename InterruptObserverT, typename ClockRateObserverT>
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 <int c>
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<Storage::Disk::Disk> 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 <typename IntT>
bool read(uint32_t address, IntT &destination) {
const Address target(address);
const auto set_byte = [&](uint8_t value) {
if constexpr (std::is_same_v<IntT, uint32_t>) {
destination = static_cast<uint32_t>(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 <typename IntT>
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 1523.
//
// Hence: use @c byte to get a current 8-bit value.
const auto byte = [](IntT original) -> uint8_t {
if constexpr (std::is_same_v<IntT, uint32_t>) {
return static_cast<uint8_t>(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<Log::Source::ARMIOC> 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<InputOutputController> 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<InputOutputController> sound_;
Video<InputOutputController, ClockRateObserverT, Sound<InputOutputController>> video_;
};
}

View File

@ -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<uint8_t>(prefix | row), static_cast<uint8_t>(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<bool>(input&4), static_cast<bool>(input&2), static_cast<bool>(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<uint8_t>(x) & 0x7f, static_cast<uint8_t>(-y) & 0x7f);
}
}
if(dequeue_next()) {
state_ = State::ExpectingBACK;
}
}
}
Inputs::Mouse &mouse() {
return mouse_;
}
private:
HalfDuplexSerial &serial_;
Log::Logger<Log::Source::Keyboard> 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<uint8_t> 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;
};
}

View File

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

View File

@ -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 <int start, int end> 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 <typename InterruptObserverT, typename ClockRateObserverT>
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<uint8_t> &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 <typename IntT>
uint32_t aligned(uint32_t address) {
if constexpr (std::is_same_v<IntT, uint32_t>) {
return address & static_cast<uint32_t>(~3);
}
return address;
}
template <typename IntT>
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<IntT, false>(address, trans);
if(!item) {
return false;
}
*item = source;
} break;
case Zone::IOControllers:
ioc_.template write<IntT>(address, source);
break;
case Zone::VideoController:
// TODO: handle byte writes correctly.
ioc_.video().write(source);
break;
case Zone::PhysicallyMappedRAM:
physical_ram<IntT>(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 <typename IntT>
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<IntT>(address);
break;
case Zone::LogicallyMappedRAM: {
const auto item = logical_ram<IntT, true>(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<IntT>(address);
break;
case Zone::IOControllers:
ioc_.template read<IntT>(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<Storage::Disk::Disk> 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<Log::Source::ARMIOC> logger;
enum class Zone {
LogicallyMappedRAM,
PhysicallyMappedRAM,
IOControllers,
LowROM,
HighROM,
VideoController,
DMAAndMEMC,
AddressTranslator,
};
static std::array<Zone, 0x20> zones(bool is_read) {
std::array<Zone, 0x20> 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<uint8_t, 4*1024*1024> ram_{};
std::array<uint8_t, 2*1024*1024> rom_;
InputOutputController<InterruptObserverT, ClockRateObserverT> ioc_;
template <typename IntT>
IntT &physical_ram(uint32_t address) {
address = aligned<IntT>(address);
address &= (ram_.size() - 1);
return *reinterpret_cast<IntT *>(&ram_[address]);
}
template <typename IntT>
IntT &high_rom(uint32_t address) {
address = aligned<IntT>(address);
return *reinterpret_cast<IntT *>(&rom_[address & (rom_.size() - 1)]);
}
std::array<Zone, 0x20> read_zones_ = zones(true);
const std::array<Zone, 0x20> 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<uint32_t, 128> 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<uint8_t *, 8192>;
std::array<MapTarget, 6> mapping_;
template <bool is_read>
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 <typename IntT, bool is_read>
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<IntT>(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<is_read>(trans, os_mode_);
if(!map[page]) {
return nullptr;
}
return reinterpret_cast<IntT *>(&map[page][address]);
}
void update_mapping() {
// For each physical page, project it into logical space.
switch(page_size_) {
default:
case PageSize::kb4: update_mapping<PageSize::kb4>(); break;
case PageSize::kb8: update_mapping<PageSize::kb8>(); break;
case PageSize::kb16: update_mapping<PageSize::kb16>(); break;
case PageSize::kb32: update_mapping<PageSize::kb32>(); break;
}
}
template <PageSize size>
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<true>(false, false)[logical] = target;
if(write) mapping<false>(false, false)[logical] = target;
};
const auto set_os = [&](bool read, bool write) {
if(read) mapping<true>(true, true)[logical] = target;
if(write) mapping<false>(true, true)[logical] = target;
};
const auto set_user = [&](bool read, bool write) {
if(read) mapping<true>(true, false)[logical] = target;
if(write) mapping<false>(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;
}
}
}
};
}

View File

@ -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 <array>
#include <cstdint>
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<int16_t, 256> generate_levels() {
std::array<int16_t, 256> 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<int>(c & 0xf);
// const auto chord = static_cast<int>((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<int>((c >> 1) & 0xf);
const auto chord = static_cast<int>((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<int16_t>((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 <typename InterruptObserverT>
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<std::size_t>(current_.start) + static_cast<std::size_t>(byte_)];
sample_ = Outputs::Speaker::StereoSample( // TODO: pan, volume.
static_cast<int16_t>((levels[raw] * positions_[byte_ & 7].left) / 6),
static_cast<int16_t>((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<true> speaker_;
Concurrency::AsyncTaskQueue<true> 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<int16_t *>(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<Outputs::Speaker::StereoSample, 4096>;
std::array<SampleBuffer, 2> samples_;
std::atomic_flag is_posting_ = ATOMIC_FLAG_INIT;
};
}

View File

@ -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 <array>
#include <cstdint>
#include <cstring>
namespace Archimedes {
template <typename InterruptObserverT, typename ClockRateObserverT, typename SoundT>
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<uint16_t>(packed[1] | (packed[0] << 8));
#else
return static_cast<uint16_t>(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<int>(time_in_phase_);
crt_.output_data(duration, static_cast<size_t>(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<int>(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<uint16_t>(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<uint16_t *>(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<size_t>(cursor_pixel_)];
if(pixel) {
pixels_[0] = cursor_colours_[pixel];
}
}
if(cursor_pixel_ < 31) {
const auto pixel = cursor_image_[static_cast<size_t>(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<int>(clock_divider_);
}
private:
Log::Logger<Log::Source::ARMIOC> 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<uint8_t, 32> cursor_image_;
// Ephemeral graphics data.
uint8_t pixel_data_[2]{};
// Colour palette, converted to internal format.
uint16_t border_colour_;
std::array<uint16_t, 16> colours_{};
std::array<uint16_t, 4> 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<int>(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();
}
};
}

View File

@ -1198,7 +1198,6 @@ class ConcreteMachine:
using namespace PCCompatible;
// See header; constructs and returns an instance of the Amstrad CPC.
std::unique_ptr<Machine> Machine::PCCompatible(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
const Target *const pc_target = dynamic_cast<const Target *>(target);

View File

@ -11,9 +11,10 @@
#include <algorithm>
// 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::DynamicMachine> Machine::MachineForTarget(const Analyse
std::unique_ptr<Machine::DynamicMachine> machine;
try {
// TODO: add Archimedes below.
#define BindD(name, m) case Analyser::Machine::m: machine = std::make_unique<Machine::TypedDynamicMachine<::name::Machine>>(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)

View File

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

View File

@ -32,6 +32,9 @@ enum Name {
Acorn1770DFS,
// Acorn Archimedes.
AcornArthur030,
AcornRISCOS200,
AcornRISCOS311,
AcornRISCOS319,
// Amiga.

View File

@ -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 = "<group>"; };
4B2530F3244E6773007980BF /* fm.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fm.json; sourceTree = "<group>"; };
4B262BFF29691F55002EC0F7 /* PersonalityTraits.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PersonalityTraits.hpp; sourceTree = "<group>"; };
4B2A1CD92BA775C5004496CE /* I2C.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = I2C.cpp; sourceTree = "<group>"; };
4B2A1CDA2BA775C5004496CE /* I2C.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = I2C.hpp; sourceTree = "<group>"; };
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 = "<group>"; };
4B2A3B5B29995FF6007CE366 /* LineBuffer.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LineBuffer.hpp; sourceTree = "<group>"; };
@ -1800,6 +1807,16 @@
4BA91E1C216D85BA00F79557 /* MasterSystemVDPTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MasterSystemVDPTests.mm; sourceTree = "<group>"; };
4BA9C3CF1D8164A9002DDB61 /* MediaTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MediaTarget.hpp; sourceTree = "<group>"; };
4BAA167B21582B1D008A3276 /* Target.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Target.hpp; sourceTree = "<group>"; };
4BAB1E522BA9D9950002C9B9 /* Disassembler.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Disassembler.hpp; sourceTree = "<group>"; };
4BAB1E532BAB5B040002C9B9 /* Sound.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Sound.hpp; sourceTree = "<group>"; };
4BAB1E542BAB5B3F0002C9B9 /* Keyboard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = "<group>"; };
4BAB1E552BAB5B6D0002C9B9 /* HalfDuplexSerial.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = HalfDuplexSerial.hpp; sourceTree = "<group>"; };
4BAB1E562BAB5BC60002C9B9 /* Video.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Video.hpp; sourceTree = "<group>"; };
4BAB1E582BAB5C210002C9B9 /* MemoryController.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MemoryController.hpp; sourceTree = "<group>"; };
4BAB1E592BAB5CB90002C9B9 /* CMOSRAM.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CMOSRAM.hpp; sourceTree = "<group>"; };
4BAB1E5A2BAB5F400002C9B9 /* InputOutputController.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = InputOutputController.hpp; sourceTree = "<group>"; };
4BAB1E5B2BAF59CB0002C9B9 /* KeyboardMapper.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = KeyboardMapper.hpp; sourceTree = "<group>"; };
4BAB1E5C2BC3727C0002C9B9 /* FloppyDisc.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FloppyDisc.hpp; sourceTree = "<group>"; };
4BAB62AC1D3272D200DF5BA0 /* Disk.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Disk.hpp; sourceTree = "<group>"; };
4BAB62AE1D32730D00DF5BA0 /* Storage.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Storage.hpp; sourceTree = "<group>"; };
4BAF2B4C2004580C00480230 /* DMK.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DMK.cpp; sourceTree = "<group>"; };
@ -2102,6 +2119,9 @@
4BB505752B962DDF0031C43C /* Plus3.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Plus3.cpp; sourceTree = "<group>"; };
4BB505762B962DDF0031C43C /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Keyboard.cpp; sourceTree = "<group>"; };
4BB505772B962DDF0031C43C /* Electron.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Electron.hpp; sourceTree = "<group>"; };
4BB505842B9634F30031C43C /* Archimedes.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Archimedes.cpp; sourceTree = "<group>"; };
4BB505852B9634F30031C43C /* Archimedes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Archimedes.hpp; sourceTree = "<group>"; };
4BB505882B9C0E6F0031C43C /* Messy ARM */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Messy ARM"; sourceTree = "<group>"; };
4BB5B995281B1D3E00522DA9 /* RegisterSizes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RegisterSizes.hpp; sourceTree = "<group>"; };
4BB5B996281B1E3F00522DA9 /* Perform.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Perform.hpp; sourceTree = "<group>"; };
4BB5B997281B1F7B00522DA9 /* Status.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Status.hpp; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -2799,6 +2821,15 @@
path = "FM Synthesis";
sourceTree = "<group>";
};
4B2A1CDB2BA775C5004496CE /* I2C */ = {
isa = PBXGroup;
children = (
4B2A1CD92BA775C5004496CE /* I2C.cpp */,
4B2A1CDA2BA775C5004496CE /* I2C.hpp */,
);
path = I2C;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 = "<group>";
@ -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 */,

View File

@ -62,7 +62,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"

View File

@ -8,54 +8,73 @@
#import <XCTest/XCTest.h>
#include "../../../InstructionSets/ARM/Disassembler.hpp"
#include "../../../InstructionSets/ARM/Executor.hpp"
#include "CSROMFetcher.hpp"
#include "NSData+dataWithContentsOfGZippedFile.h"
#include <map>
#include <sstream>
using namespace InstructionSet::ARM;
namespace {
struct Memory {
std::vector<uint8_t> rom;
struct MemoryLedger {
template <typename IntT>
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<IntT *>(&ram_[address]) = source;
// The test set sometimes worries about write alignment, sometimes not...
if(is_faulty(address) && is_faulty(address & static_cast<uint32_t>(~3))) {
return false;
}
++write_pointer;
return true;
}
template <typename IntT>
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<const IntT *>(&rom[address - 0x3800000]);
} else if(!has_moved_rom_) {
// TODO: this is true only very transiently.
source = *reinterpret_cast<const IntT *>(&rom[address]);
} else if(address < ram_.size()) {
source = *reinterpret_cast<const IntT *>(&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<uint32_t>(~3))) {
return false;
}
source = reads[read_pointer].value;
++read_pointer;
return true;
}
private:
bool has_moved_rom_ = false;
std::array<uint8_t, 4*1024*1024> ram_{};
struct Access {
size_t size;
uint32_t address;
uint32_t value;
};
template <typename IntT>
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<Access> reads;
std::vector<Access> 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<ShiftType::LogicalLeft, true>(value, 1, carry);
shift<ShiftType::LogicalLeft, true, true>(value, 1, carry);
XCTAssertEqual(value, 0);
XCTAssertNotEqual(carry, 0);
// Test a shift by 18 into carry.
value = 0x0000'4001;
shift<ShiftType::LogicalLeft, true>(value, 18, carry);
shift<ShiftType::LogicalLeft, true, true>(value, 18, carry);
XCTAssertEqual(value, 0x4'0000);
XCTAssertNotEqual(carry, 0);
// Test a shift by 17, not generating carry.
value = 0x0000'4001;
shift<ShiftType::LogicalLeft, true>(value, 17, carry);
shift<ShiftType::LogicalLeft, true, true>(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<ShiftType::LogicalRight, true>(value, 4, carry);
shift<ShiftType::LogicalRight, true, true>(value, 4, carry);
XCTAssertEqual(value, 0x1234567);
XCTAssertNotEqual(carry, 0);
shift<ShiftType::LogicalRight, true>(value, 4, carry);
shift<ShiftType::LogicalRight, true, true>(value, 4, carry);
XCTAssertEqual(value, 0x123456);
XCTAssertEqual(carry, 0);
// Test shift by 1.
value = 0x8003001;
shift<ShiftType::LogicalRight, true>(value, 1, carry);
shift<ShiftType::LogicalRight, true, true>(value, 1, carry);
XCTAssertEqual(value, 0x4001800);
XCTAssertNotEqual(carry, 0);
// Test a shift by greater than 32.
value = 0xffff'ffff;
shift<ShiftType::LogicalRight, true>(value, 33, carry);
shift<ShiftType::LogicalRight, true, true>(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<ShiftType::LogicalRight, true>(value, 32, carry);
shift<ShiftType::LogicalRight, true, true>(value, 32, carry);
XCTAssertEqual(value, 0);
XCTAssertNotEqual(carry, 0);
value = 0x7fff'ffff;
shift<ShiftType::LogicalRight, true>(value, 32, carry);
shift<ShiftType::LogicalRight, true, true>(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<ShiftType::LogicalRight, true>(value, 0, carry);
shift<ShiftType::LogicalRight, true, true>(value, 0, carry);
XCTAssertEqual(value, 0);
XCTAssertNotEqual(carry, 0);
}
@ -138,31 +157,31 @@ struct Memory {
// Test a short negative shift.
value = 0x8000'0030;
shift<ShiftType::ArithmeticRight, true>(value, 1, carry);
shift<ShiftType::ArithmeticRight, true, true>(value, 1, carry);
XCTAssertEqual(value, 0xc000'0018);
XCTAssertEqual(carry, 0);
// Test a medium negative shift without carry.
value = 0xffff'0000;
shift<ShiftType::ArithmeticRight, true>(value, 11, carry);
shift<ShiftType::ArithmeticRight, true, true>(value, 11, carry);
XCTAssertEqual(value, 0xffff'ffe0);
XCTAssertEqual(carry, 0);
// Test a medium negative shift with carry.
value = 0xffc0'0000;
shift<ShiftType::ArithmeticRight, true>(value, 23, carry);
shift<ShiftType::ArithmeticRight, true, true>(value, 23, carry);
XCTAssertEqual(value, 0xffff'ffff);
XCTAssertNotEqual(carry, 0);
// Test a long negative shift.
value = 0x8000'0000;
shift<ShiftType::ArithmeticRight, true>(value, 32, carry);
shift<ShiftType::ArithmeticRight, true, true>(value, 32, carry);
XCTAssertEqual(value, 0xffff'ffff);
XCTAssertNotEqual(carry, 0);
// Test a positive shift.
value = 0x0123'0031;
shift<ShiftType::ArithmeticRight, true>(value, 3, carry);
shift<ShiftType::ArithmeticRight, true, true>(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<ShiftType::RotateRight, true>(value, 4, carry);
shift<ShiftType::RotateRight, true, true>(value, 4, carry);
XCTAssertEqual(value, 0x4abc'd123);
XCTAssertEqual(carry, 0);
// Test a longer rotate, with carry.
value = 0xa5f9'6342;
shift<ShiftType::RotateRight, true>(value, 17, carry);
shift<ShiftType::RotateRight, true, true>(value, 17, carry);
XCTAssertEqual(value, 0xb1a1'52fc);
XCTAssertNotEqual(carry, 0);
// Test a rotate by 32 without carry.
value = 0x385f'7dce;
shift<ShiftType::RotateRight, true>(value, 32, carry);
shift<ShiftType::RotateRight, true, true>(value, 32, carry);
XCTAssertEqual(value, 0x385f'7dce);
XCTAssertEqual(carry, 0);
// Test a rotate by 32 with carry.
value = 0xfecd'ba12;
shift<ShiftType::RotateRight, true>(value, 32, carry);
shift<ShiftType::RotateRight, true, true>(value, 32, carry);
XCTAssertEqual(value, 0xfecd'ba12);
XCTAssertNotEqual(carry, 0);
// Test a rotate through carry, carry not set.
value = 0x123f'abcf;
carry = 0;
shift<ShiftType::RotateRight, true>(value, 0, carry);
shift<ShiftType::RotateRight, true, true>(value, 0, carry);
XCTAssertEqual(value, 0x091f'd5e7);
XCTAssertNotEqual(carry, 0);
// Test a rotate through carry, carry set.
value = 0x123f'abce;
carry = 1;
shift<ShiftType::RotateRight, true>(value, 0, carry);
shift<ShiftType::RotateRight, true, true>(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<Model::ARMv2, Memory>>();
// 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<Model::ARMv2>(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<model, MemoryLedger>;
std::unique_ptr<Exec> test;
struct FailureRecord {
int count = 0;
int first = 0;
NSString *sample;
};
std::map<uint32_t, FailureRecord> 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<model> disassembler;
InstructionSet::ARM::dispatch<model>(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<Exec>();
auto &registers = 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<uint8_t>(true, address, value);
continue;
}
if(label == "r.w") {
// Capture a word read for provision.
test->bus.add_access<uint32_t>(true, address, value);
continue;
}
if(label == "w.b") {
// Capture a byte write for comparison.
test->bus.add_access<uint8_t>(false, address, value);
continue;
}
if(label == "w.w") {
// Capture a word write for comparison.
test->bus.add_access<uint32_t>(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

Binary file not shown.

View File

@ -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 \
\

View File

@ -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')

View File

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

View File

@ -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(

View File

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

View File

@ -300,8 +300,8 @@ template <bool is_stereo> class PushLowpass: public LowpassBase<PushLowpass<is_s
using BaseT::process;
std::atomic<int> 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;

View File

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

View File

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

105
README.md
View File

@ -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)|
<table>
<tbody>
<tr>
<th>1:1 Pixel Copying</th>
<th>Composite Decoded</th>
</tr><tr>
<td width=50%><img alt="The Electron start screen, with a classic 1:1 pixel emulation" src="READMEImages/NaiveElectron.png"></td>
<td width=50%><img alt="The Electron start screen, decoded from an interlaced composite feed" src="READMEImages/CompositeElectron.png"></td>
</tr><tr>
<td width=50%><img alt="Repton 3 in game, with a classic 1:1 pixel emulation" src="READMEImages/NaiveRepton3.png"></td>
<td width=50%><img alt="Repton 3 in game, decoded from an interlaced composite feed" src="READMEImages/CompositeRepton3.png"></td>
</tr><tr>
<td width=50%><img alt="Stormlord with a classic 1:1 pixel emulation" src="READMEImages/NaiveStormlord.png"></td>
<td width=50%><img alt="Stormlord decoded from a composite feed" src="READMEImages/CompositeStormlord.png"></td>
</tr><tr>
<td width=50%><img alt="Road Fighter with a classic 1:1 pixel emulation" src="READMEImages/NaiveRoadFighter.png"></td>
<td width=50%><img alt="Road Fighter decoded from a composite feed" src="READMEImages/CompositeRoadFighter.png"></td>
</tr><tr>
<td width=50%><img alt="A segment of the ColecoVision Donkey Kong title screen with a classic 1:1 pixel emulation" src="READMEImages/NaivePresentsDonkeyKong.png"></td>
<td width=50%><img alt="A segment of the ColecoVision Donkey Kong title screen decoded from a composite feed" src="READMEImages/CompositePresentsDonkeyKong.png"></td>
</tr><tr>
<td width=50%><img alt="Sonic the Hedgehog with a classic 1:1 pixel emulation" src="READMEImages/NaiveSonic.jpeg"></td>
<td width=50%><img alt="Sonic the Hedgehog screen PAL decoded from a composite feed" src="READMEImages/CompositeSonic.png"></td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<th>1:1 Pixel Copying</th>
<th>Correct Aspect Ratio, Filtered</th>
</tr><tr>
<td width=50%><img alt="Amstrad text, with a classic 1:1 pixel emulation" src="READMEImages/NaiveCPC.png"></td>
<td width=50%><img alt="Amstrad text, with correct aspect ratio and subject to a lowpass filter" src="READMEImages/FilteredCPC.png"></td>
</tr><tr>
<td width=50%><img alt="The Amstrad CPC version of Stormlord, with a classic 1:1 pixel emulation" src="READMEImages/NaiveCPCStormlord.png"></td>
<td width=50%><img alt="The Amstrad CPC version of Stormlord, with correct aspect ratio and subject to a lowpass filter" src="READMEImages/CPCStormlord.png"></td>
</tr>
</tbody>
</table>
| 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)
<table>
<tbody>
<tr>
<td width=50%><img alt="Apple IIe Prince of Persia" src="READMEImages/AppleIIPrinceOfPersia.png"></td>
<td width=50%><img alt="Archimedes Star Fighter 3000" src="READMEImages/StarFighter3000.png"></td>
</tr><tr>
<td width=50%><img alt="Apple Macintosh MusicWorks" src="READMEImages/MusicWorks.png"></td>
<td width=50%><img alt="Atari ST Stunt Car Racer" src="READMEImages/STStuntCarRacer.png"></td>
</tr><tr>
<td width=50%><img alt="Amiga Indianapolis 500" src="READMEImages/AmigaIndy500.png"></td>
<td width=50%><img alt="Acorn Electron Chuckie Egg" src="READMEImages/ElectronChuckieEgg.png"></td>
</tr><tr>
<td width=50%><img alt="Amstrad CPC Chase HQ" src="READMEImages/CPCChaseHQ.png"></td>
<td width=50%><img alt="MSX 2 Space Manbow" src="READMEImages/MSX2SpaceManbow.png"></td>
</tr><tr>
<td width=50%><img alt="Amiga James Pond II" src="READMEImages/AmigaJamesPondII.png"></td>
<td width=50%><img alt="Atari 2600 Solaris" src="READMEImages/Atari2600Solaris.png"></td>
</tr><tr>
<td width=50%><img alt="Enterprise HERO" src="READMEImages/EnterpriseHERO.png"></td>
<td width=50%><img alt="Microsoft Flight Simulator" src="READMEImages/PCFlightSimulator.png"></td>
</tr><tr>
<td width=50%><img alt="ColecoVision Galaxian" src="READMEImages/ColecoVisionGalaxian.png"></td>
<td width=50%><img alt="SG1000 Chack'n'Pop" src="READMEImages/SGChackNPop.png"></td>
</tr><tr>
<td width=50%><img alt="ZX81 3D Monster Maze" src="READMEImages/ZX81MonsterMaze.png"></td>
<td width=50%><img alt="ZX80 Kong" src="READMEImages/ZX80Kong.png"></td>
</tr><tr>
<td width=50%><img alt="Vic-20 Gridrunner" src="READMEImages/Vic20Gridrunner.png"></td>
<td width=50%><img alt="ZX Spectrum Chromatrons Attack" src="READMEImages/ZXSpectrumChromatronsAttack.png"></td>
</tr><tr>
<td width=50%><img alt="ZX Spectrum Menu" src="READMEImages/ZXSpectrumMenu.png"></td>
<td width=50%><img alt="VIC-20 BASIC" src="READMEImages/Vic20BASIC.png"></td>
</tr><tr>
<td width=50%><img alt="MS-DOS Prompt" src="READMEImages/MSDOSPrompt.png"></td>
<td width=50%><img alt="RISC OS" src="READMEImages/RISCOS.png"></td>
</tr>
</tbody>
</table>
![macOS Version](READMEImages/MultipleSystems.png)
![Qt Version](READMEImages/MultipleSystems-Ubuntu.png)

BIN
READMEImages/RISCOS.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

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