Assembler and toolchain

Install and run

From the source checkout, use Python 3.11 or later in a virtual environment:

python -m venv .venv
. .venv/bin/activate
python -m pip install -e .
python -m pip install -r test/requirements.txt
python examples/v2/run.py --output build/examples-new

The last command generates 16 assembly programs and images and runs 14 checked protocol scenarios, including TX/RX together at each UART rate. Output directories must be new. The example matrix specifies pins and expected traces. These are cycle-model executions; the independent RTL pin suites establish top-level SPI-host and protocol behavior separately.

Assembly, disassembly and image format

V2 uses the existing Python APIs. The installed protocol-emulator CLI selects V1 behavior and does not provide a V2 assembler/run mode. The V1 image helpers also validate V1 encodings; use V2 decode for extension instructions.

from pathlib import Path
from protocol_emulator.v2.firmware import uart_tx
from protocol_emulator.v2.isa import assemble, decode

source = uart_tx(period=217, pin=0)
words = assemble(source, capacity=64)
image = b"".join(word.to_bytes(3, "big") for word in words)
Path("uart-v2.bin").write_bytes(image)
# Decode each instruction; there is no dedicated V2 disassemble function.
text = "\n".join(
    i.name + (" " + ", ".join(map(str, i.operands)) if i.operands else "")
    for i in map(decode, words)
)
assert assemble(text, capacity=64) == words

Images contain 1–64 words, three bytes per word, most-significant byte first, with no header. They contain at most 192 bytes. Labels identify instruction addresses, not byte offsets; decimal and 0x/0b integer literals are accepted. Comments start with # or ;. Decode every word and reject incomplete words or out-of-range targets before sending an image. Explicit capacity=64 matters: the feasibility assembler and model otherwise default to 128 words.

Model execution

from protocol_emulator.v2.model import Engine
engine = Engine(capacity=64)
engine.owner = 1
engine.load(words)
engine.push([0x55, 0xA5])
engine.start()
for cycle in range(22 * 217):
    engine.tick(0)
    value, enables = engine.pins

Model inputs are already synchronized. A board-level stimulus needs an explicit synchronizer/pin simulator. Model host operations are atomic Python calls; this loop does not model 40-clock SPI frames or operating-system scheduling. See runnable examples for independent decoding and input pipelines.

Loading and host control

A transport supplies exchange(frame: bytes) -> bytes for exactly one five-byte mode-0 SPI transaction. Given an initialized compatible transport:

from protocol_emulator.v2.host import Device
host = Device(transport)
host.check_version()
host.stop(0)
host.stop(1)
host.load(0, words)        # validate, HALT-pad, write and read back all 64 words
host.configure(0, owner=1)
host.fifo_write(0, bytes([0x55, 0xA5]))
host.start(mask=1)

Initialize/reset the physical design first using the documented sequence. The snippet assumes the other engine owns no conflicting pins. connect(transport) can choose the V1/V2 host backend by version probe; selection of a V2 ASIC default does not change the CLI. Watch and abort commands, packed FIFO service, capture, and failure handling are described in the host API.