IOS: add Starlet LLE milestone through HBC

Boot original Wii firmware through boot0, boot1, boot2 and IOS on an emulated ARM Starlet. Model the required IPC, memory, SD, USB and Bluetooth hardware behavior, including IOS reload into IOS58, and add focused tests, launch utilities and architecture documentation.
This commit is contained in:
Yaya48 committed 2026-08-25 17:11:49 +02:00
1 parent 38e70fda65
commit a77c156bc8
46 files changed
+10556 -158

No files matched your search

@@ -0,0 +1,841 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <vector>
#include <gtest/gtest.h>
#include "Common/CommonTypes.h"
#include "Core/Core.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/ARMCore.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
namespace IOS::LLE
{
namespace
{
// Native-width bus counters also verify that the CPU's generation-tagged
// caches bypass redundant traffic without changing architectural results.
class TestBus final : public ARMBus
{
public:
explicit TestBus(size_t size = 0x1000) : m_memory(size) {}
u8 Read8(u32 address) override
{
++m_read8_count;
const size_t offset = ToOffset(address);
EXPECT_LT(offset, m_memory.size());
return offset < m_memory.size() ? m_memory[offset] : 0;
}
u16 Read16(u32 address) override
{
++m_read16_count;
const size_t offset = ToOffset(address);
EXPECT_LT(offset + 1, m_memory.size());
if (offset + 1 >= m_memory.size())
return 0;
return static_cast<u16>((static_cast<u16>(m_memory[offset]) << 8) | m_memory[offset + 1]);
}
u32 Read32(u32 address) override
{
++m_read32_count;
const size_t offset = ToOffset(address);
EXPECT_LT(offset + 3, m_memory.size());
if (offset + 3 >= m_memory.size())
return 0;
return (static_cast<u32>(m_memory[offset]) << 24) |
(static_cast<u32>(m_memory[offset + 1]) << 16) |
(static_cast<u32>(m_memory[offset + 2]) << 8) | m_memory[offset + 3];
}
void Write8(u32 address, u8 value) override
{
const size_t offset = ToOffset(address);
ASSERT_LT(offset, m_memory.size());
m_memory[offset] = value;
}
void AdvanceCycles(u64 cycles) override
{
m_cycles += cycles;
++m_advance_calls;
}
bool IsIdlePollAddress(u32 address, u32 size) const override
{
const size_t offset = ToOffset(address);
return m_idle_poll_safe && size != 0 && offset <= m_memory.size() &&
size <= m_memory.size() - offset;
}
void SetIdlePollSafe(bool safe) { m_idle_poll_safe = safe; }
void WriteARM(u32 address, u32 instruction)
{
const size_t offset = ToOffset(address);
m_memory[offset] = static_cast<u8>(instruction >> 24);
m_memory[offset + 1] = static_cast<u8>(instruction >> 16);
m_memory[offset + 2] = static_cast<u8>(instruction >> 8);
m_memory[offset + 3] = static_cast<u8>(instruction);
}
void WriteThumb(u32 address, u16 instruction)
{
const size_t offset = ToOffset(address);
m_memory[offset] = static_cast<u8>(instruction >> 8);
m_memory[offset + 1] = static_cast<u8>(instruction);
}
u8 operator[](size_t address) const { return m_memory[address]; }
u64 GetCycles() const { return m_cycles; }
u64 GetAdvanceCalls() const { return m_advance_calls; }
u64 GetRead8Count() const { return m_read8_count; }
u64 GetRead16Count() const { return m_read16_count; }
u64 GetRead32Count() const { return m_read32_count; }
void ResetReadCounts()
{
m_read8_count = 0;
m_read16_count = 0;
m_read32_count = 0;
}
private:
static size_t ToOffset(u32 address)
{
return address >= 0xffff0000 ? address - 0xffff0000 : address;
}
std::vector<u8> m_memory;
u64 m_cycles = 0;
u64 m_advance_calls = 0;
u64 m_read8_count = 0;
u64 m_read16_count = 0;
u64 m_read32_count = 0;
bool m_idle_poll_safe = true;
};
TEST(WiiIPCCtrlRegister, ProducerBitsRemainLatchedUntilPeerAcknowledges)
{
IOS::CtrlRegister control;
control.ppc(0x08); // X2: completion acknowledgement.
control.ppc(0x01); // X1: next request must not clear X2.
EXPECT_EQ(control.arm() & 0x06, 0x06);
control.arm(0x06); // Starlet clears X1 and X2.
EXPECT_EQ(control.arm() & 0x06, 0x00);
control.arm(0x08); // Y2: request acknowledgement.
control.arm(0x01); // Y1: reply must not clear Y2.
EXPECT_EQ(control.ppc() & 0x06, 0x06);
control.ppc(0x06); // Broadway clears Y1 and Y2.
EXPECT_EQ(control.ppc() & 0x06, 0x00);
}
TEST(StarletTimer, ZeroDelayAlarmMatchesImmediatelyAndUsesIRQW1C)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 timer = hardware_base + 0x10;
constexpr u32 alarm = hardware_base + 0x14;
constexpr u32 arm_irq_flag = hardware_base + 0x38;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
const auto read_word = [&memory](u32 address) {
return static_cast<u32>(memory.Read8(address)) << 24 |
static_cast<u32>(memory.Read8(address + 1)) << 16 |
static_cast<u32>(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3);
};
const auto write_word = [&memory](u32 address, u32 value) {
memory.Write8(address, static_cast<u8>(value >> 24));
memory.Write8(address + 1, static_cast<u8>(value >> 16));
memory.Write8(address + 2, static_cast<u8>(value >> 8));
memory.Write8(address + 3, static_cast<u8>(value));
};
memory.AdvanceCycles(405);
ASSERT_EQ(read_word(timer), 32u);
write_word(alarm, read_word(timer));
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
// Reprogramming the comparator does not acknowledge an already latched timer
// interrupt.
write_word(alarm, 64);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
write_word(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, 0u);
memory.AdvanceCycles(404);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, 0u);
memory.AdvanceCycles(1);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
}
TEST(StarletAHBPROT, OriginalIOSMaskIsPreservedForBroadway)
{
constexpr u32 ahbprot = 0x0d800064;
constexpr u32 broadway_access_mask = 0x80000dfe;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
const auto read_word = [&memory](u32 address) {
return static_cast<u32>(memory.Read8(address)) << 24 |
static_cast<u32>(memory.Read8(address + 1)) << 16 |
static_cast<u32>(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3);
};
const auto write_word = [&memory](u32 address, u32 value) {
memory.Write8(address, static_cast<u8>(value >> 24));
memory.Write8(address + 1, static_cast<u8>(value >> 16));
memory.Write8(address + 2, static_cast<u8>(value >> 8));
memory.Write8(address + 3, static_cast<u8>(value));
};
// boot2 enables every permission. The System Menu's IOS clears Broadway's permissions, then
// IOS58 restores them when launching HBC because its TMD requests full hardware access.
EXPECT_EQ(read_word(ahbprot), 0xffffffffu);
write_word(ahbprot, 0xffffffffu & ~broadway_access_mask);
EXPECT_EQ(read_word(ahbprot), 0x7ffff201u);
write_word(ahbprot, read_word(ahbprot) | broadway_access_mask);
EXPECT_EQ(read_word(ahbprot), 0xffffffffu);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x64), 0xffffffffu);
}
TEST(StarletSDHC, EmptyControllerResetsAndStabilizesItsClock)
{
constexpr u32 base = 0x0d070000;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
StarletMemory memory(system);
memory.Reset();
const auto read_word = [&memory](u32 address) {
return static_cast<u32>(memory.Read8(address)) << 24 |
static_cast<u32>(memory.Read8(address + 1)) << 16 |
static_cast<u32>(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3);
};
const auto write_word = [&memory](u32 address, u32 value) {
memory.Write8(address, static_cast<u8>(value >> 24));
memory.Write8(address + 1, static_cast<u8>(value >> 16));
memory.Write8(address + 2, static_cast<u8>(value >> 8));
memory.Write8(address + 3, static_cast<u8>(value));
};
EXPECT_EQ(read_word(base + 0x40), 0x01603201u);
EXPECT_EQ(read_word(base + 0x24), 1U << 17);
write_word(base + 0x2c, 0x00000001); // Enable the internal clock.
EXPECT_EQ(read_word(base + 0x2c), 0x00000003u);
write_word(base + 0x2c, 0x01000003); // Reset-all in the reversed high byte.
EXPECT_EQ(read_word(base + 0x2c), 0x00000003u);
write_word(base + 0x34, 0xffffffff); // Enable normal and error statuses.
write_word(base + 0x38,
0xffffffff); // Route both status halves to the SD IRQ.
write_word(base + 0x08, 0x000001aa); // CMD8 argument.
write_word(base + 0x0c,
0x08020000); // CMD8 against an empty slot must time out.
EXPECT_EQ(read_word(base + 0x30), 0x00018000u);
write_word(base + 0x30, 0xffffffff); // Normal/error interrupt status W1C.
EXPECT_EQ(read_word(base + 0x30), 0u);
write_word(base + 0x0c,
0); // CMD0 is valid without a card and completes normally.
EXPECT_EQ(read_word(base + 0x30), 1u);
}
TEST(StarletEHCI, HollywoodResetImageAndPhyState)
{
constexpr u32 base = 0x0d040000;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
StarletMemory memory(system);
memory.Reset();
const auto read_word = [&memory](u32 address) {
return static_cast<u32>(memory.Read8(address)) << 24 |
static_cast<u32>(memory.Read8(address + 1)) << 16 |
static_cast<u32>(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3);
};
const auto write_word = [&memory](u32 address, u32 value) {
memory.Write8(address, static_cast<u8>(value >> 24));
memory.Write8(address + 1, static_cast<u8>(value >> 16));
memory.Write8(address + 2, static_cast<u8>(value >> 8));
memory.Write8(address + 3, static_cast<u8>(value));
};
EXPECT_EQ(read_word(base + 0x10), 0x00080000u);
EXPECT_EQ(read_word(base + 0x14), 0x00001000u);
write_word(base + 0xa4, 0x00004023);
write_word(base + 0xcc, 0x00000891);
write_word(base + 0x10, read_word(base + 0x10) | 2); // HCRESET self-clears.
EXPECT_EQ(read_word(base + 0x10), 0x00080000u);
EXPECT_EQ(read_word(base + 0x14), 0x00001000u);
EXPECT_EQ(read_word(base + 0xa4), 0x00004023u);
EXPECT_EQ(read_word(base + 0xcc), 0x00000891u);
}
TEST(StarletUSB, IOSReloadReturnsHostControllersToResetState)
{
constexpr u32 ehci = 0x0d040000;
constexpr u32 ohci0 = 0x0d050000;
constexpr u32 ohci1 = 0x0d060000;
constexpr u32 usb_force_reset = 0x0d800088;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
StarletMemory memory(system);
memory.Reset();
const auto read_word = [&memory](u32 address) {
return static_cast<u32>(memory.Read8(address)) << 24 |
static_cast<u32>(memory.Read8(address + 1)) << 16 |
static_cast<u32>(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3);
};
const auto write_word = [&memory](u32 address, u32 value) {
memory.Write8(address, static_cast<u8>(value >> 24));
memory.Write8(address + 1, static_cast<u8>(value >> 16));
memory.Write8(address + 2, static_cast<u8>(value >> 8));
memory.Write8(address + 3, static_cast<u8>(value));
};
write_word(ehci + 0x10, 0x00080001); // Run the high-speed controller.
write_word(ohci0 + 0x04, 0x00000283); // IOS80 leaves both OHCIs operational.
write_word(ohci1 + 0x04, 0x00000283);
ASSERT_EQ(read_word(ohci0 + 0x04), 0x00000283u);
ASSERT_EQ(read_word(ohci1 + 0x04), 0x00000283u);
// The IOS kernel asserts Hollywood's USB force-reset lines before loading
// the replacement IOS host-controller modules.
write_word(usb_force_reset, 0x000000fe);
EXPECT_EQ(read_word(ehci + 0x10), 0x00080000u);
EXPECT_EQ(read_word(ehci + 0x14), 0x00001000u);
EXPECT_EQ(read_word(ohci0), 0x10u);
EXPECT_EQ(read_word(ohci1), 0x10u);
EXPECT_EQ(read_word(ohci0 + 0x04), 0u);
EXPECT_EQ(read_word(ohci1 + 0x04), 0u);
}
TEST(StarletOHCI, InternalBluetoothPortUsesHardwareRootHubSemantics)
{
constexpr u32 ohci0 = 0x0d050000;
constexpr u32 ohci1 = 0x0d060000;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
StarletMemory memory(system);
memory.Reset();
const auto read_word = [&memory](u32 address) {
return static_cast<u32>(memory.Read8(address)) << 24 |
static_cast<u32>(memory.Read8(address + 1)) << 16 |
static_cast<u32>(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3);
};
const auto write_word = [&memory](u32 address, u32 value) {
memory.Write8(address, static_cast<u8>(value >> 24));
memory.Write8(address + 1, static_cast<u8>(value >> 16));
memory.Write8(address + 2, static_cast<u8>(value >> 8));
memory.Write8(address + 3, static_cast<u8>(value));
};
const auto read_le_word = [&memory](u32 address) {
return static_cast<u32>(memory.Read8(address)) |
static_cast<u32>(memory.Read8(address + 1)) << 8 |
static_cast<u32>(memory.Read8(address + 2)) << 16 |
static_cast<u32>(memory.Read8(address + 3)) << 24;
};
const auto write_le_word = [&memory](u32 address, u32 value) {
memory.Write8(address, static_cast<u8>(value));
memory.Write8(address + 1, static_cast<u8>(value >> 8));
memory.Write8(address + 2, static_cast<u8>(value >> 16));
memory.Write8(address + 3, static_cast<u8>(value >> 24));
};
EXPECT_EQ(read_word(ohci0), 0x10u);
EXPECT_EQ(read_word(ohci1), 0x10u);
EXPECT_EQ(read_word(ohci0 + 0x48) & 0xff, 2u);
EXPECT_EQ(read_word(ohci1 + 0x48) & 0xff, 1u);
EXPECT_EQ(read_word(ohci0 + 0x48) >> 24, 2u);
EXPECT_EQ(read_word(ohci1 + 0x48) >> 24, 2u);
EXPECT_EQ(read_word(ohci0 + 0x4c), 0u);
EXPECT_EQ(read_word(ohci1 + 0x4c),
2u); // Port 1 is the non-removable Bluetooth daughterboard.
EXPECT_EQ(read_word(ohci0 + 0x54), 0u);
EXPECT_EQ(read_word(ohci1 + 0x54), 0x00010101u);
write_word(ohci1 + 0x10, 0x80000040); // Master + root-hub status-change IRQ.
EXPECT_EQ(read_word(ohci1 + 0x10), 0x80000040u);
write_word(ohci1 + 0x04,
2U << 6); // USB operational, so its 1 ms frame clock runs.
write_word(ohci1 + 0x54, 0x00010000); // Clear connection-status-change.
EXPECT_EQ(read_word(ohci1 + 0x54), 0x00000101u);
write_word(ohci1 + 0x0c, 0x40); // HcInterruptStatus is W1C.
EXPECT_EQ(read_word(ohci1 + 0x0c) & 0x40, 0u);
write_word(ohci1 + 0x54, 1U << 4); // Reset the attached Bluetooth port.
EXPECT_EQ(read_word(ohci1 + 0x54),
0x00000111u); // Reset signaling is active until the next poll.
memory.AdvanceCycles(243000);
EXPECT_EQ(read_word(ohci1 + 0x54), 0x00000111u);
memory.AdvanceCycles(243000);
EXPECT_EQ(read_word(ohci1 + 0x54),
0x00100103u); // Enabled + reset complete change.
EXPECT_EQ(read_word(ohci1 + 0x0c) & 0x40, 0x40u);
memory.AdvanceCycles(243000);
EXPECT_EQ(read_word(ohci1 + 0x3c), 3u);
write_word(ohci1 + 0x08, 1); // Host-controller reset self-clears.
EXPECT_EQ(read_word(ohci1 + 0x08), 0u);
EXPECT_EQ(read_word(ohci1 + 0x34), 0x27782edfu);
EXPECT_EQ(read_word(ohci1 + 0x54), 0x00010101u);
// Queue a complete GET_DESCRIPTOR(Device) control transfer using the same
// little-endian ED/TD/HCCA structures that the original IOS OHCI module
// builds in Starlet memory.
constexpr u32 hcca = StarletMemory::SRAM_BASE;
constexpr u32 endpoint = hcca + 0x100;
constexpr u32 setup_td = hcca + 0x200;
constexpr u32 data_td = hcca + 0x210;
constexpr u32 status_td = hcca + 0x220;
constexpr u32 tail_td = hcca + 0x230;
constexpr u32 setup_buffer = hcca + 0x300;
constexpr u32 data_buffer = hcca + 0x340;
const std::array<u8, 8> get_device_descriptor = {0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x40, 0x00};
for (size_t i = 0; i < get_device_descriptor.size(); ++i)
memory.Write8(setup_buffer + static_cast<u32>(i), get_device_descriptor[i]);
write_word(ohci1 + 0x04,
2U << 6); // Restart the frame clock after controller reset.
write_word(ohci1 + 0x54, 1U << 4); // Reset the device to USB address zero.
memory.AdvanceCycles(2 * 243000);
write_le_word(endpoint, 64U << 16); // Address 0, endpoint 0, max packet 64.
write_le_word(endpoint + 4, tail_td);
write_le_word(endpoint + 8, setup_td);
write_le_word(endpoint + 12, 0);
write_le_word(setup_td, 0xf0000000); // SETUP, condition code NotAccessed.
write_le_word(setup_td + 4, setup_buffer);
write_le_word(setup_td + 8, data_td);
write_le_word(setup_td + 12, setup_buffer + 7);
write_le_word(data_td, 0xf0140000); // Rounding + IN.
write_le_word(data_td + 4, data_buffer);
write_le_word(data_td + 8, status_td);
write_le_word(data_td + 12, data_buffer + 63);
write_le_word(status_td, 0xf0080000); // Zero-length OUT status stage.
write_le_word(status_td + 4, 0);
write_le_word(status_td + 8, tail_td);
write_le_word(status_td + 12, 0);
write_word(ohci1 + 0x18, hcca);
write_word(ohci1 + 0x20, endpoint);
write_word(ohci1 + 0x04, (2U << 6) | (1U << 4));
EXPECT_EQ(read_le_word(endpoint + 8) & ~0xfU, tail_td);
EXPECT_EQ(read_le_word(setup_td) >> 28, 0u);
EXPECT_EQ(read_le_word(data_td) >> 28, 0u);
EXPECT_EQ(read_le_word(status_td) >> 28, 0u);
EXPECT_EQ(memory.Read8(data_buffer), 0x12u);
EXPECT_EQ(memory.Read8(data_buffer + 1), 0x01u);
EXPECT_EQ(memory.Read8(data_buffer + 8), 0x7eu);
EXPECT_EQ(memory.Read8(data_buffer + 9), 0x05u);
EXPECT_EQ(read_le_word(hcca + 0x84), status_td);
EXPECT_EQ(read_word(ohci1 + 0x0c) & (1U << 1), 1U << 1);
// A busy IOS can leave WDH pending while more transfers finish. Those TDs
// must remain in HcDoneHead instead of overwriting the unconsumed
// HCCA.DoneHead list.
constexpr u32 setup_td2 = hcca + 0x400;
constexpr u32 data_td2 = hcca + 0x410;
constexpr u32 status_td2 = hcca + 0x420;
constexpr u32 tail_td2 = hcca + 0x430;
constexpr u32 setup_buffer2 = hcca + 0x500;
constexpr u32 data_buffer2 = hcca + 0x540;
for (size_t i = 0; i < get_device_descriptor.size(); ++i)
memory.Write8(setup_buffer2 + static_cast<u32>(i), get_device_descriptor[i]);
write_le_word(endpoint + 4, tail_td2);
write_le_word(endpoint + 8, setup_td2);
write_le_word(setup_td2, 0xf0000000);
write_le_word(setup_td2 + 4, setup_buffer2);
write_le_word(setup_td2 + 8, data_td2);
write_le_word(setup_td2 + 12, setup_buffer2 + 7);
write_le_word(data_td2, 0xf0140000);
write_le_word(data_td2 + 4, data_buffer2);
write_le_word(data_td2 + 8, status_td2);
write_le_word(data_td2 + 12, data_buffer2 + 63);
write_le_word(status_td2, 0xf0080000);
write_le_word(status_td2 + 4, 0);
write_le_word(status_td2 + 8, tail_td2);
write_le_word(status_td2 + 12, 0);
write_word(ohci1 + 0x08, 1U << 1); // Ring the control-list doorbell.
EXPECT_EQ(read_le_word(endpoint + 8) & ~0xfU, tail_td2);
EXPECT_EQ(read_le_word(hcca + 0x84), status_td);
EXPECT_EQ(read_word(ohci1 + 0x30) & ~0xfU, status_td2);
write_word(ohci1 + 0x0c,
1U << 1); // IOS consumes and acknowledges the first done list.
memory.AdvanceCycles(243000);
EXPECT_EQ(read_le_word(hcca + 0x84), status_td2);
EXPECT_EQ(read_word(ohci1 + 0x0c) & (1U << 1), 1U << 1);
}
TEST(StarletARMCore, DataProcessingAndConditions)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe3a0102a); // mov r1, #42
bus.WriteARM(0x04, 0xe2812001); // add r2, r1, #1
bus.WriteARM(0x08, 0xe352002b); // cmp r2, #43
bus.WriteARM(0x0c, 0x13a03001); // movne r3, #1 (must be skipped)
bus.WriteARM(0x10, 0x03a03007); // moveq r3, #7
core.Run(5);
EXPECT_EQ(core.GetRegister(1), 42u);
EXPECT_EQ(core.GetRegister(2), 43u);
EXPECT_EQ(core.GetRegister(3), 7u);
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_Z, 0u);
EXPECT_EQ(core.GetRegister(15), 0x14u);
}
TEST(StarletARMCore, BigEndianLoadStore)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe5801004); // str r1, [r0, #4]
bus.WriteARM(0x04, 0xe5902004); // ldr r2, [r0, #4]
core.SetRegister(0, 0x100);
core.SetRegister(1, 0x11223344);
core.Run(2);
EXPECT_EQ(bus[0x104], 0x11);
EXPECT_EQ(bus[0x105], 0x22);
EXPECT_EQ(bus[0x106], 0x33);
EXPECT_EQ(bus[0x107], 0x44);
EXPECT_EQ(core.GetRegister(2), 0x11223344u);
}
TEST(StarletARMCore, VectorLiteralLoadMatchesBootROMShape)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe59ff018); // ldr pc, [pc, #24]
bus.WriteARM(0x20, 0x00000040); // reset handler address
bus.WriteARM(0x40, 0xe3a0102a); // mov r1, #42
core.Step();
EXPECT_EQ(core.GetRegister(15), 0x40u);
core.Step();
EXPECT_EQ(core.GetRegister(1), 42u);
}
TEST(StarletARMCore, LoadsToPCInterworkIntoThumb)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe59ff000); // ldr pc, [pc]
bus.WriteARM(0x08, 0x00000101); // Thumb entry address
bus.WriteThumb(0x100, 0x212a); // mov r1, #42
bus.WriteARM(0x200, 0xe8b08000); // ldmia r0!, {pc}
bus.WriteARM(0x300, 0x00000101); // Thumb entry address
core.Step();
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_T, 0u);
EXPECT_EQ(core.GetRegister(15), 0x100u);
core.Step();
EXPECT_EQ(core.GetRegister(1), 42u);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor));
core.SetRegister(0, 0x300);
core.SetRegister(15, 0x200);
core.Step();
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_T, 0u);
EXPECT_EQ(core.GetRegister(15), 0x100u);
}
TEST(StarletARMCore, BlockTransfersCanAccessTheUserBank)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe8de6001); // ldmia lr, {r0, sp, lr}^
bus.WriteARM(0x300, 0x11223344); // user r0
bus.WriteARM(0x304, 0x55667788); // user sp
bus.WriteARM(0x308, 0x99aabbcc); // user lr
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
core.SetRegister(13, 0x11111111);
core.SetRegister(14, 0x22222222);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor));
core.SetRegister(14, 0x300);
core.Step();
EXPECT_EQ(core.GetRegister(0), 0x11223344u);
EXPECT_EQ(core.GetRegister(14), 0x300u);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
EXPECT_EQ(core.GetRegister(13), 0x55667788u);
EXPECT_EQ(core.GetRegister(14), 0x99aabbccu);
}
TEST(StarletARMCore, BranchLinkAndExchange)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xeb000002); // bl 0x10
bus.WriteARM(0x10, 0xe12fff1e); // bx lr
core.Step();
EXPECT_EQ(core.GetRegister(14), 4u);
EXPECT_EQ(core.GetRegister(15), 0x10u);
core.Step();
EXPECT_EQ(core.GetRegister(15), 4u);
}
TEST(StarletARMCore, IRQUsesBankedRegistersAndSPSR)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0xffff0018, 0xe25ef004); // subs pc, lr, #4
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
core.SetRegister(13, 0x11110000);
core.SetRegister(15, 0x100);
core.SetIRQLine(true);
core.Step();
EXPECT_EQ(core.GetMode(), ARMCore::Mode::IRQ);
EXPECT_EQ(core.GetRegister(15), 0xffff0018u);
EXPECT_EQ(core.GetRegister(14), 0x104u);
core.SetIRQLine(false);
core.SetRegister(13, 0x22220000);
core.Step();
EXPECT_EQ(core.GetMode(), ARMCore::Mode::System);
EXPECT_EQ(core.GetRegister(15), 0x100u);
EXPECT_EQ(core.GetRegister(13), 0x11110000u);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::IRQ) | ARMCore::CPSR_I);
EXPECT_EQ(core.GetRegister(13), 0x22220000u);
}
TEST(StarletARMCore, CP15Identification)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xee100f10); // mrc p15, 0, r0, c0, c0, 0
core.Step();
EXPECT_EQ(core.GetRegister(0), 0x41069265u);
}
TEST(StarletARMCore, CP15TestAndCleanReportsUnmodeledCacheClean)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xee17ff7a); // mrc p15, 0, pc, c7, c10, 3
core.Step();
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_Z, 0u);
EXPECT_EQ(core.GetCPSR() & (ARMCore::CPSR_N | ARMCore::CPSR_C | ARMCore::CPSR_V), 0u);
EXPECT_EQ(core.GetRegister(15), 4u);
}
TEST(StarletARMCore, CP15InstructionCacheMaintenanceInvalidatesCachedCode)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe3a01001); // mov r1, #1
bus.WriteARM(0x04,
0xee070f15); // mcr p15, 0, r0, c7, c5, 0 (invalidate I-cache)
core.Step();
ASSERT_EQ(core.GetRegister(1), 1u);
bus.WriteARM(0x00,
0xe3a01002); // The cached instruction remains visible until CP15 c7.
core.SetRegister(15, 0);
core.Step();
EXPECT_EQ(core.GetRegister(1), 1u);
core.SetRegister(15, 4);
core.Step();
core.SetRegister(15, 0);
core.Step();
EXPECT_EQ(core.GetRegister(1), 2u);
}
TEST(StarletARMCore, WaitForInterruptFastForwardsAndWakesOnMaskedIRQ)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00,
0xee070f90); // mcr p15, 0, r0, c7, c0, 4 (wait for interrupt)
bus.WriteARM(0x04, 0xe2811001); // add r1, r1, #1
core.Step();
ASSERT_TRUE(core.IsWaitingForInterrupt());
EXPECT_EQ(core.GetRegister(15), 4u);
EXPECT_EQ(core.GetExecutedInstructions(), 1u);
EXPECT_EQ(core.RunCycles(10000), 10000u);
EXPECT_TRUE(core.IsWaitingForInterrupt());
EXPECT_EQ(core.GetRegister(15), 4u);
EXPECT_EQ(core.GetExecutedInstructions(), 1u);
EXPECT_EQ(bus.GetCycles(), 10001u);
// An asserted line wakes ARM926 even while CPSR.I masks exception entry.
core.SetIRQLine(true);
EXPECT_FALSE(core.IsWaitingForInterrupt());
core.Step();
EXPECT_EQ(core.GetRegister(1), 1u);
EXPECT_EQ(core.GetRegister(15), 8u);
}
TEST(StarletARMCore, WaitForInterruptWakesIntoUnmaskedIRQVector)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xee070f90); // mcr p15, 0, r0, c7, c0, 4
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
core.Step();
ASSERT_TRUE(core.IsWaitingForInterrupt());
core.SetIRQLine(true);
EXPECT_EQ(core.Step(), 3);
EXPECT_FALSE(core.IsWaitingForInterrupt());
EXPECT_EQ(core.GetMode(), ARMCore::Mode::IRQ);
EXPECT_EQ(core.GetRegister(15), 0xffff0018u);
}
TEST(StarletARMCore, ThumbExecutionAndConditions)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe12fff10); // bx r0
bus.WriteThumb(0x100, 0x212a); // mov r1, #42
bus.WriteThumb(0x102, 0x3101); // add r1, #1
bus.WriteThumb(0x104, 0x292b); // cmp r1, #43
bus.WriteThumb(0x106, 0xd100); // bne (must be skipped)
bus.WriteThumb(0x108, 0x2207); // mov r2, #7
core.SetRegister(0, 0x101);
core.Run(6);
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_T, 0u);
EXPECT_EQ(core.GetRegister(1), 43u);
EXPECT_EQ(core.GetRegister(2), 7u);
EXPECT_EQ(core.GetRegister(15), 0x10au);
}
TEST(StarletARMCore, MultiplyAndLongMultiply)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe0000291); // mul r0, r1, r2
bus.WriteARM(0x04, 0xe0834291); // umull r4, r3, r1, r2
core.SetRegister(1, 0x10000);
core.SetRegister(2, 0x10001);
core.Run(2);
EXPECT_EQ(core.GetRegister(0), 0x00010000u);
EXPECT_EQ(core.GetRegister(4), 0x00010000u);
EXPECT_EQ(core.GetRegister(3), 1u);
}
TEST(StarletARMCore, RunCyclesStopsAtBudget)
{
TestBus bus;
ARMCore core(bus);
bus.WriteARM(0x00, 0xe2800001); // add r0, r0, #1
bus.WriteARM(0x04, 0xeafffffd); // b 0x0
EXPECT_EQ(core.RunCycles(8), 8u);
EXPECT_EQ(core.GetExecutedInstructions(), 8u);
EXPECT_EQ(core.GetRegister(0), 4u);
EXPECT_EQ(bus.GetCycles(), 8u);
EXPECT_EQ(bus.GetAdvanceCalls(), 1u);
EXPECT_EQ(bus.GetRead32Count(),
2u); // Both words stay resident across loop iterations.
}
TEST(StarletARMCore, ThumbMemoryPollingFastForwardsSafeRAMUntilItChanges)
{
TestBus bus;
ARMCore core(bus);
bus.WriteThumb(0x00, 0x6823); // ldr r3, [r4]
bus.WriteThumb(0x02, 0x2b00); // cmp r3, #0
bus.WriteThumb(0x04, 0xd0fc); // beq 0x00
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(4, 0x100);
EXPECT_EQ(core.RunCycles(10000), 10000u);
EXPECT_EQ(core.GetExecutedInstructions(), 3u);
EXPECT_EQ(bus.GetCycles(), 10000u);
EXPECT_TRUE(core.IsWaitingForExternalEvent());
bus.Write32(0x100, 1);
EXPECT_EQ(core.RunCycles(2), 2u);
EXPECT_EQ(core.GetExecutedInstructions(), 5u);
EXPECT_EQ(core.GetRegister(3), 1u);
EXPECT_FALSE(core.IsWaitingForExternalEvent());
}
TEST(StarletARMCore, ThumbMemoryPollingDoesNotSkipMMIO)
{
TestBus bus;
ARMCore core(bus);
bus.SetIdlePollSafe(false);
bus.WriteThumb(0x00, 0x6823); // ldr r3, [r4]
bus.WriteThumb(0x02, 0x2b00); // cmp r3, #0
bus.WriteThumb(0x04, 0xd0fc); // beq 0x00
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
core.SetRegister(4, 0x100);
EXPECT_EQ(core.RunCycles(12), 12u);
EXPECT_EQ(core.GetExecutedInstructions(), 12u);
EXPECT_FALSE(core.IsWaitingForExternalEvent());
}
TEST(StarletARMCore, ExceptionEntryAdvancesDeviceCycles)
{
TestBus bus;
ARMCore core(bus);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::System));
core.SetIRQLine(true);
EXPECT_EQ(core.Step(), 3);
EXPECT_EQ(bus.GetCycles(), 3u);
}
TEST(StarletARMCore, MMUSectionTranslation)
{
TestBus bus(0x10000);
ARMCore core(bus);
bus.WriteARM(0x0000, 0xe3a0002a); // mov r0, #42
bus.WriteARM(0x0004, 0xe3a01007); // mov r1, #7
bus.WriteARM(0x6000,
0x00000c02); // VA 0x80000000 section -> PA 0, full access
core.GetCP15State().translation_table_base = 0x4000;
core.GetCP15State().domain_access_control = 3;
core.GetCP15State().control |= 1;
core.SetRegister(15, 0x80000000);
bus.ResetReadCounts();
core.Run(2);
EXPECT_EQ(core.GetRegister(0), 42u);
EXPECT_EQ(core.GetRegister(1), 7u);
EXPECT_EQ(core.GetRegister(15), 0x80000008u);
EXPECT_EQ(bus.GetRead8Count(), 0u);
EXPECT_EQ(bus.GetRead16Count(), 0u);
// The first fetch walks the section table; the second hits the 1 KiB
// translation cache.
EXPECT_EQ(bus.GetRead32Count(), 3u);
}
} // namespace
} // namespace IOS::LLE