A cycle-accurate PRU assembly simulator with an interactive HTML dashboard. No hardware required: Runs entirely on a PC, with the option for your AI to use the simulator through an MCP server.
The simulator models AM243x/AM64x (PRU_ICSSG) and AM261x/AM263x (PRU-ICSSM), all with V4 ISA. AM62x is also selectable; its profile is copied from AM261x and not verified on hardware. Integrated accelerators differ by target; see device capabilities for the hardware profiles, unsupported-transfer diagnostics and silicon evidence.
These are simulator-side capabilities, not features of the PRU hardware itself.
- Interactive dashboard — register panel, memory panel, IO pin control, disassembly view; Simulator, Devices and Events views with persistent panel visibility and dark/high-contrast themes. See dashboard controls
- ELF binary loading — load compiled .out files directly (TI PRU CGT ELF32) with automatic disassembly
- Step-back — reverse any instruction; full machine state including SD filter is restored
- Signal graph — digital logic analyzer for GPO/GPI pin transitions
- Memory graph — analog scope for memory buffer waveform visualization
- GPIO loopback — wire GPO groups directly to GPI for firmware loopback testing without hardware. Can specify loopback latency/jitter/clock-drift
- Device bus — attach external device models (
DeviceModel) to any core's GPIO pins and wire pins between cores. A sharedDeviceBusresolves open-drain nets as wired-AND and reports push-pull contention as a fault instead of last-writer-wins; devices exposeevents()andfaults()for checking firmware - SSI encoder model and reader — independent binary/Gray encoder
DeviceModelwith up to 64-bit frames, error fields and 12 encoder presets, clock-rate and monoflop faults, generated shared-memory ABI, and PRU1 reader and PRU0 emulator examples. See SSI device model and reader - UART decoder — bit-bang UART decode in the IO panel (8N1, auto-detect bit period)
- Multi-core simulation & debug view — PRU_ICSSG supports simultaneous simulation & debugging of up to 4 PRU cores (PRU0, RTU0, PRU1, RTU1), shown as PRU0 plus a selected partner; PRU-ICSS supports simultaneous simulation & debugging of both PRU cores
- Tiling window manager — drag, split, collapse/expand, and persist panel layouts
- MCP server — AI assistant integration via Model Context Protocol
Status per processor:
- ✅ implemented in the simulator
- ❌ present in silicon, not currently simulated
- N/A not present on that processor (per the PRU Subsystem Features Comparison app note)
| Category | Feature | AM243x, AM64x | AM261x, AM263x | AM62x |
|---|---|---|---|---|
| Core | Full PRU ISA (ALU, branches, bit ops, LOOP, LBCO/SBCO, LBBO/SBBO, XIN/XOUT/XCHG, MVI) | ✅ | ✅ | ✅ |
| General PRU Specs | PRU cores | ✅ | ✅ | ✅ |
| General PRU Specs | RTU_PRU (Auxiliary PRU) cores | ✅ | N/A | N/A |
| General PRU Specs | TX_PRU (Transmit PRU) cores | ❌ | N/A | N/A |
| General PRU Specs | INTC | ❌ | ❌ | ❌ |
| Accelerators: Data Processing | MPY/MAC | ✅ | ✅ | ✅ |
| Accelerators: Data Processing | Scratchpad | ✅ | ✅ | ✅ |
| Accelerators: Data Processing | IPC Scratch Pad | ✅ | N/A | N/A |
| Accelerators: Data Processing | Broadside RAM | ❌ | N/A | N/A |
| Accelerators: Data Processing | BSWAP | ✅ | N/A | N/A |
| Accelerators: Data Processing | CRC16/32 | ✅ | ✅ | ✅ |
| Accelerators: Data Processing | SUM32 | ❌ | N/A | N/A |
| Accelerators: Data Processing | Task Manager | ❌ | N/A | N/A |
| Accelerators: Data Processing | Spinlock | ❌ | N/A | N/A |
| Accelerators: Data Processing | Filter Data Base (FDB) | ❌ | N/A | N/A |
| Accelerators: Data Movement | XFR2VBUS | ❌ | ❌ | ❌ |
| Accelerators: Data Movement | PSI TX & RX | ❌ | N/A | N/A |
| Accelerators: Data Movement | XFR2TR | ✅ | N/A | N/A |
| Peripherals | Sigma-delta filter (3 ch, CIC accumulators, Fast Detect, pattern generators) | ✅ | ✅ | N/A |
| Peripherals | Peripheral Interface / 3 ch EnDAT (protocol-agnostic serializer/deserializer, used for EnDat/BiSS/HDSL/line-code) | ✅ | ✅ | N/A |
| Peripherals | UART | ❌ | ❌ | ❌ |
| Peripherals | eCAP | ❌ | ❌ | ❌ |
| Peripherals | IEP (Industrial Ethernet Peripheral: timer/compare/capture) | ✅ | ✅ | ✅ |
| Peripherals | MII_RT / MII_G_RT (RGMII) | ❌ | ❌ | N/A |
| Peripherals | MDIO | ❌ | ❌ | N/A |
| Peripherals | SGMII | ❌ | N/A | N/A |
| Peripherals | PWM | ❌ | N/A | N/A |
| Mode Class | Specific I/O Mode | Core Registers Used | Primary Use Case / Description | AM243x, AM64x | AM261x, AM263x | AM62x |
|---|---|---|---|---|---|---|
| GPI (Input) | Direct Input Mode | R31[31:0] |
Lowest latency direct pin sampling and software bit-banging | ✅ | ✅ | ✅ |
| GPI (Input) | 16-Bit Parallel Capture | R31 |
Captures 16 bits of parallel data synchronous to a sampling clock | ❌ | ❌ | ❌ |
| GPI (Input) | 28-Bit Shift In Mode | R31 |
Automatically deserializes incoming high-speed serial streams | ❌ | ❌ | ❌ |
| GPO (Output) | Direct Output Mode | R30[31:0] |
Direct software-driven pin control for custom low-latency protocols | ✅ | ✅ | ✅ |
| GPO (Output) | Shift Out Mode | R30 |
Automatically serializes data written to the register over a clock pin | ❌ | ❌ | ❌ |
| Interface | Peripheral IF Mode | Slice Hardware Co-processor | Industrial motor position encoder feedback (EnDAT, BiSS-C, Tamagawa) | ✅ | ✅ | N/A |
| Interface | SD Mode | Internal SDFM Blocks | Connects to external Sigma-Delta ADCs for isolated current/voltage sensing | ✅ | ✅ | N/A |
| Interface | MII / RGMII Mode | ICSSG Real-Time Ethernet Switch | Real-time industrial communication hardware layer (EtherCAT, PROFINET, EtherNet/IP) | ❌ | ❌ | N/A |
- Python 3.9 or later
- Dependencies:
pip install -r requirements.txt
python ui/server.pyOpen http://localhost:8080 in your browser.
pru_simulator/
├── config/ Memory layout configs (AM243x, AM64x, AM261x, AM263x, AM62x)
├── core/ PRU ISA implementation (ALU, parser, disassembler, ELF loader, …)
├── mem/ Memory bus and region model
├── mcp_server/ MCP server for AI tool integration
├── pru_io/ GPO/GPI port model, SD filter (R30/R31 interface), DeviceModel/DeviceBus, device models (SSI encoder, TCA9538), generated ABIs
├── perif/ 3-channel Peripheral Interface (SCU), GPCFG mux, TX→RX loopback
├── schema/ Shared-memory ABI schemas (source for generated .py/.inc files)
├── source/ Example PRU assembly programs
├── tests/ Pytest test suite
├── tools/ Headless runner, ABI code generators, perif drift report
├── ui/
│ ├── server.py FastAPI WebSocket server
│ └── static/ Dashboard HTML/JS (index.html, app.js, layout.js, workspace.js)
├── xfr/ XFR scratchpad + MAC accelerator
├── memory.cfg Default memory configuration (AM243x)
├── requirements.txt
└── simulator.py Simulator entry point
The simulator loads memory.cfg at startup. Five built-in configs are provided in config/:
| File | Target | Notes |
|---|---|---|
memory_am243x.cfg |
AM243x | Default |
memory_am64x.cfg |
AM64x | Same PRU_ICSSG as AM243x (the A53 cores are not modeled); uses constants_am243x.cfg |
memory_am263x.cfg |
AM263x | |
memory_am261x.cfg |
AM261x | MSRAM burst timing measured on LP-AM261; selected by cfg file, not by target |
memory_am62x.cfg |
AM62x | Copied from AM261x; MS_RAM sized to the 64 KB OCRAM at 0x70000000 (SPRUIV7C). Timing not verified on hardware |
Switch config by editing memory.cfg or copying one of the config/ files over it.
Clock keys in the [device] section:
| Key | Applies to | Default if absent |
|---|---|---|
pru_clock_mhz |
every PRU/RTU core, and the IEP OCP clock | 200 |
iep_clock_mhz |
IEP while ICSS CFG IEPCLK bit 0 is clear |
200 |
Omitting pru_clock_mhz preserves the simulator's 200 MHz core default; all PRU and RTU cores always share this one frequency (the removed pru1_clock_mhz key is rejected with an error). Explicitly configured rates, including 250 MHz, remain unchanged. The external IEP defaults independently to 200 MHz, the rate the TRM gives for ICSSG_IEP_CLK (SPRUIM2J §6.4.13.2.4). Core and IEP clocks remain independently configurable, including positive fractional MHz values, and changing the core speed leaves the external IEP clock unchanged. Firmware's IEPCLK register selects which configured source drives the timer.
With both rates omitted, 1000 unstalled core cycles produce 1000 IEP ticks with CNT_ENABLE=1 and DEFAULT_INC=1 while IEPCLK bit 0 is clear. The supplied memory.cfg explicitly selects 250 MHz cores; with the omitted external IEP rate of 200 MHz, it produces 800 ticks for the same 1000 core cycles. See clock sources and configuration for the cited TI firmware examples. RTU1 is available only on the AM243x/AM64x profiles; an omitted target selects AM243x.
The simulator's IEP GLOBAL_CFG reset value, 0x550, follows the TRM register defaults (IEP_GLOBAL_CFG_REG, Table 14-10907: CMP_INC and DEFAULT_INC reset to 5h, CNT_ENABLE to 0h). DEFAULT_INC=5 counts nanoseconds at the default 200 MHz IEP clock, where one tick is 5 ns, so firmware that enables the counter with a read-modify-write (GLOBAL_CFG | 1) gets a nanosecond count. If you set a different IEP clock, for example 250 MHz (a 4 ns tick), firmware must set DEFAULT_INC itself (4 at 250 MHz) to keep a nanosecond count. A full-word write of 1 leaves DEFAULT_INC=0, and the counter does not advance. The SSI runtime (pru_io/ssi_runtime.py) is the in-repo example: it enables a stopped IEP with DEFAULT_INC=1 written explicitly and sizes its timeouts from the active IEP clock.
All cores share one IEP timer. Each core's elapsed cycles, including stalls, are converted to time with exact rational clock periods, so cores at different clocks stay aligned without float drift.
Count-register writes preserve the running IEP clock phase, including byte and 64-bit writes. Enabling the counter or changing its increment or effective clock starts a new phase.
Dashboard core and load selectors follow the configured target, including RTU1
on AM243x/AM64x. Each core has an independent constant table. Fixed C10,
C11 and C22 addresses follow the executing core on AM243x/AM64x (SPRUIM2J,
Table 6-65, pp. 581–582); AM263x PRU0/PRU1 use their documented C11/C22
addresses. Other configured entries are preserved. Correct address selection does not
implement the task manager, control or RAT register behavior. Accesses still
require the corresponding memory region/model in the chosen configuration.
C24-C31 follow the executing core's CTBIR0/1 and CTPPR0/1 registers (CTRL block
+0x20, +0x24, +0x28, +0x2C; PRU0 0x22000, PRU1 0x24000, and on AM243x/AM64x RTU0
0x23000 and RTU1 0x23800). A write from the PRU (SBBO, or SBCO through C11) or from
the host (Simulator.ctrl_write, or a memory write to the CTRL address) updates
that core's table at once, as measured on LP-AM243 and LP-AM261: C24 = CTBIR0[7:0]
<< 8, C25 = 0x2000 | CTBIR0[23:16] << 8, C26 = 0x2E000 | CTBIR1[7:0] << 8,
C27 = 0x32000 | CTBIR1[23:16] << 8, C28 = CTPPR0[15:0] << 8, C29 = 0xD0000000 |
CTPPR0[31:16] << 8, C30 = 0xE0000000 | CTPPR1[15:0] << 8, C31 = 0xF0000000 |
CTPPR1[31:16] << 8. The registers are 0 out of reset, so C28 is 0 until firmware
programs CTPPR0 (blk_ptr = 0x100 gives 0x10000, Shared RAM); firmware that
uses C28 without doing so would also fail on silicon. The registers' power-on
value was not measured, and neither were register bits outside the fields above
(read-back returns what was written). The dashboard constant-table panel reads the
executing core's live table.
AM263x has its own config/constants_am263x.cfg, with the same values as
constants_am261x.cfg (same ICSS-M; measured on LP-AM261). AM62x has config/constants_am62x.cfg, also copied from
AM261x and not verified on hardware. C10 and C12-C14 are reserved in
the TRM; on an LP-AM261 the table holds non-zero pointers there (C10 0x2A000 on
PRU0 / 0x2A200 on PRU1, C12 0x27000, C13 0x2C000, C14 0x24800) and the window
behind each reads zero. The simulator shows those pointers and maps the reserved
ranges (0x24800-0x24BFF, 0x27000-0x27FFF, 0x2A000-0x2DFFF) as read-as-zero
windows, so a load through them returns zeros. These ranges share the ICSS-M
absent-block model in mem/icssm_absent.py (see
docs/icssm_absent_blocks.md), including the
icssg_only_blocks = strict option. A 32-bit store at the start of
PA_STATS_QRAM, TM_CFG_PRU0 and PA_STATS_CRAM was dropped on silicon; other
writes were not measured. Writes are ignored and logged once. Tracked in
#79.
Dashboard step-back uses one ordered history across all cores. Undo restores all cores, shared memory, the IEP and devices to the same saved point, and removes later history entries so peer state stays consistent.
Step-back does not yet restore XFRDMA queues, XFR2VBUS transfers or external VBUS memory, or task-ring queues and pending work. Do not use undo to validate programs that use those accelerators; reset and rerun them instead.
Paced stepping limits each follower to 1000 catch-up instructions per lead step.
step_paced and step_paced_many return complete, actual lead_steps, and the selected targets
(in exact IEP time units). An unfinished catch-up returns complete=False with
reason="pacing_incomplete"; MCP reports the same reason and dashboard Run stops
with a message. Another call continues execution from the actual core state;
elapsed time is never skipped. A follower that reaches HALT or the end of its
program finishes normally, while a recorded CPU fault remains an error. The
guard target is fixed for that catch-up even if firmware changes the mux.
Unfinished targets are retained per lead and follower set: retries finish them
before advancing that lead again, so lead_steps can be zero during catch-up.
Other follower selections do not execute or discard that saved work. A core
reset discards pending targets; dashboard step-back restores them with time.
Batched callers must count lead_steps, not the requested batch size: a
bounded catch-up may return before the batch finishes. Retry pacing_incomplete;
report pacing_catchup_failed instead of treating it as a completed batch.
Peripheral Interface timestamps use the same core-time origin as the IEP,
including after resetting one core. A full hardware reset starts both at zero.
The MCP pru_step_multicore response includes total lead_steps on success,
incomplete catch-up and core faults, so callers can account for partial batches.
Multicore pacing executes complete instructions before catching up peer cores; it does not order all shared effects cycle by cycle. A lagging core's firmware write to IEPCLK takes effect at the already-advanced shared IEP time, rather than retroactively at that core's earlier time. Configuring the clock before advancing the cores avoids this case. Chronological multicore execution, including this clock-write limitation, is tracked in #57.
Simulator.hard_reset() now calls IepTimer.hardware_reset(), so the counter, compare and capture state no longer survive a full reset. Register defaults follow AM64x/AM243x TRM SPRUIM2J, Tables 14-10907 and 14-10925: the counter stays disabled, with GLOBAL_CFG defined fields 0x550 and CAP_CFG defined fields 0x1FC00. Reset leaves DEFAULT_INC=5, so enabling only CNT_ENABLE with a read-modify-write advances the counter by five per IEP tick; firmware must set DEFAULT_INC explicitly when another increment is required. Capture configuration follows Table 14-10925: bits 9:0 enable slots, bits 17:10 configure synchronization, and bits 23:18 select external sources. The simulator latches the first event until its valid bit is cleared; input synchronization, source routing and continuous overwrite are not modelled. The earlier synthetic first/last bits have been removed because they overlapped these fields.
See getting_started.md for step-by-step walkthroughs of all examples in source/.
| File | What it demonstrates |
|---|---|
running_led.asm |
GPO pin toggle, LOOP instruction |
mem_copy.asm |
SBCO/LBCO data memory read/write |
xfr_test.asm |
XFR scratchpad (XOUT/XIN, SPAD banks) |
mac_example.asm |
MAC accelerator (MPY mode + accumulate mode) |
crc_example.asm |
CRC16/32 accelerator: CRC-16 (half-word writes) and CRC-32 (word writes) over one frame |
crc_bitswap_example.asm |
CRC16/32 accelerator bit-mirrored reads: byte-wide flip (R27) and 32-bit flip (R28), with byte-wide and 32-bit-wide data writes |
uart_tx.asm |
Bit-bang UART TX (115200 baud, 8N1) with UART decoder |
uart_rx_11frame.asm |
Bit-bang UART RX (4 Mbaud, 8N1) with frame injection from IO panel |
ssi_generic_reader/ssi_generic_reader.asm |
SSI encoder reader (1-64 bit frames) and generated shared-memory mailbox; see timing and manual 300 MHz instructions |
ssi_generic_emulator.asm |
SSI encoder emulator on PRU0 (1-64 bit frame read from a shared-memory block at every frame start); pairs with the reader in a board loopback, see docs/ssi_device_model.md |
mvi_gpio_loopback.asm |
MVIB register-indirect + GPIO loopback (walking-bit pattern) |
sdfm_sinc3_demo/ |
Free-running SINC3 filter adapted from AM261x ICSS-M firmware |
perif_duty_cycle_sweep.asm |
Peripheral Interface TX: 125 Mbit 0%→100% duty-cycle pulse sweep on PRU0 ch0 (needs memory_perif_125mbit_demo.cfg) |
pif_eth/ |
8b/10b line-coded Ethernet TX over the Peripheral Interface (PRU0 ch0): firmware PRNG, CRC-32 on the CRC16/32 broadside accelerator, running-disparity 8b/10b via DRAM0 LUT, pcap output. See PROJECT_REPORT.md (PDF) · handoff note. Experimental higher-clock variants pif_eth_tx_n2*.asm (not wired into the test suite): design note. PRU1 RX over the PRU0→PRU1 loopback (pif_eth_rx_o1_raw.asm + rx_driver.py), CI-exercised: design note · handoff note |
CI and agent workflows can execute an assembly source or a linked TI PRU ELF without starting the web UI. The command writes exactly one deterministic JSON object to stdout; load errors exit 2, unmet conditions or exhausted budgets exit 3, and unexpected runner errors exit 4.
python -m tools.headless_runner --json --assembly source/example.asm --core pru0 --max-steps 10000
python -m tools.headless_runner --json --elf build/firmware.out --until 'reg:r0==0x55' --max-cycles 5000--until accepts halt (the default), cycles>=N, register equality or
inequality, 32-bit little-endian memory comparisons, and GPI/GPO pin equality.
Both --max-steps and --max-cycles are hard budgets. JSON remains the only
stdout format when --json is omitted; the flag makes the CI contract explicit.
python -m pytest tests --tb=short -qThe maintained test suite lives under tests/. Files under references/ are
standalone experiments and may require optional packages (for example,
matplotlib); they are not part of the CI test suite.
v0.4.0 — hover over PRU SIM in the dashboard header to confirm.
See changeLog.md for the full changelog.