Skip to main content
Use xsim to run an XMOS .xe application without an xTAG or a physical board. You can check console output, record an instruction trace, watch and force pins, and run many test cases at once. Results describe the simulated target. They don’t establish real-board electrical behavior or hardware timing.

Set up the simulator

  1. Install XMOS XTC Tools and build your .xe. Simulated runs need xsim. Simulator debugging also uses xgdbserver and xgdb.
  2. Make sure Embedder can find XTC Tools. It checks your PATH, XMOS_TOOL_PATH, and the standard install folder. For any other location, set EMBEDDER_XTC_PATH to the XTC root or its bin folder in the environment that launches Embedder. See Where Embedder finds XTC Tools.
  3. Select the simulator in .embedder/xmos.json:
    .embedder/xmos.json
    Replace bin/app.xe with your project-relative .xe path.
  4. Ask Embedder to check simulator readiness before you start. The simulator is ready only when Embedder finds both xsim and the Embedder xsim plugin, which Embedder includes for macOS, Linux, and Windows on x64. Finding XTC Tools alone isn’t enough.

Project settings

These keys in .embedder/xmos.json control simulated runs: If the file has an unknown key or an invalid value, such as sim.jobs above 64, Embedder ignores the whole file and uses the defaults. Don’t put paths that contain spaces in sim.args. When you debug under the simulator, such a path doesn’t reach xsim intact, and Embedder drops --max-cycles because the debugger never stops on it.

Run and check output

Ask Embedder to run the application and wait for a message your firmware prints:
Replace BOOT_OK with a message your firmware emits. For a repeatable check, save a hardware script such as .embedder/hardware/sim_boot.py:
.embedder/hardware/sim_boot.py
Ask Embedder to run it, or run embedder script sim_boot.py from the project directory. If the message doesn’t appear in time, the script raises TimeoutError with xsim console did not emit expected output. Check the console and the simulator state before you assume a firmware defect. With the default "xscope": "auto", the simulator starts with offline xSCOPE, which needs an image built with -fxscope. For a plain image, call xsim.start(xe="bin/app.xe", xscope=False), or set "xscope": false in .embedder/xmos.json. Scripts can use these helpers: When the script ends, Embedder stops a simulation that the script started. It doesn’t stop a simulation the script reused. After a simulation ends, its full console log is in .embedder/artifacts/xsim/<session>/, with ports.vcd when sim.vcd is on and xscope.xmt from offline xSCOPE. A Serial Monitor tab can also show the simulated application’s output. Choose the XMOS xsim (simulator) port; see xSCOPE. For breakpoints, stepping, tiles, registers, or stack frames, use XMOS debugging with simulator=True. Starting a debugger replaces a standalone simulation that’s already running. While the debugger owns the simulator, xsim.start() fails until you stop the debug session. To find where a workload spends its time, see Profile a session.

Record an instruction trace

An instruction trace records every retired instruction with its tile, logical core, PC, symbol, and tile-clock cycle. xsim names the symbols from the .xe, so you don’t need a separate ELF. Start the trace before the simulation. If a simulation is already running without tracing, the trace fails with the running simulation was started without instruction tracing. Stop that simulation and start the trace first. The trace starts the simulator with offline xSCOPE, so build the image with -fxscope. Without it, the run can fail with Unable to find __sodFlag. Add -g when you want source-line results.

From the Trace tab

  1. Run Embedder: Open Embedder Monitor, select +, and choose Trace.
  2. In Source, choose the entry that ends in XMOS xsim (simulator).
  3. Set Capture type to Instruction trace.
  4. Optional: change Instruction limit, the number of recent instructions to keep. The default is 1,000,000.
  5. Select Record. Select Stop when the interval you need is complete.
The trace uses the .xe selected in a Serial Monitor tab’s App (.xe) or set as xe in .embedder/xmos.json. The result opens in the Instructions view, and you can watch it fill while it records.

From chat or a script

In a hardware script, call xsim.trace_start() and xsim.trace_stop(). See Analyze the capture from Python.

Limits

  • The instruction limit is a rolling window across all tiles and cores. Older instructions are dropped as newer ones arrive. Execution doesn’t stop at the limit.
  • A recording stops on its own after 10 minutes of wall-clock time.
  • Boot-ROM instructions aren’t traced. In a script, pass rom=True to xsim.trace_start() to include them.
  • Stopping a trace also stops a simulation that the trace started. A simulation that was already running keeps running.
Cycle values count the simulated tile clock. Convert them to time only with that tile’s simulated clock frequency. They aren’t host time or a measurement of a physical board.

Analyze the capture from Python

xsim.trace_start() returns a session ID. A hardware script can query that session while it records or after xsim.trace_stop():
.embedder/hardware/trace_bench.py
All three accept these filters:
  • tile and core limit the result to one tile or logical core.
  • start_cycle and end_cycle limit it to a cycle range, including the start and excluding the end. A cycle range needs tile, because each tile has its own clock.
To page through instructions, pass the returned next_after_index as after_index in the next call. Pages stay stable while the window rolls. If cursor_evicted is true, the instructions you asked for have already left the window. window_truncated means the trace holds only the tail of the run. Hotspots count retained instructions. Instructions without a function or source line are counted separately as unresolved_count, so the total doesn’t change silently. Each new trace keeps its own copy of the .xe, so line-level hotspots still resolve after you rebuild. A trace that joined a simulation that was already running, or a trace from an earlier Embedder version, can’t resolve source lines and reports that.

Watch and drive pins

Use the Sim IO tab to watch simulated pins change and to force their levels while the simulation runs.
  1. Run Embedder: Open Embedder Monitor, select +, and choose Sim IO.
  2. Start a simulation. Until one runs, the tab shows Start an xsim simulation to see GPIO activity.
  3. Search for pins, pick the ones you want, and select Show N lanes to add them.
  4. Use H, L, and R on a lane to force the pin high, force it low, or release it.
To sample the same pins every time, list them in sim.pins in .embedder/xmos.json. From a hardware script, use these helpers:

Read and write simulated memory

Scripts can read and write target memory with xsim.read_memory(tile, address, length) and xsim.write_memory(tile, address, data). xsim fills memory that nothing has written with 0xBADDF00D. The program image doesn’t load until the simulation runs, so before that, the whole program area reads as this fill.
  • xsim.read_memory warns when it reads the fill.
  • xsim.write_memory refuses to write there, because loading the image would overwrite the write.
To write program memory, do one of the following:
  • Resume the simulation first, for example with xsim.resume() and then xsim.wait_for_output().
  • Write at a debugger breakpoint.
  • Patch the image with write_memory in xsim.run_batch.
If the program is already running and hasn’t used that memory yet, pass force=True.

Run test cases in parallel

Use xsim.run_batch from a hardware script for a test gate with independent cases, such as one run per stimulus, vector set, parameter set, or seed. Embedder starts a separate xsim for each run, runs several at once, and waits for all of them. One failing case doesn’t stop the others. To debug a failing case with breakpoints or traces, reproduce it on its own with xsim.start() or XMOS debugging. A batch runs alongside the simulation that xsim.start() manages. It doesn’t use or disturb that simulation, the Sim IO tab, or the Trace tab. Stopping the script cancels the batch.
.embedder/hardware/limiter_gate.py
Each run is a dict with a name that’s unique in the batch, ignoring case. Names use letters, digits, ., _, and -, start with a letter or digit, and are up to 80 characters long. A batch can hold up to 1,000 runs. The batch’s own name follows the same rules, up to 40 characters. Keyword options apply to every run that doesn’t set them. defaults= sets the same options as a dict. A run can also set its own xe. Startup zeroes .bss, so Embedder refuses a write_memory target there and reports the run as error. Give the variable an initializer, such as int32_t stim[N] = {1};, to place it in .data.

How a run ends

Each run reports a status of passed, failed, max_cycles, timeout, cancelled, or error. ended_by names the condition that ended it:
  • Exit code: a single-tile C main return value becomes the xsim exit code. 0 is passed. Any other value is failed.
  • done_pattern or done_memory: the run is passed. Multi-tile and RTOS images can’t call exit(), so end them this way. Keep the program running after the marker, for example in an idle loop, so read_memory can read the buffer.
  • fail_pattern: the run is failed.
  • max_cycles or timeout_seconds: the run reports max_cycles or timeout.
When done_pattern or done_memory is set, a run that exits before the condition matches is failed, and Embedder skips its read_memory. If the program exits after the done condition matches but before Embedder reads memory, the run reports error. Keep the program running after the marker. passed means the run ended as specified, not that its output is correct. Compare each run’s output with your reference. keep_going=False cancels the remaining runs after the first one that doesn’t pass. check=True raises RuntimeError if any run didn’t pass. Each run writes its files to .embedder/artifacts/xsim-batch/<batch>/<name>/, returned as dir. The directory holds the staged files, console.log, result.json, memory dumps, and any xSCOPE, pin, or VCD output. The batch result includes batch_id, batch_dir, jobs, jobs_reason, all_passed, counts, duration_s, and runs.

Set the number of parallel runs

By default, Embedder runs one fewer job than the number of performance cores on Apple silicon, or one fewer than the available CPUs elsewhere. Free memory also caps that count. The count never exceeds the number of runs. To override it, use one of these, in order of precedence. Each accepts 1 to 64:
  1. jobs= on xsim.run_batch.
  2. sim.jobs in .embedder/xmos.json:
.embedder/xmos.json
jobs_reason in the result reports which setting applied.

Keep cases short

xsim runs hundreds of times slower than real time, so one simulated second can take many minutes. Each run also slows down when several run at once. Time one short run of your image first, then set timeout_seconds from that estimate with margin. Split a gate into many short cases instead of one long one. Give each case the shortest stimulus that proves it, and get coverage from more cases that run in parallel. Ask Embedder to run batch scripts in the background. A foreground hardware-script run stops after 60 seconds by default, and Embedder can raise that to at most 10 minutes. The time limit doesn’t apply to a background run. From a terminal, embedder script accepts --timeout <ms> for a longer limit.

Troubleshoot

  • No .xe found: build the project and correct xe in .embedder/xmos.json.
  • xsim not found: install XMOS XTC Tools, or point EMBEDDER_XTC_PATH or XMOS_TOOL_PATH at the installation.
  • the Embedder xsim plugin is not available for this platform: run Embedder on macOS, Linux, or Windows on x64, and check that XTC Tools is complete.
  • … path contains whitespace: xsim can’t take file paths with spaces, and Embedder keeps the simulator’s working files under your user folder. This affects user folders whose names contain a space.
  • the simulator is owned by the debugger session: stop the XMOS debug session before you start a standalone simulation.
  • The output wait times out: check the console, whether the simulation is paused or running, and the expected message. Increase the wait only when the firmware needs longer.
  • Unable to find __sodFlag: the image wasn’t built with -fxscope. Rebuild with it, or start a plain image with xscope=False.
  • the running simulation was started without instruction tracing: stop the running simulation and start the trace before the next run.
  • The beginning of the trace is missing: record a shorter interval or raise the instruction limit.
  • xsim.write_memory refused: the image isn’t loaded yet. See Read and write simulated memory.
  • A simulation exits at its cycle limit: check the extra simulator arguments. A standalone run with --max-cycles exits when it reaches the limit, which proves only that the limit was reached.
Last modified on October 13, 2026