IOS: boot original BootMii through MINI

This commit is contained in:
2026-08-25 19:46:28 +02:00
parent 08c9886169
commit 5387658e0c
6 changed files with 194 additions and 10 deletions
+18 -2
View File
@@ -3,6 +3,7 @@
#include "Core/HW/SI/SI.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <memory>
@@ -151,11 +152,17 @@ void SerialInterfaceManager::RunSIBuffer(u64 user_data, s64 cycles_late)
const s32 expected_response_length = ConvertSILengthField(m_com_csr.INLNGTH);
#if defined(_DEBUG)
const std::vector<u8> request_copy(m_si_buffer.data(), m_si_buffer.data() + request_length);
const std::vector<u8> request_copy(m_si_output_buffer.data(),
m_si_output_buffer.data() + request_length);
#endif
// The communication RAM is bidirectional, but the SI transfer engine retains the bytes written
// by the CPU separately from the response it places in the readable RAM. In particular, a new
// TSTART without another RAM write retransmits the previous request. BootMii relies on this
// when it probes a GameCube controller twice during startup.
auto transfer_buffer = m_si_output_buffer;
auto* const device = m_channel[m_com_csr.CHANNEL].device.get();
const s32 actual_response_length = device->RunBuffer(m_si_buffer.data(), request_length);
const s32 actual_response_length = device->RunBuffer(transfer_buffer.data(), request_length);
DEBUG_LOG_FMT(SERIALINTERFACE,
"RunSIBuffer chan: {} request_length: {} expected_response_length: {} "
@@ -186,6 +193,11 @@ void SerialInterfaceManager::RunSIBuffer(u64 user_data, s64 cycles_late)
// 2) Investigate the timeout period for NOREP0
if (actual_response_length != 0)
{
if (actual_response_length > 0)
{
const size_t response_length = std::min<size_t>(actual_response_length, m_si_buffer.size());
std::copy_n(transfer_buffer.begin(), response_length, m_si_buffer.begin());
}
m_com_csr.TSTART = 0;
m_com_csr.COMERR = actual_response_length < 0;
if (actual_response_length < 0)
@@ -226,6 +238,7 @@ void SerialInterfaceManager::DoState(PointerWrap& p)
p.Do(m_status_reg);
p.Do(m_exi_clock_count);
p.Do(m_si_buffer);
p.Do(m_si_output_buffer);
}
void SerialInterfaceManager::RegisterEvents()
@@ -285,6 +298,7 @@ void SerialInterfaceManager::Init()
// m_exi_clock_count.LOCK = 1;
m_si_buffer = {};
m_si_output_buffer = {};
}
void SerialInterfaceManager::Shutdown()
@@ -312,6 +326,7 @@ void SerialInterfaceManager::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
auto& si = system.GetSerialInterface();
val = Common::swap32(val);
std::memcpy(&si.m_si_buffer[i], &val, sizeof(val));
std::memcpy(&si.m_si_output_buffer[i], &val, sizeof(val));
}));
}
for (size_t i = 0; i < m_si_buffer.size(); i += sizeof(u16))
@@ -328,6 +343,7 @@ void SerialInterfaceManager::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
auto& si = system.GetSerialInterface();
val = Common::swap16(val);
std::memcpy(&si.m_si_buffer[i], &val, sizeof(val));
std::memcpy(&si.m_si_output_buffer[i], &val, sizeof(val));
}));
}
+1
View File
@@ -237,6 +237,7 @@ private:
USIStatusReg m_status_reg;
USIEXIClockCount m_exi_clock_count;
std::array<u8, BUFFER_SIZE> m_si_buffer{};
std::array<u8, BUFFER_SIZE> m_si_output_buffer{};
Core::System& m_system;
};
+110 -1
View File
@@ -60,6 +60,24 @@ constexpr bool IsDIAddress(u32 address)
return address >= DI_BASE && address < DI_BASE + DI_SIZE;
}
// Broadway sees the EXI register bank at 0x0d006800. Starlet reaches the same
// physical controller through Hollywood's 0x0d806800 alias. The 0x40-byte
// reset-vector aperture which follows it is separate and remains backed by
// StarletMemory for boot2 to populate.
constexpr u32 STARLET_EXI_BASE = 0x0d806800;
constexpr u32 BROADWAY_EXI_BASE = 0x0d006800;
constexpr u32 EXI_REGISTER_SIZE = 0x3c;
constexpr bool IsStarletEXIAddress(u32 address)
{
return address >= STARLET_EXI_BASE && address < STARLET_EXI_BASE + EXI_REGISTER_SIZE;
}
constexpr u32 TranslateStarletEXIAddress(u32 address)
{
return BROADWAY_EXI_BASE + address - STARLET_EXI_BASE;
}
constexpr u32 SHA_BASE = 0x0d030000;
constexpr u32 SHA_CMD = SHA_BASE + 0x00;
constexpr u32 SHA_SRC = SHA_BASE + 0x04;
@@ -316,8 +334,15 @@ constexpr u32 HW_SRNPROT = HW_BASE + 0x60;
constexpr u32 HW_AHBPROT = HW_BASE + 0x64;
constexpr u32 HW_TIMER = HW_BASE + 0x10;
constexpr u32 HW_ALARM = HW_BASE + 0x14;
constexpr u32 HW_GPIO_ENABLE = HW_BASE + 0xdc;
constexpr u32 HW_GPIO_OUT = HW_BASE + 0xe0;
constexpr u32 HW_GPIO_DIR = HW_BASE + 0xe4;
constexpr u32 HW_GPIO_IN = HW_BASE + 0xe8;
constexpr u32 HW_GPIO_INTLVL = HW_BASE + 0xec;
constexpr u32 HW_GPIO_INTFLAG = HW_BASE + 0xf0;
constexpr u32 HW_GPIO_INTMASK = HW_BASE + 0xf4;
constexpr u32 HW_GPIO_STRAPS = HW_BASE + 0xf8;
constexpr u32 HW_GPIO_OWNER = HW_BASE + 0xfc;
constexpr u32 HW_DIFLAGS = HW_BASE + 0x180;
constexpr u32 HW_SPARE0 = HW_BASE + 0x188;
constexpr u32 HW_BOOT0 = HW_BASE + 0x18c;
@@ -374,6 +399,7 @@ constexpr u32 GPIO_EEP_CS = 0x400;
constexpr u32 GPIO_EEP_CLK = 0x800;
constexpr u32 GPIO_EEP_MOSI = 0x1000;
constexpr u32 GPIO_EEP_MISO = 0x2000;
constexpr u32 GPIO_VALID_MASK = 0x00ffffff;
constexpr u32 SRNPROT_SRAM_SPLIT_MODE = 1U << 5;
constexpr u32 BOOT0_DISABLE = 1U << 12;
@@ -523,6 +549,7 @@ void StarletMemory::Reset()
m_ddr_seq_address = 0;
m_ddr_bist_address = 0;
m_gpio_out = 0;
m_gpio_interrupt_flags = 0;
m_seeprom_command = 0;
m_seeprom_output = 0;
m_seeprom_input = 0;
@@ -663,6 +690,7 @@ void StarletMemory::DoState(PointerWrap& p)
p.Do(m_ddr_seq_address);
p.Do(m_ddr_bist_address);
p.Do(m_gpio_out);
p.Do(m_gpio_interrupt_flags);
p.Do(m_seeprom_command);
p.Do(m_seeprom_output);
p.Do(m_seeprom_input);
@@ -3029,7 +3057,17 @@ u8 StarletMemory::Read8(u32 address)
}
if (word_address == HW_GPIO_IN)
{
const u32 value = m_seeprom_miso ? GPIO_EEP_MISO : 0;
const u32 value = GetGPIOInput();
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (word_address == HW_GPIO_INTFLAG)
{
const u32 value = m_gpio_interrupt_flags;
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (word_address == HW_GPIO_STRAPS)
{
constexpr u32 value = 0;
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (IsEHCIAddress(word_address))
@@ -3064,6 +3102,13 @@ u8 StarletMemory::Read8(u32 address)
const u32 value = mmio ? mmio->Read<u32>(m_system, word_address) : 0;
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (IsStarletEXIAddress(word_address))
{
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
const u32 value =
mmio ? mmio->Read<u32>(m_system, TranslateStarletEXIAddress(word_address)) : 0;
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (word_address >= SDHC_BASE && word_address < SDHC_BASE + SDHC_SIZE)
{
const u32 value = ReadSDHCRegister(word_address);
@@ -3136,6 +3181,11 @@ u32 StarletMemory::Read32(u32 address)
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
return mmio ? mmio->Read<u32>(m_system, address) : 0;
}
if ((address & 3) == 0 && IsStarletEXIAddress(address))
{
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
return mmio ? mmio->Read<u32>(m_system, TranslateStarletEXIAddress(address)) : 0;
}
if (IsMemoryAddress(address) && IsMemoryAddress(address + 3))
return m_system.GetMemory().Read_U32(address);
@@ -3223,6 +3273,20 @@ void StarletMemory::Write8(u32 address, u8 value)
}
return;
}
if (IsStarletEXIAddress(word_address))
{
WriteMapped8(address, value);
if ((address & 3) == 3)
{
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
if (mmio)
{
mmio->Write<u32>(m_system, TranslateStarletEXIAddress(word_address),
ReadRegister(word_address));
}
}
return;
}
if (ehci &&
(address == EHCI_BASE + EHCI_USB_STATUS || address == EHCI_BASE + EHCI_PORT_STATUS_1 ||
address == EHCI_BASE + EHCI_PORT_STATUS_2))
@@ -3307,6 +3371,28 @@ void StarletMemory::Write8(u32 address, u8 value)
m_sram_split_mode = (word & SRNPROT_SRAM_SPLIT_MODE) != 0;
else if (word_address == HW_GPIO_OUT)
HandleGPIOWrite(word);
else if (word_address == HW_GPIO_INTFLAG)
{
// Hollywood's GPIO interrupt flag is write-one-to-clear. A level-triggered
// flag cannot be cleared while its input still matches the selected active
// level. Keep that rule limited to already-latched flags; merely selecting
// an active level does not itself invent an edge or a pending interrupt.
const u32 pending_before = m_gpio_interrupt_flags;
const u32 active = ~(GetGPIOInput() ^ ReadRegister(HW_GPIO_INTLVL)) & GPIO_VALID_MASK;
m_gpio_interrupt_flags = (pending_before & ~word) | (pending_before & active);
UpdateGPIOInterrupt();
}
else if (word_address == HW_GPIO_INTMASK || word_address == HW_GPIO_OWNER)
{
UpdateGPIOInterrupt();
}
else if (word_address == HW_GPIO_ENABLE || word_address == HW_GPIO_DIR ||
word_address == HW_GPIO_INTLVL)
{
// These GPIO configuration registers are backed by m_registers. Mask off
// the unused upper byte exposed by Hollywood's 24-pin bank.
WriteMapped8(word_address, 0);
}
else if (ehci)
HandleEHCIWrite(word_address);
else if (ohci_controller)
@@ -3350,6 +3436,13 @@ void StarletMemory::Write32(u32 address, u32 value)
mmio->Write<u32>(m_system, address, value);
return;
}
if ((address & 3) == 0 && IsStarletEXIAddress(address))
{
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
if (mmio)
mmio->Write<u32>(m_system, TranslateStarletEXIAddress(address), value);
return;
}
if (IsMemoryAddress(address) && IsMemoryAddress(address + 3))
{
@@ -4294,4 +4387,20 @@ void StarletMemory::HandleGPIOWrite(u32 value)
}
}
}
u32 StarletMemory::GetGPIOInput() const
{
// POWER and EJECT are external active-high inputs and remain low until the
// emulator grows explicit front-panel button events. The serial EEPROM MISO
// line is driven by the emulated SEEPROM state machine.
return m_seeprom_miso ? GPIO_EEP_MISO : 0;
}
void StarletMemory::UpdateGPIOInterrupt()
{
const u32 mask = ReadRegister(HW_GPIO_INTMASK);
const u32 owner = ReadRegister(HW_GPIO_OWNER);
m_system.GetWiiIPC().SetStarletInterrupt(
INT_CAUSE_GPIO_STARLET, (m_gpio_interrupt_flags & mask & ~owner & GPIO_VALID_MASK) != 0);
}
} // namespace IOS::LLE
@@ -177,6 +177,8 @@ private:
void CompressSHA1(const u8* block);
void HandleOTPCommand(u32 command);
void HandleGPIOWrite(u32 value);
u32 GetGPIOInput() const;
void UpdateGPIOInterrupt();
u32 GetTimer() const;
Core::System& m_system;
@@ -254,6 +256,7 @@ private:
u16 m_ddr_seq_address = 0;
u16 m_ddr_bist_address = 0;
u32 m_gpio_out = 0;
u32 m_gpio_interrupt_flags = 0;
u16 m_seeprom_command = 0;
u16 m_seeprom_output = 0;
u16 m_seeprom_input = 0;
@@ -178,6 +178,41 @@ TEST(StarletTimer, ZeroDelayAlarmMatchesImmediatelyAndUsesIRQW1C)
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
}
TEST(StarletGPIO, InterruptFlagIsWriteOneToClear)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 gpio_interrupt_level = hardware_base + 0xec;
constexpr u32 gpio_interrupt_flag = hardware_base + 0xf0;
constexpr u32 gpio_interrupt_mask = hardware_base + 0xf4;
constexpr u32 gpio_owner = hardware_base + 0xfc;
constexpr u32 power = 1;
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));
};
// BootMii selects POWER as active-high, waits, then clears the pending flag.
// With the emulated front-panel button idle-low this must read back cleared.
write_word(gpio_interrupt_level, power);
write_word(gpio_interrupt_mask, power);
write_word(gpio_owner, 0);
write_word(gpio_interrupt_flag, power);
EXPECT_EQ(read_word(gpio_interrupt_flag), 0u);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_GPIO_STARLET, 0u);
}
TEST(StarletAHBPROT, OriginalIOSMaskIsPreservedForBroadway)
{
constexpr u32 ahbprot = 0x0d800064;