Using the Python Host Interface
The Signaloid-Compute-Module-Utilities repository ships a Python package for the code that runs on the host. It gives your host application a class whose methods read and write the registers and buffers of the C0-microSD, so you exchange data with your device application without writing raw block-device code of your own.
For the offsets these methods reach, see Host Interface and Protocol. For the device-side half of the application implementation, see Using the Hardware Abstraction Layer.
Install the package
The distribution is named signaloid-utilities and the importable package is
signaloid_utilities. It requires Python 3.10 or later. See Python Environment.
Install it from the Signaloid-Compute-Module-Utilities repository. The
Signaloid-Compute-Module-Application-Template lists it in
python-host-application/requirements.txt, so a template-based project installs it with the
rest of its dependencies.
python3 -m venv .venv
.venv/bin/pip install -r python-host-application/requirements.txt
Host applications need root privileges, because reaching the module means opening a raw block
device. Invoke the interpreter of your virtual environment directly under sudo, for example
sudo .venv/bin/python3. A plain sudo python3 may resolve to a different interpreter that
does not have your installed packages.
Construct the interface
The C0-microSD class lives in the c0microsd subpackage.
from signaloid_utilities.c0microsd.interface import C0microSDSignaloidSoCInterface
compute_module = C0microSDSignaloidSoCInterface("/dev/disk4")
The constructor signature is
C0microSDSignaloidSoCInterface(target_device, force_transactions=False). Pass the
raw device-node path as target_device, which looks like /dev/sdX on Linux and /dev/diskN
or /dev/rdiskN on macOS. Leave force_transactions at its default, because setting it makes
the interface press on where it would otherwise stop with an error.
Constructing the interface probes the module. It reads the configuration status words,
confirms that the device really is a C0-microSD, and records which configuration is loaded
together with its version. The constructor raises RuntimeError when the device is not a
C0-microSD, and also when a configuration switch is pending, in which case power-cycle the
module and construct the interface again.
The instance carries the offsets of this module as attributes you can read. The register and buffer values come from the major version of the loaded configuration, so the instance always matches the module in front of it. The configuration status offset is fixed across versions, because reading it is how the interface discovers the version in the first place.
| Attribute | Value | Meaning |
|---|---|---|
STATUS_REGISTER_OFFSET | 0x00000 | Status register |
SOC_CONTROL_REGISTER_OFFSET | 0x00004 | SoC control register |
COMMAND_REGISTER_OFFSET | 0x10000 | Command register |
DEVICE_CONFIGURATION_STATUS_OFFSET | 0x20000 | Base of the three configuration status words |
MOSI_BUFFER_OFFSET | 0x50000 | Input buffer, host to device, 4 KiB |
MISO_BUFFER_OFFSET | 0x60000 | Output buffer, device to host, 4 KiB |
These are SD-interface block offsets, which differ from the memory addresses your device application uses for the same registers and buffers. See Host Interface and Protocol → Host-side addressing.
The buffer sizes are available as MOSI_BUFFER_SIZE_BYTES and MISO_BUFFER_SIZE_BYTES, with
INPUT_BUFFER_SIZE_BYTES and OUTPUT_BUFFER_SIZE_BYTES as aliases. Size your transfers from
those attributes rather than from literals, so your application keeps working if a future
configuration changes them.
Check the loaded configuration
The C0-microSD boots into one of two configurations, the Bootloader or the Signaloid SoC, and your device application runs only when the Signaloid SoC configuration is loaded.
compute_module.get_status()
print(compute_module)
get_status() re-reads the configuration status words and refreshes the configuration,
configuration_version, and configuration_switching attributes. The configuration
attribute holds "bootloader" or "soc", and printing the instance gives a one-line summary
of all three.
Note that get_status() reports the loaded configuration of the module rather than the
status register of your device application. Read the status register with
get_signaloid_soc_status(), described below.
The interface has no methods for starting, stopping, or resetting the core. The device application starts when the module powers up with the Signaloid SoC configuration loaded, and switching configurations is a Bootloader operation followed by a power cycle. See Switch Between Operation Modes.
Move data through the buffers
| Method | Purpose |
|---|---|
write_input_buffer(buffer) | Write operand bytes into the input buffer. |
read_output_buffer(size=None) | Read result bytes from the output buffer, all of it by default. |
read_debug_log_buffer(size=512) | Read the device log window at the end of the output buffer. |
The first two are aliases for write_signaloid_soc_MOSI_buffer() and
read_signaloid_soc_MISO_buffer(), which name the same methods after the buffers themselves.
Prefer the aliases, because they match the interfaces of the other Signaloid compute modules.
Each write raises ValueError when the buffer you pass is longer than the 4 KiB region it
targets, and so does a read whose size is larger than the buffer.
Python struct is all you need to lay bytes out. The following packs four floats for a
device application that reads them from the input buffer.
import struct
operands = struct.pack("<4f", 5.0, 6.0, 4.0, 7.5)
compute_module.write_input_buffer(operands)
The layout you pack here is the layout your device application reads, and the RISC-V core of the module requires every load to be naturally aligned. Place each field on a multiple of its own size, adding padding where a mixed layout would otherwise misalign a later field. See Using the Hardware Abstraction Layer → Memory alignment.
Drive the round trip
| Method | Purpose |
|---|---|
send_signaloid_soc_command(value) | Write a 32-bit command word. |
get_signaloid_soc_status() | Read the 32-bit status word. |
calculate_command(...) | Run a whole blocking round trip. |
calculate_command() is the method most host applications call. Its signature is
calculate_command(command, idle_command=0, poll_sleep_time=0.5, skip_MISO_read=False, verbose=True, timeout_waiting_to_start=0.5). It writes the command, polls the status
register until the device reports done, optionally reads the output buffer back, and then
drives the device to idle by writing idle_command until the status returns to waiting for
command.
The module-level constants mirror the conventional status values of the device-side header,
namely SIGNALOID_SOC_STATUS_WAIT_FOR_COMMAND, SIGNALOID_SOC_STATUS_CALCULATING,
SIGNALOID_SOC_STATUS_DONE, and SIGNALOID_SOC_STATUS_INVALID_COMMAND, alongside
K_CALCULATE_NO_COMMAND for the idle command.
Three behaviors are worth knowing before you rely on this method.
- The default poll cadence is 0.5 seconds, so a fast computation still takes about that
long to report. Pass a smaller
poll_sleep_time, as the application template does with0.001, when latency matters. timeout_waiting_to_startcovers only the start of the work. Once the device reports calculating there is no timeout.- A failure prints rather than raises. An invalid command, a start timeout, or an
unrecognized status makes the method report the problem and return
None.
Passing skip_MISO_read=True and then calling read_output_buffer() with an explicit size
transfers less data when you only need a few result words, because the default reads the
entire 4 KiB buffer.
compute_module.calculate_command(
1,
poll_sleep_time=0.001,
skip_MISO_read=True,
)
mean, variance = struct.unpack("<2f", compute_module.read_output_buffer(8))
Inspect the module
| Method | Purpose |
|---|---|
get_status() | Refresh the loaded configuration, version, and switching flag. |
print_bitstream_information(offset) | Print the metadata of a bitstream in flash and check its CRC. |
get_bitstream_prefix(offset) | Read and decode the JSON prefix of a bitstream in flash. |
verify_bitstream_crc(offset, crc, prefix_size, size) | Check a bitstream region against an expected CRC. |
The bitstream helpers take explicit offsets from the Bootloader flash address map, and the
C0_microSD_toolkit.py script uses them when it flashes and verifies images, so a typical
host application never calls them. For the flash layout, see
Modes and Custom Bitstream.
A minimal host application
import struct
from signaloid_utilities.c0microsd.interface import (
C0microSDSignaloidSoCInterface,
SIGNALOID_SOC_STATUS_WAIT_FOR_COMMAND,
)
K_MY_COMMAND_ADDITION = 1
compute_module = C0microSDSignaloidSoCInterface("/dev/disk4")
# Confirm that the Signaloid SoC configuration is loaded.
if compute_module.configuration != "soc":
raise SystemExit("Switch the module to the Signaloid SoC configuration first.")
# Wait for the device application to be ready.
while (
compute_module.get_signaloid_soc_status()
!= SIGNALOID_SOC_STATUS_WAIT_FOR_COMMAND
):
pass
# Send the operands, run the command, and read the results back.
compute_module.write_input_buffer(struct.pack("<4f", 5.0, 6.0, 4.0, 7.5))
compute_module.calculate_command(
K_MY_COMMAND_ADDITION,
poll_sleep_time=0.001,
skip_MISO_read=True,
)
mean, variance = struct.unpack("<2f", compute_module.read_output_buffer(8))
print(f"Mean {mean}, variance {variance}")
# Release the device descriptor.
compute_module.device.close()
The command number has to match the command your device application recognizes. Nothing in the library enforces that agreement, so treat the command numbers as a contract between your two halves.
The interface has no close() method and no context manager. Call
compute_module.device.close() when you want the underlying descriptor released
deterministically.
Device access model
A register read issued through an ordinary buffered file descriptor is subject to page-cache coherence rules, so a poll loop can observe a stale status word and a command write can sit in the cache instead of reaching the hardware. The package therefore inspects the device node and selects a cache-bypassing strategy for it.
| Device node | Mechanism |
|---|---|
Linux block node, /dev/sdX | Opened with O_DIRECT, which bypasses the page cache |
macOS character node, /dev/rdiskN | Positioned pread and pwrite on a descriptor held open |
macOS block node, /dev/diskN | Reopened per operation, because macOS invalidates the cache on close |
Two consequences are worth knowing.
- Transfers are aligned for you. The unbuffered strategies require block-aligned access, so a read fetches the covering blocks and returns your slice, and a write fetches them, patches your range, and writes them back. Any offset and length are valid, and bytes next to your range inside the same block survive.
- Each instance serializes its own access. One lock covers every read and write, including the read-modify-write that a partial write performs, so an instance is safe to share across the threads of a single process. Coordinating two processes on the same device remains your responsibility.
Failures name the device path. A missing node raises FileNotFoundError, denied access raises
PermissionError, and any other device-level failure raises OSError.
Read the device log
When your device application is built with logging enabled, read_debug_log_buffer() returns
the raw bytes of the log window. The package also ships a ready-made reader, the
C0_debug_logger.py script at the root of the
Signaloid-Compute-Module-Utilities repository, which polls that window
and prints it.
sudo python3 C0_debug_logger.py /dev/disk4
The script defaults to the C0-microSD, so it needs no variant flag. To read the log from inside your own host application instead, see Embed the Log Reader in Your Host Application.
Next steps
- Developing UxHw Applications, the end-to-end workflow this interface fits into.
- Using the Hardware Abstraction Layer, the device-side library your host application talks to.
- Host Interface and Protocol, the registers, buffers, and handshake behind these methods.