This guide walks through installing the PRU Simulator and running each example program in source/ from scratch.
Prerequisites: Python 3.9 or later.
cd pru_simulator
pip install -r requirements.txtpython ui/server.pyOpen http://localhost:8080 in your browser. You should see the PRU Simulator dashboard.
Hover over PRU SIM in the top-left to confirm the version.
The dashboard is divided into resizable panels. You can drag panel title bars to rearrange them or split the window. Layouts are saved automatically per session mode (Single-core / Multi-core).
| Panel | Purpose |
|---|---|
| Source / Disassembly | Paste assembly source; shows assembled instructions with addresses |
| Editor | Text editor for writing or loading .asm files |
| Registers | R0–R31 live values; double-click any value to edit inline |
| Memory | Hex dump of DRAM0/DRAM1/ICSS_SHARED; double-click a cell to edit |
| IO | GPO (R30) and GPI (R31) pin controls; loopback strip; UART decoder; SD filter panel |
| Signal Graph | Logic analyzer + analog scope; records GPO/GPI (GP mode), Peripheral Interface out/out_en/clock lanes (perif mode), and memory channels |
| Memory Graph | Analog waveform view for memory buffer channels |
| Counters | Cycle count, instruction count, stall cycles, IPC |
Toolbar buttons:
| Button | Action |
|---|---|
| Step | Execute one instruction |
| Step Back | Reverse the last instruction (full machine state restored) |
| Run | Execute continuously at the configured interval |
| Reset | Reset PC and registers to 0, and clear this core's Peripheral Interface status/FIFOs |
| HW Reset | Full hardware reset — clears all SPAD banks, all cores, and every core's Peripheral Interface state (TX/RX FIFOs, overrun/underrun, RX valid/overflow, busy). Perif config registers, GP Mux and loopback settings are kept |
| Multi-core | Toggle dual-core (PRU0 + RTU0) debug view |
What it does: Shifts a single bit through the GPO pins on each LOOP iteration — simulates a running LED on a GPIO port.
Source:
start:
ldi r30, 1
loop endloop, 19
lsl r30, r30, 1
endloop:
qba startSteps:
- Open the Editor panel and paste the source above (or click Open and select
source/running_led.asm). - Click Load & Assemble. The Source / Disassembly panel shows the assembled instructions.
- Open the IO panel. GPO pins 0–19 are shown as indicators.
- Click Step repeatedly and watch bit 0 of R30 shift left through the GPO pins on each loop iteration.
- Set the SIM step interval to
0.1(seconds) and click Run to see the LED pattern animate automatically. - Click Reset to stop and return to the start.
What to observe:
- R30 in the Registers panel updates with each step:
0x00000001→0x00000002→0x00000004→ … - GPO pin 0 lights, then pin 1, then pin 2, and so on across the row.
- After pin 19 the program jumps back to
startand resets to pin 0.
What it does: Writes values 1–100 into DRAM0 using SBCO, then copies them to ICSS_SHARED using LBCO+SBCO. Demonstrates the constant table addressing for data memory access.
Source:
start:
ldi r2, 1
loop endloop, 100
sbco &r2, c28, r2.b0, 1 ; write r2 → DRAM0[r2.b0]
add r2, r2, 1
endloop:
ldi r2, 1
loop endloop1, 25
lbco &r3, c28, r2.b0, 1 ; read DRAM0[r2.b0] → r3
sbco &r3, c24, r2.b0, 1 ; write r3 → ICSS_SHARED[r2.b0]
add r2, r2, 1
endloop1:
halt
c28= DRAM0 base,c24= ICSS_SHARED base (PRU constant table).
Steps:
- Paste the source (or open
source/mem_copy.asm) and click Load & Assemble. - Click Reset to clear memory to zero.
- Open the Memory panel. Select DRAM0 from the region selector.
- Click Run and let the program reach
halt(the Run button stops automatically on HALT). - Observe DRAM0: bytes at offsets 0x01–0x64 should read
01 02 03 … 64. - Switch the Memory panel region to ICSS_SHARED: offsets 0x01–0x19 should be a copy of DRAM0[0x01–0x19].
What to observe:
- R2 counts from 1 upward in the Registers panel during execution.
- The write loop fills DRAM0; the copy loop copies the first 25 values to ICSS_SHARED.
- Counters panel shows ~130 instruction cycles to complete.
What it does: Writes a value (0xCAFE) into PRU scratchpad bank 0 via XOUT, clears the register, reads it back via XIN, then distributes the value to all three SPAD banks. Demonstrates the XFR transfer mechanism.
Source:
fill &r2, 32
ldi r2, 0xCAFE
xout 10, &r2, 32 ; write r2 → SPAD Bank0 (device 10)
zero &r2, 32 ; clear r2
xin 10, &r2, 32 ; read SPAD Bank0 → r2
xout 11, &r2, 32 ; copy → Bank1 (device 11)
xout 12, &r2, 32 ; copy → Bank2 (device 12)
xout 15, &r2, 32 ; copy → system events (device 15)
mov r10, r2
ldi r0.b0, 4
xout 10, &r2, 32
ldi r0.b0, 0
xin 10, &r2, 32
haltDevice IDs: 10 = SPAD Bank0, 11 = Bank1, 12 = Bank2, 15 = System Events.
Steps:
- Open
source/xfr_test.asmand click Load & Assemble. - Click Reset.
- Click Step once —
fill &r2, 32sets R2–R9 to0xFFFFFFFF. - Step again —
ldi r2, 0xCAFEsets R2 =0x0000CAFE. - Step through
xout 10, &r2, 32— R2's value is latched into SPAD Bank0. - Step through
zero &r2, 32— R2 returns to0x00000000. - Step through
xin 10, &r2, 32— R2 restores to0x0000CAFE. XFR round-trip confirmed. - Continue stepping to HALT. Final state: R2 =
0x0000CAFE, R10 =0x0000CAFE.
What to observe:
- After
zero, R2 = 0 — the scratchpad is the only copy of the value. - After
xin, R2 =0x0000CAFE— retrieved from SPAD Bank0. - R10 =
0x0000CAFEaftermov r10, r2.
What it does: Demonstrates the ICSSG broadside multiplier (device_id=0) in two modes:
- MPY mode — single multiply: 50 × 25 = 1250 (
0x000004E2) - MAC mode — dot product: (1,2,3) · (4,5,6) = 4 + 10 + 18 = 32 (
0x00000020)
The result is read back via XIN into R26 (low 32 bits) and R27 (high 32 bits).
Register map used:
| Register | Role |
|---|---|
| R25 | MAC_CTRL_STATUS (bit0 = MAC_MODE, bit1 = clear ACC_CARRY) |
| R26 | Result low 32 bits |
| R27 | Result high 32 bits |
| R28 | Operand A (auto-sampled on each XOUT R25) |
| R29 | Operand B (auto-sampled on each XOUT R25) |
Steps:
- Open
source/mac_example.asmand click Load & Assemble. - Click Reset.
Part 1 — MPY mode:
- Step through the
mpy_demosection (lines up to the firstxin DEVICE_ID, &r27, 4). - After these steps, check the Registers panel:
- R26 =
0x000004E2(1250 = 50 × 25) - R27 =
0x00000000 - R28 =
0x00000032(50) - R29 =
0x00000019(25)
- R26 =
Part 2 — MAC mode:
-
Continue stepping through
mac_demo. Watch R28/R29 load successive vector elements. -
Each
xout DEVICE_ID, &r25, 1triggers an accumulate: the hardware adds R28×R29 to the running total. -
After the final
xin DEVICE_ID, &r27, 4(just before HALT), check:- R26 =
0x00000020(32 = dot product) - R27 =
0x00000000
- R26 =
-
Click Run (or continue stepping) to reach HALT.
Dashboard tip: The Registers panel shows a MAC indicator row (near the Carry flag) that displays the current MAC mode and whether ACC_CARRY is set — useful for tracking accumulator state without stepping through every instruction.
What it does: Bit-bangs a single UART byte ('A' = 0x41) on GPO pin 0 at 115200 baud (8N1 framing). The IO panel's UART decoder reconstructs the byte from the recorded GPO transitions.
Steps:
- Open
source/uart_tx.asmand click Load & Assemble. - Open the Signal Graph panel and click REC to arm recording.
- Click Run — the program completes and halts automatically.
- Open the IO panel. In the UART Decoder section, verify:
- GPO selector is set to
GPO0 - Mode is
8N1
- GPO selector is set to
- Click ▶ Decode. The decoded result appears as:
41 A
What to observe:
- The Signal Graph shows the start bit (LOW), 8 data bits LSB-first, and stop bit (HIGH) on the GPO0 lane.
- The UART decoder auto-detects the bit period from the recorded transitions.
- Edit
TX_CHARin the source to transmit any ASCII character.
What it does: Receives 11-byte UART frames at 4 Mbaud on GPI pin 0 (R31 bit 0). The IO panel's UART RX Inject section generates the stimulus waveform. Received bytes are stored to DRAM0, verifiable in the Memory panel.
Steps:
- Open
source/uart_rx_11frame.asmand click Load & Assemble. - Open the IO panel. Scroll down to the UART RX Inject section (below UART Decoder).
- Enter a payload in the hex field, e.g.
48 65 6C 6C 6F 57 6F 72 6C 64 21(or switch to ASCII and typeHelloWorld!). - Verify settings: Pin = GPI0, Baud = 4.00 Mb, Frames = 1.
- Click ▶ Inject. The status line shows:
✓ Armed: 11 bytes × 1 frame · trigger cycle N · run to receive. - Click Run (or step ~8000 instructions). The firmware detects the start bit, samples all 11 bytes, and stores them via SBCO.
- Open the Memory panel. Set address to
0x00000000and refresh. The 11 injected bytes appear at offset 0.
What to observe:
- The firmware polls GPI0 for a falling edge (start bit), then samples at mid-bit intervals.
- Each received byte is accumulated LSB-first and packed into registers R2–R4.
- After 11 bytes, a single
SBCO &R2, c24, R1, 11stores the frame to DRAM0. - R20 (frame counter) increments by 1 after each successful frame.
- If a framing error occurs (invalid stop bit), DRAM0[0x0FFE] is set to 1 and the frame is discarded.
Variations:
- Change baudrate to test tolerance (receiver handles down to ~3.75 Mbaud).
- Set Frames > 1 to inject multiple consecutive frames — each is stored sequentially in DRAM0.
- Use the MCP tool
pru_uart_injectfor automated testing from Claude.
What it does: Drives a walking-bit pattern on GPO (R30.b0) via MVIB register-file indirect, then reads the looped-back value from GPI (R31.b0) into a capture buffer. Demonstrates MVIB addressing and the IO panel's loopback feature.
Setup:
- Open
source/mvi_gpio_loopback.asmand click Load & Assemble. - Open the IO panel. Between the GPO and GPI rows you will see five labeled loopback toggle buttons:
3:0 7:4 11:8 15:12 19:16. - Enable groups 3:0 and 7:4 (both amber).
Steps:
- Click Run (or step through the inner loop 16 times).
- After the program completes, check the Registers panel:
- R2–R5 hold the TX pattern (walking bits)
- R10–R13 hold the RX capture and should mirror R2–R5
What to observe:
- Each MVIB write drives one byte of R30 (GPO), and the loopback immediately feeds it back to R31 (GPI).
- The next MVIB read captures the GPI byte into the RX buffer.
- Without loopback enabled, R10–R13 will remain zero (GPI pins not driven).
What it does: Free-running SINC3 (3rd-order CIC) sigma-delta filter, adapted from real AM261x ICSS-M firmware. Configures the SD peripheral via memory-mapped registers, then reads filtered accumulator values in a loop.
See source/sdfm_sinc3_demo/README.md for full setup instructions, expected register values, and notes on differences from hardware.
Quick steps:
- Open the project: click Project in the Editor panel and select
source/sdfm_sinc3_demo/. - Click Load & Assemble.
- Open the IO panel. The SD panel appears automatically once the firmware writes
sd_en=1to R30. - Set the step interval to
0.01s and click Run. - In the SD panel, watch the acc3 (SINC3) shadow values update every OSR ticks. With the default DC=0.5 input and OSR=64, acc3 should stabilise around
0x200000.
What to observe:
- The SD panel shows R30 decode fields (ch_sel, sd_en, data_sel) updating as firmware runs.
- Each channel card shows live accumulator values and the shadow latch with the valid flag.
- The pattern generator controls let you switch between DC and sine inputs in real time.
What it does: Streams the classic bit patterns out of Peripheral Interface channel 0 so the Signal Graph shows a waveform you can check by eye. Self-configuring — it writes the GP Mux, TXCFG and CH0CFG0 itself.
Edit PATTERN at the top of the file and reload to pick one:
PATTERN |
transmits |
|---|---|
0 (default) |
the sequence below, then the counter forever |
1 / 2 |
0x00 / 0xFF — line held low / high |
3 / 4 |
0xAA / 0x55 — alternating bits, opposite phases |
5 / 6 |
walking 1 (01 02 04 … 80) / walking 0 (FE FD FB … 7F) |
7 |
counter only, same stream as perif_tx_pattern.asm |
The PATTERN = 0 sequence is one byte of each, twenty bytes total:
00 FF AA 55 | 01 02 04 08 10 20 40 80 | FE FD FB F7 EF DF BF 7F | 00 01 02 ...
Steps:
- Open
source/perif_tx_patterns.asmand click Load & Assemble. - Set the Signal Graph window to 2048 — the sequence spans about 1280 samples.
- Click ● REC, then Run. The capture is single-shot in peripheral mode: it fills the window once and REC switches itself off.
What to observe:
- Three lanes:
perif0_out,perif0_out_enandperif0_clk. The GPO/GPI lanes are hidden — those pads belong to the Peripheral Interface now. 0xAAand0x55toggle the data line on every bit, soperif0_outruns at half theperif0_clkrate.0x00and0xFFare flat — the widest low and high runs the link will ever see.- Walking 1 and walking 0 sweep a single edge across the byte, one bit later each time.
- Byte boundaries sit one bit after the first rising edge: the firmware pre-shifts the payload behind a start bit that the RX consumes (same framing as
perif_tx_pattern.asm). The clock toggles once per bit, so every clock edge is a bit boundary, not just the rising ones. - To receive it, enable the ch0 loopback and run
perif_rx_capture.asmon PRU1.
What it does: Bit-bangs I2C at 1 Mbit/s on SCL=R30/R31 bit0, SDA=bit1 (open-drain: writing 1 releases the line, writing 0 drives it low) against a simulated TCA9538 8-bit IO expander at address 0x23. Configures all 8 ports as outputs, then walks a single bit 0x01→0x80 into the Output Port register forever — a running LED across the expander's 8 physical pins.
Steps:
- Open
source/i2c_tca9538_running_led.asmand click Load & Assemble. - In the IO panel, click Attach TCA9538 @ 0x23 (SCL=bit0, SDA=bit1).
- Set the SIM step interval to
0.01(seconds) and click Run. - The I2C · TCA9538 section appears once the firmware issues its first START condition.
What to observe:
CONFIG=0x00in the I2C section's mode bar once the init transaction (address, Configuration register 0x03, data 0x00) completes.- The 8 LED indicators show one lit LED walking left to right, wrapping from P7 back to P0, in step with each completed Output Port write.
- The transaction log line under the LEDs shows the address, register pointer, data byte and ACK/NACK of the most recently completed transaction.
- Detaching the toggle hides the section and restores bits 0/1 to plain
GPIO — safe to then load
spi_master_tx.asmormvi_gpio_loopback.asm, which use those same bits for SCLK/MOSI and loopback respectively. - DRAM0 offset
0x0FFE(visible in the Memory panel) is a soft error flag: the firmware sets it to 1 whenever arun_loopwrite gets NACKed. It's sticky — once set it stays 1 even after a later successful transaction — so it only tells you a NACK happened at some point, not whether the bus is currently healthy.
The Signal Graph panel records GPO/GPI pin states and optional memory addresses over time.
To record a running_led trace:
- Load
running_led.asmand click Reset. - Open the Signal Graph panel. The GPO and GPI channels are added by default.
- Set the step interval to
0.05s and click Run. - The graph draws a digital lane per pin — pin 0 through pin 7 shift in sequence.
- Click Export CSV to save the trace for further analysis.
To read a Peripheral Interface trace:
In peripheral mode the GPO/GPI lanes are not drawn: the GP Mux has handed
those pads to the Peripheral Interface, so R30/R31 bits no longer describe
anything on the wire. The graph shows three lanes per active channel instead —
perifN_out (the serial data line), perifN_out_en (asserted while the channel
is actually driving the pad) and perifN_clk (its bit clock). A channel appears
as soon as any one of its three signals toggles, and then all three lanes are
drawn, so a steady out_en stays visible next to the data it qualifies.
The perif serializer sends raw MSB-first bits with no framing of its own, so use
the clock lane to find bit boundaries — there is one clock edge per bit, and the
data line is stable across each half-period. At the usual TXCFG divider
(div=7 → core/8) one bit is 8 samples wide, since the graph takes one sample
per instruction.
Sample rate. SIM steps one instruction per message, so it always records at full rate. Run executes a chunk of instructions per message, and the server samples the graph inside that loop:
| mode | rate | buffer |
|---|---|---|
| Peripheral | every instruction — a channel-0 bit at the N=2 divider is only 2 core cycles wide |
single-shot: fills the window once, then REC switches itself off |
| GP / SD | every 100th instruction — GP traces are firmware-paced (a 115200-baud bit-bang bit is ~1736 cycles) | rolling |
So for a peripheral capture: arm REC, click Run, and the trace freezes
when the window is full. Pick the window size (128…8192) to set how many
instructions the capture covers. perif_duty_cycle_sweep.asm transmits its
whole nine-byte sweep in about 160 instructions, well inside the smallest
window — note it needs CH0CFG0 = 0 (continuous mode) set from the host before
loading, or the FIFO transmits one fixed-size frame and the sweep never appears.
Firmware that prefixes a start bit (see source/perif_tx_pattern.asm) shifts
the whole payload right by one bit, so byte boundaries on the wire sit one bit
after the first rising edge — drop that first bit before grouping into octets.
To add a memory channel:
- Click + Mem Channel in the Signal Graph panel.
- Enter a memory address (e.g.
0x0001for DRAM0 offset 1) and click Add. - Run
mem_copy.asm— the graph plots the byte value at that address as an analog lane.
- Edit a register directly: Double-click any value in the Registers panel, type a new hex value (without
0x), and press Enter. R30 edits update GPO pins immediately; R31 edits update GPI pins. - Edit a memory cell: Double-click any cell in the Memory panel and type a new hex byte.
- Toggle a GPI pin: Click any pin indicator in the IO panel to set it high; click again to clear. The R31 register updates in real time.
- GPIO loopback: The five toggle buttons between GPO and GPI rows wire 4-bit groups of GPO directly to GPI. Useful for firmware that reads back what it writes without physical wiring.
- Step back: Click Step Back (or press
←) to reverse the last instruction. The full machine state — registers, memory, SD filter — is restored exactly. - Layout: Drag a panel title bar onto another panel to split the window. Layouts persist across sessions.
- Reset Layout: Click the ⊞ Reset Layout button in the toolbar to restore the default arrangement.