diff --git a/Source/Core/Core/HW/SI/SI.cpp b/Source/Core/Core/HW/SI/SI.cpp index 429897c0db..538953cf1f 100644 --- a/Source/Core/Core/HW/SI/SI.cpp +++ b/Source/Core/Core/HW/SI/SI.cpp @@ -3,6 +3,7 @@ #include "Core/HW/SI/SI.h" +#include #include #include #include @@ -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 request_copy(m_si_buffer.data(), m_si_buffer.data() + request_length); + const std::vector 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(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)); })); } diff --git a/Source/Core/Core/HW/SI/SI.h b/Source/Core/Core/HW/SI/SI.h index daec278831..d9444de417 100644 --- a/Source/Core/Core/HW/SI/SI.h +++ b/Source/Core/Core/HW/SI/SI.h @@ -237,6 +237,7 @@ private: USIStatusReg m_status_reg; USIEXIClockCount m_exi_clock_count; std::array m_si_buffer{}; + std::array m_si_output_buffer{}; Core::System& m_system; }; diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp index fb6ba488c5..c19ba22803 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.cpp +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.cpp @@ -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(value >> (24 - (address & 3) * 8)); + } + if (word_address == HW_GPIO_INTFLAG) + { + const u32 value = m_gpio_interrupt_flags; + return static_cast(value >> (24 - (address & 3) * 8)); + } + if (word_address == HW_GPIO_STRAPS) + { + constexpr u32 value = 0; return static_cast(value >> (24 - (address & 3) * 8)); } if (IsEHCIAddress(word_address)) @@ -3064,6 +3102,13 @@ u8 StarletMemory::Read8(u32 address) const u32 value = mmio ? mmio->Read(m_system, word_address) : 0; return static_cast(value >> (24 - (address & 3) * 8)); } + if (IsStarletEXIAddress(word_address)) + { + MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping(); + const u32 value = + mmio ? mmio->Read(m_system, TranslateStarletEXIAddress(word_address)) : 0; + return static_cast(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(m_system, address) : 0; } + if ((address & 3) == 0 && IsStarletEXIAddress(address)) + { + MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping(); + return mmio ? mmio->Read(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(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(m_system, address, value); return; } + if ((address & 3) == 0 && IsStarletEXIAddress(address)) + { + MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping(); + if (mmio) + mmio->Write(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 diff --git a/Source/Core/Core/IOS/Starlet/StarletMemory.h b/Source/Core/Core/IOS/Starlet/StarletMemory.h index 7cecba7cae..846d2e3dde 100644 --- a/Source/Core/Core/IOS/Starlet/StarletMemory.h +++ b/Source/Core/Core/IOS/Starlet/StarletMemory.h @@ -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; diff --git a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp index 4d51e20be1..619b23104a 100644 --- a/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp +++ b/Source/UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp @@ -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(memory.Read8(address)) << 24 | + static_cast(memory.Read8(address + 1)) << 16 | + static_cast(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3); + }; + const auto write_word = [&memory](u32 address, u32 value) { + memory.Write8(address, static_cast(value >> 24)); + memory.Write8(address + 1, static_cast(value >> 16)); + memory.Write8(address + 2, static_cast(value >> 8)); + memory.Write8(address + 3, static_cast(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; diff --git a/docs/Wii_IOS_LLE.md b/docs/Wii_IOS_LLE.md index 34287c034e..8d4bc622b9 100644 --- a/docs/Wii_IOS_LLE.md +++ b/docs/Wii_IOS_LLE.md @@ -7,7 +7,9 @@ stub, release of both Broadway reset lines, and execution of the IOS-loaded Powe Dolphin's normal Broadway core. The end-to-end path has rendered the original French System Menu health-and-safety screen through Dolphin's Direct3D 11 backend. The original IOS80 Bluetooth stack has also accepted an emulated paired Wii Remote, completed both L2CAP HID channels, exchanged the -Menu's setup reports, and delivered sustained input reports. +Menu's setup reports, and delivered sustained input reports. Later validation launched the +Homebrew Channel through an original IOS58 reload, ran an unmodified HackMii Installer through +LetterBomb, and reached the original BootMii menu from its boot2 installation. The implementation never writes to `nand.bin`. The whole `dumps/` directory is ignored by Git so that boot ROMs, console keys, and NAND contents cannot accidentally be committed. @@ -131,10 +133,19 @@ X2 and immediately submit the next request with X1 before Starlet is scheduled a Starlet IRQ/FIQ masks. - BootMii SEEPROM data exposed through the original 93C56-style GPIO serial protocol, including read, write-enable/disable, word write/erase, and whole-array write/erase commands in COW memory. +- Hollywood's Starlet GPIO bank, including enable/output/direction/input, ownership, straps, + interrupt level/mask, and write-one-to-clear interrupt flags. POWER and EJECT have their idle + levels and the EEPROM MISO pin remains connected to the existing serial model. - Immediate AHM memory-flush acknowledgement, the indirect DDR/SEQ/BIST register banks used by boot1 training, and the hardware-controlled boot0 ROM overlay/SRAM-bank swap. - PPC/ARM IPC mailboxes, Starlet-side access to both control registers, and Broadway SRESET/HRESET hold/release transitions. +- The Starlet EXI window at `0x0d806800` is forwarded to Dolphin's existing EXI controller at + `0x0d006800`, while the adjacent reset-vector aperture remains backed by Starlet memory. This + exposes the original EXI device state and transfer completion semantics used by MINI. +- The Broadway Serial Interface keeps an outgoing request latch separate from the readable + response RAM. Starting another transfer without rewriting communication RAM therefore repeats + the previous command, matching the controller probing performed by BootMii's `ppcboot.elf`. - External SD host-controller and card path at `0x0d070000`: reversed-little-endian SDHCI capabilities/version, card-detect state, self-clearing software reset, internal-clock stabilization, write-one-to-clear interrupt status, the IOS initialization command subset, @@ -279,6 +290,11 @@ An isolated boot probe using the local, mutually matching dumps has executed thi Wii Remote input, and opened the HackMii installation menu. No HackMii binary, timer, syscall, or detection check was patched. Its preparation phase is currently much slower than on hardware because ARM-heavy IOS transients still run through the Starlet interpreter. +20. The dump's existing BootMii-as-boot2 installation intercepted a cold boot and loaded its + original `armboot.bin`/MINI from the virtual SD card. MINI initialized the emulated GPIO and + EXI paths, published its EXI Broadway boot vector, released Broadway, and ran the original + `ppcboot.elf`. The interactive four-icon BootMii menu rendered without a host-side firmware + jump, a patched BootMii binary, or a synthesized SI reply. The probe never prints ROM, NAND, key, or firmware instruction bytes. The committed unit suite covers ARM-to-Thumb loads into PC, high Starlet exception vectors, privileged `LDM ... ^` user-bank @@ -301,12 +317,13 @@ the three pre-existing bad-ECC pages in the source dump, a valid EXI reset vecto The current end-to-end boundary is a rendered, controller-connected and post-health-screen System Menu with its populated channel grid, followed by a successful original IOS80-to-IOS58 reload, a -rendered Homebrew Channel, and LetterBomb reaching the interactive HackMii Installer menu. The path -sustains PPC-to-original-IOS filesystem, DI, Bluetooth HID, SDIO/Wi-Fi and network-service traffic. -The stabilized Menu has been measured at 59.91 FPS and HBC at 59.94 FPS. It proves the emulated -first Wii Remote's pairing, L2CAP setup, command exchange and input-report path, but not every -extension, multiple-controller scenario, reconnection edge case, packet-level networking, resource -manager, or timing-sensitive exploit. +rendered Homebrew Channel, LetterBomb reaching the interactive HackMii Installer menu, and the +dump's BootMii-as-boot2 installation reaching its original interactive UI through MINI and +`ppcboot.elf`. The path sustains PPC-to-original-IOS filesystem, DI, Bluetooth HID, SDIO/Wi-Fi and +network-service traffic. The stabilized Menu has been measured at 59.91 FPS and HBC at 59.94 FPS. +It proves the emulated first Wii Remote's pairing, L2CAP setup, command exchange and input-report +path, but not every extension, multiple-controller scenario, reconnection edge case, packet-level +networking, resource manager, or timing-sensitive exploit. ## Remaining blockers @@ -342,6 +359,9 @@ than a drop-in replacement for Dolphin's mature IOS HLE mode. public Starlet/boot-chain emulator and high-vector behavior. - [WiiBrew External Interface](https://www.wiibrew.org/wiki/Hardware/External_Interface) for the documented EXI boot-buffer and Broadway reset-vector address mapping. +- [WiiBrew GPIOs](https://www.wiibrew.org/wiki/Hardware/Hollywood/GPIOs) and + [Serial Interface](https://www.wiibrew.org/wiki/Hardware/Serial_Interface) for the public GPIO + register and SI communication-RAM behavior. - [WiiBrew NAND Interface](https://www.wiibrew.org/wiki/Hardware/NAND_Interface) and [NAND layout](https://www.wiibrew.org/wiki/Hardware/NAND) for the command register, DMA buffers, chip geometry, and supported device IDs.