mirror of https://github.com/Treeki/WindEmu.git
broken draft of a fully custom ARM emulator core
This commit is contained in:
parent
868112e9fc
commit
c93b268061
|
@ -5,10 +5,11 @@
|
|||
#-------------------------------------------------
|
||||
|
||||
QT -= core gui
|
||||
QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.14
|
||||
|
||||
TARGET = WindCore
|
||||
TEMPLATE = lib
|
||||
CONFIG += staticlib
|
||||
CONFIG += staticlib c++17
|
||||
|
||||
# The following define makes your compiler emit warnings if you use
|
||||
# any feature of Qt which has been marked as deprecated (the exact warnings
|
||||
|
@ -22,6 +23,7 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
|||
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
SOURCES += \
|
||||
arm710a.cpp \
|
||||
etna.cpp \
|
||||
wind.cpp \
|
||||
isa-arm.c \
|
||||
|
@ -31,6 +33,7 @@ SOURCES += \
|
|||
emu.cpp
|
||||
|
||||
HEADERS += \
|
||||
arm710a.h \
|
||||
etna.h \
|
||||
wind_hw.h \
|
||||
wind.h \
|
||||
|
|
|
@ -0,0 +1,976 @@
|
|||
#include "arm710a.h"
|
||||
#include "common.h"
|
||||
|
||||
// this will need changing if this code ever compiles on big-endian procs
|
||||
inline uint32_t read32LE(uint8_t *p) {
|
||||
return *((uint32_t *)p);
|
||||
}
|
||||
inline void write32LE(uint8_t *p, uint32_t v) {
|
||||
*((uint32_t *)p) = v;
|
||||
}
|
||||
|
||||
|
||||
void ARM710a::switchBank(BankIndex newBank) {
|
||||
if (newBank != bank) {
|
||||
// R13 and R14 need saving/loading for all banks
|
||||
allModesBankedRegisters[bank][0] = GPRs[13];
|
||||
allModesBankedRegisters[bank][1] = GPRs[14];
|
||||
GPRs[13] = allModesBankedRegisters[newBank][0];
|
||||
GPRs[14] = allModesBankedRegisters[newBank][1];
|
||||
|
||||
// R8 to R12 are only banked in FIQ mode
|
||||
auto oldBankR8to12 = (bank == FiqBank) ? FiqBank : MainBank;
|
||||
auto newBankR8to12 = (newBank == FiqBank) ? FiqBank : MainBank;
|
||||
if (oldBankR8to12 != newBankR8to12) {
|
||||
// swap these sets around
|
||||
for (int i = 0; i < 5; i++)
|
||||
fiqBankedRegisters[oldBankR8to12][i] = GPRs[8 + i];
|
||||
for (int i = 0; i < 5; i++)
|
||||
GPRs[8 + i] = fiqBankedRegisters[newBankR8to12][i];
|
||||
}
|
||||
|
||||
bank = newBank;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void ARM710a::switchMode(Mode newMode) {
|
||||
auto oldMode = currentMode();
|
||||
if (newMode != oldMode) {
|
||||
switchBank(modeToBank[newMode & 0xF]);
|
||||
|
||||
CPSR &= ~CPSR_ModeMask;
|
||||
CPSR |= newMode;
|
||||
}
|
||||
}
|
||||
|
||||
void ARM710a::raiseException(Mode mode, uint32_t savedPC, uint32_t newPC) {
|
||||
auto bankIndex = modeToBank[mode & 0xF];
|
||||
SPSRs[bankIndex] = CPSR;
|
||||
|
||||
switchMode(mode);
|
||||
|
||||
prefetchCount = 0;
|
||||
GPRs[14] = savedPC;
|
||||
GPRs[15] = newPC;
|
||||
}
|
||||
|
||||
void ARM710a::requestFIQ() {
|
||||
raiseException(FIQ32, GPRs[15], 0x1C);
|
||||
CPSR |= CPSR_FIQDisable;
|
||||
CPSR |= CPSR_IRQDisable;
|
||||
}
|
||||
|
||||
void ARM710a::requestIRQ() {
|
||||
raiseException(FIQ32, GPRs[15], 0x18);
|
||||
CPSR |= CPSR_IRQDisable;
|
||||
}
|
||||
|
||||
void ARM710a::reset() {
|
||||
clearCache();
|
||||
raiseException(Supervisor32, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
uint32_t ARM710a::tick() {
|
||||
// pop an instruction off the end of the pipeline
|
||||
bool haveInsn = false;
|
||||
uint32_t insn;
|
||||
MMUFault insnFault;
|
||||
if (prefetchCount == 2) {
|
||||
haveInsn = true;
|
||||
insn = prefetch[1];
|
||||
insnFault = prefetchFaults[1];
|
||||
}
|
||||
|
||||
// move the instruction we fetched last tick once along
|
||||
if (prefetchCount >= 1) {
|
||||
prefetch[1] = prefetch[0];
|
||||
prefetchFaults[1] = prefetchFaults[0];
|
||||
}
|
||||
|
||||
// fetch a new instruction
|
||||
auto newInsn = readVirtual(GPRs[15], V32);
|
||||
GPRs[15] += 4;
|
||||
prefetch[0] = newInsn.first.value_or(0);
|
||||
prefetchFaults[0] = newInsn.second;
|
||||
if (prefetchCount < 2)
|
||||
prefetchCount++;
|
||||
|
||||
// now deal with the one we popped
|
||||
uint32_t clocks = 1;
|
||||
if (haveInsn) {
|
||||
if (insnFault != NoFault) {
|
||||
// Raise a prefetch error
|
||||
// These do not set FSR or FAR
|
||||
raiseException(Abort32, GPRs[15] - 8, 0xC);
|
||||
} else {
|
||||
clocks += executeInstruction(insn);
|
||||
}
|
||||
}
|
||||
|
||||
if (faultTriggeredThisCycle) {
|
||||
// data abort time!
|
||||
faultTriggeredThisCycle = false;
|
||||
raiseException(Abort32, GPRs[15] - 4, 0x10);
|
||||
}
|
||||
|
||||
return clocks;
|
||||
}
|
||||
|
||||
|
||||
static inline uint32_t extract(uint32_t value, uint32_t hiBit, uint32_t loBit) {
|
||||
return (value >> loBit) & ((1 << (hiBit - loBit + 1)) - 1);
|
||||
// return (value >> (32 - offset - length)) & ((1 << length) - 1);
|
||||
}
|
||||
static inline bool extract1(uint32_t value, uint32_t bit) {
|
||||
return (value >> bit) & 1;
|
||||
}
|
||||
|
||||
|
||||
uint32_t ARM710a::executeInstruction(uint32_t i) {
|
||||
uint32_t cycles = 1;
|
||||
|
||||
// a big old dispatch thing here
|
||||
// but first, conditions!
|
||||
if (!checkCondition(extract(i, 31, 28)))
|
||||
return cycles;
|
||||
|
||||
if ((i & 0x0F000000) == 0x0F000000)
|
||||
raiseException(Supervisor32, GPRs[15] - 8, 0x08);
|
||||
else if ((i & 0x0F000F10) == 0x0E000F10)
|
||||
cycles += execCP15RegisterTransfer(extract(i,23,21), extract1(i,20), extract(i,19,16), extract(i,15,12), extract(i,7,5), extract(i,3,0));
|
||||
else if ((i & 0x0E000000) == 0x0A000000)
|
||||
cycles += execBranch(extract1(i,24), extract(i,23,0));
|
||||
else if ((i & 0x0E000000) == 0x08000000)
|
||||
cycles += execBlockDataTransfer(extract(i,24,20), extract(i,19,16), extract(i,15,0));
|
||||
else if ((i & 0x0C000000) == 0x04000000)
|
||||
cycles += execSingleDataTransfer(extract(i,25,20), extract(i,19,16), extract(i,15,12), extract(i,11,0));
|
||||
else if ((i & 0x0FB00FF0) == 0x01000090)
|
||||
cycles += execSingleDataSwap(extract1(i,22), extract(i,19,16), extract(i,15,12), extract(i,3,0));
|
||||
else if ((i & 0x0F8000F0) == 0x00000090)
|
||||
cycles += execMultiply(extract(i,21,20), extract(i,19,16), extract(i,15,12), extract(i,11,8), extract(i,3,0));
|
||||
else if ((i & 0x0C000000) == 0x00000000)
|
||||
cycles += execDataProcessing(extract1(i,25), extract(i,24,21), extract1(i,20), extract(i,19,16), extract(i,15,12), extract(i,11,0));
|
||||
else
|
||||
raiseException(Undefined32, GPRs[15] - 8, 0x04);
|
||||
|
||||
return cycles;
|
||||
}
|
||||
|
||||
uint32_t ARM710a::execDataProcessing(bool I, uint32_t Opcode, bool S, uint32_t Rn, uint32_t Rd, uint32_t Operand2)
|
||||
{
|
||||
uint32_t cycles = 0; // TODO increment me semi-accurately
|
||||
bool shifterCarryOutput;
|
||||
|
||||
// compute our Op1 (may be unnecessary but that's ok)
|
||||
uint32_t op1 = GPRs[Rn];
|
||||
|
||||
// compute our Op2
|
||||
uint32_t op2;
|
||||
if (!I) {
|
||||
// REGISTER
|
||||
uint32_t Rm = extract(Operand2, 3, 0);
|
||||
op2 = GPRs[Rm];
|
||||
|
||||
uint8_t shiftBy;
|
||||
|
||||
// this is the real painful one, honestly
|
||||
if (extract(Operand2, 4, 4)) {
|
||||
// Shift by Register
|
||||
uint32_t Rs = extract(Operand2, 11, 8);
|
||||
shiftBy = GPRs[Rs] & 0xFF;
|
||||
} else {
|
||||
// Shift by Immediate
|
||||
shiftBy = extract(Operand2, 11, 7);
|
||||
|
||||
if (Rn == 15) // if PC is fetched...
|
||||
op1 -= 4; // compensate for prefetching
|
||||
if (Rm == 15)
|
||||
op2 -= 4;
|
||||
}
|
||||
|
||||
switch (extract(Operand2, 6, 5)) {
|
||||
case 0: // Logical Left (LSL)
|
||||
if (shiftBy == 0) {
|
||||
shifterCarryOutput = flagC();
|
||||
// no change to op2!
|
||||
} else if (shiftBy <= 31) {
|
||||
shifterCarryOutput = extract1(op2, 31 - shiftBy);
|
||||
op2 <<= shiftBy;
|
||||
} else if (shiftBy == 32) {
|
||||
shifterCarryOutput = extract1(op2, 0);
|
||||
op2 = 0;
|
||||
} else /*if (shiftBy >= 33)*/ {
|
||||
shifterCarryOutput = false;
|
||||
op2 = 0;
|
||||
}
|
||||
break;
|
||||
case 1: // Logical Right (LSR)
|
||||
if (shiftBy == 0 || shiftBy == 32) {
|
||||
shifterCarryOutput = extract1(op2, 31);
|
||||
op2 = 0;
|
||||
} else if (shiftBy <= 31) {
|
||||
shifterCarryOutput = extract1(op2, shiftBy - 1);
|
||||
op2 >>= shiftBy;
|
||||
} else /*if (shiftBy >= 33)*/ {
|
||||
shifterCarryOutput = false;
|
||||
op2 = 0;
|
||||
}
|
||||
break;
|
||||
case 2: // Arithmetic Right (ASR)
|
||||
if (shiftBy == 0 || shiftBy >= 32) {
|
||||
shifterCarryOutput = extract1(op2, 31);
|
||||
op2 = (int32_t)op2 >> 31;
|
||||
} else /*if (shiftBy <= 31)*/ {
|
||||
shifterCarryOutput = extract1(op2, shiftBy - 1);
|
||||
op2 = (int32_t)op2 >> shiftBy;
|
||||
}
|
||||
break;
|
||||
case 3: // Rotate Right (ROR)
|
||||
if (shiftBy == 0) { // treated as RRX
|
||||
shifterCarryOutput = op2 & 1;
|
||||
op2 >>= 1;
|
||||
op2 |= flagC() ? 0x80000000 : 0;
|
||||
} else {
|
||||
shiftBy %= 32;
|
||||
if (shiftBy == 0) { // like 32
|
||||
shifterCarryOutput = extract1(op2, 31);
|
||||
// no change to op2
|
||||
} else {
|
||||
shifterCarryOutput = extract1(op2, shiftBy - 1);
|
||||
op2 = ROR(op2, shiftBy);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// IMMEDIATE
|
||||
if (Rn == 15) // if PC is fetched...
|
||||
op1 -= 4; // compensate for prefetching
|
||||
|
||||
uint32_t Rotate = extract(Operand2, 11, 8);
|
||||
uint32_t Imm = extract(Operand2, 7, 0);
|
||||
Imm = (uint32_t)(int8_t)Imm;
|
||||
op2 = ROR(Imm, Rotate * 2);
|
||||
shifterCarryOutput = flagC(); // correct? unsure...
|
||||
}
|
||||
|
||||
// we have our operands, what next
|
||||
uint64_t result = 0;
|
||||
uint32_t flags = 0;
|
||||
|
||||
#define LOGICAL_OP(v) \
|
||||
result = v; \
|
||||
flags |= (result & 0xFFFFFFFF) ? 0 : CPSR_Z; \
|
||||
flags |= (result & 0x80000000) ? CPSR_N : 0; \
|
||||
flags |= shifterCarryOutput ? CPSR_C : 0; \
|
||||
flags |= (CPSR & CPSR_V);
|
||||
|
||||
#define ADD_OP(a, b, c) \
|
||||
result = a + b + (uint32_t)(c); \
|
||||
flags |= (result & 0xFFFFFFFF) ? 0 : CPSR_Z; \
|
||||
flags |= (result & 0x80000000) ? CPSR_N : 0; \
|
||||
flags |= (result & 0x100000000) ? CPSR_C : 0; \
|
||||
flags |= (result >= 0x100000000) ? CPSR_V : 0;
|
||||
|
||||
#define SUB_OP(a, b, c) ADD_OP(a, ~b, c)
|
||||
|
||||
|
||||
switch (Opcode) {
|
||||
case 0: LOGICAL_OP(op1 & op2); break; // AND
|
||||
case 1: LOGICAL_OP(op1 ^ op2); break; // EOR
|
||||
case 2: SUB_OP(op1, op2, 1); break; // SUB
|
||||
case 3: SUB_OP(op2, op1, 1); break; // RSB
|
||||
case 4: ADD_OP(op1, op2, 0); break; // ADD
|
||||
case 5: ADD_OP(op1, op2, flagC()); break; // ADC
|
||||
case 6: SUB_OP(op1, op2, flagC()); break; // SBC
|
||||
case 7: SUB_OP(op2, op1, flagC()); break; // RSC
|
||||
case 8: LOGICAL_OP(op1 & op2); break; // TST
|
||||
case 9: LOGICAL_OP(op1 ^ op2); break; // TEQ
|
||||
case 0xA: SUB_OP(op1, op2, 1); break; // CMP
|
||||
case 0xB: ADD_OP(op1, op2, 0); break; // CMN
|
||||
case 0xC: LOGICAL_OP(op1 | op2); break; // ORR
|
||||
case 0xD: LOGICAL_OP(op2); break; // MOV
|
||||
case 0xE: LOGICAL_OP(op1 & ~op2); break; // BIC
|
||||
case 0xF: LOGICAL_OP(~op2); break; // MVN
|
||||
}
|
||||
|
||||
if (Opcode >= 8 && Opcode <= 0xB) {
|
||||
// Output-less opcodes: special behaviour
|
||||
if (S) {
|
||||
CPSR = (CPSR & ~CPSR_FlagMask) | flags;
|
||||
} else if (Opcode == 8) {
|
||||
// MRS, CPSR -> Reg
|
||||
GPRs[Rd] = CPSR;
|
||||
} else if (Opcode == 9) {
|
||||
// MSR, Reg -> CPSR
|
||||
bool canChangeMode = extract1(Rn, 0);
|
||||
if (canChangeMode && isPrivileged()) {
|
||||
auto newCPSR = GPRs[extract(Operand2, 3, 0)];
|
||||
switchMode(modeFromCPSR(newCPSR));
|
||||
CPSR = newCPSR;
|
||||
} else {
|
||||
// for the flag-only version, immediates are allowed
|
||||
// so we just re-use what was calculated earlier...
|
||||
auto newFlag = I ? op2 : GPRs[extract(Operand2, 3, 0)];
|
||||
CPSR &= ~CPSR_FlagMask;
|
||||
CPSR |= (newFlag & CPSR_FlagMask);
|
||||
}
|
||||
} else if (Opcode == 0xA) {
|
||||
// MRS, SPSR -> Reg
|
||||
if (isPrivileged())
|
||||
GPRs[Rd] = SPSRs[currentBank()];
|
||||
} else /*if (Opcode == 0xB)*/ {
|
||||
bool canChangeMode = extract1(Rn, 0);
|
||||
if (isPrivileged()) {
|
||||
if (canChangeMode) {
|
||||
SPSRs[currentBank()] = GPRs[extract(Operand2, 3, 0)];
|
||||
} else {
|
||||
// same hat
|
||||
auto newFlag = I ? op2 : GPRs[extract(Operand2, 3, 0)];
|
||||
SPSRs[currentBank()] &= ~CPSR_FlagMask;
|
||||
SPSRs[currentBank()] |= (newFlag & CPSR_FlagMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
GPRs[Rd] = result & 0xFFFFFFFF;
|
||||
|
||||
if (Rd == 15) {
|
||||
// Writing to PC
|
||||
// Special things occur here!
|
||||
prefetchCount = 0;
|
||||
if (S && isPrivileged()) {
|
||||
// We SHOULD be privileged
|
||||
// (Raise an error otherwise...?)
|
||||
auto saved = SPSRs[currentBank()];
|
||||
switchMode(modeFromCPSR(saved));
|
||||
CPSR = saved;
|
||||
}
|
||||
} else if (S) {
|
||||
CPSR = (CPSR & ~CPSR_FlagMask) | flags;
|
||||
}
|
||||
}
|
||||
|
||||
return cycles;
|
||||
}
|
||||
|
||||
uint32_t ARM710a::execMultiply(uint32_t AS, uint32_t Rd, uint32_t Rn, uint32_t Rs, uint32_t Rm)
|
||||
{
|
||||
// no need for R15 fuckery
|
||||
// datasheet says it's not allowed here
|
||||
if (AS & 2)
|
||||
GPRs[Rd] = GPRs[Rm] * GPRs[Rs] + GPRs[Rn];
|
||||
else
|
||||
GPRs[Rd] = GPRs[Rm] * GPRs[Rs];
|
||||
|
||||
if (AS & 1) {
|
||||
CPSR &= ~(CPSR_N | CPSR_Z);
|
||||
CPSR |= GPRs[Rd] ? 0 : CPSR_Z;
|
||||
CPSR |= (GPRs[Rd] & 0x80000000) ? CPSR_N : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t ARM710a::execSingleDataSwap(bool B, uint32_t Rn, uint32_t Rd, uint32_t Rm)
|
||||
{
|
||||
auto valueSize = B ? V8 : V32;
|
||||
auto readResult = readVirtual(GPRs[Rn], valueSize);
|
||||
auto fault = readResult.second;
|
||||
|
||||
if (fault == NoFault) {
|
||||
fault = writeVirtual(GPRs[Rm], GPRs[Rn], valueSize);
|
||||
if (fault == NoFault)
|
||||
GPRs[Rd] = readResult.first.value();
|
||||
}
|
||||
|
||||
if (fault != NoFault)
|
||||
reportFault(fault);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t ARM710a::execSingleDataTransfer(uint32_t IPUBWL, uint32_t Rn, uint32_t Rd, uint32_t offset)
|
||||
{
|
||||
bool load = extract1(IPUBWL, 0);
|
||||
bool writeback = extract1(IPUBWL, 1);
|
||||
auto valueSize = extract1(IPUBWL, 2) ? V8 : V32;
|
||||
bool up = extract1(IPUBWL, 3);
|
||||
bool preIndex = extract1(IPUBWL, 4);
|
||||
bool immediate = extract1(IPUBWL, 5);
|
||||
|
||||
// calculate the offset
|
||||
uint32_t calcOffset;
|
||||
if (!immediate) {
|
||||
// REGISTER
|
||||
uint32_t Rm = extract(offset, 3, 0);
|
||||
calcOffset = GPRs[Rm];
|
||||
|
||||
uint8_t shiftBy = extract(offset, 11, 7);
|
||||
|
||||
switch (extract(offset, 6, 5)) {
|
||||
case 0: // Logical Left (LSL)
|
||||
if (shiftBy > 0)
|
||||
calcOffset <<= shiftBy;
|
||||
break;
|
||||
case 1: // Logical Right (LSR)
|
||||
if (shiftBy == 0)
|
||||
calcOffset = 0;
|
||||
else
|
||||
calcOffset >>= shiftBy;
|
||||
break;
|
||||
case 2: // Arithmetic Right (ASR)
|
||||
if (shiftBy == 0)
|
||||
calcOffset = (int32_t)calcOffset >> 31;
|
||||
else
|
||||
calcOffset = (int32_t)calcOffset >> shiftBy;
|
||||
break;
|
||||
case 3: // Rotate Right (ROR)
|
||||
if (shiftBy == 0) { // treated as RRX
|
||||
calcOffset >>= 1;
|
||||
calcOffset |= flagC() ? 0x80000000 : 0;
|
||||
} else
|
||||
calcOffset = ROR(calcOffset, shiftBy);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// IMMEDIATE
|
||||
uint32_t Rotate = extract(offset, 11, 8);
|
||||
uint32_t Imm = extract(offset, 7, 0);
|
||||
Imm = (uint32_t)(int8_t)Imm;
|
||||
calcOffset = ROR(Imm, Rotate * 2);
|
||||
}
|
||||
|
||||
uint32_t base = GPRs[Rn];
|
||||
if (Rn == 15) base -= 4; // prefetch adjustment
|
||||
uint32_t modifiedBase = up ? (base + calcOffset) : (base - calcOffset);
|
||||
uint32_t transferAddr = preIndex ? modifiedBase : base;
|
||||
|
||||
bool changeModes = !preIndex && writeback && isPrivileged();
|
||||
auto saveMode = currentMode();
|
||||
|
||||
MMUFault fault;
|
||||
|
||||
if (load) {
|
||||
if (changeModes) switchMode(User32);
|
||||
auto readResult = readVirtual(transferAddr, valueSize);
|
||||
if (changeModes) switchMode(saveMode);
|
||||
if (readResult.first.has_value())
|
||||
GPRs[Rd] = readResult.first.value();
|
||||
fault = readResult.second;
|
||||
} else {
|
||||
uint32_t value = GPRs[Rd];
|
||||
if (changeModes) switchMode(User32);
|
||||
fault = writeVirtual(value, transferAddr, valueSize);
|
||||
if (changeModes) switchMode(saveMode);
|
||||
}
|
||||
|
||||
if (preIndex && writeback)
|
||||
GPRs[Rn] = modifiedBase;
|
||||
|
||||
if (fault != NoFault)
|
||||
reportFault(fault);
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
uint32_t ARM710a::execBlockDataTransfer(uint32_t PUSWL, uint32_t Rn, uint32_t registerList)
|
||||
{
|
||||
bool load = extract1(PUSWL, 0);
|
||||
bool store = !load;
|
||||
bool writeback = extract1(PUSWL, 1);
|
||||
bool psrForceUser = extract1(PUSWL, 2);
|
||||
bool up = extract1(PUSWL, 3);
|
||||
bool preIndex = extract1(PUSWL, 4);
|
||||
|
||||
MMUFault fault = NoFault;
|
||||
uint32_t base = GPRs[Rn] & ~3;
|
||||
uint32_t blockSize = popcount32(registerList) * 4;
|
||||
|
||||
uint32_t lowAddr, updatedBase;
|
||||
if (up) {
|
||||
updatedBase = base + blockSize;
|
||||
lowAddr = base + (preIndex ? 4 : 0);
|
||||
} else {
|
||||
updatedBase = base - blockSize;
|
||||
lowAddr = updatedBase + (preIndex ? 0 : 4);
|
||||
}
|
||||
|
||||
auto saveBank = bank;
|
||||
if (psrForceUser && (store || !(registerList & 0x8000)))
|
||||
switchBank(MainBank);
|
||||
|
||||
bool doneWriteback = false;
|
||||
if (load && writeback) {
|
||||
doneWriteback = true;
|
||||
GPRs[Rn] = updatedBase;
|
||||
}
|
||||
|
||||
uint32_t addr = lowAddr;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (registerList & (1 << i)) {
|
||||
// work on this one
|
||||
if (load) {
|
||||
// handling for LDM faults may be kinda iffy...
|
||||
// wording on datasheet is a bit unclear
|
||||
auto readResult = readVirtual(addr, V32);
|
||||
if (readResult.first.has_value())
|
||||
GPRs[i] = readResult.first.value();
|
||||
if (readResult.second != NoFault) {
|
||||
fault = readResult.second;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
auto newFault = writeVirtual(GPRs[i], addr, V32);
|
||||
if (newFault != NoFault)
|
||||
fault = newFault;
|
||||
}
|
||||
|
||||
addr += 4;
|
||||
|
||||
if (writeback && !doneWriteback) {
|
||||
doneWriteback = true;
|
||||
GPRs[Rn] = updatedBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// datasheet specifies that base register must be
|
||||
// restored if an error occurs during LDM
|
||||
if (load && fault != NoFault)
|
||||
GPRs[Rn] = writeback ? updatedBase : base;
|
||||
|
||||
if (psrForceUser && (!load || !(registerList & 0x8000)))
|
||||
switchBank(saveBank);
|
||||
|
||||
if (fault != NoFault)
|
||||
reportFault(fault);
|
||||
|
||||
return 0; // fixme
|
||||
}
|
||||
|
||||
uint32_t ARM710a::execBranch(bool L, uint32_t offset)
|
||||
{
|
||||
if (L)
|
||||
GPRs[14] = GPRs[15] - 8;
|
||||
|
||||
// start with 24 bits, shift left 2, sign extend to 32
|
||||
int32_t sextOffset = (int32_t)(offset << 8) >> 6;
|
||||
|
||||
prefetchCount = 0;
|
||||
GPRs[15] -= 4; // account for our prefetch being +4 too much
|
||||
GPRs[15] += sextOffset;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t ARM710a::execCP15RegisterTransfer(uint32_t CPOpc, bool L, uint32_t CRn, uint32_t Rd, uint32_t CP, uint32_t CRm)
|
||||
{
|
||||
(void)CPOpc; // not used by ARM CP15
|
||||
(void)CP;
|
||||
(void)CRm;
|
||||
|
||||
if (!isPrivileged())
|
||||
return 0;
|
||||
|
||||
if (L) {
|
||||
// read a value
|
||||
uint32_t what = 0;
|
||||
|
||||
switch (CRn) {
|
||||
case 0: what = cp15_id; break;
|
||||
case 5: what = cp15_faultStatus; break;
|
||||
case 6: what = cp15_faultAddress; break;
|
||||
}
|
||||
|
||||
if (Rd == 15)
|
||||
CPSR = (CPSR & ~CPSR_FlagMask) | (what & CPSR_FlagMask);
|
||||
else
|
||||
GPRs[Rd] = what;
|
||||
} else {
|
||||
// store a value
|
||||
uint32_t what = GPRs[Rd];
|
||||
|
||||
switch (CRn) {
|
||||
case 1: cp15_control = what; break;
|
||||
case 2: cp15_translationTableBase = what; break;
|
||||
case 3: cp15_domainAccessControl = what; break;
|
||||
case 5: flushTlb(); break;
|
||||
case 6: flushTlb(what); break;
|
||||
case 7: clearCache(); break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ARM710a::clearCache() {
|
||||
for (uint32_t i = 0; i < CacheSets; i++) {
|
||||
for (uint32_t j = 0; j < CacheBlocksPerSet; j++) {
|
||||
cacheBlockTags[i][j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *ARM710a::findCacheLine(uint32_t virtAddr) {
|
||||
uint32_t set = virtAddr & CacheAddressSetMask;
|
||||
uint32_t tag = virtAddr & CacheAddressTagMask;
|
||||
set >>= CacheAddressSetShift;
|
||||
|
||||
for (uint32_t i = 0; i < CacheBlocksPerSet; i++) {
|
||||
if (cacheBlockTags[set][i] & CacheBlockEnabled) {
|
||||
if ((cacheBlockTags[set][i] & ~CacheBlockEnabled) == tag)
|
||||
return &cacheBlocks[set][i][0];
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
pair<MaybeU32, ARM710a::MMUFault> ARM710a::addCacheLineAndRead(uint32_t physAddr, uint32_t virtAddr, ValueSize valueSize, int domain, bool isPage) {
|
||||
uint32_t set = virtAddr & CacheAddressSetMask;
|
||||
uint32_t tag = virtAddr & CacheAddressTagMask;
|
||||
set >>= CacheAddressSetShift;
|
||||
|
||||
// "it will be randomly placed in a cache bank"
|
||||
// - the ARM710a data sheet, 6-2 (p90)
|
||||
uint32_t i = rand() % CacheBlocksPerSet;
|
||||
uint8_t *block = &cacheBlocks[set][i][0];
|
||||
MaybeU32 result;
|
||||
MMUFault fault = NoFault;
|
||||
|
||||
for (uint32_t j = 0; j < CacheBlockSize; j += 4) {
|
||||
auto word = readPhysical(physAddr + j, V32);
|
||||
if (word.has_value()) {
|
||||
write32LE(&block[j], word.value());
|
||||
if (valueSize == V8 && j == (virtAddr & CacheAddressLineMask & ~3))
|
||||
result = (word.value() >> ((virtAddr & 3) * 8)) & 0xFF;
|
||||
else if (valueSize == V32 && j == (virtAddr & CacheAddressLineMask))
|
||||
result = word.value();
|
||||
} else {
|
||||
// read error, great
|
||||
// TODO: should probably prioritise specific kinds of faults over others
|
||||
fault = encodeFaultSorP(SorPLinefetchError, isPage, domain, virtAddr & ~CacheAddressLineMask);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// the cache block is only stored if it's complete
|
||||
if (fault == NoFault)
|
||||
cacheBlockTags[set][i] = tag | CacheBlockEnabled;
|
||||
|
||||
return make_pair(result, fault);
|
||||
}
|
||||
|
||||
MaybeU32 ARM710a::readCached(uint32_t virtAddr, ValueSize valueSize) {
|
||||
uint8_t *line = findCacheLine(virtAddr);
|
||||
if (line) {
|
||||
if (valueSize == V8)
|
||||
return line[virtAddr & CacheAddressLineMask];
|
||||
else /*if (valueSize == V32)*/
|
||||
return read32LE(&line[virtAddr & CacheAddressLineMask]);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
bool ARM710a::writeCached(uint32_t value, uint32_t virtAddr, ValueSize valueSize) {
|
||||
uint8_t *line = findCacheLine(virtAddr);
|
||||
if (line) {
|
||||
if (valueSize == V8)
|
||||
line[virtAddr & CacheAddressLineMask] = value & 0xFF;
|
||||
else /*if (valueSize == V32)*/
|
||||
write32LE(&line[virtAddr & CacheAddressLineMask], value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
uint32_t ARM710a::physAddrFromTlbEntry(TlbEntry *tlbEntry, uint32_t virtAddr) {
|
||||
if ((tlbEntry->lv2Entry & 3) == 2) {
|
||||
// Smøl page
|
||||
return (tlbEntry->lv2Entry & 0xFFFFF000) | (virtAddr & 0xFFF);
|
||||
} else if ((tlbEntry->lv2Entry & 3) == 1) {
|
||||
// Lørge page
|
||||
return (tlbEntry->lv2Entry & 0xFFFF0000) | (virtAddr & 0xFFFF);
|
||||
} else {
|
||||
// Section
|
||||
return (tlbEntry->lv1Entry & 0xFFF00000) | (virtAddr & 0xFFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MaybeU32 ARM710a::virtToPhys(uint32_t virtAddr) {
|
||||
if (!isMMUEnabled())
|
||||
return virtAddr;
|
||||
|
||||
TlbEntry tempEntry;
|
||||
auto translated = translateAddressUsingTlb(virtAddr, &tempEntry);
|
||||
if (holds_alternative<TlbEntry *>(translated)) {
|
||||
auto tlbEntry = get<TlbEntry *>(translated);
|
||||
return physAddrFromTlbEntry(tlbEntry, virtAddr);
|
||||
} else {
|
||||
return MaybeU32();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MaybeU32 ARM710a::readVirtualDebug(uint32_t virtAddr, ValueSize valueSize) {
|
||||
if (auto v = virtToPhys(virtAddr); v.has_value())
|
||||
return readPhysical(v.value(), valueSize);
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
pair<MaybeU32, ARM710a::MMUFault> ARM710a::readVirtual(uint32_t virtAddr, ValueSize valueSize) {
|
||||
if (isAlignmentFaultEnabled() && valueSize == V32 && virtAddr & 3)
|
||||
return make_pair(MaybeU32(), encodeFault(AlignmentFault, 0, virtAddr));
|
||||
|
||||
// fast path: cache
|
||||
if (auto v = readCached(virtAddr, valueSize); v.has_value())
|
||||
return make_pair(v.value(), NoFault);
|
||||
|
||||
if (!isMMUEnabled()) {
|
||||
// things are very simple without a MMU
|
||||
if (auto v = readPhysical(virtAddr, valueSize); v.has_value())
|
||||
return make_pair(v.value(), NoFault);
|
||||
else
|
||||
return make_pair(MaybeU32(), NonMMUError);
|
||||
}
|
||||
|
||||
auto translated = translateAddressUsingTlb(virtAddr);
|
||||
if (holds_alternative<MMUFault>(translated))
|
||||
return make_pair(MaybeU32(), get<MMUFault>(translated));
|
||||
|
||||
// resolve this boy
|
||||
auto tlbEntry = get<TlbEntry *>(translated);
|
||||
|
||||
if (auto f = checkAccessPermissions(tlbEntry, virtAddr, false); f != NoFault)
|
||||
return make_pair(MaybeU32(), f);
|
||||
|
||||
int domain = (tlbEntry->lv1Entry >> 5) & 0xF;
|
||||
bool isPage = (tlbEntry->lv2Entry != 0);
|
||||
|
||||
uint32_t physAddr = physAddrFromTlbEntry(tlbEntry, virtAddr);
|
||||
bool cacheable = tlbEntry->lv2Entry ? (tlbEntry->lv2Entry & 8) : (tlbEntry->lv1Entry & 8);
|
||||
|
||||
if (cacheable && isCacheEnabled())
|
||||
return addCacheLineAndRead(physAddr, virtAddr, valueSize, domain, isPage);
|
||||
else if (auto result = readPhysical(physAddr, valueSize); result.has_value())
|
||||
return make_pair(result, NoFault);
|
||||
else
|
||||
return make_pair(result, encodeFaultSorP(SorPOtherBusError, isPage, domain, virtAddr));
|
||||
}
|
||||
|
||||
ARM710a::MMUFault ARM710a::writeVirtual(uint32_t value, uint32_t virtAddr, ValueSize valueSize) {
|
||||
if (isAlignmentFaultEnabled() && valueSize == V32 && virtAddr & 3)
|
||||
return encodeFault(AlignmentFault, 0, virtAddr);
|
||||
|
||||
if (!isMMUEnabled()) {
|
||||
// direct virtual -> physical mapping, sans MMU
|
||||
if (!writePhysical(value, virtAddr, valueSize))
|
||||
return NonMMUError;
|
||||
} else {
|
||||
auto translated = translateAddressUsingTlb(virtAddr);
|
||||
if (holds_alternative<MMUFault>(translated))
|
||||
return get<MMUFault>(translated);
|
||||
|
||||
// resolve this boy
|
||||
auto tlbEntry = get<TlbEntry *>(translated);
|
||||
|
||||
if (auto f = checkAccessPermissions(tlbEntry, virtAddr, true); f != NoFault)
|
||||
return f;
|
||||
|
||||
uint32_t physAddr = physAddrFromTlbEntry(tlbEntry, virtAddr);
|
||||
int domain = (tlbEntry->lv1Entry >> 5) & 0xF;
|
||||
bool isPage = (tlbEntry->lv2Entry != 0);
|
||||
|
||||
if (!writePhysical(value, physAddr, valueSize))
|
||||
return encodeFaultSorP(SorPOtherBusError, isPage, domain, virtAddr);
|
||||
}
|
||||
|
||||
// commit to cache if all was good
|
||||
writeCached(value, virtAddr, valueSize);
|
||||
return NoFault;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// TLB
|
||||
void ARM710a::flushTlb() {
|
||||
for (TlbEntry &e : tlb)
|
||||
e = {0, 0, 0, 0};
|
||||
}
|
||||
void ARM710a::flushTlb(uint32_t virtAddr) {
|
||||
for (TlbEntry &e : tlb) {
|
||||
if (e.addrMask && (virtAddr & e.addrMask) == e.addr) {
|
||||
e = {0, 0, 0, 0};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ARM710a::TlbEntry *ARM710a::_allocateTlbEntry(uint32_t addrMask, uint32_t addr) {
|
||||
TlbEntry *entry = &tlb[nextTlbIndex];
|
||||
entry->addrMask = addrMask;
|
||||
entry->addr = addr & addrMask;
|
||||
nextTlbIndex = (nextTlbIndex + 1) % TlbSize;
|
||||
return entry;
|
||||
}
|
||||
|
||||
variant<ARM710a::TlbEntry *, ARM710a::MMUFault> ARM710a::translateAddressUsingTlb(uint32_t virtAddr, TlbEntry *useMe) {
|
||||
// first things first, do we have a matching entry in the TLB?
|
||||
for (TlbEntry &e : tlb) {
|
||||
if (e.addrMask && (virtAddr & e.addrMask) == e.addr)
|
||||
return &e;
|
||||
}
|
||||
|
||||
// no, so do a page table walk
|
||||
TlbEntry *entry;
|
||||
uint32_t tableIndex = virtAddr >> 20;
|
||||
|
||||
// fetch the Level 1 entry
|
||||
auto lv1EntryOpt = readPhysical(cp15_translationTableBase | (tableIndex << 2), V32);
|
||||
if (!lv1EntryOpt.has_value())
|
||||
return Lv1TranslationError;
|
||||
auto lv1Entry = lv1EntryOpt.value();
|
||||
int domain = (lv1Entry >> 5) & 0xF;
|
||||
|
||||
switch (lv1Entry & 3) {
|
||||
case 0:
|
||||
case 3:
|
||||
// invalid!
|
||||
return encodeFault(SectionTranslationFault, domain, virtAddr);
|
||||
case 2:
|
||||
// a Section entry is straightforward
|
||||
// we just throw that immediately into the TLB
|
||||
entry = useMe ? useMe : _allocateTlbEntry(0xFFF00000, virtAddr);
|
||||
entry->lv1Entry = lv1Entry;
|
||||
entry->lv2Entry = 0;
|
||||
return entry;
|
||||
case 1:
|
||||
// a Page requires a Level 2 read
|
||||
uint32_t pageTableAddr = lv1Entry & 0xFFFFFC00;
|
||||
uint32_t lv2TableIndex = (virtAddr >> 12) & 0xFF;
|
||||
|
||||
auto lv2EntryOpt = readPhysical(pageTableAddr | (lv2TableIndex << 2), V32);
|
||||
if (!lv2EntryOpt.has_value())
|
||||
return encodeFault(Lv2TranslationError, domain, virtAddr);
|
||||
auto lv2Entry = lv2EntryOpt.value();
|
||||
|
||||
switch (lv2Entry & 3) {
|
||||
case 0:
|
||||
case 3:
|
||||
// invalid!
|
||||
return encodeFault(PageTranslationFault, domain, virtAddr);
|
||||
case 1:
|
||||
// Large 64kb page
|
||||
entry = useMe ? useMe : _allocateTlbEntry(0xFFFF0000, virtAddr);
|
||||
entry->lv1Entry = lv1Entry;
|
||||
entry->lv2Entry = lv2Entry;
|
||||
return entry;
|
||||
case 2:
|
||||
// Small 4kb page
|
||||
entry = useMe ? useMe : _allocateTlbEntry(0xFFFFF000, virtAddr);
|
||||
entry->lv1Entry = lv1Entry;
|
||||
entry->lv2Entry = lv2Entry;
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
// we should never get here as the switch covers 0, 1, 2, 3
|
||||
// but this satisfies a compiler warning
|
||||
return SectionTranslationFault;
|
||||
}
|
||||
|
||||
|
||||
|
||||
ARM710a::MMUFault ARM710a::checkAccessPermissions(ARM710a::TlbEntry *entry, uint32_t virtAddr, bool isWrite) const {
|
||||
int domain;
|
||||
int accessPerms;
|
||||
bool isPage;
|
||||
|
||||
// extract info from the entries
|
||||
domain = (entry->lv1Entry >> 5) & 0xF;
|
||||
if (entry->lv2Entry) {
|
||||
// Page
|
||||
accessPerms = (entry->lv2Entry >> 4) & 0xFF;
|
||||
|
||||
int permIndex;
|
||||
if ((entry->lv2Entry & 3) == 1) // Large 64kb
|
||||
permIndex = (virtAddr >> 14) & 3;
|
||||
else // Small 4kb
|
||||
permIndex = (virtAddr >> 10) & 3;
|
||||
|
||||
accessPerms >>= (permIndex * 2);
|
||||
accessPerms &= 3;
|
||||
isPage = true;
|
||||
} else {
|
||||
// Section
|
||||
accessPerms = (entry->lv1Entry >> 10) & 3;
|
||||
isPage = false;
|
||||
}
|
||||
|
||||
// now, do our checks
|
||||
int primaryAccessControls = (cp15_domainAccessControl >> (domain * 2)) & 3;
|
||||
|
||||
// Manager: always allowed
|
||||
if (primaryAccessControls == 3)
|
||||
return NoFault;
|
||||
|
||||
// Client: enforce checks!
|
||||
if (primaryAccessControls == 1) {
|
||||
#define OK_IF_TRUE(b) return ((b) ? NoFault : encodeFaultSorP(SorPPermissionFault, isPage, domain, virtAddr))
|
||||
bool System = cp15_control & 0x100;
|
||||
bool ROM = cp15_control & 0x200;
|
||||
|
||||
if (accessPerms == 0) {
|
||||
if (!System && !ROM) {
|
||||
// 00/0/0: Any access generates a permission fault
|
||||
OK_IF_TRUE(false);
|
||||
} else if (System && !ROM) {
|
||||
// 00/1/0: Supervisor read only permitted
|
||||
OK_IF_TRUE(!isWrite && isPrivileged());
|
||||
} else if (!System && ROM) {
|
||||
// 00/0/1: Any write generates a permission fault
|
||||
OK_IF_TRUE(!isWrite);
|
||||
} else /*if (System && ROM)*/ {
|
||||
// Reserved
|
||||
OK_IF_TRUE(false);
|
||||
}
|
||||
} else if (accessPerms == 1) {
|
||||
// 01/x/x: Access allowed only in Supervisor mode
|
||||
OK_IF_TRUE(isPrivileged());
|
||||
} else if (accessPerms == 2) {
|
||||
// 10/x/x: Writes in User mode cause permission fault
|
||||
OK_IF_TRUE(!isWrite || isPrivileged());
|
||||
} else /*if (accessPerms == 3)*/ {
|
||||
// 11/x/x: All access types permitted in both modes
|
||||
OK_IF_TRUE(true);
|
||||
}
|
||||
#undef OK_IF_TRUE
|
||||
}
|
||||
|
||||
// No Access or Reserved: never allowed (Domain Fault)
|
||||
return encodeFaultSorP(SorPDomainFault, isPage, domain, virtAddr);
|
||||
}
|
||||
|
||||
|
||||
void ARM710a::reportFault(MMUFault fault) {
|
||||
if (fault != NoFault) {
|
||||
if ((fault & 0xF) != NonMMUError) {
|
||||
cp15_faultStatus = fault & 0xFFFF;
|
||||
cp15_faultAddress = fault >> 32;
|
||||
}
|
||||
|
||||
// this signals a branch to DataAbort after the
|
||||
// instruction is done executing
|
||||
faultTriggeredThisCycle = true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
using namespace std;
|
||||
|
||||
// ASSUMPTIONS:
|
||||
// - Little-endian will be used
|
||||
// - 26-bit address spaces will not be used
|
||||
// - Alignment faults will always be on
|
||||
|
||||
// Write buffer is 4 address FIFO, 8 data FIFO
|
||||
// TLB is 64 entries
|
||||
|
||||
typedef optional<uint32_t> MaybeU32;
|
||||
|
||||
class ARM710a
|
||||
{
|
||||
public:
|
||||
enum ValueSize { V8 = 0, V32 = 1 };
|
||||
|
||||
enum MMUFault : uint64_t {
|
||||
// ref: datasheet 9-13 (p111)
|
||||
NoFault = 0,
|
||||
AlignmentFault = 1,
|
||||
// the ARM gods say there is to be no fault 2 or 3
|
||||
SectionLinefetchError = 4,
|
||||
SectionTranslationFault = 5,
|
||||
PageLinefetchError = 6,
|
||||
PageTranslationFault = 7,
|
||||
SectionOtherBusError = 8,
|
||||
SectionDomainFault = 9,
|
||||
PageOtherBusError = 0xA,
|
||||
PageDomainFault = 0xB,
|
||||
Lv1TranslationError = 0xC,
|
||||
SectionPermissionFault = 0xD,
|
||||
Lv2TranslationError = 0xE,
|
||||
PagePermissionFault = 0xF,
|
||||
|
||||
// not actually in the ARM datasheet
|
||||
// so we are reusing it for nefarious purposes
|
||||
NonMMUError = 3,
|
||||
|
||||
MMUFaultDomainMask = 0xF0,
|
||||
MMUFaultAddressMask = 0xFFFFFFFF00000000
|
||||
};
|
||||
|
||||
|
||||
|
||||
ARM710a() {
|
||||
cp15_id = 0x41047100;
|
||||
clearAllValues();
|
||||
}
|
||||
virtual ~ARM710a() { }
|
||||
|
||||
void clearAllValues() {
|
||||
bank = MainBank;
|
||||
CPSR = 0;
|
||||
for (int i = 0; i < 16; i++) GPRs[i] = 0;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
fiqBankedRegisters[0][i] = 0;
|
||||
fiqBankedRegisters[1][i] = 0;
|
||||
SPSRs[i] = 0;
|
||||
}
|
||||
for (int i = 0; i < 6; i++) {
|
||||
allModesBankedRegisters[i][0] = 0;
|
||||
allModesBankedRegisters[i][1] = 0;
|
||||
}
|
||||
|
||||
cp15_control = 0;
|
||||
cp15_translationTableBase = 0;
|
||||
cp15_domainAccessControl = 0;
|
||||
cp15_faultStatus = 0;
|
||||
cp15_faultAddress = 0;
|
||||
prefetchCount = 0;
|
||||
clearCache();
|
||||
flushTlb();
|
||||
}
|
||||
|
||||
void setProcessorID(uint32_t v) { cp15_id = v; }
|
||||
void requestFIQ(); // pull nFIQ low
|
||||
void requestIRQ(); // pull nIRQ low
|
||||
void reset(); // pull nRESET low
|
||||
|
||||
uint32_t tick(); // run the chip for at least 1 clock cycle
|
||||
|
||||
MaybeU32 readVirtualDebug(uint32_t virtAddr, ValueSize valueSize);
|
||||
MaybeU32 virtToPhys(uint32_t virtAddr);
|
||||
|
||||
pair<MaybeU32, MMUFault> readVirtual(uint32_t virtAddr, ValueSize valueSize);
|
||||
virtual MaybeU32 readPhysical(uint32_t physAddr, ValueSize valueSize) = 0;
|
||||
MMUFault writeVirtual(uint32_t value, uint32_t virtAddr, ARM710a::ValueSize valueSize);
|
||||
virtual bool writePhysical(uint32_t value, uint32_t physAddr, ARM710a::ValueSize valueSize) = 0;
|
||||
|
||||
uint32_t getGPR(int index) const { return GPRs[index]; }
|
||||
|
||||
private:
|
||||
enum { Nop = 0xE1A00000 };
|
||||
|
||||
enum Mode : uint8_t {
|
||||
User32 = 0x10,
|
||||
FIQ32 = 0x11,
|
||||
IRQ32 = 0x12,
|
||||
Supervisor32 = 0x13,
|
||||
Abort32 = 0x17,
|
||||
Undefined32 = 0x1B
|
||||
};
|
||||
|
||||
enum BankIndex : uint8_t {
|
||||
FiqBank,
|
||||
IrqBank,
|
||||
SvcBank,
|
||||
AbtBank,
|
||||
UndBank,
|
||||
MainBank
|
||||
};
|
||||
|
||||
constexpr static const BankIndex modeToBank[16] = {
|
||||
MainBank, FiqBank, IrqBank, SvcBank,
|
||||
MainBank, MainBank, MainBank, AbtBank,
|
||||
MainBank, MainBank, MainBank, UndBank,
|
||||
MainBank, MainBank, MainBank, MainBank
|
||||
};
|
||||
|
||||
enum : uint32_t {
|
||||
CPSR_ModeMask = 0x0000001F,
|
||||
CPSR_FIQDisable = 0x00000040,
|
||||
CPSR_IRQDisable = 0x00000080,
|
||||
CPSR_V = 0x10000000,
|
||||
CPSR_C = 0x20000000,
|
||||
CPSR_Z = 0x40000000,
|
||||
CPSR_N = 0x80000000,
|
||||
CPSR_FlagMask = 0xF0000000
|
||||
};
|
||||
|
||||
// active state
|
||||
BankIndex bank;
|
||||
uint32_t CPSR;
|
||||
uint32_t GPRs[16];
|
||||
|
||||
// saved state
|
||||
uint32_t fiqBankedRegisters[2][5]; // R8..R12 inclusive
|
||||
uint32_t allModesBankedRegisters[6][2]; // R13, R14
|
||||
uint32_t SPSRs[5];
|
||||
|
||||
// coprocessor 15
|
||||
uint32_t cp15_id; // 0: read-only
|
||||
uint32_t cp15_control; // 1: write-only
|
||||
uint32_t cp15_translationTableBase; // 2: write-only
|
||||
uint32_t cp15_domainAccessControl; // 3: write-only
|
||||
uint8_t cp15_faultStatus; // 5: read-only (writing has unrelated effects)
|
||||
uint32_t cp15_faultAddress; // 6: read-only (writing has unrelated effects)
|
||||
|
||||
bool flagV() const { return CPSR & CPSR_V; }
|
||||
bool flagC() const { return CPSR & CPSR_C; }
|
||||
bool flagZ() const { return CPSR & CPSR_Z; }
|
||||
bool flagN() const { return CPSR & CPSR_N; }
|
||||
bool checkCondition(int cond) const {
|
||||
switch (cond) {
|
||||
case 0: return flagZ();
|
||||
case 1: return !flagZ();
|
||||
case 2: return flagC();
|
||||
case 3: return !flagC();
|
||||
case 4: return flagN();
|
||||
case 5: return !flagN();
|
||||
case 6: return flagV();
|
||||
case 7: return !flagV();
|
||||
case 8: return flagC() && !flagZ();
|
||||
case 9: return !flagC() || flagZ();
|
||||
case 0xA: return flagN() == flagV();
|
||||
case 0xB: return flagN() != flagV();
|
||||
case 0xC: return !flagZ() && (flagN() == flagV());
|
||||
case 0xD: return flagZ() || (flagN() != flagV());
|
||||
case 0xE: return true;
|
||||
/*case 0xF:*/
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Mode modeFromCPSR(uint32_t v) { return (Mode)(v & CPSR_ModeMask); }
|
||||
Mode currentMode() const { return modeFromCPSR(CPSR); }
|
||||
BankIndex currentBank() const { return modeToBank[(Mode)(CPSR & 0xF)]; }
|
||||
bool isPrivileged() const { return (CPSR & 0x1F) > User32; }
|
||||
bool isMMUEnabled() const { return (cp15_control & 1); }
|
||||
bool isAlignmentFaultEnabled() const { return (cp15_control & 2); }
|
||||
bool isCacheEnabled() const { return (cp15_control & 4); }
|
||||
bool isWriteBufferEnabled() const { return (cp15_control & 8); }
|
||||
|
||||
void switchMode(Mode mode);
|
||||
void switchBank(BankIndex bank);
|
||||
void raiseException(Mode mode, uint32_t savedPC, uint32_t newPC);
|
||||
|
||||
// MMU/TLB
|
||||
enum MMUFaultSorP : uint64_t {
|
||||
SorPLinefetchError = 4,
|
||||
SorPTranslationFault = 5,
|
||||
SorPOtherBusError = 8,
|
||||
SorPDomainFault = 9,
|
||||
SorPPermissionFault = 0xD,
|
||||
};
|
||||
|
||||
MMUFault encodeFault(MMUFault fault, int domain, uint32_t virtAddr) const {
|
||||
return (MMUFault)(fault | (domain << 4) | ((uint64_t)virtAddr << 32));
|
||||
}
|
||||
MMUFault encodeFaultSorP(MMUFaultSorP baseFault, bool isPage, int domain, uint32_t virtAddr) const {
|
||||
return (MMUFault)(baseFault | (isPage ? 2 : 0) | (domain << 4) | ((uint64_t)virtAddr << 32));
|
||||
}
|
||||
|
||||
enum { TlbSize = 64 };
|
||||
struct TlbEntry { uint32_t addrMask, addr, lv1Entry, lv2Entry; };
|
||||
TlbEntry tlb[TlbSize];
|
||||
int nextTlbIndex = 0;
|
||||
|
||||
void flushTlb();
|
||||
void flushTlb(uint32_t virtAddr);
|
||||
variant<TlbEntry *, MMUFault> translateAddressUsingTlb(uint32_t virtAddr, TlbEntry *useMe=nullptr);
|
||||
TlbEntry *_allocateTlbEntry(uint32_t addrMask, uint32_t addr);
|
||||
|
||||
static uint32_t physAddrFromTlbEntry(TlbEntry *tlbEntry, uint32_t virtAddr);
|
||||
MMUFault checkAccessPermissions(TlbEntry *entry, uint32_t virtAddr, bool isWrite) const;
|
||||
|
||||
bool faultTriggeredThisCycle = false;
|
||||
void reportFault(MMUFault fault);
|
||||
|
||||
// Instruction/Data Cache
|
||||
enum {
|
||||
CacheSets = 4,
|
||||
CacheBlocksPerSet = 128,
|
||||
CacheBlockSize = 0x10,
|
||||
|
||||
CacheAddressLineMask = 0x0000000F,
|
||||
CacheAddressSetMask = 0x00000030, CacheAddressSetShift = 4,
|
||||
CacheAddressTagMask = 0xFFFFFFC0,
|
||||
|
||||
CacheBlockEnabled = 1
|
||||
};
|
||||
uint32_t cacheBlockTags[CacheSets][CacheBlocksPerSet];
|
||||
uint8_t cacheBlocks[CacheSets][CacheBlocksPerSet][CacheBlockSize];
|
||||
|
||||
void clearCache();
|
||||
uint8_t *findCacheLine(uint32_t virtAddr);
|
||||
pair<MaybeU32, MMUFault> addCacheLineAndRead(uint32_t physAddr, uint32_t virtAddr, ValueSize valueSize, int domain, bool isPage);
|
||||
MaybeU32 readCached(uint32_t virtAddr, ValueSize valueSize);
|
||||
bool writeCached(uint32_t value, uint32_t virtAddr, ValueSize valueSize);
|
||||
|
||||
// Instruction Loop
|
||||
int prefetchCount;
|
||||
uint32_t prefetch[2];
|
||||
MMUFault prefetchFaults[2];
|
||||
|
||||
uint32_t executeInstruction(uint32_t insn);
|
||||
|
||||
uint32_t execDataProcessing(bool I, uint32_t Opcode, bool S, uint32_t Rn, uint32_t Rd, uint32_t Operand2);
|
||||
uint32_t execMultiply(uint32_t AS, uint32_t Rd, uint32_t Rn, uint32_t Rs, uint32_t Rm);
|
||||
uint32_t execSingleDataSwap(bool B, uint32_t Rn, uint32_t Rd, uint32_t Rm);
|
||||
uint32_t execSingleDataTransfer(uint32_t IPUBWL, uint32_t Rn, uint32_t Rd, uint32_t offset);
|
||||
uint32_t execBlockDataTransfer(uint32_t PUSWL, uint32_t Rn, uint32_t registerList);
|
||||
uint32_t execBranch(bool L, uint32_t offset);
|
||||
uint32_t execCP15RegisterTransfer(uint32_t CPOpc, bool L, uint32_t CRn, uint32_t Rd, uint32_t CP, uint32_t CRm);
|
||||
};
|
664
WindCore/emu.cpp
664
WindCore/emu.cpp
|
@ -2,6 +2,7 @@
|
|||
#include "wind.h"
|
||||
#include "wind_hw.h"
|
||||
#include <time.h>
|
||||
#include "common.h"
|
||||
|
||||
|
||||
#define INCLUDE_BANK1
|
||||
|
@ -41,19 +42,19 @@ uint32_t Emu::readReg8(uint32_t reg) {
|
|||
} else if (reg == PDDDR) {
|
||||
return portDirections & 0xFF;
|
||||
} else {
|
||||
// printf("RegRead8 unknown:: pc=%08x lr=%08x reg=%03x\n", cpu.gprs[ARM_PC]-4, cpu.gprs[ARM_LR], reg);
|
||||
// printf("RegRead8 unknown:: pc=%08x lr=%08x reg=%03x\n", getGPR(15)-4, getGPR(14), reg);
|
||||
return 0xFF;
|
||||
}
|
||||
}
|
||||
uint32_t Emu::readReg32(uint32_t reg) {
|
||||
if (reg == LCDCTL) {
|
||||
printf("LCD control read pc=%08x lr=%08x !!!\n", cpu.gprs[ARM_PC], cpu.gprs[ARM_LR]);
|
||||
printf("LCD control read pc=%08x lr=%08x !!!\n", getGPR(15), getGPR(14));
|
||||
return lcdControl;
|
||||
} else if (reg == LCDST) {
|
||||
printf("LCD state read pc=%08x lr=%08x !!!\n", cpu.gprs[ARM_PC], cpu.gprs[ARM_LR]);
|
||||
printf("LCD state read pc=%08x lr=%08x !!!\n", getGPR(15), getGPR(14));
|
||||
return 0xFFFFFFFF;
|
||||
} else if (reg == PWRSR) {
|
||||
// printf("!!! PWRSR read pc=%08x lr=%08x !!!\n", cpu.gprs[ARM_PC], cpu.gprs[ARM_LR]);
|
||||
// printf("!!! PWRSR read pc=%08x lr=%08x !!!\n", getGPR(15), getGPR(14));
|
||||
return pwrsr;
|
||||
} else if (reg == INTSR) {
|
||||
return pendingInterrupts & interruptMask;
|
||||
|
@ -85,7 +86,7 @@ uint32_t Emu::readReg32(uint32_t reg) {
|
|||
} else if (reg == KSCAN) {
|
||||
return kScan;
|
||||
} else {
|
||||
// printf("RegRead32 unknown:: pc=%08x lr=%08x reg=%03x\n", cpu.gprs[ARM_PC]-4, cpu.gprs[ARM_LR], reg);
|
||||
// printf("RegRead32 unknown:: pc=%08x lr=%08x reg=%03x\n", getGPR(15)-4, getGPR(14), reg);
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
|
@ -108,13 +109,13 @@ void Emu::writeReg8(uint32_t reg, uint8_t value) {
|
|||
uint32_t oldPorts = portValues;
|
||||
portValues &= 0xFF00FFFF;
|
||||
portValues |= (uint32_t)value << 16;
|
||||
if ((portValues & 0x10000) && !(oldPorts & 0x10000))
|
||||
etna.setPromBit0High();
|
||||
else if (!(portValues & 0x10000) && (oldPorts & 0x10000))
|
||||
etna.setPromBit0Low();
|
||||
if ((portValues & 0x20000) && !(oldPorts & 0x20000))
|
||||
etna.setPromBit1High();
|
||||
diffPorts(oldPorts, portValues);
|
||||
if ((portValues & 0x10000) && !(oldPorts & 0x10000))
|
||||
etna.setPromBit0High();
|
||||
else if (!(portValues & 0x10000) && (oldPorts & 0x10000))
|
||||
etna.setPromBit0Low();
|
||||
if ((portValues & 0x20000) && !(oldPorts & 0x20000))
|
||||
etna.setPromBit1High();
|
||||
diffPorts(oldPorts, portValues);
|
||||
} else if (reg == PCDR) {
|
||||
uint32_t oldPorts = portValues;
|
||||
portValues &= 0xFFFF00FF;
|
||||
|
@ -140,7 +141,7 @@ void Emu::writeReg8(uint32_t reg, uint8_t value) {
|
|||
} else if (reg == KSCAN) {
|
||||
kScan = value;
|
||||
} else {
|
||||
// printf("RegWrite8 unknown:: pc=%08x reg=%03x value=%02x\n", cpu.gprs[ARM_PC]-4, reg, value);
|
||||
// printf("RegWrite8 unknown:: pc=%08x reg=%03x value=%02x\n", getGPR(15)-4, reg, value);
|
||||
}
|
||||
}
|
||||
void Emu::writeReg32(uint32_t reg, uint32_t value) {
|
||||
|
@ -163,7 +164,7 @@ void Emu::writeReg32(uint32_t reg, uint32_t value) {
|
|||
// diffInterrupts(interruptMask, interruptMask &~ value);
|
||||
interruptMask &= ~value;
|
||||
} else if (reg == HALT) {
|
||||
cpu.halted = true;
|
||||
halted = true;
|
||||
// BLEOI = 0x410,
|
||||
// MCEOI = 0x414,
|
||||
} else if (reg == TEOI) {
|
||||
|
@ -184,511 +185,280 @@ void Emu::writeReg32(uint32_t reg, uint32_t value) {
|
|||
} else if (reg == TC2EOI) {
|
||||
pendingInterrupts &= ~(1 << TC2OI);
|
||||
} else {
|
||||
// printf("RegWrite32 unknown:: pc=%08x reg=%03x value=%08x\n", cpu.gprs[ARM_PC]-4, reg, value);
|
||||
// printf("RegWrite32 unknown:: pc=%08x reg=%03x value=%08x\n", getGPR(15)-4, reg, value);
|
||||
}
|
||||
}
|
||||
|
||||
bool Emu::isPhysAddressValid(uint32_t physAddress) const {
|
||||
uint8_t region = (physAddress >> 24) & 0xF1;
|
||||
switch (region) {
|
||||
case 0: return true;
|
||||
case 0x80: return (physAddress <= 0x80000FFF);
|
||||
case 0xC0: return true;
|
||||
case 0xC1: return true;
|
||||
case 0xD0: return true;
|
||||
case 0xD1: return true;
|
||||
default: return false;
|
||||
}
|
||||
uint8_t region = (physAddress >> 24) & 0xF1;
|
||||
switch (region) {
|
||||
case 0: return true;
|
||||
case 0x80: return (physAddress <= 0x80000FFF);
|
||||
case 0xC0: return true;
|
||||
case 0xC1: return true;
|
||||
case 0xD0: return true;
|
||||
case 0xD1: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Emu::readPhys8(uint32_t physAddress) {
|
||||
uint32_t result = 0xFF;
|
||||
uint8_t region = (physAddress >> 24) & 0xF1;
|
||||
if (region == 0)
|
||||
result = ROM[physAddress & 0xFFFFFF];
|
||||
else if (region == 0x20 && physAddress <= 0x20000FFF)
|
||||
result = etna.readReg8(physAddress & 0xFFF);
|
||||
else if (region == 0x80 && physAddress <= 0x80000FFF)
|
||||
result = readReg8(physAddress & 0xFFF);
|
||||
else if (region == 0xC0)
|
||||
result = MemoryBlockC0[physAddress & MemoryBlockMask];
|
||||
|
||||
MaybeU32 Emu::readPhysical(uint32_t physAddr, ValueSize valueSize) {
|
||||
uint8_t region = (physAddr >> 24) & 0xF1;
|
||||
if (valueSize == V8) {
|
||||
if (region == 0)
|
||||
return ROM[physAddr & 0xFFFFFF];
|
||||
else if (region == 0x20 && physAddr <= 0x20000FFF)
|
||||
return etna.readReg8(physAddr & 0xFFF);
|
||||
else if (region == 0x80 && physAddr <= 0x80000FFF)
|
||||
return readReg8(physAddr & 0xFFF);
|
||||
else if (region == 0xC0)
|
||||
return MemoryBlockC0[physAddr & MemoryBlockMask];
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xC1)
|
||||
result = MemoryBlockC1[physAddress & MemoryBlockMask];
|
||||
else if (region == 0xC1)
|
||||
return MemoryBlockC1[physAddr & MemoryBlockMask];
|
||||
#endif
|
||||
else if (region == 0xD0)
|
||||
result = MemoryBlockD0[physAddress & MemoryBlockMask];
|
||||
else if (region == 0xD0)
|
||||
return MemoryBlockD0[physAddr & MemoryBlockMask];
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xD1)
|
||||
result = MemoryBlockD1[physAddress & MemoryBlockMask];
|
||||
else if (region == 0xD1)
|
||||
return MemoryBlockD1[physAddr & MemoryBlockMask];
|
||||
#endif
|
||||
// else
|
||||
// printf("<%08x> unmapped read8 addr p:%08x\n", cpu.gprs[ARM_PC] - 4, physAddress);
|
||||
return result;
|
||||
}
|
||||
uint32_t Emu::readPhys16(uint32_t physAddress) {
|
||||
uint32_t result = 0xFFFFFFFF;
|
||||
uint8_t region = (physAddress >> 24) & 0xF1;
|
||||
if (region == 0)
|
||||
LOAD_16LE(result, physAddress & 0xFFFFFF, ROM);
|
||||
else if (region == 0xC0)
|
||||
LOAD_16LE(result, physAddress & MemoryBlockMask, MemoryBlockC0);
|
||||
} else {
|
||||
uint32_t result;
|
||||
if (region == 0)
|
||||
LOAD_32LE(result, physAddr & 0xFFFFFF, ROM);
|
||||
else if (region == 0x20 && physAddr <= 0x20000FFF)
|
||||
result = etna.readReg32(physAddr & 0xFFF);
|
||||
else if (region == 0x80 && physAddr <= 0x80000FFF)
|
||||
result = readReg32(physAddr & 0xFFF);
|
||||
else if (region == 0xC0)
|
||||
LOAD_32LE(result, physAddr & MemoryBlockMask, MemoryBlockC0);
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xC1)
|
||||
LOAD_16LE(result, physAddress & MemoryBlockMask, MemoryBlockC1);
|
||||
else if (region == 0xC1)
|
||||
LOAD_32LE(result, physAddr & MemoryBlockMask, MemoryBlockC1);
|
||||
#endif
|
||||
else if (region == 0xD0)
|
||||
LOAD_16LE(result, physAddress & MemoryBlockMask, MemoryBlockD0);
|
||||
else if (region == 0xD0)
|
||||
LOAD_32LE(result, physAddr & MemoryBlockMask, MemoryBlockD0);
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xD1)
|
||||
LOAD_16LE(result, physAddress & MemoryBlockMask, MemoryBlockD1);
|
||||
else if (region == 0xD1)
|
||||
LOAD_32LE(result, physAddr & MemoryBlockMask, MemoryBlockD1);
|
||||
#endif
|
||||
// else
|
||||
// printf("<%08x> unmapped read16 addr p:%08x\n", cpu.gprs[ARM_PC] - 4, physAddress);
|
||||
return result;
|
||||
}
|
||||
uint32_t Emu::readPhys32(uint32_t physAddress) {
|
||||
uint32_t result = 0xFFFFFFFF;
|
||||
uint8_t region = (physAddress >> 24) & 0xF1;
|
||||
if (region == 0)
|
||||
LOAD_32LE(result, physAddress & 0xFFFFFF, ROM);
|
||||
else if (region == 0x20 && physAddress <= 0x20000FFF)
|
||||
result = etna.readReg32(physAddress & 0xFFF);
|
||||
else if (region == 0x80 && physAddress <= 0x80000FFF)
|
||||
result = readReg32(physAddress & 0xFFF);
|
||||
else if (region == 0xC0)
|
||||
LOAD_32LE(result, physAddress & MemoryBlockMask, MemoryBlockC0);
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xC1)
|
||||
LOAD_32LE(result, physAddress & MemoryBlockMask, MemoryBlockC1);
|
||||
#endif
|
||||
else if (region == 0xD0)
|
||||
LOAD_32LE(result, physAddress & MemoryBlockMask, MemoryBlockD0);
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xD1)
|
||||
LOAD_32LE(result, physAddress & MemoryBlockMask, MemoryBlockD1);
|
||||
#endif
|
||||
// else
|
||||
// printf("<%08x> unmapped read32 addr p:%08x\n", cpu.gprs[ARM_PC] - 4, physAddress);
|
||||
return result;
|
||||
else
|
||||
return {};
|
||||
return result;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void Emu::writePhys8(uint32_t physAddress, uint8_t value) {
|
||||
uint8_t region = (physAddress >> 24) & 0xF1;
|
||||
if (region == 0xC0)
|
||||
MemoryBlockC0[physAddress & MemoryBlockMask] = (uint8_t)value;
|
||||
bool Emu::writePhysical(uint32_t value, uint32_t physAddr, ValueSize valueSize) {
|
||||
uint8_t region = (physAddr >> 24) & 0xF1;
|
||||
if (valueSize == V8) {
|
||||
if (region == 0xC0)
|
||||
MemoryBlockC0[physAddr & MemoryBlockMask] = (uint8_t)value;
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xC1)
|
||||
MemoryBlockC1[physAddress & MemoryBlockMask] = (uint8_t)value;
|
||||
else if (region == 0xC1)
|
||||
MemoryBlockC1[physAddr & MemoryBlockMask] = (uint8_t)value;
|
||||
#endif
|
||||
else if (region == 0xD0)
|
||||
MemoryBlockD0[physAddress & MemoryBlockMask] = (uint8_t)value;
|
||||
else if (region == 0xD0)
|
||||
MemoryBlockD0[physAddr & MemoryBlockMask] = (uint8_t)value;
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xD1)
|
||||
MemoryBlockD1[physAddress & MemoryBlockMask] = (uint8_t)value;
|
||||
else if (region == 0xD1)
|
||||
MemoryBlockD1[physAddr & MemoryBlockMask] = (uint8_t)value;
|
||||
#endif
|
||||
else if (region == 0x20 && physAddress <= 0x20000FFF)
|
||||
etna.writeReg8(physAddress & 0xFFF, value);
|
||||
else if (region == 0x80 && physAddress <= 0x80000FFF)
|
||||
writeReg8(physAddress & 0xFFF, value);
|
||||
// else
|
||||
// printf("<%08x> unmapped write8 addr p:%08x :: %02x\n", cpu.gprs[ARM_PC] - 4, physAddress, value);
|
||||
}
|
||||
void Emu::writePhys16(uint32_t physAddress, uint16_t value) {
|
||||
uint8_t region = (physAddress >> 24) & 0xF1;
|
||||
if (region == 0xC0)
|
||||
STORE_16LE(value, physAddress & MemoryBlockMask, MemoryBlockC0);
|
||||
else if (region == 0x20 && physAddr <= 0x20000FFF)
|
||||
etna.writeReg8(physAddr & 0xFFF, value);
|
||||
else if (region == 0x80 && physAddr <= 0x80000FFF)
|
||||
writeReg8(physAddr & 0xFFF, value);
|
||||
else
|
||||
return false;
|
||||
} else {
|
||||
uint8_t region = (physAddr >> 24) & 0xF1;
|
||||
if (region == 0xC0)
|
||||
STORE_32LE(value, physAddr & MemoryBlockMask, MemoryBlockC0);
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xC1)
|
||||
STORE_16LE(value, physAddress & MemoryBlockMask, MemoryBlockC1);
|
||||
else if (region == 0xC1)
|
||||
STORE_32LE(value, physAddr & MemoryBlockMask, MemoryBlockC1);
|
||||
#endif
|
||||
else if (region == 0xD0)
|
||||
STORE_16LE(value, physAddress & MemoryBlockMask, MemoryBlockD0);
|
||||
else if (region == 0xD0)
|
||||
STORE_32LE(value, physAddr & MemoryBlockMask, MemoryBlockD0);
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xD1)
|
||||
STORE_16LE(value, physAddress & MemoryBlockMask, MemoryBlockD1);
|
||||
else if (region == 0xD1)
|
||||
STORE_32LE(value, physAddr & MemoryBlockMask, MemoryBlockD1);
|
||||
#endif
|
||||
// else
|
||||
// printf("<%08x> unmapped write16 addr p:%08x :: %04x\n", cpu.gprs[ARM_PC] - 4, physAddress, value);
|
||||
}
|
||||
void Emu::writePhys32(uint32_t physAddress, uint32_t value) {
|
||||
uint8_t region = (physAddress >> 24) & 0xF1;
|
||||
if (region == 0xC0)
|
||||
STORE_32LE(value, physAddress & MemoryBlockMask, MemoryBlockC0);
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xC1)
|
||||
STORE_32LE(value, physAddress & MemoryBlockMask, MemoryBlockC1);
|
||||
#endif
|
||||
else if (region == 0xD0)
|
||||
STORE_32LE(value, physAddress & MemoryBlockMask, MemoryBlockD0);
|
||||
#ifdef INCLUDE_BANK1
|
||||
else if (region == 0xD1)
|
||||
STORE_32LE(value, physAddress & MemoryBlockMask, MemoryBlockD1);
|
||||
#endif
|
||||
else if (region == 0x20 && physAddress <= 0x20000FFF)
|
||||
etna.writeReg32(physAddress & 0xFFF, value);
|
||||
else if (region == 0x80 && physAddress <= 0x80000FFF)
|
||||
writeReg32(physAddress & 0xFFF, value);
|
||||
// else
|
||||
// printf("<%08x> unmapped write32 addr p:%08x :: %08x\n", cpu.gprs[ARM_PC] - 4, physAddress, value);
|
||||
else if (region == 0x20 && physAddr <= 0x20000FFF)
|
||||
etna.writeReg32(physAddr & 0xFFF, value);
|
||||
else if (region == 0x80 && physAddr <= 0x80000FFF)
|
||||
writeReg32(physAddr & 0xFFF, value);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t Emu::virtToPhys(uint32_t virtAddress) {
|
||||
if (!isMMU())
|
||||
return virtAddress;
|
||||
|
||||
// find the TTB
|
||||
uint32_t ttbEntryAddr = translationTableBase & 0xFFFFC000;
|
||||
ttbEntryAddr |= ((virtAddress & 0xFFF00000) >> 18);
|
||||
uint32_t ttbEntry = readPhys32(ttbEntryAddr);
|
||||
|
||||
if ((ttbEntry & 3) == 1) {
|
||||
// Page
|
||||
uint32_t pageTableAddr = ttbEntry & 0xFFFFFC00;
|
||||
pageTableAddr |= ((virtAddress & 0x000FF000) >> 10);
|
||||
uint32_t pageTableEntry = readPhys32(pageTableAddr);
|
||||
if ((pageTableEntry & 3) == 1) {
|
||||
// Large Page
|
||||
uint32_t lpBaseAddr = pageTableEntry & 0xFFFF0000;
|
||||
return lpBaseAddr | (virtAddress & 0x0000FFFF);
|
||||
} else if ((pageTableEntry & 3) == 2) {
|
||||
// Small Page
|
||||
uint32_t lpBaseAddr = pageTableEntry & 0xFFFFF000;
|
||||
return lpBaseAddr | (virtAddress & 0x00000FFF);
|
||||
} else {
|
||||
// Fault/Reserved
|
||||
// TODO: should raise Abort here?
|
||||
printf("!!! lv2 bad entry=%d vaddr=%08x !!!\n", pageTableEntry & 3, virtAddress);
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
} else if ((ttbEntry & 3) == 2) {
|
||||
// Section
|
||||
uint32_t sectBaseAddr = ttbEntry & 0xFFF00000;
|
||||
return sectBaseAddr | (virtAddress & 0x000FFFFF);
|
||||
} else {
|
||||
// Fault/Reserved
|
||||
// TODO: should raise Abort here?
|
||||
printf("!!! lv1 bad entry=%d vaddr=%08x !!!\n", ttbEntry & 3, virtAddress);
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void Emu::configure() {
|
||||
if (configured) return;
|
||||
configured = true;
|
||||
if (configured) return;
|
||||
configured = true;
|
||||
|
||||
uart1.cpu = &cpu;
|
||||
uart2.cpu = &cpu;
|
||||
memset(&tc1, 0, sizeof(tc1));
|
||||
memset(&tc2, 0, sizeof(tc1));
|
||||
cpu.owner = this;
|
||||
uart1.cpu = this;
|
||||
uart2.cpu = this;
|
||||
memset(&tc1, 0, sizeof(tc1));
|
||||
memset(&tc2, 0, sizeof(tc1));
|
||||
|
||||
nextTickAt = TICK_INTERVAL;
|
||||
rtc = getRTC();
|
||||
nextTickAt = TICK_INTERVAL;
|
||||
rtc = getRTC();
|
||||
|
||||
configureMemoryBindings();
|
||||
configureCpuHandlers();
|
||||
|
||||
ARMReset(&cpu);
|
||||
}
|
||||
|
||||
void Emu::configureMemoryBindings() {
|
||||
cpu.memory.load8 = [](struct ARMCore *cpu, uint32_t address, int *) {
|
||||
return ((Emu *)cpu->owner)->readVirt8(address);
|
||||
};
|
||||
cpu.memory.load16 = [](struct ARMCore *cpu, uint32_t address, int *) {
|
||||
return ((Emu *)cpu->owner)->readVirt16(address);
|
||||
};
|
||||
cpu.memory.load32 = [](struct ARMCore *cpu, uint32_t address, int *) {
|
||||
return ((Emu *)cpu->owner)->readVirt32(address);
|
||||
};
|
||||
cpu.memory.loadMultiple = [](struct ARMCore *cpu, uint32_t address, int mask, enum LSMDirection direction, int *cycleCounter) {
|
||||
uint32_t value;
|
||||
int i, offset = 4, popcount = 0;
|
||||
|
||||
if (direction & LSM_D) {
|
||||
offset = -4;
|
||||
popcount = popcount32(mask);
|
||||
address -= (popcount << 2) - 4;
|
||||
}
|
||||
if (direction & LSM_B)
|
||||
address += offset;
|
||||
|
||||
if (!mask) {
|
||||
value = cpu->memory.load32(cpu, address, cycleCounter);
|
||||
cpu->gprs[ARM_PC] = value;
|
||||
address += 64;
|
||||
}
|
||||
for (i = 0; i < 16; i++) {
|
||||
if (mask & (1 << i)) {
|
||||
value = cpu->memory.load32(cpu, address, cycleCounter);
|
||||
cpu->gprs[i] = value;
|
||||
address += 4;
|
||||
}
|
||||
}
|
||||
|
||||
if (direction & LSM_B)
|
||||
address -= offset;
|
||||
if (direction & LSM_D)
|
||||
address -= (popcount << 2) + 4;
|
||||
|
||||
return address;
|
||||
};
|
||||
|
||||
cpu.memory.store8 = [](struct ARMCore *cpu, uint32_t address, int8_t value, int *) {
|
||||
((Emu *)cpu->owner)->writeVirt8(address, value);
|
||||
};
|
||||
cpu.memory.store16 = [](struct ARMCore *cpu, uint32_t address, int16_t value, int *) {
|
||||
((Emu *)cpu->owner)->writeVirt16(address, value);
|
||||
};
|
||||
cpu.memory.store32 = [](struct ARMCore *cpu, uint32_t address, int32_t value, int *) {
|
||||
((Emu *)cpu->owner)->writeVirt32(address, value);
|
||||
};
|
||||
cpu.memory.storeMultiple = [](struct ARMCore *cpu, uint32_t address, int mask, enum LSMDirection direction, int *cycleCounter) {
|
||||
uint32_t value;
|
||||
int i, offset = 4, popcount = 0;
|
||||
|
||||
if (direction & LSM_D) {
|
||||
offset = -4;
|
||||
popcount = popcount32(mask);
|
||||
address -= (popcount << 2) - 4;
|
||||
}
|
||||
if (direction & LSM_B)
|
||||
address += offset;
|
||||
|
||||
if (!mask) {
|
||||
value = cpu->gprs[ARM_PC] + 4;
|
||||
cpu->memory.store32(cpu, address, value, cycleCounter);
|
||||
address += 64;
|
||||
}
|
||||
for (i = 0; i < 16; i++) {
|
||||
if (mask & (1 << i)) {
|
||||
value = cpu->gprs[i];
|
||||
if (i == ARM_PC) value += 4;
|
||||
cpu->memory.store32(cpu, address, value, cycleCounter);
|
||||
address += 4;
|
||||
}
|
||||
}
|
||||
|
||||
if (direction & LSM_B)
|
||||
address -= offset;
|
||||
if (direction & LSM_D)
|
||||
address -= (popcount << 2) + 4;
|
||||
|
||||
return address;
|
||||
};
|
||||
|
||||
cpu.memory.activeSeqCycles32 = 0;
|
||||
cpu.memory.activeNonseqCycles32 = 0;
|
||||
cpu.memory.stall = [](struct ARMCore *cpu, int32_t wait) {
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
void Emu::configureCpuHandlers() {
|
||||
cpu.irqh.reset = [](struct ARMCore *cpu) {
|
||||
printf("reset...\n");
|
||||
};
|
||||
cpu.irqh.processEvents = [](struct ARMCore *cpu) {
|
||||
// printf("processEvents...\n");
|
||||
};
|
||||
cpu.irqh.swi32 = [](struct ARMCore *cpu, int immediate) {
|
||||
ARMRaiseSWI(cpu);
|
||||
};
|
||||
cpu.irqh.hitIllegal = [](struct ARMCore *cpu, uint32_t opcode) {
|
||||
printf("hitIllegal... %08x\n", opcode);
|
||||
};
|
||||
cpu.irqh.bkpt32 = [](struct ARMCore *cpu, int immediate) {
|
||||
printf("bkpt32... %08x\n", immediate);
|
||||
};
|
||||
cpu.irqh.readCPSR = [](struct ARMCore *cpu) {
|
||||
// printf("readCPSR...\n");
|
||||
// printf("at %08x our priv became %s\n", cpu->gprs[ARM_PC]-4, privname(cpu));
|
||||
};
|
||||
cpu.irqh.hitStub = [](struct ARMCore *cpu, uint32_t opcode) {
|
||||
Emu *emu = (Emu *)cpu->owner;
|
||||
if ((opcode & 0x0F100F10) == 0x0E100F10) {
|
||||
// coprocessor read
|
||||
int cpReg = (opcode & 0x000F0000) >> 16;
|
||||
int armReg = (opcode & 0x0000F000) >> 12;
|
||||
if (cpReg == 0)
|
||||
cpu->gprs[armReg] = 0x41807100; //5mx device id
|
||||
} else if ((opcode & 0x0F100F10) == 0x0E000F10) {
|
||||
// coprocessor write
|
||||
int cpReg = (opcode & 0x000F0000) >> 16;
|
||||
int armReg = (opcode & 0x0000F000) >> 12;
|
||||
if (cpReg == 1) {
|
||||
emu->controlReg = cpu->gprs[armReg];
|
||||
printf("mmu is now %s\n", emu->isMMU() ? "on" : "off");
|
||||
} else if (cpReg == 2) {
|
||||
emu->translationTableBase = cpu->gprs[armReg];
|
||||
} else if (cpReg == 3) {
|
||||
emu->domainAccessControl = cpu->gprs[armReg];
|
||||
}
|
||||
} else {
|
||||
printf("hitStub... %08x\n", opcode);
|
||||
}
|
||||
};
|
||||
reset();
|
||||
}
|
||||
|
||||
void Emu::loadROM(const char *path) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
fread(ROM, 1, sizeof(ROM), f);
|
||||
fclose(f);
|
||||
FILE *f = fopen(path, "rb");
|
||||
fread(ROM, 1, sizeof(ROM), f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
void Emu::executeUntil(int64_t cycles) {
|
||||
if (!configured)
|
||||
configure();
|
||||
if (!configured)
|
||||
configure();
|
||||
|
||||
while (!asleep && cpu.cycles < cycles) {
|
||||
if (cpu.cycles >= nextTickAt) {
|
||||
// increment RTCDIV
|
||||
if ((pwrsr & 0x3F) == 0x3F) {
|
||||
rtc++;
|
||||
pwrsr &= ~0x3F;
|
||||
} else {
|
||||
pwrsr++;
|
||||
}
|
||||
while (!asleep && passedCycles < cycles) {
|
||||
if (passedCycles >= nextTickAt) {
|
||||
// increment RTCDIV
|
||||
if ((pwrsr & 0x3F) == 0x3F) {
|
||||
rtc++;
|
||||
pwrsr &= ~0x3F;
|
||||
} else {
|
||||
pwrsr++;
|
||||
}
|
||||
|
||||
nextTickAt += TICK_INTERVAL;
|
||||
pendingInterrupts |= (1<<TINT);
|
||||
}
|
||||
if (tc1.tick(cpu.cycles))
|
||||
pendingInterrupts |= (1<<TC1OI);
|
||||
if (tc2.tick(cpu.cycles))
|
||||
pendingInterrupts |= (1<<TC2OI);
|
||||
nextTickAt += TICK_INTERVAL;
|
||||
pendingInterrupts |= (1<<TINT);
|
||||
}
|
||||
if (tc1.tick(passedCycles))
|
||||
pendingInterrupts |= (1<<TC1OI);
|
||||
if (tc2.tick(passedCycles))
|
||||
pendingInterrupts |= (1<<TC2OI);
|
||||
|
||||
if ((pendingInterrupts & interruptMask & FIQ_INTERRUPTS) != 0)
|
||||
ARMRaiseFIQ(&cpu);
|
||||
if ((pendingInterrupts & interruptMask & IRQ_INTERRUPTS) != 0)
|
||||
ARMRaiseIRQ(&cpu);
|
||||
if ((pendingInterrupts & interruptMask & FIQ_INTERRUPTS) != 0)
|
||||
requestFIQ();
|
||||
if ((pendingInterrupts & interruptMask & IRQ_INTERRUPTS) != 0)
|
||||
requestIRQ();
|
||||
|
||||
// if (cpu.cycles >= 30000000) {
|
||||
// static bool lcdtest = false;
|
||||
// if (!lcdtest) {
|
||||
// printf("lcdtest\n");
|
||||
// pendingInterrupts |= (1<<LCDINT);
|
||||
// lcdtest = true;
|
||||
// }
|
||||
// }
|
||||
// what's running?
|
||||
if (halted) {
|
||||
// keep the clock moving
|
||||
passedCycles++;
|
||||
} else {
|
||||
passedCycles += tick();
|
||||
|
||||
// what's running?
|
||||
if (cpu.halted) {
|
||||
// keep the clock moving
|
||||
cpu.cycles++;
|
||||
} else {
|
||||
uint32_t pc = cpu.gprs[ARM_PC] - 4;
|
||||
uint32_t phys_pc = virtToPhys(pc);
|
||||
debugPC(phys_pc);
|
||||
ARMRun(&cpu);
|
||||
|
||||
uint32_t new_pc = cpu.gprs[ARM_PC] - 4;
|
||||
if (_breakpoints.find(new_pc) != _breakpoints.end())
|
||||
return;
|
||||
}
|
||||
}
|
||||
uint32_t new_pc = getGPR(15) - 0xC;
|
||||
if (_breakpoints.find(new_pc) != _breakpoints.end())
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Emu::dumpRAM(const char *path) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
fwrite(MemoryBlockC0, 1, sizeof(MemoryBlockC0), f);
|
||||
fwrite(MemoryBlockC1, 1, sizeof(MemoryBlockC1), f);
|
||||
fwrite(MemoryBlockD0, 1, sizeof(MemoryBlockD0), f);
|
||||
fwrite(MemoryBlockD1, 1, sizeof(MemoryBlockD1), f);
|
||||
fclose(f);
|
||||
FILE *f = fopen(path, "wb");
|
||||
fwrite(MemoryBlockC0, 1, sizeof(MemoryBlockC0), f);
|
||||
fwrite(MemoryBlockC1, 1, sizeof(MemoryBlockC1), f);
|
||||
fwrite(MemoryBlockD0, 1, sizeof(MemoryBlockD0), f);
|
||||
fwrite(MemoryBlockD1, 1, sizeof(MemoryBlockD1), f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Emu::printRegs() {
|
||||
printf("R00:%08x R01:%08x R02:%08x R03:%08x\n", cpu.gprs[0], cpu.gprs[1], cpu.gprs[2], cpu.gprs[3]);
|
||||
printf("R04:%08x R05:%08x R06:%08x R07:%08x\n", cpu.gprs[4], cpu.gprs[5], cpu.gprs[6], cpu.gprs[7]);
|
||||
printf("R08:%08x R09:%08x R10:%08x R11:%08x\n", cpu.gprs[8], cpu.gprs[9], cpu.gprs[10], cpu.gprs[11]);
|
||||
printf("R12:%08x R13:%08x R14:%08x R15:%08x\n", cpu.gprs[12], cpu.gprs[13], cpu.gprs[14], cpu.gprs[15]);
|
||||
printf("cpsr=%08x spsr=%08x\n", cpu.cpsr.packed, cpu.spsr.packed);
|
||||
printf("R00:%08x R01:%08x R02:%08x R03:%08x\n", getGPR(0), getGPR(1), getGPR(2), getGPR(3));
|
||||
printf("R04:%08x R05:%08x R06:%08x R07:%08x\n", getGPR(4), getGPR(5), getGPR(6), getGPR(7));
|
||||
printf("R08:%08x R09:%08x R10:%08x R11:%08x\n", getGPR(8), getGPR(9), getGPR(10), getGPR(11));
|
||||
printf("R12:%08x R13:%08x R14:%08x R15:%08x\n", getGPR(12), getGPR(13), getGPR(14), getGPR(15));
|
||||
// printf("cpsr=%08x spsr=%08x\n", cpu.cpsr.packed, cpu.spsr.packed);
|
||||
}
|
||||
|
||||
const char *Emu::identifyObjectCon(uint32_t ptr) {
|
||||
if (ptr == readVirt32(0x80000980)) return "process";
|
||||
if (ptr == readVirt32(0x80000984)) return "thread";
|
||||
if (ptr == readVirt32(0x80000988)) return "chunk";
|
||||
// if (ptr == readVirt32(0x8000098C)) return "semaphore";
|
||||
// if (ptr == readVirt32(0x80000990)) return "mutex";
|
||||
if (ptr == readVirt32(0x80000994)) return "logicaldevice";
|
||||
if (ptr == readVirt32(0x80000998)) return "physicaldevice";
|
||||
if (ptr == readVirt32(0x8000099C)) return "channel";
|
||||
if (ptr == readVirt32(0x800009A0)) return "server";
|
||||
// if (ptr == readVirt32(0x800009A4)) return "unk9A4"; // name always null
|
||||
if (ptr == readVirt32(0x800009AC)) return "library";
|
||||
// if (ptr == readVirt32(0x800009B0)) return "unk9B0"; // name always null
|
||||
// if (ptr == readVirt32(0x800009B4)) return "unk9B4"; // name always null
|
||||
return NULL;
|
||||
if (ptr == readVirtualDebug(0x80000980, V32).value()) return "process";
|
||||
if (ptr == readVirtualDebug(0x80000984, V32).value()) return "thread";
|
||||
if (ptr == readVirtualDebug(0x80000988, V32).value()) return "chunk";
|
||||
// if (ptr == readVirtualDebug(0x8000098C, V32).value()) return "semaphore";
|
||||
// if (ptr == readVirtualDebug(0x80000990, V32).value()) return "mutex";
|
||||
if (ptr == readVirtualDebug(0x80000994, V32).value()) return "logicaldevice";
|
||||
if (ptr == readVirtualDebug(0x80000998, V32).value()) return "physicaldevice";
|
||||
if (ptr == readVirtualDebug(0x8000099C, V32).value()) return "channel";
|
||||
if (ptr == readVirtualDebug(0x800009A0, V32).value()) return "server";
|
||||
// if (ptr == readVirtualDebug(0x800009A4, V32).value()) return "unk9A4"; // name always null
|
||||
if (ptr == readVirtualDebug(0x800009AC, V32).value()) return "library";
|
||||
// if (ptr == readVirtualDebug(0x800009B0, V32).value()) return "unk9B0"; // name always null
|
||||
// if (ptr == readVirtualDebug(0x800009B4, V32).value()) return "unk9B4"; // name always null
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void Emu::fetchStr(uint32_t str, char *buf) {
|
||||
if (str == 0) {
|
||||
strcpy(buf, "<NULL>");
|
||||
return;
|
||||
}
|
||||
int size = readVirt32(str);
|
||||
for (int i = 0; i < size; i++) {
|
||||
buf[i] = readVirt8(str + 4 + i);
|
||||
}
|
||||
buf[size] = 0;
|
||||
if (str == 0) {
|
||||
strcpy(buf, "<NULL>");
|
||||
return;
|
||||
}
|
||||
int size = readVirtualDebug(str, V32).value();
|
||||
for (int i = 0; i < size; i++) {
|
||||
buf[i] = readVirtualDebug(str + 4 + i, V8).value();
|
||||
}
|
||||
buf[size] = 0;
|
||||
}
|
||||
|
||||
void Emu::fetchName(uint32_t obj, char *buf) {
|
||||
fetchStr(readVirt32(obj + 0x10), buf);
|
||||
fetchStr(readVirtualDebug(obj + 0x10, V32).value(), buf);
|
||||
}
|
||||
|
||||
void Emu::fetchProcessFilename(uint32_t obj, char *buf) {
|
||||
fetchStr(readVirt32(obj + 0x3C), buf);
|
||||
fetchStr(readVirtualDebug(obj + 0x3C, V32).value(), buf);
|
||||
}
|
||||
|
||||
void Emu::debugPC(uint32_t pc) {
|
||||
char objName[1000];
|
||||
if (pc == 0x2CBC4) {
|
||||
// CObjectCon::AddL()
|
||||
uint32_t container = cpu.gprs[0];
|
||||
uint32_t obj = cpu.gprs[1];
|
||||
const char *wut = identifyObjectCon(container);
|
||||
if (wut) {
|
||||
fetchName(obj, objName);
|
||||
printf("OBJS: added %s at %08x <%s>", wut, obj, objName);
|
||||
if (strcmp(wut, "process") == 0) {
|
||||
fetchProcessFilename(obj, objName);
|
||||
printf(" <%s>", objName);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
char objName[1000];
|
||||
if (pc == 0x2CBC4) {
|
||||
// CObjectCon::AddL()
|
||||
uint32_t container = getGPR(0);
|
||||
uint32_t obj = getGPR(1);
|
||||
const char *wut = identifyObjectCon(container);
|
||||
if (wut) {
|
||||
fetchName(obj, objName);
|
||||
printf("OBJS: added %s at %08x <%s>", wut, obj, objName);
|
||||
if (strcmp(wut, "process") == 0) {
|
||||
fetchProcessFilename(obj, objName);
|
||||
printf(" <%s>", objName);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const uint8_t *Emu::getLCDBuffer() const {
|
||||
if ((lcdAddress >> 24) == 0xC0)
|
||||
return &MemoryBlockC0[lcdAddress & MemoryBlockMask];
|
||||
else
|
||||
return nullptr;
|
||||
if ((lcdAddress >> 24) == 0xC0)
|
||||
return &MemoryBlockC0[lcdAddress & MemoryBlockMask];
|
||||
else
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
uint8_t Emu::readKeyboard() {
|
||||
uint8_t val = 0;
|
||||
if (kScan & 8) {
|
||||
// Select one keyboard
|
||||
int whichColumn = kScan & 7;
|
||||
for (int i = 0; i < 7; i++)
|
||||
if (keyboardKeys[whichColumn * 7 + i])
|
||||
val |= (1 << i);
|
||||
} else if (kScan == 0) {
|
||||
// Report all columns combined
|
||||
// EPOC's keyboard driver relies on this...
|
||||
for (int i = 0; i < 8*7; i++)
|
||||
if (keyboardKeys[i])
|
||||
val |= (1 << (i % 7));
|
||||
}
|
||||
return val;
|
||||
uint8_t val = 0;
|
||||
if (kScan & 8) {
|
||||
// Select one keyboard
|
||||
int whichColumn = kScan & 7;
|
||||
for (int i = 0; i < 7; i++)
|
||||
if (keyboardKeys[whichColumn * 7 + i])
|
||||
val |= (1 << i);
|
||||
} else if (kScan == 0) {
|
||||
// Report all columns combined
|
||||
// EPOC's keyboard driver relies on this...
|
||||
for (int i = 0; i < 8*7; i++)
|
||||
if (keyboardKeys[i])
|
||||
val |= (1 << (i % 7));
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#pragma once
|
||||
#include "arm.h"
|
||||
#include "arm710a.h"
|
||||
#include "wind_hw.h"
|
||||
#include "etna.h"
|
||||
#include <unordered_set>
|
||||
|
||||
class Emu {
|
||||
class Emu : public ARM710a {
|
||||
public:
|
||||
uint8_t ROM[0x1000000];
|
||||
uint8_t MemoryBlockC0[0x800000];
|
||||
|
@ -14,9 +14,6 @@ public:
|
|||
enum { MemoryBlockMask = 0x7FFFFF };
|
||||
|
||||
private:
|
||||
uint32_t controlReg;
|
||||
uint32_t translationTableBase;
|
||||
uint32_t domainAccessControl;
|
||||
uint16_t pendingInterrupts = 0;
|
||||
uint16_t interruptMask = 0;
|
||||
uint32_t portValues = 0;
|
||||
|
@ -27,19 +24,14 @@ private:
|
|||
uint32_t kScan = 0;
|
||||
uint32_t rtc = 0;
|
||||
|
||||
int64_t passedCycles = 0;
|
||||
int64_t nextTickAt = 0;
|
||||
Timer tc1, tc2;
|
||||
UART uart1, uart2;
|
||||
Etna etna;
|
||||
bool asleep = false;
|
||||
Etna etna;
|
||||
bool halted = false, asleep = false;
|
||||
|
||||
std::unordered_set<uint32_t> _breakpoints;
|
||||
|
||||
struct ARMCore cpu;
|
||||
|
||||
inline bool isMMU() {
|
||||
return (controlReg & 1);
|
||||
}
|
||||
std::unordered_set<uint32_t> _breakpoints;
|
||||
|
||||
uint32_t getRTC();
|
||||
|
||||
|
@ -49,31 +41,15 @@ private:
|
|||
void writeReg32(uint32_t reg, uint32_t value);
|
||||
|
||||
public:
|
||||
bool isPhysAddressValid(uint32_t physAddress) const;
|
||||
|
||||
uint32_t readPhys8(uint32_t physAddress);
|
||||
uint32_t readPhys16(uint32_t physAddress);
|
||||
uint32_t readPhys32(uint32_t physAddress);
|
||||
void writePhys8(uint32_t physAddress, uint8_t value);
|
||||
void writePhys16(uint32_t physAddress, uint16_t value);
|
||||
void writePhys32(uint32_t physAddress, uint32_t value);
|
||||
|
||||
uint32_t virtToPhys(uint32_t virtAddress);
|
||||
|
||||
uint32_t readVirt8(uint32_t virtAddress) { return readPhys8(virtToPhys(virtAddress)); }
|
||||
uint32_t readVirt16(uint32_t virtAddress) { return readPhys16(virtToPhys(virtAddress)); }
|
||||
uint32_t readVirt32(uint32_t virtAddress) { return readPhys32(virtToPhys(virtAddress)); }
|
||||
void writeVirt8(uint32_t virtAddress, uint8_t value) { writePhys8(virtToPhys(virtAddress), value); }
|
||||
void writeVirt16(uint32_t virtAddress, uint16_t value) { writePhys16(virtToPhys(virtAddress), value); }
|
||||
void writeVirt32(uint32_t virtAddress, uint32_t value) { writePhys32(virtToPhys(virtAddress), value); }
|
||||
bool isPhysAddressValid(uint32_t addr) const;
|
||||
MaybeU32 readPhysical(uint32_t physAddr, ValueSize valueSize) override;
|
||||
bool writePhysical(uint32_t value, uint32_t physAddr, ValueSize valueSize) override;
|
||||
|
||||
const uint8_t *getLCDBuffer() const;
|
||||
|
||||
private:
|
||||
bool configured = false;
|
||||
void configure();
|
||||
void configureMemoryBindings();
|
||||
void configureCpuHandlers();
|
||||
|
||||
void printRegs();
|
||||
const char *identifyObjectCon(uint32_t ptr);
|
||||
|
@ -90,8 +66,7 @@ public:
|
|||
Emu();
|
||||
void loadROM(const char *path);
|
||||
void dumpRAM(const char *path);
|
||||
void executeUntil(int64_t cycles);
|
||||
int64_t currentCycles() const { return cpu.cycles; }
|
||||
uint32_t getGPR(int index) const { return cpu.gprs[index]; }
|
||||
std::unordered_set<uint32_t> &breakpoints() { return _breakpoints; }
|
||||
void executeUntil(int64_t cycles);
|
||||
std::unordered_set<uint32_t> &breakpoints() { return _breakpoints; }
|
||||
uint64_t currentCycles() const { return passedCycles; }
|
||||
};
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#pragma once
|
||||
#include "wind.h"
|
||||
#include "arm.h"
|
||||
#include "arm710a.h"
|
||||
#include <stdio.h>
|
||||
|
||||
struct Timer {
|
||||
struct ARMCore *cpu;
|
||||
ARM710a *cpu;
|
||||
|
||||
enum {
|
||||
TICK_INTERVAL_SLOW = CLOCK_SPEED / 2000,
|
||||
|
@ -50,7 +50,7 @@ struct Timer {
|
|||
};
|
||||
|
||||
struct UART {
|
||||
struct ARMCore *cpu;
|
||||
ARM710a *cpu;
|
||||
|
||||
enum {
|
||||
IntRx = 1,
|
||||
|
@ -105,7 +105,7 @@ struct UART {
|
|||
// UART0INTM?
|
||||
// UART0INTR?
|
||||
} else {
|
||||
printf("unhandled 8bit uart read %x at pc=%08x lr=%08x\n", reg, cpu->gprs[ARM_PC], cpu->gprs[ARM_LR]);
|
||||
printf("unhandled 8bit uart read %x at pc=%08x lr=%08x\n", reg, cpu->getGPR(15), cpu->getGPR(14));
|
||||
return 0xFF;
|
||||
}
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ struct UART {
|
|||
// we pretend we are never busy, never have full fifo
|
||||
return FlagReceiveFifoEmpty;
|
||||
} else {
|
||||
printf("unhandled 32bit uart read %x at pc=%08x lr=%08x\n", reg, cpu->gprs[ARM_PC], cpu->gprs[ARM_LR]);
|
||||
printf("unhandled 32bit uart read %x at pc=%08x lr=%08x\n", reg, cpu->getGPR(15), cpu->getGPR(14));
|
||||
return 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ struct UART {
|
|||
printf("uart interruptmask updated: %d\n", value);
|
||||
// UART0INTR?
|
||||
} else {
|
||||
printf("unhandled 8bit uart write %x value %02x at pc=%08x lr=%08x\n", reg, value, cpu->gprs[ARM_PC], cpu->gprs[ARM_LR]);
|
||||
printf("unhandled 8bit uart write %x value %02x at pc=%08x lr=%08x\n", reg, value, cpu->getGPR(15), cpu->getGPR(14));
|
||||
}
|
||||
}
|
||||
void writeReg32(uint32_t reg, uint32_t value) {
|
||||
|
@ -151,7 +151,7 @@ struct UART {
|
|||
printf("uart interrupts %x -> %x\n", interrupts, value);
|
||||
interrupts = value;
|
||||
} else {
|
||||
printf("unhandled 32bit uart write %x value %08x at pc=%08x lr=%08x\n", reg, value, cpu->gprs[ARM_PC], cpu->gprs[ARM_LR]);
|
||||
printf("unhandled 32bit uart write %x value %08x at pc=%08x lr=%08x\n", reg, value, cpu->getGPR(15), cpu->getGPR(14));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -20,7 +20,8 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
|||
# You can also select to disable deprecated APIs only up to a certain version of Qt.
|
||||
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
CONFIG += c++11
|
||||
CONFIG += c++17
|
||||
QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.14
|
||||
|
||||
SOURCES += \
|
||||
main.cpp \
|
||||
|
|
|
@ -12,13 +12,13 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
ui->setupUi(this);
|
||||
|
||||
emu = new Emu;
|
||||
emu->loadROM("/Users/ash/src/psion/Sys$rom.bin");
|
||||
emu->loadROM("/Users/ash/src/psion/Sys$rom.bin");
|
||||
|
||||
timer = new QTimer(this);
|
||||
timer->setInterval(1000/64);
|
||||
connect(timer, SIGNAL(timeout()), SLOT(execTimer()));
|
||||
|
||||
updateScreen();
|
||||
updateScreen();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
|
@ -30,10 +30,10 @@ void MainWindow::updateScreen()
|
|||
{
|
||||
ui->cycleCounter->setText(QString("Cycles: %1").arg(emu->currentCycles()));
|
||||
|
||||
updateMemory();
|
||||
updateMemory();
|
||||
|
||||
ui->regsLabel->setText(
|
||||
QString("R0: %1 / R1: %2 / R2: %3 / R3: %4 / R4: %5 / R5: %6 / R6: %7 / R7: %8\nR8: %9 / R9: %10 / R10:%11 / R11:%12 / R12:%13 / SP: %14 / LR: %15 / PC: %16")
|
||||
QString("R0: %1 / R1: %2 / R2: %3 / R3: %4 / R4: %5 / R5: %6 / R6: %7 / R7: %8\nR8: %9 / R9: %10 / R10:%11 / R11:%12 / R12:%13 / SP: %14 / LR: %15 / PC: %16")
|
||||
.arg(emu->getGPR(0), 8, 16)
|
||||
.arg(emu->getGPR(1), 8, 16)
|
||||
.arg(emu->getGPR(2), 8, 16)
|
||||
|
@ -56,24 +56,27 @@ void MainWindow::updateScreen()
|
|||
const int context = 8 * 4;
|
||||
uint32_t pc = emu->getGPR(15) - 4;
|
||||
uint32_t minCode = pc - context;
|
||||
if (minCode >= (UINT32_MAX - context))
|
||||
if (minCode >= (UINT32_MAX - context))
|
||||
minCode = 0;
|
||||
uint32_t maxCode = pc + context;
|
||||
if (maxCode < context)
|
||||
if (maxCode < context)
|
||||
maxCode = UINT32_MAX;
|
||||
|
||||
QStringList codeLines;
|
||||
for (uint32_t addr = minCode; addr >= minCode && addr <= maxCode; addr += 4) {
|
||||
const char *prefix = (addr == pc) ? "==>" : " ";
|
||||
QStringList codeLines;
|
||||
for (uint32_t addr = minCode; addr >= minCode && addr <= maxCode; addr += 4) {
|
||||
const char *prefix = (addr == pc) ? "==>" : " ";
|
||||
struct ARMInstructionInfo info;
|
||||
char buffer[512];
|
||||
|
||||
uint32_t opcode = emu->readVirt32(addr);
|
||||
ARMDecodeARM(opcode, &info);
|
||||
ARMDisassemble(&info, addr, buffer, sizeof(buffer));
|
||||
codeLines.append(QString("%1 %2 | %3 | %4").arg(prefix).arg(addr, 8, 16).arg(opcode, 8, 16).arg(buffer));
|
||||
auto result = emu->readVirtual(addr, ARM710a::V32);
|
||||
if (result.first.has_value()) {
|
||||
uint32_t opcode = result.first.value();
|
||||
ARMDecodeARM(opcode, &info);
|
||||
ARMDisassemble(&info, addr, buffer, sizeof(buffer));
|
||||
codeLines.append(QString("%1 %2 | %3 | %4").arg(prefix).arg(addr, 8, 16).arg(opcode, 8, 16).arg(buffer));
|
||||
}
|
||||
}
|
||||
ui->codeLabel->setText(codeLines.join('\n'));
|
||||
ui->codeLabel->setText(codeLines.join('\n'));
|
||||
|
||||
// now, the actual screen
|
||||
const uint8_t *lcdBuf = emu->getLCDBuffer();
|
||||
|
@ -95,12 +98,12 @@ void MainWindow::updateScreen()
|
|||
for (int x = 0; x < img.width(); x++) {
|
||||
uint8_t byte = lcdBuf[lineOffs + (x / ppb)];
|
||||
int shift = (x & (ppb - 1)) * bpp;
|
||||
int mask = (1 << bpp) - 1;
|
||||
int mask = (1 << bpp) - 1;
|
||||
int palIdx = (byte >> shift) & mask;
|
||||
int palValue = palette[palIdx];
|
||||
|
||||
palValue |= (palValue << 4);
|
||||
scanline[x] = palValue ^ 0xFF;
|
||||
palValue |= (palValue << 4);
|
||||
scanline[x] = palValue ^ 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -198,7 +201,7 @@ void MainWindow::keyPressEvent(QKeyEvent *event)
|
|||
void MainWindow::keyReleaseEvent(QKeyEvent *event)
|
||||
{
|
||||
int k = resolveKey(event->key());
|
||||
if (k >= 0)
|
||||
if (k >= 0)
|
||||
emu->keyboardKeys[k] = false;
|
||||
}
|
||||
|
||||
|
@ -243,71 +246,72 @@ void MainWindow::execTimer()
|
|||
|
||||
void MainWindow::on_addBreakButton_clicked()
|
||||
{
|
||||
uint32_t addr = ui->breakpointAddress->text().toUInt(nullptr, 16);
|
||||
emu->breakpoints().insert(addr);
|
||||
updateBreakpointsList();
|
||||
uint32_t addr = ui->breakpointAddress->text().toUInt(nullptr, 16);
|
||||
emu->breakpoints().insert(addr);
|
||||
updateBreakpointsList();
|
||||
}
|
||||
|
||||
void MainWindow::on_removeBreakButton_clicked()
|
||||
{
|
||||
uint32_t addr = ui->breakpointAddress->text().toUInt(nullptr, 16);
|
||||
emu->breakpoints().erase(addr);
|
||||
updateBreakpointsList();
|
||||
uint32_t addr = ui->breakpointAddress->text().toUInt(nullptr, 16);
|
||||
emu->breakpoints().erase(addr);
|
||||
updateBreakpointsList();
|
||||
}
|
||||
|
||||
void MainWindow::updateBreakpointsList()
|
||||
{
|
||||
ui->breakpointsList->clear();
|
||||
for (uint32_t addr : emu->breakpoints()) {
|
||||
ui->breakpointsList->addItem(QString::number(addr, 16));
|
||||
}
|
||||
ui->breakpointsList->clear();
|
||||
for (uint32_t addr : emu->breakpoints()) {
|
||||
ui->breakpointsList->addItem(QString::number(addr, 16));
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_memoryViewAddress_textEdited(const QString &)
|
||||
{
|
||||
updateMemory();
|
||||
updateMemory();
|
||||
}
|
||||
|
||||
void MainWindow::updateMemory()
|
||||
{
|
||||
uint32_t virtBase = ui->memoryViewAddress->text().toUInt(nullptr, 16) & ~0xFF;
|
||||
uint32_t physBase = emu->virtToPhys(virtBase);
|
||||
bool ok = (physBase != 0xFFFFFFFF) && emu->isPhysAddressValid(physBase);
|
||||
if (ok && (virtBase != physBase))
|
||||
ui->physicalAddressLabel->setText(QStringLiteral("Physical: %1").arg(physBase, 8, 16, QLatin1Char('0')));
|
||||
uint32_t virtBase = ui->memoryViewAddress->text().toUInt(nullptr, 16) & ~0xFF;
|
||||
auto physBaseOpt = emu->virtToPhys(virtBase);
|
||||
auto physBase = physBaseOpt.value_or(0xFFFFFFFF);
|
||||
bool ok = physBaseOpt.has_value() && emu->isPhysAddressValid(physBase);
|
||||
if (ok && (virtBase != physBase))
|
||||
ui->physicalAddressLabel->setText(QStringLiteral("Physical: %1").arg(physBase, 8, 16, QLatin1Char('0')));
|
||||
|
||||
uint8_t block[0x100];
|
||||
if (ok) {
|
||||
for (int i = 0; i < 0x100; i++) {
|
||||
block[i] = emu->readPhys8(physBase + i);
|
||||
}
|
||||
}
|
||||
uint8_t block[0x100];
|
||||
if (ok) {
|
||||
for (int i = 0; i < 0x100; i++) {
|
||||
block[i] = emu->readPhysical(physBase + i, ARM710a::V8).value();
|
||||
}
|
||||
}
|
||||
|
||||
QStringList output;
|
||||
for (int row = 0; row < 16; row++) {
|
||||
QString outLine;
|
||||
outLine.reserve(8 + 2 + (2 * 16) + 3 + 16);
|
||||
outLine.append(QStringLiteral("%1 |").arg(virtBase + (row * 16), 8, 16));
|
||||
for (int col = 0; col < 16; col++) {
|
||||
if (ok)
|
||||
outLine.append(QStringLiteral(" %1").arg(block[row*16+col], 2, 16, QLatin1Char('0')));
|
||||
else
|
||||
outLine.append(QStringLiteral(" ??"));
|
||||
}
|
||||
outLine.append(QStringLiteral(" | "));
|
||||
for (int col = 0; col < 16; col++) {
|
||||
uint8_t byte = block[row*16+col];
|
||||
if (!ok)
|
||||
outLine.append('?');
|
||||
else if (byte >= 0x20 && byte <= 0x7E)
|
||||
outLine.append(byte);
|
||||
else
|
||||
outLine.append('.');
|
||||
}
|
||||
output.append(outLine);
|
||||
}
|
||||
QStringList output;
|
||||
for (int row = 0; row < 16; row++) {
|
||||
QString outLine;
|
||||
outLine.reserve(8 + 2 + (2 * 16) + 3 + 16);
|
||||
outLine.append(QStringLiteral("%1 |").arg(virtBase + (row * 16), 8, 16));
|
||||
for (int col = 0; col < 16; col++) {
|
||||
if (ok)
|
||||
outLine.append(QStringLiteral(" %1").arg(block[row*16+col], 2, 16, QLatin1Char('0')));
|
||||
else
|
||||
outLine.append(QStringLiteral(" ??"));
|
||||
}
|
||||
outLine.append(QStringLiteral(" | "));
|
||||
for (int col = 0; col < 16; col++) {
|
||||
uint8_t byte = block[row*16+col];
|
||||
if (!ok)
|
||||
outLine.append('?');
|
||||
else if (byte >= 0x20 && byte <= 0x7E)
|
||||
outLine.append(byte);
|
||||
else
|
||||
outLine.append('.');
|
||||
}
|
||||
output.append(outLine);
|
||||
}
|
||||
|
||||
ui->memoryViewLabel->setText(output.join('\n'));
|
||||
ui->memoryViewLabel->setText(output.join('\n'));
|
||||
}
|
||||
|
||||
void MainWindow::on_memoryAdd1_clicked() { adjustMemoryAddress(1); }
|
||||
|
@ -320,24 +324,24 @@ void MainWindow::on_memorySub10_clicked() { adjustMemoryAddress(-0x10); }
|
|||
void MainWindow::on_memorySub100_clicked() { adjustMemoryAddress(-0x100); }
|
||||
|
||||
void MainWindow::adjustMemoryAddress(int offset) {
|
||||
uint32_t address = ui->memoryViewAddress->text().toUInt(nullptr, 16);
|
||||
address += offset;
|
||||
ui->memoryViewAddress->setText(QString("%1").arg(address, 8, 16, QLatin1Char('0')));
|
||||
updateMemory();
|
||||
uint32_t address = ui->memoryViewAddress->text().toUInt(nullptr, 16);
|
||||
address += offset;
|
||||
ui->memoryViewAddress->setText(QString("%1").arg(address, 8, 16, QLatin1Char('0')));
|
||||
updateMemory();
|
||||
}
|
||||
|
||||
void MainWindow::on_writeByteButton_clicked()
|
||||
{
|
||||
uint32_t address = ui->memoryViewAddress->text().toUInt(nullptr, 16);
|
||||
uint8_t value = (uint8_t)ui->memoryWriteValue->text().toUInt(nullptr, 16);
|
||||
emu->writeVirt8(address, value);
|
||||
updateMemory();
|
||||
uint32_t address = ui->memoryViewAddress->text().toUInt(nullptr, 16);
|
||||
uint8_t value = (uint8_t)ui->memoryWriteValue->text().toUInt(nullptr, 16);
|
||||
emu->writeVirtual(value, address, ARM710a::V8);
|
||||
updateMemory();
|
||||
}
|
||||
|
||||
void MainWindow::on_writeDwordButton_clicked()
|
||||
{
|
||||
uint32_t address = ui->memoryViewAddress->text().toUInt(nullptr, 16);
|
||||
uint32_t value = ui->memoryWriteValue->text().toUInt(nullptr, 16);
|
||||
emu->writeVirt32(address, value);
|
||||
updateMemory();
|
||||
uint32_t address = ui->memoryViewAddress->text().toUInt(nullptr, 16);
|
||||
uint32_t value = ui->memoryWriteValue->text().toUInt(nullptr, 16);
|
||||
emu->writeVirtual(value, address, ARM710a::V32);
|
||||
updateMemory();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue