Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions src/RP2040Sharp/Core/Cpu/CortexM0Plus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ public sealed unsafe class CortexM0Plus
/// </summary>
public Action<uint, uint>? OnLockup;

/// <summary>
/// Called when the CPU takes an exception (ISR / fault / SysTick / PendSV / SVCall).
/// The parameter is the exception number being entered (same value written to IPSR).
/// Fires once per exception entry — never on the per-instruction hot path — and is
/// intended for profiling/tracing the boundary between thread code and handler code.
/// </summary>
public Action<uint>? OnExceptionEntry;

/// <summary>
/// Called when the CPU returns from an exception (EXC_RETURN consumed). The parameter
/// is the EXC_RETURN value that drove the return. By the time this fires the registers,
/// SP and PC have already been restored to the resumed context, so a profiler can read
/// the return PC / stack to identify the task being resumed.
/// Fires once per exception return — never on the per-instruction hot path.
/// </summary>
public Action<uint>? OnExceptionReturn;

/// <summary>
/// Native hooks: when the PC equals a registered address (Thumb bit stripped), the
/// corresponding delegate is called instead of fetching/executing an instruction.
Expand Down Expand Up @@ -228,6 +245,112 @@ public void Run(int instructions)
_fetchMask = fetchMask;
}

/// <summary>
/// Profiling-only sibling of <see cref="Run"/>. Identical execution semantics, but
/// invokes <paramref name="observer"/> once per instruction (before dispatch). This is a
/// deliberately separate method so the hot <see cref="Run"/> path is byte-for-byte
/// unchanged and pays nothing for profiling — mirroring AVR8Sharp's
/// Execute()/ExecuteProfiling() split. Do not use for throughput-sensitive simulation.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void RunProfiled(int instructions, IProfilingObserver observer)
{
var decoder = _decoder;

var fetchPtr = _fetchPtr;
var fetchMask = _fetchMask;
var regionId = _currentRegionId;

while (instructions-- > 0)
{
if (IsLockedUp) return;

if (Registers.InterruptsUpdated)
{
Registers.InterruptsUpdated = false;
if (CheckForInterrupts())
{
UpdateFetchCache(Registers.PC);
fetchPtr = _fetchPtr;
fetchMask = _fetchMask;
regionId = _currentRegionId;
}
}

if (Registers.Waiting)
{
if (Registers.EventRegistered)
{
Registers.Waiting = false;
Registers.EventRegistered = false;
}
else
{
Cycles += (uint)(instructions + 1);
return;
}
}

var pc = Registers.PC;

if ((pc >> 28) != regionId)
{
UpdateFetchCache(pc);

fetchPtr = _fetchPtr;
fetchMask = _fetchMask;
regionId = _currentRegionId;

if (fetchPtr == null)
{
ExceptionEntry(EXC_HARDFAULT);
if (IsLockedUp) return;
UpdateFetchCache(Registers.PC);
fetchPtr = _fetchPtr;
fetchMask = _fetchMask;
regionId = _currentRegionId;
continue;
}
}

if (_nativeHooks != null && pc <= _nativeHookMax && _nativeHooks.TryGetValue(pc, out var nativeHook))
{
observer.OnInstruction(pc, Unsafe.ReadUnaligned<ushort>(fetchPtr + (pc & fetchMask)), Cycles);

var pcBeforeHook = Registers.PC;
nativeHook(this);
if (Registers.PC == pcBeforeHook)
{
var hookLr = Registers.LR;
if (hookLr >= 0xFFFFFFF0)
ExceptionReturn(hookLr);
else
Registers.PC = hookLr & ~1u;
}
UpdateFetchCache(Registers.PC);
fetchPtr = _fetchPtr;
fetchMask = _fetchMask;
regionId = _currentRegionId;
Cycles++;
continue;
}

var opcode = Unsafe.ReadUnaligned<ushort>(fetchPtr + (pc & fetchMask));

observer.OnInstruction(pc, opcode, Cycles);

Registers.PC = pc + 2;

Cycles++;

decoder.Dispatch(opcode, this);
}

_currentRegionId = regionId;
_fetchPtr = fetchPtr;
_fetchMask = fetchMask;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Step()
{
Expand Down Expand Up @@ -332,6 +455,8 @@ public void ExceptionEntry(uint exceptionNumber)
Registers.PC = targetPc & 0xFFFFFFFE;

Cycles += 12; // Exception Entry cost (aprox 12-15 cycles)

OnExceptionEntry?.Invoke(exceptionNumber);
}

// ================================================================
Expand Down Expand Up @@ -490,5 +615,7 @@ public void ExceptionReturn(uint excReturn)
// that already happened. rp2040js cortex-m0-core.ts:339-341 sets both flags.
Registers.InterruptsUpdated = true;
Registers.EventRegistered = true;

OnExceptionReturn?.Invoke(excReturn);
}
}
22 changes: 22 additions & 0 deletions src/RP2040Sharp/Core/Cpu/IProfilingObserver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace RP2040.Core.Cpu;

/// <summary>
/// Observation hook invoked once per executed instruction on the profiling-only
/// execution path (<see cref="CortexM0Plus.RunProfiled"/>).
///
/// This is deliberately a separate path from the hot <see cref="CortexM0Plus.Run"/>
/// loop: the normal simulation never references an observer, so per-instruction
/// profiling overhead cannot leak into the fast path. (Same separation AVR8Sharp
/// uses between its struct LUT decoders and its <c>ProfilingDecoder</c>.)
///
/// The callback fires <b>before</b> the instruction is dispatched, so the observer
/// sees the architectural state (PC, registers, memory) as it was on entry to the
/// instruction at <paramref name="pc"/>.
/// </summary>
public interface IProfilingObserver
{
/// <param name="pc">Program counter of the instruction about to execute (Thumb bit stripped).</param>
/// <param name="opcode">The 16-bit halfword fetched at <paramref name="pc"/>.</param>
/// <param name="cycles">Cycle counter immediately before this instruction executes.</param>
void OnInstruction(uint pc, ushort opcode, long cycles);
}
36 changes: 25 additions & 11 deletions src/RP2040Sharp/Core/Memory/Ram.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace RP2040.Core.Memory;

public sealed unsafe class RandomAccessMemory : IMemoryMappedDevice, IDisposable
{
readonly byte[] _memory;
GCHandle _pinnedHandle;

// Backing store is unmanaged native memory (not a pinned managed array). This keeps the
// multi-megabyte RAM/Flash/BootROM blocks out of the GC heap entirely — a pinned array of
// this size would fragment the heap and resist compaction for its whole lifetime. Mirrors
// the unmanaged-buffer pattern used by InstructionDecoder.
public readonly byte* BasePtr;
public uint Size { get; }

private bool _disposed;

public RandomAccessMemory(int size)
{
_memory = new byte[size];
_pinnedHandle = GCHandle.Alloc(_memory, GCHandleType.Pinned);
BasePtr = (byte*)_pinnedHandle.AddrOfPinnedObject();
// AllocZeroed preserves the zero-initialised semantics of `new byte[size]`.
BasePtr = (byte*)NativeMemory.AllocZeroed((nuint)size);
Size = (uint)size;
}

Expand Down Expand Up @@ -49,11 +51,23 @@ public void WriteHalfWord(uint address, ushort value) =>
public void WriteWord(uint address, uint value) =>
Unsafe.WriteUnaligned(BasePtr + address, value);

[ExcludeFromCodeCoverage]
~RandomAccessMemory()
{
Free();
}

public void Dispose()
{
if (_pinnedHandle.IsAllocated)
{
_pinnedHandle.Free();
}
Free();
GC.SuppressFinalize(this);
}

private void Free()
{
if (_disposed)
return;
NativeMemory.Free(BasePtr);
_disposed = true;
}
}
Loading
Loading