mms_ok is a Python interface layer for Opal Kelly FPGA boards used in lab and hardware-control workflows. It wraps common FrontPanel operations—device configuration, wires, triggers, pipes, block pipes, register access, and built-in self-test (BIST)—behind a small Python API and a command-line entry point.
The package is intended for researchers and lab users who already have Opal Kelly hardware and bitstreams, and who want a repeatable way to install the Python package, check the FrontPanel SDK, run a board test, and script typical host-to-FPGA interactions.
mms_ok currently provides board-specific classes for:
XEM7310(XEM7310A75andXEM7310A200)XEM7360(XEM7360K160T)
The package also includes BIST bitstreams under mms_ok/bitstreams for supported board-test flows.
XEM(...) can open other attached Opal Kelly boards for best-effort common
FrontPanel API access, but unknown boards are not hardware-verified by mms_ok
and do not get board-specific LED behavior or BIST support.
- Context-managed FPGA setup and cleanup.
- Bitstream loading through the Opal Kelly FrontPanel API.
- Wire-in and wire-out helpers.
- Trigger-in and trigger-out helpers.
- Pipe and block-pipe transfer helpers for strings, byte buffers, and NumPy arrays.
- Register bridge read/write helpers.
- LED helpers for supported XEM boards.
- Device discovery from Python and the CLI.
- CLI commands for version checks, SDK checks, FrontPanel setup, device listing, and BIST.
- Windows only.
mms_oksupports Windows environments only.- macOS and Linux are not supported.
- Python 3.7 or newer.
- An Opal Kelly board. XEM7310/XEM7360 boards are hardware-verified by this package; other Opal Kelly boards are best-effort for common FrontPanel APIs.
- The Opal Kelly FrontPanel SDK for Windows. FrontPanel SDK
5.3.6is recommended. - Python dependencies installed by
pip:numpy,bitslice,loguru,rich, andtqdm.
mms_ok imports the FrontPanel Python module as ok. When the Windows SDK is installed in the default Opal Kelly location, mms_ok setup-frontpanel can copy the default FrontPanel Python files into the user's Windows home directory, such as %USERPROFILE%\mms_ok.
Install the Opal Kelly FrontPanel SDK before installing mms_ok, so the SDK files and Windows driver are already present when mms_ok verifies the environment.
-
Go to the Opal Kelly downloads page and sign in with an authorized Opal Kelly account.
-
Open File Downloads.
-
Download the Windows x64 installer for the recommended SDK version. The expected filename is:
FrontPanelUSB-Win-x64-5.3.6.exe -
Run the installer and keep the default install location unless your lab setup requires otherwise.
After the FrontPanel SDK is installed, install mms_ok from PyPI:
pip install mms_okThen check that the command-line entry point is available:
mms_ok --help
mms_ok versionRunning mms_ok with no arguments opens an arrow-key setup helper menu for
common FrontPanel and Jupyter environment diagnostics.
Finally, verify that mms_ok can import the FrontPanel SDK as ok:
mms_ok check-sdkIf the SDK is installed but Python still cannot import ok, run the helper command and check again:
mms_ok setup-frontpanel
mms_ok check-sdkList attached FrontPanel devices from the CLI:
mms_ok devices
python -m mms_ok devicesThe command prints device_id, model, and serial.
device_id is the Opal Kelly okTDeviceInfo.deviceID string.
Use the same discovery from Python:
import mms_ok
for device in mms_ok.list_devices():
print(device.serial, device.model, device.device_id, device.product_id)mms_ok.list_devices() imports the FrontPanel SDK lazily, so import mms_ok
does not require the SDK to be installed.
Use XEM(...) when you want mms_ok to choose the concrete FPGA class
from the connected board:
from mms_ok import XEM
with XEM("path/to/design.bit") as fpga:
print(type(fpga).__name__)Autodetect behavior:
- Exactly one attached XEM7310-A75/A200 opens as
XEM7310. - Exactly one attached XEM7360-K160T opens as
XEM7360. - Other Opal Kelly product IDs open as a generic unverified device for best-effort common FrontPanel APIs.
- Zero attached devices raises
FPGASelectionError. - More than one attached device requires
serial=...and raisesFPGASelectionErrorif omitted. - A provided
serialmust match a discovered device, otherwiseFPGASelectionErroris raised. - If the selected device changes serial or exact product ID between discovery
and the constructor reopen,
ProductIDMismatchErroris raised beforeConfigureFPGAis called.
XEM is public at the top level:
from mms_ok import XEMAdvanced users can import facade details and the shared base class from
mms_ok.fpga. For legacy compatibility, mms_ok.fpga.XEM remains the base
class; the facade exposes the autodetect factory as autodetect_xem.
from mms_ok.fpga import FPGASelectionError, ProductIDMismatchError, UnverifiedXEM, XEM, XEMBase, autodetect_xemUnverifiedXEM is facade-visible for introspection and tests, but it is not a
top-level public constructor promise. Prefer top-level mms_ok.XEM(...) for
normal autodetect usage.
BIST is the quickest way to confirm that the installed package, FrontPanel SDK, connected board, and packaged board-test bitstream can work together.
Run it from the CLI:
mms_ok bistEquivalent module execution:
python -m mms_ok bistThe package first looks for its packaged BIST bitstreams. For backward compatibility, it can also fall back to %USERPROFILE%\mms_ok\bitstreams if packaged files are unavailable.
XEM(bitstream_path), XEM7310(bitstream_path), and
XEM7360(bitstream_path) accept absolute paths, such as
C:\path\to\design.bit. ~ and environment variables are expanded. When only
a bitstream filename such as design.bit is provided, it is loaded from
../bitstreams relative to the current working directory.
Examples:
from mms_ok import XEM7310
# Absolute path: uses this exact file after path expansion.
fpga = XEM7310(r"C:\Users\JY\fpga\design.bit")
# User-home path: "~" expands to the current user's home directory.
fpga = XEM7310("~/fpga/design.bit")
# Environment variable path: "%USERPROFILE%" expands before loading.
fpga = XEM7310(r"%USERPROFILE%\fpga\design.bit")
# Filename only: loads "../bitstreams/design.bit" from the current working directory.
fpga = XEM7310("design.bit")If a bitstream is missing, the error message includes the checked candidate
paths. BIST continues to use the packaged bitstreams under mms_ok/bitstreams
first, then the legacy %USERPROFILE%\mms_ok\bitstreams fallback.
A typical session is:
- Install the Windows FrontPanel SDK with the setup guide above.
- Install
mms_okand verify that it can import the FrontPanel SDK withmms_ok check-sdk. - Connect an Opal Kelly board and verify visibility with
mms_ok devices. - When appropriate, verify the board with
mms_ok bist. - Load your
.bitfile withXEM,XEM7310, orXEM7360. - Use wires, triggers, pipes, and registers to control and inspect your FPGA design.
- Close the device when finished. Prefer a context manager in scripts.
Use this pattern for normal Python scripts so the device is closed automatically:
from mms_ok import XEM
BITSTREAM = "path/to/design.bit"
with XEM(BITSTREAM) as fpga:
# Optional: reset the design through a wire endpoint.
fpga.reset(reset_address=0x00, reset_time=1.0, active_low=True)
# Optional board-specific LED control. Generic unverified boards raise
# NotImplementedError because their LED wiring is not hardware-validated.
try:
fpga.SetLED(0xFF)
except NotImplementedError:
pass
# Write control words through wire-in endpoints.
fpga.SetWireInValue(0x00, 0x12345678)
fpga.SetWireInValue(0x01, 0x00000001)
# Read status through a wire-out endpoint.
status = fpga.GetWireOutValue(0x20)
print(f"status = 0x{status:08X}")
# Start an operation with a trigger-in endpoint.
fpga.ActivateTriggerIn(0x40, 0)
# Wait for a trigger-out completion flag.
fpga.CheckTriggered(0x60, 0x01, timeout=2.0)XEM(...) autodetects the single attached FrontPanel device and returns
XEM7310, XEM7360, or a generic unverified device for other Opal Kelly
product IDs. If more than one device is attached, pass a serial number from
mms_ok devices or mms_ok.list_devices():
from mms_ok import XEM
with XEM("path/to/design.bit", serial="SERIAL123") as fpga:
fpga.SetWireInValue(0x00, 0x00000001)The board-specific constructors also accept serial as a keyword-only argument
when you want to require a verified board family and target a specific device:
from mms_ok import XEM7310
with XEM7310("path/to/design.bit", serial="SERIAL123") as fpga:
fpga.SetLED(0xFF)Passing the serial positionally is intentionally not supported; use
serial="...".
You can still use strict verified constructors directly when you want to require a specific board family. These constructors reject the wrong connected product before configuring the FPGA:
from mms_ok import XEM7310, XEM7360
with XEM7310("path/to/design.bit") as fpga:
fpga.SetLED(0xFF)
with XEM7360("path/to/design.bit") as fpga:
fpga.SetLED(0x0F)Generic unverified boards returned by XEM(...) emit a runtime warning.
They inherit common wires, triggers, pipes, block pipes, registers, reset, and
lifecycle methods, but board-specific helpers such as SetLED(...) are not
supported and BIST remains limited to the verified boards listed above.
In notebooks, it is often convenient to keep the object open across cells. Close it explicitly when you are done:
from mms_ok import XEM7360
fpga = XEM7360("path/to/design.bit")
# Run interactive experiments here.
fpga.SetWireInValue(0x00, 0x00000001)
value = fpga.GetWireOutValue(0x20)
print(value)
fpga.close()Wire and trigger output reads are automatically updated by default. You can disable automatic updates when you want to batch operations manually:
from mms_ok import XEM7310
with XEM7310("path/to/design.bit") as fpga:
fpga.SetAutoWireIn(False)
fpga.SetWireInValue(0x00, 0x11111111)
fpga.SetWireInValue(0x01, 0x22222222)
fpga.UpdateWireIns()
fpga.SetAutoWireOut(False)
fpga.UpdateWireOuts()
result = fpga.GetWireOutValue(0x20)
# One call can still request an update explicitly.
latest = fpga.GetWireOutValue(0x21, auto_update=True)Use pipe endpoints for host-to-FPGA and FPGA-to-host data movement. Strings, bytearray objects, and NumPy arrays are accepted for writes.
Hexadecimal strings are interpreted as FPGA words using endian="little" by default. Pass endian="big" to preserve the original hex byte order.
import numpy as np
from mms_ok import XEM7310
with XEM7310("path/to/design.bit") as fpga:
# Write a hexadecimal payload to a pipe-in endpoint.
written = fpga.WriteToPipeIn(0x80, "AABBCCDDEEFF00112233445566778899")
print(f"wrote {written} bytes")
# Write a NumPy array.
samples = np.array([1, 2, 3, 4], dtype=np.uint32)
fpga.WriteToPipeIn(0x80, samples)
# Read 16 bytes from a pipe-out endpoint.
packet = fpga.ReadFromPipeOut(0xA0, 16)
print(packet.hex_data)
print(packet.raw_data)
# Convert received bytes to a typed NumPy view.
words = packet.to_ndarray(dtype=np.uint32)
print(words)A pipe transaction is one explicit buffer transfer between the host program and a FrontPanel pipe endpoint:
WriteToPipeIn(0x80, data, ...)sends one prepared byte buffer from the host to a pipe-in endpoint in the0x80-0x9Frange.ReadFromPipeOut(0xA0, size_or_buffer, ...)fills a host buffer from a pipe-out endpoint in the0xA0-0xBFrange.- Normal pipe transfers must be a multiple of 16 bytes. For reads, pass either
an integer byte count, such as
16, or an existingbytearraybuffer. - Successful reads return
PipeOutData. Useraw_datafor the exact bytes from the FPGA,hex_datafor word-formatted hexadecimal text, andto_ndarray(dtype)for a NumPy view over the raw bytes. - Negative FrontPanel return codes are raised as
RuntimeError.
For write calls, each supported input type is prepared differently:
| Input type | Pipe behavior |
|---|---|
| Hex string | Parsed as FPGA words and converted to bytes using endian. Whitespace accepted by bytearray.fromhex is allowed. |
bytearray |
Sent exactly as provided. endian and reverse do not rewrite raw byte buffers. |
| NumPy integer array | Flattened in C order, then each element is encoded using endian, independent of the array dtype byte order. |
| NumPy non-integer array | Flattened in C order and sent as contiguous raw bytes. |
Pass verbose=True to pipe-in write helpers when you need to inspect the exact
FPGA-cycle payload sent to the endpoint. The verbose log prints the prepared
payload as uppercase hexadecimal chunks after endian and reverse have been
applied, so it is useful for matching software writes against HDL waveforms.
payload = "AABBCCDD11223344556677889900A1B2"
# Logs four 32-bit FPGA payload cycles for a normal pipe-in transfer.
fpga.WriteToPipeIn(0x80, payload, verbose=True)For normal WriteToPipeIn calls, verbose logging uses 32-bit cycle chunks.
WriteToBlockPipeIn supports the same option and also includes the selected
block_size in the summary log. Block pipe verbose chunks follow the transport
formatting policy: USB 2 block pipes log 16-bit FPGA payload cycles, while USB
3, PCIe, and the default policy log 32-bit cycles.
# Logs the selected block_size plus per-cycle FPGA payload chunks.
fpga.WriteToBlockPipeIn(0x80, payload, block_size=16, verbose=True)verbose=True only adds debug logging; it does not change the bytes sent or the
return value. The device-wide set_verbose_level(...) option remains separate
and logs high-level method summaries such as the byte count written.
endian controls byte order inside each formatted word. For normal pipes, hex
strings and PipeOutData.hex_data use 32-bit words:
# Four 32-bit words: AABBCCDD 11223344 55667788 9900A1B2
payload = "AABBCCDD11223344556677889900A1B2"
# Default: endian="little"; each 32-bit word is byte-swapped on the wire.
fpga.WriteToPipeIn(0x80, payload)
# bytes sent: DD CC BB AA 44 33 22 11 88 77 66 55 B2 A1 00 99
# Big endian preserves the hex byte order inside each word.
fpga.WriteToPipeIn(0x80, payload, endian="big")
# bytes sent: AA BB CC DD 11 22 33 44 55 66 77 88 99 00 A1 B2The same option affects the display format for reads. If the FPGA returns raw
bytes DD CC BB AA, ReadFromPipeOut(..., endian="little").hex_data is
"AABBCCDD", while endian="big" reports "DDCCBBAA". raw_data is never
changed by formatting options.
reverse=True reverses transfer/display order at the word or element level:
- Hex-string writes reverse the order of 32-bit words before applying
endian. - NumPy writes reverse flattened array elements before byte conversion.
- Pipe reads reverse the order of 32-bit words in
hex_dataonly;raw_dataremains in the exact order received from FrontPanel. bytearraywrites are treated as already-final raw bytes, soreverse=Trueleaves them unchanged.
Example with the payload above:
fpga.WriteToPipeIn(0x80, payload, reverse=True)
# logical word order becomes: 9900A1B2 55667788 11223344 AABBCCDD
# default little-endian bytes sent:
# B2 A1 00 99 88 77 66 55 44 33 22 11 DD CC BB AA
packet = fpga.ReadFromPipeOut(0xA0, 16, reverse=True)
print(packet.raw_data) # unchanged raw bytes
print(packet.hex_data) # 32-bit words displayed latest-word firstreorder_str is still accepted for older code, but it is deprecated. Prefer
endian="little" for the old reordered-word behavior or endian="big" when
the original hex byte order should be preserved.
Use block pipes for larger transfers when your FPGA design exposes block pipe endpoints:
import numpy as np
from mms_ok import XEM7310
with XEM7310("path/to/design.bit") as fpga:
payload = np.arange(1024, dtype=np.uint32)
fpga.WriteToBlockPipeIn(0x80, payload)
received = fpga.ReadFromBlockPipeOut(0xA0, 4096)
data = received.to_ndarray(dtype=np.uint32)Block pipe write and read helpers accept the same endian and reverse
options. The difference is transfer sizing: block pipes use a block_size
selected from the connected transport when omitted, and the total transfer byte
count must be compatible with that block size. For hex strings, USB 2 block
pipes format 16-bit words; USB 3, PCIe, and the default policy format 32-bit
words. reverse=True still reverses 32-bit groups for hex-string writes and
hex_data display.
If your bitstream exposes a FrontPanel register bridge, use WriteRegister and ReadRegister with 32-bit register addresses and values:
from mms_ok import XEM7310
with XEM7310("path/to/design.bit") as fpga:
fpga.WriteRegister(0x1000, 0x12345678)
value = fpga.ReadRegister(0x1000)
print(f"register = 0x{value:08X}")FrontPanel endpoint ranges used by the helpers follow the standard Opal Kelly layout:
| Endpoint type | Address range |
|---|---|
| Wire in | 0x00–0x1F |
| Wire out | 0x20–0x3F |
| Trigger in | 0x40–0x5F |
| Trigger out | 0x60–0x7F |
| Pipe in | 0x80–0x9F |
| Pipe out | 0xA0–0xBF |
Import the public classes from mms_ok:
from mms_ok import BIST, XEM, XEM7310, XEM7360, list_devicesFacade-only FPGA helpers and the shared base class are available from
mms_ok.fpga. For compatibility, mms_ok.fpga.XEM is the shared base class;
use top-level mms_ok.XEM(...) or facade autodetect_xem(...) for
autodetect construction:
from mms_ok.fpga import FPGASelectionError, ProductIDMismatchError, UnverifiedXEM, XEM, XEMBase, autodetect_xemCommon methods on XEM7310, XEM7360, and generic devices returned by
XEM(...) include:
- Discovery:
list_devices(),XEM(...). - Device lifecycle:
close(), context manager support,reset(...). - LEDs:
SetLED(...)on verified XEM7310/XEM7360 only. - Wires:
SetWireInValue(...),UpdateWireIns(),UpdateWireOuts(),GetWireOutValue(...). - Triggers:
ActivateTriggerIn(...),UpdateTriggerOuts(),IsTriggered(...),CheckTriggered(...). - Pipes:
WriteToPipeIn(...),ReadFromPipeOut(...),WriteToBlockPipeIn(...),ReadFromBlockPipeOut(...). - Registers:
WriteRegister(...),ReadRegister(...).
On success, ReadFromPipeOut and ReadFromBlockPipeOut return a PipeOutData
object with error_code, transfer_byte, raw_data, hex_data, and
to_ndarray(dtype). Negative Opal Kelly return codes raise RuntimeError
instead of returning PipeOutData(error_code=<negative>).
For questions, bug reports, or feature requests, contact:
juyoung.oh@snu.ac.kr