Skip to content

Commit cce1c11

Browse files
committed
Python API examples
1 parent a7ea39d commit cce1c11

File tree

4 files changed

+111
-3
lines changed

4 files changed

+111
-3
lines changed

README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ This is a simple and readable **RISC-V RV32I emulator** written in pure Python.
1414
- **Passes all `rv32ui` and `rv32mi` unit tests** provided by [RISC-V International](https://github.com/riscv-software-src/riscv-tests)
1515
- **Supports logging** of register values, function calls, system calls, traps, invalid memory accesses, and violations of invariants
1616
- Runs [MicroPython](https://micropython.org/), [CircuitPython](https://circuitpython.org/) with emulated peripherals, and [FreeRTOS](https://www.freertos.org/) with preemptive multitasking
17-
- Self-contained, modular, extensible codebase
17+
- Self-contained, modular, extensible codebase. Provides a **Python API** enabling users to control execution, inspect state, and script complex tests directly in Python.
1818

1919
## 🔧 Requirements
2020

@@ -47,6 +47,7 @@ pip install -r requirements.txt
4747
├── tests/test_bare*.c # Example C programs without Newlib support
4848
├── tests/test_newlib*.c # Example C programs with Newlib-nano support
4949
├── tests/test_peripheral*.c # Example C programs using emulated peripherals
50+
├── tests/test_api*.py # Examples of programmatic control of the emulator in Python
5051
├── build/ # Executable and binaries
5152
├── prebuilt/ # Pre-built examples
5253
├── run_unit_tests.py # Runs RISC-V unit tests (RV32UI and RV32MI)
@@ -259,6 +260,43 @@ Test rv32mi-p-ma_fetch : PASS
259260
Test rv32mi-p-sbreak : PASS
260261
```
261262

263+
### Using the Python API
264+
265+
The emulator provides a Python API that allows users to control execution, set and inspect state, and run complex tests directly from Python programs. Here is an example of how you can load and run a simple program:
266+
```python
267+
from cpu import CPU
268+
from ram import RAM
269+
270+
ram = RAM(1024)
271+
cpu = CPU(ram)
272+
273+
# Store into RAM a simple program that sums integers from 1 to 100 and returns the result in t0
274+
ram.store_word(0x00000000, 0x00000293) # li t0, 0
275+
ram.store_word(0x00000004, 0x00100313) # li t1, 1
276+
ram.store_word(0x00000008, 0x06400393) # li t2, 100
277+
ram.store_word(0x0000000c, 0x006282b3) # <loop> add t0, t0, t1
278+
ram.store_word(0x00000010, 0x00130313) # addi t1, t1, 1
279+
ram.store_word(0x00000014, 0xfe63dce3) # bge t2, t1, c <loop>
280+
ram.store_word(0x00000018, 0x00100073) # ebreak
281+
282+
# Run the program
283+
cpu.pc = 0x00000000 # set initial PC
284+
while True:
285+
inst = ram.load_word(cpu.pc) # fetch
286+
cpu.execute(inst) # decode & execute
287+
cpu.pc = cpu.next_pc # update PC
288+
289+
if cpu.pc == 0x00000018: # when we reach this address, the program has finished
290+
break
291+
292+
print (cpu.registers[5]) # Print result stored in t0/x5
293+
```
294+
295+
Example Python programs using programmatic access to the emulator are provided in the `tests` directory. Run them from the top-level directory of the emulator, e.g.:
296+
```
297+
PYTHONPATH=. python tests/test_python1.py
298+
```
299+
262300
## Design Goals
263301
- Simplicity over speed (though it is highly optimized for speed and performs near the limit of what is possible in pure Python)
264302
- Emphasis on correctness and compliance with RISC-V specifications

tests/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030

3131
- `test_newlib13.c`: Test using `setjump`/`longjump` C exception handling.
3232

33-
- `test_peripheral_uart.c`: Test the memory-mapped UART implementation backed by a pseudo-terminal on the host. Run this example with the `--uart` option, and then connect to the indicated PTY using your preferred terminal program, e.g., `screen /dev/ttys015 115200`.
33+
- `test_peripheral_uart.c`: Tests the memory-mapped UART implementation backed by a pseudo-terminal on the host. Run this example with the `--uart` option, and then connect to the indicated PTY using your preferred terminal program, e.g., `screen /dev/ttys015 115200`.
3434

35-
- `test_peripheral_blkdev.c`: Test the memory-mapped block device implementation backed by a file on the host. Run this example with the `--blkdev=image` option, where `image` is the filename you want to use. If the file does not exist, it will be created by the emulator.
35+
- `test_peripheral_blkdev.c`: Tests the memory-mapped block device implementation backed by a file on the host. Run this example with the `--blkdev=image` option, where `image` is the filename you want to use. If the file does not exist, it will be created by the emulator.
36+
37+
- `test_api1.py`: Python API example: loads and executes a simple program.
38+
39+
- `test_api2.py`: Python API example: loads a flat binary executable into RAM, runs it, intercepts a trap.

tests/test_api1.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python3
2+
# Example of programmatic access to the RISC-V Python emulator
3+
4+
from cpu import CPU
5+
from ram import RAM
6+
7+
# instantiate CPU / RAM / machine
8+
ram = RAM(1024)
9+
cpu = CPU(ram)
10+
11+
# Load program into RAM
12+
# (a simple RISC-V program that sums integers from 1 to 100 and returns the result in t0)
13+
ram.store_word(0x00000000, 0x00000293) # li t0, 0
14+
ram.store_word(0x00000004, 0x00100313) # li t1, 1
15+
ram.store_word(0x00000008, 0x06400393) # li t2, 100
16+
ram.store_word(0x0000000c, 0x006282b3) # <loop> add t0, t0, t1
17+
ram.store_word(0x00000010, 0x00130313) # addi t1, t1, 1
18+
ram.store_word(0x00000014, 0xfe63dce3) # bge t2, t1, c <loop>
19+
ram.store_word(0x00000018, 0x00100073) # ebreak
20+
21+
cpu.pc = 0x00000000
22+
23+
# Run the program
24+
while True:
25+
inst = ram.load_word(cpu.pc) # fetch
26+
cpu.execute(inst) # decode and execute
27+
cpu.pc = cpu.next_pc # update program counter
28+
29+
# Print pc, t1, and t0 registers
30+
print (f"pc={cpu.pc:08X}, t1={cpu.registers[6]}, t0={cpu.registers[5]}")
31+
32+
# When pc == 0x00000018 we know the program has reached the end (we don't execute the ebreak)
33+
if cpu.pc == 0x00000018:
34+
break
35+
36+
print ("Result =", cpu.registers[5]) # Print the value in register t0/x5 (should be 5050)

tests/test_api2.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env python3
2+
# Example of programmatic access to the RISC-V Python emulator
3+
4+
from machine import Machine
5+
from cpu import CPU
6+
from ram import RAM
7+
8+
# instantiate CPU / RAM / machine
9+
ram = RAM(1024 * 1024) # 1 MB of RAM
10+
cpu = CPU(ram)
11+
machine = Machine(cpu, ram)
12+
13+
# Load flat binary executable into memory
14+
machine.load_flatbinary("prebuilt/test_bare1.bin") # flat binary from the prebuilt examples
15+
cpu.pc = 0x00000000
16+
cpu.csrs[0x305] = 0xDEAD0000 # set MTVEC address
17+
18+
# Run the program
19+
while True:
20+
inst = ram.load_word(cpu.pc) # fetch
21+
cpu.execute(inst) # decode and execute
22+
cpu.pc = cpu.next_pc # update program counter
23+
24+
# When pc == 0xDEAD0000 we know a trap (the ECALL in start_bare.S) has occurred and we stop execution.
25+
if cpu.pc == 0xDEAD0000:
26+
break
27+
28+
print ("Result =", cpu.registers[10]) # Print the return value (value of register a0/x10, should be 4950)
29+
30+
cpu.print_registers()

0 commit comments

Comments
 (0)