Skip to main content

Developing UxHw® Applications

The Signaloid SoC in the C0-microSD+ can compute directly on probability distributions using Signaloid's UxHw technology. Your application performs ordinary arithmetic on distributional values, and the hardware propagates the distributions from inputs to outputs in a single execution (see UxHw in Silicon). This guide shows you how to develop such an application, starting from the official Signaloid-Compute-Module-Application-Template repository, a minimal, working application pair (a C device application and a Python host application) that you extend with your own computation.

Compiling UxHw applications does not require a local cross-compilation toolchain. The template uses the Signaloid CLI to build your C application on the Signaloid Cloud Compute Engine, targeting the C0-microSD+, and downloads the resulting binary for you to flash.

Prerequisites

  • A C0-microSD+ and its device path on your host. See Identify your module.
  • A Signaloid account on the Signaloid Cloud Developer Platform and an API key.
  • A GitHub account, so that you can fork the template repository and push your changes, connected to your Signaloid account as shown in GitHub Login.
  • The Signaloid CLI, installed and authenticated as shown in its installation and authentication documentation.
  • Python 3.10 or later for the host application and the flashing toolkit. See Python Environment.
  • The jq command-line JSON processor, which the template Makefile uses to parse Signaloid CLI responses.
  • Root privileges (sudo) for raw block-device access.

Fork and clone the template

The Signaloid Cloud Compute Engine builds your application from your repository on GitHub, so any modification you make must live in a repository you can push to. Start by forking the Signaloid-Compute-Module-Application-Template repository to your own GitHub account. Make sure you have connected your GitHub account to your Signaloid account (see GitHub Login), so that the Signaloid Cloud Compute Engine can access your fork.

Then clone your fork recursively, so that you also get the Signaloid-Compute-Module-Utilities submodule (the flashing toolkit and the hardware abstraction layer the template builds on). Replace <your-username> with your GitHub username.

git clone --recursive https://github.com/<your-username>/Signaloid-Compute-Module-Application-Template.git

The repository contains one application in two halves, plus the build machinery.

PathContents
signaloid-soc-application/main.cThe device application that runs on the Signaloid SoC and performs the UxHw computation.
signaloid-soc-application/config.mkThe build configuration, which lists your source files, include paths, and build flags.
python-host-application/The host application that sends inputs to the module and reads back results.
MakefileDrives the whole flow, building through the Signaloid CLI, downloading the binary, and flashing it.

The template application

The template implements one example command, CalculateAddition, and it is the easiest entry point for understanding the whole flow. The host application sends four floats, the bounds of two uniform distributions X and Y. The device application constructs the two distributions, adds them, and returns the mean and variance of the resulting distribution for the host to print. With one command, four input floats, and two output floats, every part of the round trip stays small enough to read in one sitting.

The core of main.c is a loop that waits for a command from the host, dispatches on it, and reports status, following the polled handshake documented in The MMIO Interfacing Model. The kCalculateAddition case is where the UxHw computation happens.

case kCalculateAddition:
/*
* Create distributional values from inputs
*/
inputDistributionA = UxHwFloatUniformDist(kC0HALInputBufferFloat[0], kC0HALInputBufferFloat[1]);
inputDistributionB = UxHwFloatUniformDist(kC0HALInputBufferFloat[2], kC0HALInputBufferFloat[3]);

/*
* Calculate
*/
result = inputDistributionA + inputDistributionB;

/*
* Compute the mean (first moment) and variance
* (second centralised moment) of the result.
*/
mean = UxHwFloatNthMoment(result, 1);
variance = UxHwFloatNthMoment(result, 2);

/*
* Return the mean and variance as two floats by writing them
* to the output buffer.
*/
kC0HALOutputBufferFloat[0] = mean;
kC0HALOutputBufferFloat[1] = variance;

Three details are worth noting.

  • UxHw values are ordinary C floats. UxHwFloatUniformDist() returns a float that carries a distribution, and inputDistributionA + inputDistributionB is plain C addition, and the hardware carries the distributions through it. You query the result with functions such as UxHwFloatNthMoment(). The full API surface is in the UxHw API documentation.
  • The hardware abstraction layer hides the MMIO details. The C0HAL* functions and the kC0HALInputBufferFloat and kC0HALOutputBufferFloat arrays map onto the COMMAND and STATUS registers and onto the two halves of the single shared MMIO buffer of the MMIO interfacing model.
  • Commands are a shared contract. The SignaloidSoCCommand enum in main.c and the Commands enum in python-host-application/host_application.py must stay in sync, and each new computation you add is a new entry in both.

Build with the Signaloid CLI

The Signaloid CLI is the command-line interface to the Signaloid Cloud Compute Engine. It connects a GitHub repository to your Signaloid account, starts builds of that repository on a Signaloid core, and downloads the build products (see the Signaloid CLI introduction). Building on the Signaloid Cloud Compute Engine is what gives your application its UxHw capabilities, because the platform provides the compiler and the uxhw.h API, matched to the C0 core you build for.

You do not need to invoke the CLI by hand, because the template's Makefile drives it. First, configure three variables at the top of the Makefile.

  1. Set DEVICE to the device path of your C0-microSD+, for example /dev/disk4.
  2. Set DEVICE_TYPE to SIGNALOID_C0_MICROSD_PLUS. This makes the Makefile offer the C0-microSD+ core IDs and use the C0_SD_toolkit.py flashing and core-control commands for this module. The variable stays on your host and is not passed to the cloud build, which derives its own DEVICE_TYPE from the core ID you select next. That is what selects the C0-microSD+ hardware abstraction layer and register map in signaloid-soc-application/config.mk.
  3. Set CORE_ID to the C0 core to build for. The core determines the distributional representation precision and whether the build tracks correlations, and the Makefile lists the core IDs of the available C0-microSD+ core variants. See Execution Cores for how cores differ.

Then start the build.

make

On the first run, the Makefile uses the CLI to look up (or connect) the repository and branch on the Signaloid Cloud Compute Engine, pointing it at the signaloid-soc-application/ directory and your selected core. It then starts a build, streams the build status until completion, and downloads the resulting binary as signaloid-soc-application/<build-id>.main.bin. The build inputs (source files, include paths, and build flags) are defined in signaloid-soc-application/config.mk.

note

The Signaloid Cloud Compute Engine builds the repository as it exists on GitHub, so commit and push your changes to your fork before running make.

The C0-microSD+ can run an application from the 320 KiB of on-chip SRAM, and it also supports execute in place (XIP), running code directly from the 16 MiB SPI flash so that only writable data has to live in SRAM.

You select between the two with the MODE variable at the top of signaloid-soc-application/config.mk, the same file that lists your source files in the SOURCES variable. The template ships with MODE set to lram, which runs the whole application from the on-chip SRAM. Set it to xip to build the application to execute in place from the SPI flash.

MODE := xip

The two modes trade speed against capacity. lram is the fastest, and it bounds your whole application by the 320 KiB of SRAM. xip suits larger binaries, and its performance is bounded by the flash latency and bandwidth. Commit and push the change to your fork, then run make again to rebuild with the new mode.

Flash and run

Flashing and running your application takes two steps, because the host application brings the core up for you.

  1. Flash the downloaded binary into the SPI flash user data region of the module.

    make flash

    The Makefile first stops the core, so that the running application does not access flash while it is being written, and then flashes the binary with the C0_SD_toolkit.py script from the Signaloid-Compute-Module-Utilities submodule.

  2. Set up and run the Python host application from the repository root, passing --reset-on-launch so that it restarts the core on the binary you just flashed.

    python3 -m venv .venv
    source .venv/bin/activate
    pip install -r python-host-application/requirements.txt
    cd python-host-application
    sudo ../.venv/bin/python3 host_application.py --device-path /dev/disk4 --variant C0-microSD+ --reset-on-launch CalculateAddition 5.0 6.0 4.0 7.5

    Replace /dev/disk4 with your device path. The command adds a uniform distribution over 5.0 to 6.0 to a uniform distribution over 4.0 to 7.5 on the module, and the host prints the mean and variance of the result. You invoke the interpreter of the virtual environment directly, because a plain sudo python3 may resolve to a different interpreter that does not have the packages you installed in the virtual environment.

Bringing the core up from the host application

You do not need a separate step to start the core, because the host application always brings it up during initialization and then waits until the device application reports the waiting for command status before it sends anything. What --reset-on-launch changes is how the core is brought up.

  • Without --reset-on-launch, the host application starts the core. A core that is already running stays running, so the device application keeps whatever state it was left in by an earlier run.
  • With --reset-on-launch, the host application stops the core, waits briefly, and starts it again. That restarts the device application from its entry point with its state cleared.

Pass --reset-on-launch on the run that follows a flash, and whenever an earlier run left the device application halted on a trap or stopped part way through a transaction. It is the flag to reach for on every edit, build, and run cycle, because it guarantees that the code now executing is the binary you just flashed.

The matching --stop-on-exit flag stops the core and turns off the on-board LEDs when the host application finishes, which leaves the module idle in its slot rather than spinning in the polling loop of your device application.

For the register and buffer contract your host code writes against, see Host Interface and Protocol. For driving the core directly with the toolkit, see Start and stop the core.

Make it your own

Use the following workflow to turn the template into your own application. The modifications in these steps must reach GitHub for the Signaloid Cloud Compute Engine to build them, so make them in your fork of the template.

  1. Develop and test your computation in the Code Playground of the Signaloid Cloud Developer Platform, where you can iterate on UxHw code without flashing hardware.
  2. Copy the computation into signaloid-soc-application/main.c as a new command. Add an entry to the SignaloidSoCCommand enum and a matching case that reads inputs from kC0HALInputBufferFloat and writes results to kC0HALOutputBufferFloat.
  3. Mirror the new command in the host application. Extend the Commands enum and the argument parsing in python-host-application/host_application.py, and adapt the input packing and output parsing helpers in python-host-application/app_helpers.py.
  4. If you split your code into more .c files, add them to the SOURCES variable in signaloid-soc-application/config.mk.
  5. Commit and push your changes to your fork, then build, flash, and run as shown above.

For complete, working applications built this way, see Examples and Demos.

Next steps