Target Control

Introduction

To simulate entire systems, device models such as flash, cameras, microphones, or speakers can be connected to chip models to interact with them. Although this is useful for reproducing real system behaviors, it may not be convenient for dedicated tests where fine control over chip interactions is preferred.

The target control feature allows a Python script to connect to the GVSOC engine to interact with the models from the script. This enables:

  • Controlling the simulation engine (running, stopping, stepping, etc.).

  • Injecting memory-map requests to interact with the simulated system and retrieve memory content for debugging.

  • Interacting with pads to inject or extract data, such as replicating microphone behavior with precise control over the data.

Compared to real device models, the target control feature is useful when dynamic decisions need to be made, which can be easily implemented in a Python script.

The API is thread-safe, allowing multiple threads to call any method simultaneously. However, this can make the user script complex and should be avoided when possible. Multiple threads controlling the engine simultaneously can easily lead to deadlocks if not done properly.

The following sections provide an overview of the target control feature. See the Target control section for a detailed API description.

Usage

To use this feature, a Python script must be provided to GVSOC through the –target-control=<path to script> option.

The script will be dynamically imported by the GVSOC runner, so its extension must be .py. The path can be either absolute or relative to the working directory where GVSOC is launched.

When GVSOC is launched with this option, the runner will start two processes. One runs the GVSOC engine, which opens a proxy socket waiting for a connection from the Python script. The other process executes the Python script, which connects to the listening socket. Both processes then run in parallel.

This entire procedure is handled automatically by the runner when the –target-control option is specified.

Python Script Format

When the –target-control option is given, the GVSOC runner dynamically imports the specified Python script, looks for a method called target_control, and executes it in a separate process parallel to the GVSOC process.

The target_control method must have the following prototype:

def target_control(gv: gvsoc.gvsoc_control.Proxy) -> int:

The argument is an instance of a GVSOC proxy that the target_control script can use to interact with GVSOC. It provides an API for interacting with the engine and devices.

The script must perform all interactions with GVSOC and then return an integer as a return status, which makes GVSOC exit with this status. This status is then passed to the shell as the exit code.

Engine Control

The GVSOC proxy Python class (gvsoc.gvsoc_control.Proxy) provides methods for controlling the simulation progress.

When the target_control method of the user script is called, the simulation is stopped at time 0, and the script must call methods of the provided class to control it.

The most basic way to control the simulation is to call the run method:

gv.run()

This method is asynchronous and returns immediately. Meanwhile, the GVSOC engine runs the simulation in parallel, as the engine and script run in separate processes.

The simulation continues until a stop event occurs, either triggered by the script or the simulated software.

The script can stop the engine by calling the stop method, which is also asynchronous and returns immediately.

The simulated software can also stop the engine by calling a method from the target/gvsoc.h header:

static inline void gv_stop();

This function uses semi-hosting to interact with the GVSOC engine, resulting in a slight runtime impact.

Since the run and stop methods are asynchronous, they may return before the command is processed. Methods are available to block the caller until the command is acknowledged.

After calling run, the wait_running method can be used to block the caller until the engine is running.

Similarly, after calling stop, the wait_stopped method can be used to block the caller until the engine stops.

This method of controlling the simulation is not very precise, as it is asynchronous. Running the simulation twice may yield different results, as commands are processed at different times.

To achieve predictable simulations, the step mode can be used by providing a duration in picoseconds to the run method. This runs the simulation for the specified duration and then stops:

gv.run(1000000)  # Runs for 1us and stops. Caller is blocked until duration elapses.

This method is synchronous. The caller is blocked until the duration is simulated. The simulation may stop during the specified duration, but the caller will remain blocked until the duration elapses.

Trace Control

GVSOC system traces can be dynamically controlled through the trace_add, trace_remove, and trace_level methods.

These methods behave similarly to the corresponding command-line options but are processed dynamically, making them useful for configuring specific time intervals.

Similarly, VCD traces can be dynamically controlled through the event_add and event_remove methods.

Memory-Map Accesses

To inject memory-mapped accesses from the Python script into the simulated system, a reference to a proxy router must first be retrieved. This proxy router is a special model connected to the main system interconnect to convert access requests from the script into real system accesses.

This reference is retrieved by instantiating the gv.gvsoc_control.Router class:

router = gv.gvsoc_control.Router(gv)

Accesses can then be performed by calling mem_read and mem_write on this instance:

array = router.mem_read_int(0x1c000000, 16)
router.mem_write_int(0x1c000000, 16, array)

Convenience methods are also available for simpler data retrieval or writing, such as using an integer format.

These methods are blocking and do not require time to progress, so they can be called when the engine is stopped.

I2S Interface

The target control feature allows interaction with the I2S pads to inject or extract data.

A dedicated device model called testbench is connected to the simulated chip’s pads and interacts with the Python script.

To interact with it, instantiate the gv.gvsoc_control.Testbench class similarly to the Router class:

testbench = gv.gvsoc_control.Testbench(gv)

Then, instantiate the gv.gvsoc_control.Testbench_i2s class for each I2S interface to be controlled:

i2s_0 = testbench.i2s_get(0)

This instance can be used to set up the interface. First, configure the interface itself, such as setting the sampling rate, clock type, and data width:

i2s_0.open(sampling_freq=44100, word_size=16, nb_slots=1, is_ext_clk=True, is_ext_ws=True)

Next, configure the slots if the interface uses the TDM feature:

i2s_0.slot_open(slot=0, is_rx=True, word_size=16, is_msb=True, sign_extend=False, left_align=False)

Finally, configure how to interact with the data pads. The most common method is to use WAV files.

To inject data into the I2S interface, use the following reader:

i2s_0.slot_rx_file_reader(slot=0, filetype="wav", filepath='sound_in.wav')

The specified WAV file is opened and streamed into the specified TDM slot. One file per slot is required. The path is relative to the directory where GVSOC is launched or can be absolute.

To extract data, use a file dumper:

i2s_0.slot_tx_file_dumper(slot=1, filetype="wav", filepath='sound_out.wav')

Once everything is set up, start the clock:

i2s_0.clk_start()

At this point, commands have been enqueued to configure the desired interactions on the pad. The simulation can now run for a while to let the testbench inject and extract data from the interface.

Once finished, everything can be closed:

i2s_0.clk_stop()
i2s_0.slot_close(slot=0)
i2s_0.close()