From 817d1629443c0c3e4e85f18640f748141e2ee991 Mon Sep 17 00:00:00 2001 From: Gary Beihl Date: Thu, 19 Mar 2026 13:50:00 -0400 Subject: [PATCH 1/4] ast2600: Add Aspeed SCU peripheral System Control Unit model matching QEMU ast2600_a3_resets[] register layout. Implements all registers needed for u-boot SPL and Linux clk-ast2600 driver: - Protection Key unlock (0x1688A8A8) - Silicon Revision (0x05030303 for A3) - 5 PLL pairs (HPLL/APLL/MPLL/EPLL/DPLL) with lock bit (bit 31) - Clock Stop/Source Selection registers (0x080-0x350) - Hardware Strap 1 (0x500) and 2 (0x510) - Pin Control registers (0x400-0x4FC) - CPU scratch pad registers (0x180-0x1CC) - MiscCtrl (0x0C0) with UART_DIV13_EN - Reset Control, Debug Control, Chip ID, RNG - Fallback R/W storage for unnamed registers Signed-off-by: Gary Beihl --- .../Peripherals/Miscellaneous/Aspeed_SCU.cs | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/Emulator/Peripherals/Peripherals/Miscellaneous/Aspeed_SCU.cs diff --git a/src/Emulator/Peripherals/Peripherals/Miscellaneous/Aspeed_SCU.cs b/src/Emulator/Peripherals/Peripherals/Miscellaneous/Aspeed_SCU.cs new file mode 100644 index 000000000..0755bc49a --- /dev/null +++ b/src/Emulator/Peripherals/Peripherals/Miscellaneous/Aspeed_SCU.cs @@ -0,0 +1,347 @@ +// +// Copyright (c) 2026 Microsoft +// Licensed under the MIT License. +// + +using System.Collections.Generic; +using Antmicro.Renode.Core; +using Antmicro.Renode.Core.Structure.Registers; +using Antmicro.Renode.Logging; +using Antmicro.Renode.Peripherals.Bus; + +namespace Antmicro.Renode.Peripherals.Miscellaneous +{ + // Aspeed AST2600 System Control Unit (SCU) + // Register offsets and reset values from QEMU hw/misc/aspeed_scu.c (ast2600_a3_resets) + [AllowedTranslations(AllowedTranslation.ByteToDoubleWord | AllowedTranslation.WordToDoubleWord)] + public sealed class Aspeed_SCU : BasicDoubleWordPeripheral, IKnownSize + { + public Aspeed_SCU(IMachine machine) : base(machine) + { + DefineRegisters(); + } + + public long Size => 0x1000; + + private bool IsUnlocked => protectionKey.Value == ProtectionKeyValue; + + private void DefineRegisters() + { + // SCU000 - Protection Key Register + Registers.ProtectionKey.Define(this, 0x0) + .WithValueField(0, 32, out protectionKey, name: "PROTECTION_KEY", + writeCallback: (_, value) => + { + if(value == ProtectionKeyValue) + { + this.Log(LogLevel.Debug, "SCU unlocked"); + } + }); + + // SCU004 - Silicon Revision ID + Registers.SiliconRevision.Define(this, SiliconRevisionAST2600A3) + .WithValueField(0, 32, FieldMode.Read, name: "SILICON_REV"); + + // SCU010 - Protection Key 2 + Registers.ProtectionKey2.Define(this, 0x0) + .WithValueField(0, 32, name: "PROTECTION_KEY2"); + + // SCU014 - Silicon Revision 2 + Registers.SiliconRevision2.Define(this, SiliconRevisionAST2600A3) + .WithValueField(0, 32, FieldMode.Read, name: "SILICON_REV2"); + + // SCU040 - System Reset Control Register 1 + Registers.SysResetCtrl.Define(this, 0xF7C3FED8) + .WithValueField(0, 32, name: "SYS_RST_CTRL"); + + // SCU044 - System Reset Control Clear 1 (W1C - write 1 to clear bits in SysResetCtrl) + Registers.SysResetCtrlClear.Define(this) + .WithValueField(0, 32, FieldMode.Write, name: "SYS_RST_CTRL_CLR", + writeCallback: (_, value) => + { + this.Log(LogLevel.Debug, "SYS_RST_CTRL clear: 0x{0:X8}", value); + }); + + // SCU050 - System Reset Control Register 2 + Registers.SysResetCtrl2.Define(this, 0x0DFFFFFC) + .WithValueField(0, 32, name: "SYS_RST_CTRL2"); + + // SCU054 - System Reset Control Clear 2 + Registers.SysResetCtrl2Clear.Define(this) + .WithValueField(0, 32, FieldMode.Write, name: "SYS_RST_CTRL2_CLR"); + + // SCU080 - Clock Stop Control Register + Registers.ClockStopCtrl.Define(this, 0xFFFF7F8A) + .WithValueField(0, 32, name: "CLK_STOP_CTRL"); + + // SCU084 - Clock Stop Control Clear + Registers.ClockStopCtrlClear.Define(this) + .WithValueField(0, 32, FieldMode.Write, name: "CLK_STOP_CTRL_CLR"); + + // SCU090 - Clock Stop Control Register 2 + Registers.ClockStopCtrl2.Define(this, 0xFFF0FFF0) + .WithValueField(0, 32, name: "CLK_STOP_CTRL2"); + + // SCU094 - Clock Stop Control Clear 2 + Registers.ClockStopCtrl2Clear.Define(this) + .WithValueField(0, 32, FieldMode.Write, name: "CLK_STOP_CTRL2_CLR"); + + // SCU0C0 - Misc Control (Linux clk-ast2600 reads bit 12 for UART_DIV13_EN) + // Reset value 0: UART clock = 24 MHz (matches QEMU) + Registers.MiscCtrl.Define(this, 0x0) + .WithValueField(0, 32, name: "MISC_CTRL"); + + // SCU0C8 - Debug Control + Registers.DebugCtrl.Define(this, 0x00000FFF) + .WithValueField(0, 32, name: "DEBUG_CTRL"); + + // SCU0D8 - Debug Control 2 + Registers.DebugCtrl2.Define(this, 0x000000FF) + .WithValueField(0, 32, name: "DEBUG_CTRL2"); + + // SCU100 - SDRAM Handshake + Registers.SdramHandshake.Define(this, 0x00000000) + .WithValueField(0, 32, name: "SDRAM_HANDSHAKE"); + + // --- CPU scratch pad registers (0x180-0x1CC, 20 dwords) --- + for(uint i = 0; i < 20; i++) + { + var offset = 0x180 + (i * 4); + ((Registers)offset).Define(this, 0x0) + .WithValueField(0, 32, name: $"CPU_SCRATCH_{i}"); + } + + // --- PLL Parameter Registers --- + // Reset values from QEMU ast2600_a3_resets[] + // Extension registers: bit 31 = PLL lock status (always locked in emulation) + + // SCU200 - HPLL Parameter + Registers.HPLL.Define(this, 0x1000408F) + .WithValueField(0, 32, name: "HPLL_PARAM"); + + // SCU204 - HPLL Extension (bit 31 = locked) + Registers.HPLLExt.Define(this, 0x0) + .WithValueField(0, 31, name: "HPLL_EXT_PARAMS") + .WithFlag(31, FieldMode.Read, name: "HPLL_LOCK", + valueProviderCallback: _ => true); + + // SCU210 - APLL Parameter + Registers.APLL.Define(this, 0x1000405F) + .WithValueField(0, 32, name: "APLL_PARAM"); + + // SCU214 - APLL Extension + Registers.APLLExt.Define(this, 0x0) + .WithValueField(0, 32, name: "APLL_EXT"); + + // SCU220 - MPLL Parameter (DDR clock) + Registers.MPLL.Define(this, 0x1008405F) + .WithValueField(0, 32, name: "MPLL_PARAM"); + + // SCU224 - MPLL Extension (bit 31 = locked) + Registers.MPLLExt.Define(this, 0x0) + .WithValueField(0, 31, name: "MPLL_EXT_PARAMS") + .WithFlag(31, FieldMode.Read, name: "MPLL_LOCK", + valueProviderCallback: _ => true); + + // SCU240 - EPLL Parameter (Ethernet clock) + Registers.EPLL.Define(this, 0x1004077F) + .WithValueField(0, 32, name: "EPLL_PARAM"); + + // SCU244 - EPLL Extension (bit 31 = locked) + Registers.EPLLExt.Define(this, 0x0) + .WithValueField(0, 31, name: "EPLL_EXT_PARAMS") + .WithFlag(31, FieldMode.Read, name: "EPLL_LOCK", + valueProviderCallback: _ => true); + + // SCU260 - DPLL Parameter (Display clock) + Registers.DPLL.Define(this, 0x1078405F) + .WithValueField(0, 32, name: "DPLL_PARAM"); + + // SCU264 - DPLL Extension + Registers.DPLLExt.Define(this, 0x0) + .WithValueField(0, 32, name: "DPLL_EXT"); + + // --- Clock Source Selection --- + // Reset values from QEMU ast2600_a3_resets[] + + // SCU300 - Clock Source Selection 1 + Registers.ClockSel1.Define(this, 0xF3940000) + .WithValueField(0, 32, name: "CLK_SEL1"); + + // SCU304 - Clock Source Selection 2 + Registers.ClockSel2.Define(this, 0x00700000) + .WithValueField(0, 32, name: "CLK_SEL2"); + + // SCU308 - Clock Source Selection 3 + Registers.ClockSel3.Define(this, 0x00000000) + .WithValueField(0, 32, name: "CLK_SEL3"); + + // SCU310 - Clock Source Selection 4 + Registers.ClockSel4.Define(this, 0xF3F40000) + .WithValueField(0, 32, name: "CLK_SEL4"); + + // SCU314 - Clock Source Selection 5 + Registers.ClockSel5.Define(this, 0x30000000) + .WithValueField(0, 32, name: "CLK_SEL5"); + + // SCU338 - UART Clock Generation + Registers.UartClk.Define(this, 0x00014506) + .WithValueField(0, 32, name: "UARTCLK"); + + // SCU33C - High-Speed UART Clock Generation + Registers.HUartClk.Define(this, 0x000145C0) + .WithValueField(0, 32, name: "HUARTCLK"); + + // SCU350 - Clock Duty Selection + Registers.ClockDuty.Define(this, 0x0) + .WithValueField(0, 32, name: "CLK_DUTY"); + + // --- Miscellaneous registers accessed during boot --- + + // SCU400-4B4: Multi-function pin control / clock duty registers + // Linux reads these during clock/pinctrl init + for(uint offset = 0x400; offset <= 0x4FC; offset += 4) + { + ((Registers)offset).Define(this, 0x0) + .WithValueField(0, 32, name: $"PIN_CTRL_{offset:X3}"); + } + + // --- Hardware Strap registers (AST2600: at 0x500) --- + + // SCU500 - Hardware Strap 1 + Registers.HWStrap1.Define(this, AST2600_EVB_HW_STRAP1) + .WithValueField(0, 32, name: "HW_STRAP1"); + + // SCU504 - Hardware Strap 1 Clear + Registers.HWStrap1Clear.Define(this) + .WithValueField(0, 32, FieldMode.Write, name: "HW_STRAP1_CLR"); + + // SCU508 - Hardware Strap 1 Protection + Registers.HWStrap1Prot.Define(this, 0x0) + .WithValueField(0, 32, name: "HW_STRAP1_PROT"); + + // SCU510 - Hardware Strap 2 + Registers.HWStrap2.Define(this, AST2600_EVB_HW_STRAP2) + .WithValueField(0, 32, name: "HW_STRAP2"); + + // SCU514 - Hardware Strap 2 Clear + Registers.HWStrap2Clear.Define(this) + .WithValueField(0, 32, FieldMode.Write, name: "HW_STRAP2_CLR"); + + // SCU518 - Hardware Strap 2 Protection + Registers.HWStrap2Prot.Define(this, 0x0) + .WithValueField(0, 32, name: "HW_STRAP2_PROT"); + + // SCU524 - RNG Control + Registers.RngCtrl.Define(this, 0x0) + .WithValueField(0, 32, name: "RNG_CTRL"); + + // SCU540 - RNG Data (returns random data on read) + Registers.RngData.Define(this, 0x0) + .WithValueField(0, 32, FieldMode.Read, name: "RNG_DATA", + valueProviderCallback: _ => (uint)rng.Next()); + + // SCU5B0 - Chip ID 0 + Registers.ChipId0.Define(this, 0x1234ABCD) + .WithValueField(0, 32, FieldMode.Read, name: "CHIP_ID0"); + + // SCU5B4 - Chip ID 1 + Registers.ChipId1.Define(this, 0x88884444) + .WithValueField(0, 32, FieldMode.Read, name: "CHIP_ID1"); + + // SCU820/824/C24 - Misc config registers touched by u-boot + Registers.MiscCtrl820.Define(this, 0x0) + .WithValueField(0, 32, name: "MISC_CTRL_820"); + + Registers.MiscCtrl824.Define(this, 0x0) + .WithValueField(0, 32, name: "MISC_CTRL_824"); + + Registers.MiscCtrlC24.Define(this, 0x0) + .WithValueField(0, 32, name: "MISC_CTRL_C24"); + } + + // Override to provide fallback R/W storage for undefined registers. + // QEMU's SCU stores all offsets in a flat regs[] array; we mirror that behavior. + public override uint ReadDoubleWord(long offset) + { + uint result; + if(RegistersCollection.TryRead(offset, out result)) + { + return result; + } + fallbackStorage.TryGetValue(offset, out result); + return result; + } + + public override void WriteDoubleWord(long offset, uint value) + { + if(!RegistersCollection.TryWrite(offset, value)) + { + fallbackStorage[offset] = value; + } + } + + private IValueRegisterField protectionKey; + private readonly System.Random rng = new System.Random(); + private readonly Dictionary fallbackStorage = new Dictionary(); + + private const uint SiliconRevisionAST2600A3 = 0x05030303; + private const uint ProtectionKeyValue = 0x1688A8A8; + // AST2600-EVB hardware strap values (from QEMU aspeed_ast2600_evb.c) + private const uint AST2600_EVB_HW_STRAP1 = 0x000000C0; + private const uint AST2600_EVB_HW_STRAP2 = 0x00000003; + + private enum Registers + { + ProtectionKey = 0x000, + SiliconRevision = 0x004, + ProtectionKey2 = 0x010, + SiliconRevision2 = 0x014, + SysResetCtrl = 0x040, + SysResetCtrlClear = 0x044, + SysResetCtrl2 = 0x050, + SysResetCtrl2Clear = 0x054, + ClockStopCtrl = 0x080, + ClockStopCtrlClear = 0x084, + ClockStopCtrl2 = 0x090, + ClockStopCtrl2Clear = 0x094, + MiscCtrl = 0x0C0, + DebugCtrl = 0x0C8, + DebugCtrl2 = 0x0D8, + SdramHandshake = 0x100, + // CPU scratch pad: 0x180-0x1CC (defined dynamically) + HPLL = 0x200, + HPLLExt = 0x204, + APLL = 0x210, + APLLExt = 0x214, + MPLL = 0x220, + MPLLExt = 0x224, + EPLL = 0x240, + EPLLExt = 0x244, + DPLL = 0x260, + DPLLExt = 0x264, + ClockSel1 = 0x300, + ClockSel2 = 0x304, + ClockSel3 = 0x308, + ClockSel4 = 0x310, + ClockSel5 = 0x314, + UartClk = 0x338, + HUartClk = 0x33C, + ClockDuty = 0x350, + // 0x400-0x4FC: Pin control (defined dynamically) + HWStrap1 = 0x500, + HWStrap1Clear = 0x504, + HWStrap1Prot = 0x508, + HWStrap2 = 0x510, + HWStrap2Clear = 0x514, + HWStrap2Prot = 0x518, + RngCtrl = 0x524, + RngData = 0x540, + ChipId0 = 0x5B0, + ChipId1 = 0x5B4, + MiscCtrl820 = 0x820, + MiscCtrl824 = 0x824, + MiscCtrlC24 = 0xC24, + } + } +} From f8356ad9616e94fd157f47a4c9567b33ea8b93e0 Mon Sep 17 00:00:00 2001 From: Gary Beihl Date: Thu, 19 Mar 2026 13:52:03 -0400 Subject: [PATCH 2/4] ast2600: Add Aspeed SDMC peripheral SDRAM Memory Controller model matching QEMU aspeed_sdmc.c behavior: - Protection key with QEMU-style transform: unlock (0xFC600309) sets 0x01, hardlock (0xDEADDEAD) sets 0x10, else 0x00 - Memory configuration register (1 GiB DRAM encoding) - Status1 (0x60): always set PHY_PLL_LOCK, clear BUSY - ECC Test Ctrl (0x70): always set FINISHED, clear FAIL - Config (0x04): preserves read-only HW config bits on write - Full 0x000-0xFFF register file with R/W storage - PHY registers at 0x400+ initialized with passing defaults u-boot SPL reads MCR04 for DRAM size and boots through full DDR training sequence. Signed-off-by: Gary Beihl --- .../Peripherals/Miscellaneous/Aspeed_SDMC.cs | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 src/Emulator/Peripherals/Peripherals/Miscellaneous/Aspeed_SDMC.cs diff --git a/src/Emulator/Peripherals/Peripherals/Miscellaneous/Aspeed_SDMC.cs b/src/Emulator/Peripherals/Peripherals/Miscellaneous/Aspeed_SDMC.cs new file mode 100644 index 000000000..213bf2680 --- /dev/null +++ b/src/Emulator/Peripherals/Peripherals/Miscellaneous/Aspeed_SDMC.cs @@ -0,0 +1,177 @@ +// +// Copyright (c) 2026 Microsoft +// Licensed under the MIT License. +// + +using System; +using Antmicro.Renode.Core; +using Antmicro.Renode.Core.Structure.Registers; +using Antmicro.Renode.Logging; +using Antmicro.Renode.Peripherals.Bus; + +namespace Antmicro.Renode.Peripherals.Miscellaneous +{ + // Aspeed AST2600 SDRAM Memory Controller (SDMC) + // Reference: QEMU hw/misc/aspeed_sdmc.c, u-boot drivers/ram/aspeed/sdram_ast2600.c + // + // Large R/W register file matching QEMU behavior. Any offset can be written/read. + // Special handling for protection key, config, status, and ECC test registers. + [AllowedTranslations(AllowedTranslation.ByteToDoubleWord | AllowedTranslation.WordToDoubleWord)] + public sealed class Aspeed_SDMC : BasicDoubleWordPeripheral, IKnownSize + { + public Aspeed_SDMC(IMachine machine) : base(machine) + { + DefineRegisters(); + Reset(); + } + + public long Size => RegisterSpaceSize; + + public override void Reset() + { + base.Reset(); + Array.Clear(storage, 0, storage.Length); + + // Protection key: locked on reset + storage[0x00 / 4] = ProtSoftlocked; + + // Config: default hardware config + storage[0x04 / 4] = DefaultConfig; + + // Status1: PHY PLL locked, not busy + storage[0x60 / 4] = PhyPllLockStatus; + + // PHY status registers at 0x400+ + storage[0x400 / 4] = 0x00000002; // PHY init done + storage[0x430 / 4] = 0x00000001; // PHY DLL locked + storage[0x450 / 4] = 0x0FFFFFFF; // eye window 1 + storage[0x468 / 4] = 0x000000FF; // eye window 2 + storage[0x47C / 4] = 0x000000FF; // eye window 3 + storage[0x488 / 4] = 0x000000FF; // eye window pass + storage[0x490 / 4] = 0x000000FF; // eye window pass + storage[0x4C8 / 4] = 0x000000FF; // eye window pass + } + + public override uint ReadDoubleWord(long offset) + { + if(offset >= 0 && offset < RegisterSpaceSize) + { + return storage[(uint)offset / 4]; + } + this.Log(LogLevel.Warning, "Read from offset 0x{0:X} beyond register space", offset); + return 0; + } + + public override void WriteDoubleWord(long offset, uint value) + { + if(offset < 0 || offset >= RegisterSpaceSize) + { + this.Log(LogLevel.Warning, "Write to offset 0x{0:X} beyond register space", offset); + return; + } + + var reg = (uint)offset / 4; + + switch((uint)offset) + { + case 0x00: // Protection key — transform like QEMU + if(value == ProtKeyUnlock) + { + storage[reg] = ProtUnlocked; + this.Log(LogLevel.Debug, "SDMC unlocked"); + } + else if(value == ProtKeyHardlock) + { + storage[reg] = ProtHardlocked; + this.Log(LogLevel.Debug, "SDMC hardlocked"); + } + else + { + storage[reg] = ProtSoftlocked; + this.Log(LogLevel.Debug, "SDMC softlocked"); + } + return; + + case 0x04: // Config — preserve readonly bits + value = ComputeConfig(value); + break; + + case 0x60: // Status1 — clear busy, always set PLL lock + value &= ~PhyBusyState; + value |= PhyPllLockStatus; + break; + + case 0x70: // ECC test control — always done, never fail + value |= EccTestFinished; + value &= ~EccTestFail; + break; + + default: + if(!IsExemptFromProtection((uint)offset) && !IsUnlocked) + { + return; + } + break; + } + + storage[reg] = value; + } + + private uint ComputeConfig(uint data) + { + data &= ~ReadonlyConfigMask; + return data | FixedConfig; + } + + private bool IsUnlocked => storage[0x00 / 4] == ProtUnlocked; + + private bool IsExemptFromProtection(uint offset) + { + switch(offset) + { + case 0x00: // R_PROT + case 0x04: // R_CONF (special handling) + case 0x50: // R_ISR + case 0x60: // R_STATUS1 (special handling) + case 0x6C: // R_MCR6C + case 0x70: // R_ECC_TEST_CTRL (special handling) + case 0x74: // R_TEST_START_LEN + case 0x78: // R_TEST_FAIL_DQ + case 0x7C: // R_TEST_INIT_VAL + case 0x88: // R_DRAM_SW + case 0x8C: // R_DRAM_TIME + case 0xB4: // R_ECC_ERR_INJECT + return true; + default: + return false; + } + } + + private void DefineRegisters() + { + // No framework registers — everything goes through storage array + // via ReadDoubleWord/WriteDoubleWord overrides + } + + private readonly uint[] storage = new uint[RegisterSpaceSize / 4]; + + // Protection key constants (per QEMU) + private const uint ProtKeyUnlock = 0xFC600309; + private const uint ProtKeyHardlock = 0xDEADDEAD; + private const uint ProtUnlocked = 0x01; + private const uint ProtHardlocked = 0x10; + private const uint ProtSoftlocked = 0x00; + + // AST2600 1GiB: HW_VERSION=3, VGA=64MB(3), DRAM=1GiB(2) + private const uint DefaultConfig = (3u << 28) | (3u << 2) | 2u; + private const uint FixedConfig = DefaultConfig; + private const uint ReadonlyConfigMask = 0xF000000F; + + private const uint PhyBusyState = 1u << 0; + private const uint PhyPllLockStatus = 1u << 4; + private const uint EccTestFinished = 1u << 12; + private const uint EccTestFail = 1u << 13; + + private const int RegisterSpaceSize = 0x1000; + } +} From 5c988f5f8df6f36c7ee0717867e680ff6d78b74c Mon Sep 17 00:00:00 2001 From: Gary Beihl Date: Thu, 19 Mar 2026 13:52:31 -0400 Subject: [PATCH 3/4] ast2600: Add Aspeed WDT and Timer peripherals Aspeed_WDT: Watchdog Timer (4 instances on AST2600). Supports counter status/reload, restart magic (0x4755), control with AST2600-specific sanitization, reset width with polarity magic. Aspeed_Timer: 8-channel down-counting timer with shared CTRL register (4 bits/timer), IRQ status (W1C), match registers. Implements INumberedGPIOOutput for GIC wiring. Signed-off-by: Gary Beihl --- .../Peripherals/Timers/Aspeed_Timer.cs | 230 ++++++++++++++++++ .../Peripherals/Timers/Aspeed_WDT.cs | 161 ++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 src/Emulator/Peripherals/Peripherals/Timers/Aspeed_Timer.cs create mode 100644 src/Emulator/Peripherals/Peripherals/Timers/Aspeed_WDT.cs diff --git a/src/Emulator/Peripherals/Peripherals/Timers/Aspeed_Timer.cs b/src/Emulator/Peripherals/Peripherals/Timers/Aspeed_Timer.cs new file mode 100644 index 000000000..508cf93b6 --- /dev/null +++ b/src/Emulator/Peripherals/Peripherals/Timers/Aspeed_Timer.cs @@ -0,0 +1,230 @@ +// +// Copyright (c) 2026 Microsoft +// Licensed under the MIT License. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Antmicro.Renode.Core; +using Antmicro.Renode.Core.Structure.Registers; +using Antmicro.Renode.Logging; +using Antmicro.Renode.Peripherals.Bus; +using Antmicro.Renode.Time; + +namespace Antmicro.Renode.Peripherals.Timers +{ + // Aspeed AST2600 Timer Controller + // Reference: QEMU hw/timer/aspeed_timer.c (AST2600 variant) + // + // 8 down-counting timers. Each has STATUS (current count), RELOAD, + // MATCH1, MATCH2 registers. A shared CTRL register at 0x30 has 4 bits + // per timer (enable, ext_clock, overflow_irq, pulse_enable). + // + // Register layout: + // Timer 1-4: 0x00-0x2C (timer_idx = offset >> 4) + // CTRL: 0x30 + // IRQ_STS: 0x34 (AST2600-specific) + // Timer 5-8: 0x40-0x8C (timer_idx = (offset >> 4) - 1) + [AllowedTranslations(AllowedTranslation.ByteToDoubleWord | AllowedTranslation.WordToDoubleWord)] + public sealed class Aspeed_Timer : IDoubleWordPeripheral, IKnownSize, INumberedGPIOOutput + { + public Aspeed_Timer(IMachine machine) + { + this.machine = machine; + var dict = new Dictionary(); + for(int i = 0; i < TimerCount; i++) + { + dict[i] = new GPIO(); + } + Connections = new ReadOnlyDictionary(dict); + + timerReload = new uint[TimerCount]; + timerMatch0 = new uint[TimerCount]; + timerMatch1 = new uint[TimerCount]; + timerStatus = new uint[TimerCount]; + timerEnabled = new bool[TimerCount]; + + ctrl = 0; + irqStatus = 0; + } + + public IReadOnlyDictionary Connections { get; } + + public long Size => 0x100; + + public void Reset() + { + ctrl = 0; + irqStatus = 0; + for(int i = 0; i < TimerCount; i++) + { + timerReload[i] = 0; + timerMatch0[i] = 0; + timerMatch1[i] = 0; + timerStatus[i] = 0; + timerEnabled[i] = false; + Connections[i].Unset(); + } + } + + public uint ReadDoubleWord(long offset) + { + if(offset == CtrlOffset) + { + return ctrl; + } + if(offset == IrqStsOffset) + { + return irqStatus; + } + + int timerIdx = GetTimerIndex(offset); + if(timerIdx < 0) + { + return 0; + } + + int reg = ((int)offset & 0xF) / 4; + switch(reg) + { + case RegStatus: + if(timerEnabled[timerIdx] && timerReload[timerIdx] > 0) + { + if(timerStatus[timerIdx] > 0) + { + timerStatus[timerIdx]--; + } + else + { + timerStatus[timerIdx] = timerReload[timerIdx]; + if(IsOverflowIrqEnabled(timerIdx)) + { + irqStatus |= (1u << timerIdx); + Connections[timerIdx].Set(); + } + } + return timerStatus[timerIdx]; + } + return timerReload[timerIdx]; + + case RegReload: + return timerReload[timerIdx]; + + case RegMatch1: + return timerMatch0[timerIdx]; + + case RegMatch2: + return timerMatch1[timerIdx]; + + default: + return 0; + } + } + + public void WriteDoubleWord(long offset, uint value) + { + if(offset == CtrlOffset) + { + SetControl(value); + return; + } + if(offset == IrqStsOffset) + { + irqStatus &= ~value; + for(int i = 0; i < TimerCount; i++) + { + if((value & (1u << i)) != 0) + { + Connections[i].Unset(); + } + } + return; + } + + int timerIdx = GetTimerIndex(offset); + if(timerIdx < 0) + { + return; + } + + int reg = ((int)offset & 0xF) / 4; + switch(reg) + { + case RegStatus: + timerStatus[timerIdx] = value; + break; + case RegReload: + timerReload[timerIdx] = value; + if(timerEnabled[timerIdx] && value > 0) + { + timerStatus[timerIdx] = value; + } + break; + case RegMatch1: + timerMatch0[timerIdx] = value; + break; + case RegMatch2: + timerMatch1[timerIdx] = value; + break; + } + } + + private void SetControl(uint value) + { + uint oldCtrl = ctrl; + ctrl = value; + + for(int i = 0; i < TimerCount; i++) + { + bool wasEnabled = (oldCtrl & (1u << (i * CtrlBitsPerTimer))) != 0; + bool nowEnabled = (value & (1u << (i * CtrlBitsPerTimer))) != 0; + + timerEnabled[i] = nowEnabled; + + if(!wasEnabled && nowEnabled) + { + timerStatus[i] = timerReload[i]; + } + } + } + + private bool IsOverflowIrqEnabled(int timerIdx) + { + return (ctrl & (1u << (timerIdx * CtrlBitsPerTimer + 2))) != 0; + } + + private int GetTimerIndex(long offset) + { + if(offset >= 0x00 && offset <= 0x2F) + { + return (int)(offset >> 4); + } + if(offset >= 0x40 && offset <= 0x8F) + { + return (int)((offset >> 4) - 1); + } + return -1; + } + + private readonly IMachine machine; + + private uint[] timerReload; + private uint[] timerMatch0; + private uint[] timerMatch1; + private uint[] timerStatus; + private bool[] timerEnabled; + private uint ctrl; + private uint irqStatus; + + private const int TimerCount = 8; + private const int CtrlBitsPerTimer = 4; + private const int CtrlOffset = 0x30; + private const int IrqStsOffset = 0x34; + + private const int RegStatus = 0; + private const int RegReload = 1; + private const int RegMatch1 = 2; + private const int RegMatch2 = 3; + } +} diff --git a/src/Emulator/Peripherals/Peripherals/Timers/Aspeed_WDT.cs b/src/Emulator/Peripherals/Peripherals/Timers/Aspeed_WDT.cs new file mode 100644 index 000000000..5a37ac72a --- /dev/null +++ b/src/Emulator/Peripherals/Peripherals/Timers/Aspeed_WDT.cs @@ -0,0 +1,161 @@ +// +// Copyright (c) 2026 Microsoft +// Licensed under the MIT License. +// + +using Antmicro.Renode.Core; +using Antmicro.Renode.Core.Structure.Registers; +using Antmicro.Renode.Logging; +using Antmicro.Renode.Peripherals.Bus; + +namespace Antmicro.Renode.Peripherals.Timers +{ + // Aspeed AST2600 Watchdog Timer + // Reference: QEMU hw/watchdog/wdt_aspeed.c (AST2600 variant) + // + // 4 instances on AST2600. Each has a 32-bit down-counter at 1 MHz. + // Writing magic 0x4755 to RESTART copies RELOAD into STATUS. + [AllowedTranslations(AllowedTranslation.ByteToDoubleWord | AllowedTranslation.WordToDoubleWord)] + public sealed class Aspeed_WDT : BasicDoubleWordPeripheral, IKnownSize + { + public Aspeed_WDT(IMachine machine) : base(machine) + { + DefineRegisters(); + // Set initial state (Reset() may not be called before first access) + counterStatus = DefaultStatus; + reloadValue = DefaultReloadValue; + controlReg = 0; + resetWidth = 0xFF; + } + + public long Size => 0x40; + + public override void Reset() + { + base.Reset(); + counterStatus = DefaultStatus; + reloadValue = DefaultReloadValue; + controlReg = 0; + resetWidth = 0xFF; + } + + private void DefineRegisters() + { + Registers.Status.Define(this, DefaultStatus) + .WithValueField(0, 32, name: "WDT_STATUS", + valueProviderCallback: _ => counterStatus, + writeCallback: (_, __) => + { + this.Log(LogLevel.Warning, "Write to read-only WDT_STATUS"); + }); + + Registers.ReloadValue.Define(this, DefaultReloadValue) + .WithValueField(0, 32, name: "WDT_RELOAD", + valueProviderCallback: _ => reloadValue, + writeCallback: (_, value) => { reloadValue = (uint)value; }); + + Registers.Restart.Define(this, 0x0) + .WithValueField(0, 32, name: "WDT_RESTART", + valueProviderCallback: _ => 0, + writeCallback: (_, value) => + { + if((value & 0xFFFF) == RestartMagic) + { + counterStatus = reloadValue; + this.Log(LogLevel.Debug, "WDT restarted, counter = 0x{0:X8}", counterStatus); + } + }); + + Registers.Control.Define(this, 0x0) + .WithValueField(0, 32, name: "WDT_CTRL", + valueProviderCallback: _ => controlReg, + writeCallback: (_, value) => + { + controlReg = (uint)(value & ~(0x7u << 7)); + }); + + Registers.TimeoutStatus.Define(this, 0x0) + .WithValueField(0, 32, name: "WDT_TIMEOUT_STS"); + + Registers.TimeoutClear.Define(this, 0x0) + .WithValueField(0, 32, name: "WDT_TIMEOUT_CLR"); + + Registers.ResetWidth.Define(this, 0xFF) + .WithValueField(0, 32, name: "WDT_RESET_WIDTH", + valueProviderCallback: _ => resetWidth, + writeCallback: (_, value) => + { + uint polarity = (uint)(value & 0xFF000000); + switch(polarity) + { + case ActiveHighMagic: + resetWidth |= (1u << 31); + break; + case ActiveLowMagic: + resetWidth &= ~(1u << 31); + break; + case PushPullMagic: + resetWidth |= (1u << 30); + break; + case OpenDrainMagic: + resetWidth &= ~(1u << 30); + break; + } + resetWidth = (resetWidth & 0xFFF00000) | ((uint)value & 0x000FFFFF); + }); + + Registers.ResetMask1.Define(this, 0x0) + .WithValueField(0, 32, name: "WDT_RESET_MASK1"); + + Registers.ResetMask2.Define(this, 0x0) + .WithValueField(0, 32, name: "WDT_RESET_MASK2"); + + Registers.SWResetControl.Define(this, 0x0) + .WithValueField(0, 32, name: "WDT_SW_RESET_CTRL", + writeCallback: (_, value) => + { + if((uint)value == SWResetEnable) + { + this.Log(LogLevel.Warning, "WDT SW reset requested (ignored in emulation)"); + } + }); + + Registers.SWResetMask1.Define(this, 0x0) + .WithValueField(0, 32, name: "WDT_SW_RESET_MASK1"); + + Registers.SWResetMask2.Define(this, 0x0) + .WithValueField(0, 32, name: "WDT_SW_RESET_MASK2"); + } + + private uint counterStatus; + private uint reloadValue; + private uint controlReg; + private uint resetWidth; + + private const uint DefaultStatus = 0x014FB180; + private const uint DefaultReloadValue = 0x014FB180; + private const uint RestartMagic = 0x4755; + private const uint SWResetEnable = 0xAEEDF123; + + private const uint ActiveHighMagic = 0xA5000000; + private const uint ActiveLowMagic = 0x5A000000; + private const uint PushPullMagic = 0xA8000000; + private const uint OpenDrainMagic = 0x8A000000; + + private enum Registers + { + Status = 0x00, + ReloadValue = 0x04, + Restart = 0x08, + Control = 0x0C, + TimeoutStatus = 0x10, + TimeoutClear = 0x14, + ResetWidth = 0x18, + ResetMask1 = 0x1C, + ResetMask2 = 0x20, + SWResetControl = 0x24, + SWResetMask1 = 0x28, + SWResetMask2 = 0x2C, + } + } +} From ff4f1e26d25d9e1bbcbc2abef7109f1a6098f1c3 Mon Sep 17 00:00:00 2001 From: Gary Beihl Date: Thu, 19 Mar 2026 13:53:26 -0400 Subject: [PATCH 4/4] ast2600: Add Aspeed FMC SPI flash controller Firmware Memory Controller with dual ConnectionRegion registration: - "registers" at 0x1E620000 for MMIO control - "flash" at 0x20000000 for memory-mapped flash window Flash window supports two modes via CE0 Control Register: - Normal mode (type=0): reads/writes pass through to MappedMemory - User mode (type=3): SPI commands routed to GenericSpiFlash User mode enables Linux spi-aspeed-smc driver to send JEDEC Read ID (0x9F). Flash configured as Winbond W25Q512JV (0xEF/0x40/0x20), recognized by kernel spi-nor driver for MTD partitions. Also includes: - DMA engine with grant handshake (0xAEED0000/0xDEEA0000 magic) - DMA flash-to-DRAM copy and checksum accumulation - CE0-CE2 control registers with SPI mode and clock config - Timing calibration with HCLK divisor table - DMA completion interrupt support Signed-off-by: Gary Beihl --- .../Peripherals/Peripherals/SPI/Aspeed_FMC.cs | 614 ++++++++++++++++++ 1 file changed, 614 insertions(+) create mode 100644 src/Emulator/Peripherals/Peripherals/SPI/Aspeed_FMC.cs diff --git a/src/Emulator/Peripherals/Peripherals/SPI/Aspeed_FMC.cs b/src/Emulator/Peripherals/Peripherals/SPI/Aspeed_FMC.cs new file mode 100644 index 000000000..ffc7a5baf --- /dev/null +++ b/src/Emulator/Peripherals/Peripherals/SPI/Aspeed_FMC.cs @@ -0,0 +1,614 @@ +// +// Copyright (c) 2026 Microsoft +// Licensed under the MIT License. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Antmicro.Renode.Core; +using Antmicro.Renode.Core.Structure.Registers; +using Antmicro.Renode.Logging; +using Antmicro.Renode.Peripherals.Bus; +using Antmicro.Renode.Peripherals.Memory; + +namespace Antmicro.Renode.Peripherals.SPI +{ + // Aspeed AST2600 Firmware Memory Controller (FMC) + // Reference: QEMU hw/ssi/aspeed_smc.c (ast2600 FMC variant) + // + // The FMC provides SPI flash access through two bus regions: + // 1. "registers" at 0x1E620000 — control/status/DMA registers + // 2. "flash" at 0x20000000 — memory-mapped flash window + // + // The flash window supports two modes controlled by CE0 Control Register: + // - Normal mode (type=0): reads/writes go to backing MappedMemory + // - User mode (type=3): reads/writes send SPI bytes to GenericSpiFlash + // + // User mode is used by the Linux spi-aspeed-smc driver to send JEDEC + // Read ID (0x9F) and other SPI commands to identify the flash chip. + // + // The DMA engine reads/writes through the system bus so it can access + // both the flash window (at 0x20000000) and DRAM (at 0x80000000). + // + // AST2600-specific features: + // - DMA grant handshake (0xAEED0000 request / 0xDEEA0000 clear) + // - WDT2 alternate boot control + // - Read timing calibration with failure injection + // + // For boot: u-boot SPL runs from the flash window. During init it + // uses DMA checksum for SPI timing calibration, then loads the next + // stage from flash to DRAM via DMA. + [AllowedTranslations(AllowedTranslation.ByteToDoubleWord | AllowedTranslation.WordToDoubleWord)] + public sealed class Aspeed_FMC : BasicDoubleWordPeripheral, IKnownSize, INumberedGPIOOutput + { + public Aspeed_FMC(IMachine machine, MappedMemory flashMemory) : base(machine) + { + sysbus = machine.GetSystemBus(this); + this.flashMemory = flashMemory; + this.spiFlash = new GenericSpiFlash(flashMemory, + manufacturerId: 0xEF, memoryType: 0x40, capacityCode: 0x20); + + var dict = new Dictionary(); + dict[0] = new GPIO(); // DMA completion IRQ + Connections = new ReadOnlyDictionary(dict); + + dmaCtrl = 0; + dmaFlashAddr = 0; + dmaDramAddr = 0; + dmaLen = 0; + dmaChecksum = 0; + + // CE0 defaults: normal mode (type=0), CE_STOP_ACTIVE + ceType = DefaultCECtrl & 0x3; + ceStopActive = (DefaultCECtrl & 0x4) != 0; + + DefineRegisters(); + } + + public IReadOnlyDictionary Connections { get; } + + public long Size => 0x200; + + public override void Reset() + { + base.Reset(); + spiFlash.Reset(); + dmaCtrl = 0; + dmaFlashAddr = 0; + dmaDramAddr = 0; + dmaLen = 0; + dmaChecksum = 0; + ceType = DefaultCECtrl & 0x3; + ceStopActive = (DefaultCECtrl & 0x4) != 0; + Connections[0].Unset(); + } + + // --- Register region (0x1E620000) --- + + [ConnectionRegion("registers")] + public override uint ReadDoubleWord(long offset) + { + return RegistersCollection.Read(offset); + } + + [ConnectionRegion("registers")] + public override void WriteDoubleWord(long offset, uint value) + { + RegistersCollection.Write(offset, value); + } + + // --- Flash region (0x20000000) --- + + [ConnectionRegion("flash")] + public byte FlashReadByte(long offset) + { + if(IsUserMode()) + { + return spiFlash.Transmit(0); + } + return flashMemory.ReadByte(offset); + } + + [ConnectionRegion("flash")] + public void FlashWriteByte(long offset, byte value) + { + if(IsUserMode()) + { + spiFlash.Transmit(value); + } + else + { + flashMemory.WriteByte(offset, value); + } + } + + [ConnectionRegion("flash")] + public uint FlashReadDoubleWord(long offset) + { + if(IsUserMode()) + { + uint result = 0; + for(int i = 0; i < 4; i++) + { + result |= (uint)spiFlash.Transmit(0) << (i * 8); + } + return result; + } + return flashMemory.ReadDoubleWord(offset); + } + + [ConnectionRegion("flash")] + public void FlashWriteDoubleWord(long offset, uint value) + { + if(IsUserMode()) + { + for(int i = 0; i < 4; i++) + { + spiFlash.Transmit((byte)(value >> (i * 8))); + } + } + else + { + flashMemory.WriteDoubleWord(offset, value); + } + } + + // --- CE0 mode tracking --- + + private void UpdateCEMode(uint value) + { + var newType = value & 0x3; + var stopActive = (value & 0x4) != 0; + + if(!ceStopActive && stopActive) + { + // CE deasserted — end SPI transaction + spiFlash.FinishTransmission(); + } + + ceType = newType; + ceStopActive = stopActive; + } + + private bool IsUserMode() + { + return ceType == UserModeType && !ceStopActive; + } + + private void DefineRegisters() + { + // 0x00 — CE Type Setting Register + // AST2600 default: CE0 = SPI (0x2), write-enabled + Registers.Configuration.Define(this, DefaultConf) + .WithValueField(0, 32, name: "FMC_CONF"); + + // 0x04 — CE Control Register (4-byte addr mode per CE) + Registers.CEControl.Define(this, 0x0) + .WithValueField(0, 32, name: "CE_CTRL"); + + // 0x08 — Interrupt Control and Status + Registers.InterruptControl.Define(this, 0x0) + .WithValueField(0, 32, name: "INTR_CTRL", + writeCallback: (_, value) => + { + // Store the full value; bits[2:0] = enables, bits[11:9] = status + // DMA status bit (11) is read-only, set by DMA completion + }); + + // 0x0C — Command Control Register + Registers.CommandControl.Define(this, 0x0) + .WithValueField(0, 8, name: "CE_CMD_CTRL"); + + // 0x10-0x20 — CE0-CE2 Control Registers + Registers.CE0Control.Define(this, DefaultCECtrl) + .WithValueField(0, 32, name: "CE0_CTRL", + writeCallback: (_, value) => UpdateCEMode((uint)value)); + Registers.CE1Control.Define(this, DefaultCECtrl) + .WithValueField(0, 32, name: "CE1_CTRL"); + Registers.CE2Control.Define(this, DefaultCECtrl) + .WithValueField(0, 32, name: "CE2_CTRL"); + + // 0x30-0x38 — Segment Address Registers + Registers.SegmentAddr0.Define(this, DefaultSeg0) + .WithValueField(0, 32, name: "SEG_ADDR0"); + Registers.SegmentAddr1.Define(this, DefaultSeg1) + .WithValueField(0, 32, name: "SEG_ADDR1"); + Registers.SegmentAddr2.Define(this, 0x0) + .WithValueField(0, 32, name: "SEG_ADDR2"); + + // 0x50 — Misc Control #1 + Registers.MiscControl1.Define(this, 0x0) + .WithValueField(0, 32, name: "MISC_CTRL1"); + + // 0x54 — Dummy Data + Registers.DummyData.Define(this, 0x0) + .WithValueField(0, 8, name: "DUMMY_DATA"); + + // 0x64 — FMC WDT2 Control (alternate boot) + Registers.WDT2Control.Define(this, 0x0) + .WithFlag(0, name: "WDT2_EN") + .WithReservedBits(1, 3) + .WithFlag(4, name: "BOOT_SOURCE") + .WithFlag(5, name: "SINGLE_BOOT") + .WithFlag(6, name: "ALT_BOOT_MODE") + .WithReservedBits(7, 25); + + // 0x80 — DMA Control/Status + Registers.DMAControl.Define(this, 0x0) + .WithValueField(0, 32, name: "DMA_CTRL", + valueProviderCallback: _ => dmaCtrl, + writeCallback: (_, value) => HandleDmaCtrlWrite((uint)value)); + + // 0x84 — DMA Flash Side Address + Registers.DMAFlashAddr.Define(this, 0x0) + .WithValueField(0, 32, name: "DMA_FLASH_ADDR", + valueProviderCallback: _ => dmaFlashAddr, + writeCallback: (_, value) => + { + if(IsDmaGranted()) + { + dmaFlashAddr = (uint)(value & DmaFlashMask); + } + }); + + // 0x88 — DMA DRAM Side Address + Registers.DMADramAddr.Define(this, 0x0) + .WithValueField(0, 32, name: "DMA_DRAM_ADDR", + valueProviderCallback: _ => dmaDramAddr, + writeCallback: (_, value) => + { + if(IsDmaGranted()) + { + dmaDramAddr = (uint)(value & DmaDramMask); + } + }); + + // 0x8C — DMA Length + Registers.DMALength.Define(this, 0x0) + .WithValueField(0, 32, name: "DMA_LEN", + valueProviderCallback: _ => dmaLen, + writeCallback: (_, value) => + { + if(IsDmaGranted()) + { + dmaLen = (uint)(value & DmaLenMask); + } + }); + + // 0x90 — DMA Checksum (read-only result) + Registers.DMAChecksum.Define(this, 0x0) + .WithValueField(0, 32, FieldMode.Read, name: "DMA_CHECKSUM", + valueProviderCallback: _ => dmaChecksum); + + // 0x94 — Read Timing Compensation + Registers.Timings.Define(this, 0x0) + .WithValueField(0, 32, name: "TIMINGS", + valueProviderCallback: _ => timingsReg, + writeCallback: (_, value) => { timingsReg = (uint)value; }); + } + + // --- DMA Engine --- + + private void HandleDmaCtrlWrite(uint value) + { + // AST2600 DMA grant handshake + // Preserve existing request/grant bits + value |= (dmaCtrl & (DmaCtrlRequest | DmaCtrlGrant)); + + if(value == DmaGrantRequestMagic) + { + // Automatically grant request + dmaCtrl |= (DmaCtrlRequest | DmaCtrlGrant); + this.Log(LogLevel.Debug, "DMA grant requested and auto-granted"); + return; + } + + if(value == DmaGrantClearMagic) + { + // Clear request/grant + dmaCtrl &= ~(DmaCtrlRequest | DmaCtrlGrant); + this.Log(LogLevel.Debug, "DMA grant cleared"); + return; + } + + if(!IsDmaGranted()) + { + this.Log(LogLevel.Warning, "DMA operation without grant"); + return; + } + + if((value & DmaCtrlEnable) == 0) + { + // Disable DMA + dmaCtrl = value; + DmaStop(); + return; + } + + // Check if DMA already in progress (completion bit set means done) + if((dmaCtrl & DmaCtrlEnable) != 0 && (intrCtrlValue & IntrDmaStatus) == 0) + { + this.Log(LogLevel.Warning, "DMA already in progress"); + return; + } + + dmaCtrl = value; + + if((dmaCtrl & DmaCtrlCksum) != 0) + { + if((dmaCtrl & DmaCtrlCalib) != 0) + { + DmaCalibration(); + } + DmaChecksum(); + } + else + { + DmaReadWrite(); + } + + DmaDone(); + + // Clear grant after operation + dmaCtrl &= ~(DmaCtrlRequest | DmaCtrlGrant); + } + + private void DmaChecksum() + { + if((dmaCtrl & DmaCtrlWrite) != 0) + { + this.Log(LogLevel.Warning, "Invalid DMA direction for checksum"); + return; + } + + uint len = AlignedDmaLen(); + dmaChecksum = 0; + + this.Log(LogLevel.Debug, "DMA checksum: flash=0x{0:X8} len=0x{1:X}", dmaFlashAddr, len); + + while(len > 0) + { + uint data; + try + { + data = sysbus.ReadDoubleWord(dmaFlashAddr); + } + catch + { + this.Log(LogLevel.Warning, "DMA flash read failed at 0x{0:X8}", dmaFlashAddr); + return; + } + + dmaChecksum += data; + dmaFlashAddr += 4; + len -= 4; + dmaLen = len; + } + + // Inject read failure for high-speed timing calibration + if(ShouldInjectFailure()) + { + dmaChecksum = 0xBADC0DE; + this.Log(LogLevel.Debug, "DMA checksum: injecting failure for calibration"); + } + } + + private void DmaReadWrite() + { + uint len = AlignedDmaLen(); + bool isWrite = (dmaCtrl & DmaCtrlWrite) != 0; + + this.Log(LogLevel.Debug, "DMA {0}: flash=0x{1:X8} dram=0x{2:X8} len=0x{3:X}", + isWrite ? "write" : "read", dmaFlashAddr, dmaDramAddr, len); + + while(len > 0) + { + try + { + if(isWrite) + { + // DRAM → Flash + uint data = sysbus.ReadDoubleWord(dmaDramAddr); + sysbus.WriteDoubleWord(dmaFlashAddr, data); + } + else + { + // Flash → DRAM + uint data = sysbus.ReadDoubleWord(dmaFlashAddr); + sysbus.WriteDoubleWord(dmaDramAddr, data); + } + } + catch(Exception e) + { + this.Log(LogLevel.Warning, "DMA transfer failed: {0}", e.Message); + return; + } + + dmaChecksum += sysbus.ReadDoubleWord(dmaFlashAddr); + dmaDramAddr += 4; + dmaFlashAddr += 4; + len -= 4; + dmaLen = len; + } + } + + private void DmaCalibration() + { + // Extract calibration parameters from DMA_CTRL + uint delay = (dmaCtrl >> DmaCtrlDelayShift) & 0xF; + uint hclkMask = (dmaCtrl >> DmaCtrlFreqShift) & 0xF; + uint hclkDiv = HclkDivisor(hclkMask); + uint hclkShift = (hclkDiv - 1) * 4; + + // Update timing register + if(hclkDiv > 0 && hclkDiv < 6) + { + timingsReg &= ~(0xFu << (int)hclkShift); + timingsReg |= (delay << (int)hclkShift); + } + + // Update CE0 clock frequency + uint ce0Ctrl = RegistersCollection.Read((long)Registers.CE0Control); + ce0Ctrl &= ~(0xFu << CeCtrlClockFreqShift); + ce0Ctrl |= ((hclkDiv & 0xF) << CeCtrlClockFreqShift); + RegistersCollection.Write((long)Registers.CE0Control, ce0Ctrl); + + this.Log(LogLevel.Debug, "DMA calibration: hclk_div={0} delay={1}", hclkDiv, delay); + } + + private bool ShouldInjectFailure() + { + if((dmaCtrl & DmaCtrlCalib) == 0) + { + return false; + } + + uint delay = (dmaCtrl >> DmaCtrlDelayShift) & 0xF; + uint hclkMask = (dmaCtrl >> DmaCtrlFreqShift) & 0xF; + uint hclkDiv = HclkDivisor(hclkMask); + + switch(hclkDiv) + { + case 4: case 5: case 6: case 7: case 8: + case 9: case 10: case 11: case 12: case 13: + case 14: case 15: case 16: + return false; + case 3: + return (delay & 0x7) < 1; + case 2: + return (delay & 0x7) < 2; + case 1: + return true; // > 100MHz, always fail + default: + return false; + } + } + + private static uint HclkDivisor(uint mask) + { + // HCLK/1 .. HCLK/16 lookup table (from QEMU) + byte[] divisors = { 15, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 0 }; + for(int i = 0; i < divisors.Length; i++) + { + if(mask == divisors[i]) + { + return (uint)(i + 1); + } + } + return 1; + } + + private void DmaDone() + { + // Set DMA status bit in interrupt register + intrCtrlValue |= IntrDmaStatus; + RegistersCollection.Write((long)Registers.InterruptControl, intrCtrlValue); + + // Raise IRQ if DMA interrupt enabled + if((intrCtrlValue & IntrDmaEn) != 0) + { + Connections[0].Set(); + } + } + + private void DmaStop() + { + intrCtrlValue &= ~IntrDmaStatus; + RegistersCollection.Write((long)Registers.InterruptControl, intrCtrlValue); + dmaChecksum = 0; + Connections[0].Unset(); + } + + private bool IsDmaGranted() + { + return (dmaCtrl & DmaCtrlGrant) != 0; + } + + private uint AlignedDmaLen() + { + // AST2600: DMA length starts at 1 byte, align up to 4 + uint len = dmaLen + 1; // dma_start_length = 1 + return (len + 3) & ~3u; + } + + // --- Fields --- + + private new readonly IBusController sysbus; + private readonly MappedMemory flashMemory; + private readonly GenericSpiFlash spiFlash; + + private uint dmaCtrl; + private uint dmaFlashAddr; + private uint dmaDramAddr; + private uint dmaLen; + private uint dmaChecksum; + private uint timingsReg; + private uint intrCtrlValue; + private uint ceType; + private bool ceStopActive; + + // --- Constants --- + + // CE0 default: SPI type, write enabled + private const uint DefaultConf = (0x2u << 0) | (1u << 16); + // CE default control: read mode, READ cmd (0x03), CE_STOP_ACTIVE + private const uint DefaultCECtrl = (0x03u << 16) | (1u << 2); + // Segment 0: 0-128MB (in 8MB units: end=0x08, start=0x00) + private const uint DefaultSeg0 = (0x08u << 24) | (0x00u << 16); + // Segment 1: 128-256MB + private const uint DefaultSeg1 = (0x10u << 24) | (0x08u << 16); + + // User mode type value + private const uint UserModeType = 3; + + // DMA control bits + private const uint DmaCtrlRequest = (1u << 31); + private const uint DmaCtrlGrant = (1u << 30); + private const uint DmaCtrlCalib = (1u << 3); + private const uint DmaCtrlCksum = (1u << 2); + private const uint DmaCtrlWrite = (1u << 1); + private const uint DmaCtrlEnable = (1u << 0); + private const int DmaCtrlDelayShift = 8; + private const int DmaCtrlFreqShift = 4; + + // DMA magic values (AST2600) + private const uint DmaGrantRequestMagic = 0xAEED0000; + private const uint DmaGrantClearMagic = 0xDEEA0000; + + // DMA address masks + private const uint DmaFlashMask = 0xFFFFFFFC; + private const uint DmaDramMask = 0xFFFFFFFC; + private const uint DmaLenMask = 0x01FFFFFF; + + // Interrupt control bits + private const uint IntrDmaStatus = (1u << 11); + private const uint IntrDmaEn = (1u << 3); + + // CE control + private const int CeCtrlClockFreqShift = 8; + + private enum Registers + { + Configuration = 0x00, + CEControl = 0x04, + InterruptControl = 0x08, + CommandControl = 0x0C, + CE0Control = 0x10, + CE1Control = 0x14, + CE2Control = 0x18, + SegmentAddr0 = 0x30, + SegmentAddr1 = 0x34, + SegmentAddr2 = 0x38, + MiscControl1 = 0x50, + DummyData = 0x54, + WDT2Control = 0x64, + DMAControl = 0x80, + DMAFlashAddr = 0x84, + DMADramAddr = 0x88, + DMALength = 0x8C, + DMAChecksum = 0x90, + Timings = 0x94, + } + } +}