diff --git a/src/Emulator/Peripherals/Peripherals/Memory/MRAMMemory.cs b/src/Emulator/Peripherals/Peripherals/Memory/MRAMMemory.cs
new file mode 100644
index 000000000..50300e0f6
--- /dev/null
+++ b/src/Emulator/Peripherals/Peripherals/Memory/MRAMMemory.cs
@@ -0,0 +1,513 @@
+//
+// Copyright (c) 2010-2026 Antmicro
+//
+// This file is licensed under the MIT License.
+// Full license text is available in 'licenses/MIT.txt'.
+//
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using Antmicro.Renode.Logging;
+using Antmicro.Renode.Peripherals.Bus;
+
+namespace Antmicro.Renode.Peripherals.Memory
+{
+ ///
+ /// Non-volatile memory model with configurable word-write semantics and
+ /// power-loss fault injection. Suitable for MRAM, FRAM, or any byte-
+ /// addressable NVM whose writes are atomic at a fixed word granularity.
+ ///
+ /// Key behaviors:
+ /// - Reset() preserves storage contents (non-volatile).
+ /// - When is true, every write is
+ /// decomposed into erase-then-program cycles at
+ /// boundaries, matching real hardware behavior.
+ /// - simulates a power cut mid-program:
+ /// the first half of the target word is written, the second half is
+ /// zeroed. This is the core primitive for fault-injection campaigns.
+ /// - selects between power-loss (partial
+ /// program) and bit-corruption (deterministic bit flips) fault models.
+ /// - records each word write as a
+ /// (writeIndex, wordOffset) tuple for offline trace replay.
+ /// - controls whether the un-programmed
+ /// half of a partial write retains old data (MRAM) or is filled with
+ /// (flash).
+ ///
+ public class MRAMMemory : ArrayMemory, IBytePeripheral, IWordPeripheral, IDoubleWordPeripheral
+ {
+ public MRAMMemory(ulong size = DefaultSize, int wordSize = DefaultWordSize) : base(size)
+ {
+ if(wordSize <= 0 || (wordSize & (wordSize - 1)) != 0)
+ {
+ throw new ArgumentException("WordSize must be a positive power of two");
+ }
+
+ this.wordSize = wordSize;
+ writeTrace = new List<(ulong writeIndex, long wordOffset)>();
+ }
+
+ public new void Reset()
+ {
+ // Intentionally do NOT call base.Reset() or clear storage:
+ // this models non-volatile memory that retains data across resets.
+ WriteInProgress = false;
+ LastFaultInjected = false;
+ FaultEverFired = false;
+ TotalWordWrites = 0;
+ ReadFaultFired = false;
+ ReadFaultTotalReads = 0;
+ WriteTraceClear();
+ }
+
+ public override void WriteByte(long offset, byte value)
+ {
+ WriteBytesWithWordSemantics(offset, new[] { value });
+ }
+
+ public override void WriteWord(long offset, ushort value)
+ {
+ WriteBytesWithWordSemantics(offset, new[]
+ {
+ (byte)(value & 0xFF),
+ (byte)((value >> 8) & 0xFF),
+ });
+ }
+
+ public override void WriteDoubleWord(long offset, uint value)
+ {
+ WriteBytesWithWordSemantics(offset, new[]
+ {
+ (byte)(value & 0xFF),
+ (byte)((value >> 8) & 0xFF),
+ (byte)((value >> 16) & 0xFF),
+ (byte)((value >> 24) & 0xFF),
+ });
+ }
+
+ public override void WriteQuadWord(long offset, ulong value)
+ {
+ WriteBytesWithWordSemantics(offset, new[]
+ {
+ (byte)(value & 0xFF),
+ (byte)((value >> 8) & 0xFF),
+ (byte)((value >> 16) & 0xFF),
+ (byte)((value >> 24) & 0xFF),
+ (byte)((value >> 32) & 0xFF),
+ (byte)((value >> 40) & 0xFF),
+ (byte)((value >> 48) & 0xFF),
+ (byte)((value >> 56) & 0xFF),
+ });
+ }
+
+ // Read methods: re-implement IBytePeripheral/IWordPeripheral/IDoubleWordPeripheral
+ // to intercept reads for transient fault injection. NVM contents are NOT modified;
+ // only the value returned to the CPU is corrupted.
+
+ public new byte ReadByte(long offset)
+ {
+ var value = base.ReadByte(offset);
+ return (byte)ApplyReadFault(offset, 1, value);
+ }
+
+ public new ushort ReadWord(long offset)
+ {
+ var value = base.ReadWord(offset);
+ return (ushort)ApplyReadFault(offset, 2, value);
+ }
+
+ public new uint ReadDoubleWord(long offset)
+ {
+ var value = base.ReadDoubleWord(offset);
+ return ApplyReadFault(offset, 4, value);
+ }
+
+ ///
+ /// Simulate a power cut during a word-aligned write. The second half of
+ /// the word at is filled with EraseFill; the
+ /// first half is left unchanged. Call after writing partial data to
+ /// model a mid-program power loss.
+ ///
+ public void InjectPartialWrite(long address)
+ {
+ var aligned = AlignDown(address);
+ if(aligned < 0 || aligned + wordSize > Size)
+ {
+ this.Log(LogLevel.Error, "InjectPartialWrite at 0x{0:X} is outside memory bounds", address);
+ return;
+ }
+
+ var half = wordSize / 2;
+ for(var i = half; i < wordSize; i++)
+ {
+ array[aligned + i] = EraseFill;
+ }
+
+ LastFaultInjected = true;
+ }
+
+ ///
+ /// Overwrite a region with a fixed pattern, modeling arbitrary corruption.
+ ///
+ public void InjectFault(long address, long length, byte pattern = 0x00)
+ {
+ if(length <= 0)
+ {
+ return;
+ }
+
+ if(address < 0 || address + length > Size)
+ {
+ this.Log(LogLevel.Error, "InjectFault at 0x{0:X} length {1} is outside memory bounds", address, length);
+ return;
+ }
+
+ for(var i = 0L; i < length; i++)
+ {
+ array[address + i] = pattern;
+ }
+ LastFaultInjected = true;
+ }
+
+ ///
+ /// Query the total number of word-granularity write operations performed.
+ /// Useful for setting up fault injection at a specific write index.
+ ///
+ public ulong GetWordWriteCount()
+ {
+ return TotalWordWrites;
+ }
+
+ ///
+ /// Return all recorded write trace entries as a CSV string.
+ /// Each line is "writeIndex,wordOffset\n".
+ ///
+ public string WriteTraceToString()
+ {
+ var sb = new StringBuilder();
+ foreach(var entry in writeTrace)
+ {
+ sb.AppendFormat("{0},{1}\n", entry.writeIndex, entry.wordOffset);
+ }
+ return sb.ToString();
+ }
+
+ ///
+ /// Clear all recorded write trace entries.
+ ///
+ public void WriteTraceClear()
+ {
+ writeTrace.Clear();
+ }
+
+ public int WordSize
+ {
+ get { return wordSize; }
+ set
+ {
+ if(value <= 0 || (value & (value - 1)) != 0)
+ {
+ throw new ArgumentException("WordSize must be a positive power of two");
+ }
+ wordSize = value;
+ }
+ }
+
+ public bool EnforceWordWriteSemantics { get; set; } = true;
+
+ public byte EraseFill { get; set; }
+
+ public bool WriteInProgress { get; private set; }
+
+ public bool LastFaultInjected { get; set; }
+
+ ///
+ /// Sticky flag: set when any fault fires, never cleared by subsequent
+ /// writes. Use this instead of when
+ /// you need to check after the CPU has continued past the faulted write.
+ ///
+ public bool FaultEverFired { get; set; }
+
+ public ulong TotalWordWrites { get; set; }
+
+ public ulong FaultAtWordWrite { get; set; } = ulong.MaxValue;
+
+ ///
+ /// Fault model selector:
+ /// 0 = power_loss (default): partial program, first half written,
+ /// second half erased or retained depending on
+ /// .
+ /// 1 = bit_corruption: full word is written, then 1-3 deterministic
+ /// bits are flipped to model partial cell state transitions.
+ ///
+ public int WriteFaultMode { get; set; }
+
+ ///
+ /// Seed for the bit-corruption PRNG. When 0, the word-aligned address
+ /// is used as the seed, giving address-dependent deterministic results.
+ ///
+ public uint CorruptionSeed { get; set; }
+
+ ///
+ /// When true (default), the un-programmed half of a partial write
+ /// retains the old data that was present before the erase step. This
+ /// models MRAM/FRAM where erase is implicit and old data survives
+ /// partial programming. When false, the un-programmed half is filled
+ /// with , modeling flash where erase physically
+ /// clears the cell before programming.
+ ///
+ public bool RetainOldDataOnFault { get; set; } = true;
+
+ ///
+ /// When true, each word-granularity write is recorded as a
+ /// (writeIndex, wordOffset) tuple. Retrieve with
+ /// and clear with
+ /// .
+ ///
+ public bool WriteTraceEnabled { get; set; }
+
+ // Read-fault injection: one-shot transient read corruption.
+ // NVM content is unchanged; only the value returned to the CPU
+ // on the first matching read is corrupted.
+
+ ///
+ /// Master enable for read-fault injection. When true, the next read
+ /// that overlaps (after skipping
+ /// qualifying reads) will have
+ /// deterministic bit flips applied to the returned value. The fault
+ /// is one-shot: after firing, is
+ /// automatically cleared and is set.
+ ///
+ public bool ReadFaultEnabled { get; set; }
+
+ ///
+ /// Peripheral-relative address that arms the read fault. Reads
+ /// overlapping the 4-byte window [ReadFaultAddress, ReadFaultAddress+4)
+ /// are candidates for corruption. Set to -1 (default) to disable.
+ ///
+ public long ReadFaultAddress { get; set; } = -1;
+
+ ///
+ /// PRNG seed for read-fault bit selection. When 0, a deterministic
+ /// seed derived from is used.
+ ///
+ public uint ReadFaultSeed { get; set; }
+
+ ///
+ /// Set to true after a read fault fires. Sticky until explicitly cleared.
+ ///
+ public bool ReadFaultFired { get; set; }
+
+ ///
+ /// Number of qualifying reads to skip before firing. Useful for
+ /// targeting the Nth read of a specific address (e.g., the second
+ /// CRC validation pass).
+ ///
+ public ulong ReadFaultSkipCount { get; set; }
+
+ ///
+ /// Running count of qualifying reads seen since the fault was armed.
+ ///
+ public ulong ReadFaultTotalReads { get; set; }
+
+ ///
+ /// Number of bits to flip on the faulted read. When 0, a
+ /// seed-dependent count of 1-3 is used.
+ ///
+ public int ReadFaultBitFlips { get; set; }
+
+ private void WriteBytesWithWordSemantics(long offset, byte[] data)
+ {
+ if(data.Length == 0)
+ {
+ return;
+ }
+
+ if(!EnforceWordWriteSemantics)
+ {
+ for(var i = 0; i < data.Length; i++)
+ {
+ base.WriteByte(offset + i, data[i]);
+ }
+ return;
+ }
+
+ var firstWordStart = AlignDown(offset);
+ var lastWordStart = AlignDown(offset + data.Length - 1);
+
+ WriteInProgress = true;
+ LastFaultInjected = false;
+
+ try
+ {
+ for(var wordStart = firstWordStart; wordStart <= lastWordStart; wordStart += wordSize)
+ {
+ // Read-modify-write: read current word, merge new data, erase, program.
+ var oldWord = new byte[wordSize];
+ var mergedWord = new byte[wordSize];
+ for(var i = 0; i < wordSize; i++)
+ {
+ oldWord[i] = array[wordStart + i];
+ mergedWord[i] = array[wordStart + i];
+ }
+
+ for(var i = 0; i < data.Length; i++)
+ {
+ var absoluteAddress = offset + i;
+ if(absoluteAddress < wordStart || absoluteAddress >= wordStart + wordSize)
+ {
+ continue;
+ }
+ mergedWord[absoluteAddress - wordStart] = data[i];
+ }
+
+ // Erase the word.
+ for(var i = 0; i < wordSize; i++)
+ {
+ array[wordStart + i] = EraseFill;
+ }
+
+ // Check for automatic fault injection at this write index.
+ var currentWriteIndex = TotalWordWrites + 1;
+ if(currentWriteIndex == FaultAtWordWrite)
+ {
+ if(WriteFaultMode == 1)
+ {
+ // Bit-corruption mode: write the full merged word,
+ // then flip 1-3 deterministic bits to model partial
+ // cell state transitions during interrupted NVM programming.
+ for(var i = 0; i < wordSize; i++)
+ {
+ array[wordStart + i] = mergedWord[i];
+ }
+ ApplyBitCorruption(wordStart);
+ }
+ else
+ {
+ // Power-loss mode: partial program, first half written.
+ var partialBytes = wordSize / 2;
+ for(var i = 0; i < partialBytes; i++)
+ {
+ array[wordStart + i] = mergedWord[i];
+ }
+ // Second half: retain old data (MRAM) or leave as EraseFill (flash).
+ if(RetainOldDataOnFault)
+ {
+ for(var i = partialBytes; i < wordSize; i++)
+ {
+ array[wordStart + i] = oldWord[i];
+ }
+ }
+ }
+ LastFaultInjected = true;
+ FaultEverFired = true;
+ TotalWordWrites++;
+ RecordWriteTrace(TotalWordWrites, wordStart);
+ break;
+ }
+
+ // Full program.
+ for(var i = 0; i < wordSize; i++)
+ {
+ array[wordStart + i] = mergedWord[i];
+ }
+ TotalWordWrites++;
+ RecordWriteTrace(TotalWordWrites, wordStart);
+ }
+ }
+ finally
+ {
+ WriteInProgress = false;
+ }
+ }
+
+ private void RecordWriteTrace(ulong writeIndex, long wordOffset)
+ {
+ if(!WriteTraceEnabled || writeTrace.Count >= WriteTraceMaxEntries)
+ {
+ return;
+ }
+ writeTrace.Add((writeIndex, wordOffset));
+ }
+
+ ///
+ /// Apply deterministic bit corruption to the word at the given aligned
+ /// address. Flips 1-3 bits using an LCG PRNG, modeling partial cell
+ /// state transitions during interrupted NVM programming.
+ ///
+ private void ApplyBitCorruption(long wordStart)
+ {
+ var seed = CorruptionSeed != 0 ? CorruptionSeed : (uint)wordStart;
+ var totalBits = wordSize * 8;
+
+ // Determine number of bits to flip: 1-3 from first LCG step.
+ seed = LcgNext(seed);
+ var numFlips = (int)(seed % 3) + 1;
+
+ for(var f = 0; f < numFlips; f++)
+ {
+ seed = LcgNext(seed);
+ var bitPos = (int)(seed % (uint)totalBits);
+ var byteIndex = bitPos / 8;
+ var bitIndex = bitPos % 8;
+ array[wordStart + byteIndex] ^= (byte)(1 << bitIndex);
+ }
+ }
+
+ private static uint LcgNext(uint seed)
+ {
+ return (uint)((seed * 1103515245UL + 12345UL) & 0xFFFFFFFF);
+ }
+
+ private uint ApplyReadFault(long offset, int accessSize, uint value)
+ {
+ if(!ReadFaultEnabled || ReadFaultFired || ReadFaultAddress < 0)
+ {
+ return value;
+ }
+
+ var armedEnd = ReadFaultAddress + 4;
+ var accessEnd = offset + accessSize;
+ if(offset >= armedEnd || accessEnd <= ReadFaultAddress)
+ {
+ return value;
+ }
+
+ ReadFaultTotalReads++;
+ if(ReadFaultTotalReads <= ReadFaultSkipCount)
+ {
+ return value;
+ }
+
+ // Fire: flip deterministic bits. NVM is NOT modified.
+ ReadFaultFired = true;
+ ReadFaultEnabled = false;
+ var seed = ReadFaultSeed != 0 ? ReadFaultSeed : (uint)(ReadFaultAddress ^ 0xDEAD);
+ if(seed == 0)
+ {
+ seed = 0xDEAD;
+ }
+ var flipCount = ReadFaultBitFlips > 0 ? (uint)ReadFaultBitFlips : 1u + (seed % 3);
+ var accessBits = accessSize * 8;
+ for(var i = 0u; i < flipCount; i++)
+ {
+ seed = LcgNext(seed);
+ var bitPos = (int)(seed % (uint)accessBits);
+ value ^= (uint)(1 << bitPos);
+ }
+ return value;
+ }
+
+ private long AlignDown(long value)
+ {
+ return value & ~((long)wordSize - 1);
+ }
+
+ private int wordSize;
+ private readonly List<(ulong writeIndex, long wordOffset)> writeTrace;
+
+ private const ulong DefaultSize = 0x80000;
+ private const int DefaultWordSize = 8;
+ private const int WriteTraceMaxEntries = 100000;
+ }
+}
diff --git a/src/Infrastructure.csproj b/src/Infrastructure.csproj
index 93c301cdf..42a84738e 100644
--- a/src/Infrastructure.csproj
+++ b/src/Infrastructure.csproj
@@ -657,6 +657,7 @@
+