mirror of https://github.com/TomHarte/CLK.git
Merge pull request #1355 from TomHarte/Archimedes
Add an inaccurate, basic Archimedes.
This commit is contained in:
commit
87d1a476a4
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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_;
|
||||
|
||||
};
|
||||
|
||||
}
|
|
@ -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 = ®isters_[transfer.base()];
|
||||
address = *base;
|
||||
}
|
||||
|
||||
// For an LDM pc_proxy will receive any read R15 value;
|
||||
// for an STM it'll hold the value to be written.
|
||||
uint32_t pc_proxy = 0;
|
||||
|
||||
// Read the base address and take a copy in case a data abort means that
|
||||
// it has to be restored later, and to write that value rather than
|
||||
// the final address if the base register is first in the write-out list.
|
||||
uint32_t address = transfer.base() == 15 ?
|
||||
registers_.pc_status(4) :
|
||||
registers_[transfer.base()];
|
||||
const uint32_t initial_address = address;
|
||||
// it has to be restored later.
|
||||
uint32_t initial_address = address;
|
||||
|
||||
// Figure out what the final address will be, since that's what'll be
|
||||
// in the output if the base register is second or beyond in the
|
||||
// write-out list.
|
||||
// Grab the register list and decide whether user registers are being used.
|
||||
const uint16_t list = transfer.register_list();
|
||||
const bool adopt_user_mode = flags.load_psr() && (!is_ldm || !(list & (1 << 15)));
|
||||
|
||||
// Write back will prima facie occur if:
|
||||
// (i) the instruction asks for it; and
|
||||
// (ii) the write-back register isn't R15.
|
||||
bool write_back = base && flags.write_back_address();
|
||||
|
||||
// Collate a transfer list; this is a very long-winded way of implementing STM
|
||||
// and LDM but right now the objective is merely correctness.
|
||||
//
|
||||
// If this is LDM and it turns out that base is also in the transfer list,
|
||||
// disable write back.
|
||||
uint32_t *transfer_sources[16];
|
||||
uint32_t total = 0;
|
||||
for(uint32_t c = 0; c < 15; c++) {
|
||||
if(list & (1 << c)) {
|
||||
uint32_t *const next = ®isters_.reg(adopt_user_mode, c);
|
||||
if(is_ldm && next == base) write_back = false;
|
||||
transfer_sources[total++] = next;
|
||||
}
|
||||
}
|
||||
|
||||
// If the last thing in the list is R15, redirect it to the PC proxy,
|
||||
// possibly populating with a meaningful value.
|
||||
if(list & (1 << 15)) {
|
||||
if(!is_ldm) {
|
||||
pc_proxy = registers_.pc_status(8);
|
||||
}
|
||||
transfer_sources[total++] = &pc_proxy;
|
||||
}
|
||||
|
||||
// If this is STM and the first thing in the list is the same as base,
|
||||
// point it at initial_address instead.
|
||||
if(!is_ldm && total && transfer_sources[0] == base) {
|
||||
transfer_sources[0] = &initial_address;
|
||||
}
|
||||
|
||||
// Calculate final_address, which is what will be written back if required;
|
||||
// update address to point to the low end of the transfer block.
|
||||
//
|
||||
// Writes are always ordered from lowest address to highest; adjust the
|
||||
// start address if this write is supposed to fill memory downward from
|
||||
// the base.
|
||||
|
||||
// TODO: use std::popcount when adopting C++20.
|
||||
uint32_t total = ((list & 0xa) >> 1) + (list & 0x5);
|
||||
total = ((list & 0xc) >> 2) + (list & 0x3);
|
||||
|
||||
uint32_t final_address;
|
||||
if constexpr (!flags.add_offset()) {
|
||||
final_address = address + total * 4;
|
||||
// Decrementing mode; final_address is the value the base register should
|
||||
// have after this operation if writeback is enabled, so it's below
|
||||
// the original address. But also writes always occur from lowest address
|
||||
// to highest, so push the current address to the bottom.
|
||||
final_address = address - total * 4;
|
||||
address = final_address;
|
||||
} else {
|
||||
final_address = address + total * 4;
|
||||
}
|
||||
|
||||
// For loads, keep a record of the value replaced by the last load and
|
||||
// where it came from. A data abort cancels both the current load and
|
||||
// the one before it, so this is used by this implementation to undo
|
||||
// the previous load in that case.
|
||||
struct {
|
||||
uint32_t *target = nullptr;
|
||||
uint32_t value;
|
||||
} last_replacement;
|
||||
|
||||
// Check whether access is forced ot the user bank; if so then switch
|
||||
// to it now. Also keep track of the original mode to switch back at
|
||||
// the end.
|
||||
const Mode original_mode = registers_.mode();
|
||||
const bool adopt_user_mode =
|
||||
(
|
||||
flags.operation() == BlockDataTransferFlags::Operation::STM &&
|
||||
flags.load_psr()
|
||||
) ||
|
||||
(
|
||||
flags.operation() == BlockDataTransferFlags::Operation::LDM &&
|
||||
!(list & (1 << 15))
|
||||
);
|
||||
if(adopt_user_mode) {
|
||||
registers_.set_mode(Mode::User);
|
||||
// Write back if enabled.
|
||||
if(write_back) {
|
||||
*base = final_address;
|
||||
}
|
||||
|
||||
bool address_error = false;
|
||||
// Update address in advance for:
|
||||
// * pre-indexed upward stores; and
|
||||
// * post-indxed downward stores.
|
||||
if constexpr (flags.pre_index() == flags.add_offset()) {
|
||||
address += 4;
|
||||
}
|
||||
|
||||
// Keep track of whether all accesses succeeded in order potentially to
|
||||
// throw a data abort later.
|
||||
// Perform all memory accesses, tracking whether either kind of abort will be
|
||||
// required.
|
||||
const bool trans = registers_.mode() == Mode::User;
|
||||
const bool address_error = is_invalid_address(address);
|
||||
bool accesses_succeeded = true;
|
||||
const auto access = [&](uint32_t &value) {
|
||||
// Update address in advance for:
|
||||
// * pre-indexed upward stores; and
|
||||
// * post-indxed downward stores.
|
||||
if constexpr (flags.pre_index() == flags.add_offset()) {
|
||||
address += 4;
|
||||
}
|
||||
|
||||
if constexpr (flags.operation() == BlockDataTransferFlags::Operation::STM) {
|
||||
if(!address_error) {
|
||||
// "If the abort occurs during a store multiple instruction, ARM takes little action until
|
||||
// the instruction completes, whereupon it enters the data abort trap. The memory manager is
|
||||
// responsible for preventing erroneous writes to the memory."
|
||||
accesses_succeeded &= bus.template write<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 ®isters() {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 ®(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:
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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_;
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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 15–23.
|
||||
//
|
||||
// Hence: use @c byte to get a current 8-bit value.
|
||||
const auto byte = [](IntT original) -> uint8_t {
|
||||
if constexpr (std::is_same_v<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_;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -32,6 +32,9 @@ enum Name {
|
|||
Acorn1770DFS,
|
||||
|
||||
// Acorn Archimedes.
|
||||
AcornArthur030,
|
||||
AcornRISCOS200,
|
||||
AcornRISCOS311,
|
||||
AcornRISCOS319,
|
||||
|
||||
// Amiga.
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableASanStackUseAfterReturn = "YES"
|
||||
|
|
|
@ -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 ®isters = test->registers();
|
||||
if(label == "Before:") {
|
||||
// This is the start of a new test.
|
||||
|
||||
// Establish implicit register values.
|
||||
for(uint32_t c = 0; c < 15; c++) {
|
||||
registers.set_mode(Mode::FIQ);
|
||||
registers[c] = 0x200 | c;
|
||||
|
||||
registers.set_mode(Mode::Supervisor);
|
||||
registers[c] = 0x300 | c;
|
||||
|
||||
registers.set_mode(Mode::IRQ);
|
||||
registers[c] = 0x400 | c;
|
||||
|
||||
registers.set_mode(Mode::User);
|
||||
registers[c] = 0x100 | c;
|
||||
}
|
||||
|
||||
// Apply provided state.
|
||||
registers.set_mode(Mode::Supervisor); // To make sure the actual mode is applied.
|
||||
registers.set_pc(regs[15] - 8);
|
||||
registers.set_status(regs[15]);
|
||||
for(uint32_t c = 0; c < 15; c++) {
|
||||
registers[c] = regs[c];
|
||||
}
|
||||
} else {
|
||||
// Execute test and compare.
|
||||
++test_count;
|
||||
|
||||
if(opcode == 0xe1a0ae2f && test_count == 2) {
|
||||
printf("");
|
||||
}
|
||||
|
||||
execute(opcode, *test);
|
||||
NSMutableString *error = nil;
|
||||
|
||||
for(uint32_t c = 0; c < 15; c++) {
|
||||
if((regs[c] & masks[c]) != (registers[c] & masks[c])) {
|
||||
if(!error) error = [[NSMutableString alloc] init]; else [error appendString:@"; "];
|
||||
[error appendFormat:@"R%d %08x v %08x", c, regs[c], registers[c]];
|
||||
}
|
||||
}
|
||||
if((regs[15] & masks[15]) != (registers.pc_status(test_pc_offset) & masks[15])) {
|
||||
if(!error) error = [[NSMutableString alloc] init]; else [error appendString:@"; "];
|
||||
[error appendFormat:@"; PC/PSR %08x/%08x v %08x/%08x",
|
||||
regs[15] & 0x03ff'fffc, regs[15] & ~0x03ff'fffc,
|
||||
registers.pc(test_pc_offset), registers.status()];
|
||||
}
|
||||
|
||||
if(error) {
|
||||
++failures[opcode].count;
|
||||
if(failures[opcode].count == 1) {
|
||||
failures[opcode].first = test_count;
|
||||
failures[opcode].sample = error;
|
||||
}
|
||||
}
|
||||
|
||||
test.reset();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: supply information below to ledger, and then use and test it.
|
||||
|
||||
uint32_t address;
|
||||
uint32_t value;
|
||||
input >> address >> value;
|
||||
|
||||
if(label == "r.b") {
|
||||
// Capture a byte read for provision.
|
||||
test->bus.add_access<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.
|
@ -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 \
|
||||
\
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
105
README.md
|
@ -31,6 +31,7 @@ It currently contains emulations of the:
|
|||
|
||||
Also present but very much upcoming are the:
|
||||
|
||||
* Acorn Archimedes;
|
||||
* Commodore Amiga; and
|
||||
* early PC compatible.
|
||||
|
||||
|
@ -59,19 +60,48 @@ Similar effort is put into audio generation. If the real machine normally genera
|
|||
|
||||
### Samples
|
||||
|
||||
| 1:1 Pixel Copying | Composite Decoded |
|
||||
|---|---|
|
||||
|![The Electron start screen, with a classic 1:1 pixel emulation](READMEImages/NaiveElectron.png)|![The Electron start screen, decoded from an interlaced composite feed](READMEImages/CompositeElectron.png)|
|
||||
|![Repton 3 in game, with a classic 1:1 pixel emulation](READMEImages/NaiveRepton3.png)|![Repton 3 in game, decoded from an interlaced composite feed](READMEImages/CompositeRepton3.png)|
|
||||
|![Stormlord with a classic 1:1 pixel emulation](READMEImages/NaiveStormlord.png)|![Stormlord decoded from a composite feed](READMEImages/CompositeStormlord.png)|
|
||||
|![Road Fighter with a classic 1:1 pixel emulation](READMEImages/NaiveRoadFighter.png)|![Road Fighter decoded from a composite feed](READMEImages/CompositeRoadFighter.png)|
|
||||
|![A segment of the ColecoVision Donkey Kong title screen with a classic 1:1 pixel emulation](READMEImages/NaivePresentsDonkeyKong.png)|![A segment of the ColecoVision Donkey Kong title screen decoded from a composite feed](READMEImages/CompositePresentsDonkeyKong.png)|
|
||||
|![Sonic the Hedgehog with a classic 1:1 pixel emulation](READMEImages/NaiveSonic.jpeg)|![Sonic the Hedgehog screen PAL decoded from a composite feed](READMEImages/CompositeSonic.png)|
|
||||
<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)
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue