Skip to main content

Using the Hardware Abstraction Layer

The Signaloid-Compute-Module-Utilities repository ships a C library for the code that runs on the module. Its Hardware Abstraction Layer, referred to as the HAL, gives your device application named functions and typed pointers for the registers and buffers of the C0-microSD+, so you write against an API rather than against raw addresses.

The HAL is variant-agnostic. One application source tree targets any Signaloid compute module unchanged, because the module-specific register layout sits behind the HAL and is selected at build time.

For the addresses the HAL wraps, see Host Interface and Protocol. For a working application that uses everything on this page, see Developing UxHw Applications.

What the library contains

The C library lives under src/c in the Signaloid-Compute-Module-Utilities repository.

PathContents
src/c/includePublic headers. Your device application includes from here.
src/c/srcDevice-side implementations, one HAL.c per module variant, plus the logger.
src/c/libHost-side helpers. A device application never uses these.
src/c/regmapsAuto-generated register maps, marked do not modify.

The lib directory holds HostUtils implementations, which run on the host and take a block-device path, so a device application must never include HostUtils.h. The regmaps directory is generated from the hardware definition and reaches your build through the headers, so you do not call into it directly.

Include one header

A device application includes exactly one header.

#include "C0HAL.h"

C0HAL.h is the public entry point. It provides the shared status values, then includes the header for the module you are building for. Do not include C0microSDPlus/HAL.h, C0microSDPlus/Constants.h, or C0mmioCommonHAL.h yourself.

Select the target module

The HAL dispatches on a preprocessor macro named BUILD_FOR, which your build defines on the compiler command line. SignaloidBuildTargets.h names the targets, and the identifier for this module is SIGNALOID_C0_MICROSD_PLUS.

-DBUILD_FOR=SIGNALOID_C0_MICROSD_PLUS

If BUILD_FOR is unset or names a target the HAL does not recognize, the build stops with #error "BUILD_FOR is not set to a known Signaloid compute-module target."

The build system adds exactly one HAL.c to the source list, as the next section shows, and each variant's HAL.c carries a matching guard of its own, so compiling it for the wrong BUILD_FOR stops with an #error rather than silently building the wrong register layout. For the C0-microSD+ that file is src/c/src/C0microSDPlus/HAL.c.

How the application template wires it in

The Signaloid-Compute-Module-Application-Template vendors the utilities as a git submodule at submodules/Signaloid-Compute-Module-Utilities and does the wiring for you in signaloid-soc-application/config.mk.

UTILITIES_DIR := ../submodules/Signaloid-Compute-Module-Utilities

# ...

ifeq ($(DEVICE_TYPE),SIGNALOID_C0_MICROSD)
# ...
else ifeq ($(DEVICE_TYPE),SIGNALOID_C0_MICROSD_PLUS)
INC_DIRS += $(UTILITIES_DIR)/src/c/regmaps/C0microSDPlus
SOURCES += $(wildcard $(UTILITIES_DIR)/src/c/regmaps/C0microSDPlus/*.c)
SOURCES += $(UTILITIES_DIR)/src/c/src/C0microSDPlus/HAL.c
# ...
endif

# Include helper headers from the Signaloid Compute Module Utilities package
INC_DIRS += $(UTILITIES_DIR)/src/c/include

The Signaloid Cloud Compute Engine sets DEVICE_TYPE from the core ID you build for, so selecting a C0-microSD+ core is what pulls in the sources above. The DEVICE_TYPE variable in the top-level Makefile is its local counterpart, and it selects the flashing and core-control commands rather than the build. Add your own source files to the SOURCES variable in the same config.mk.

The register API

Every function below is declared in C0HAL.h and implemented for this module in src/c/src/C0microSDPlus/HAL.c.

FunctionPurpose
uint32_t C0HALGetCommandRegister(void)Read the raw command word the host wrote.
SignaloidSoCStatus C0HALGetStatusRegister(void)Read back the status word.
void C0HALSetStatusRegister(SignaloidSoCStatus status)Publish a status value to the host.
C0HALConfigRegister C0HALGetConfigRegister(void)Read the whole configuration register.

The status values are a convention rather than a requirement. The shared header defines kSignaloidSoCStatusWaitingForCommand as 0, kSignaloidSoCStatusCalculating as 1, kSignaloidSoCStatusDone as 2, and kSignaloidSoCStatusInvalidCommand as 3, and your application is free to define its own. Command values are entirely yours, with 0 conventionally meaning no command.

Configuration register fields

The C0HALConfigRegister union exposes the bitfields of the configuration register in order as rstn, swLedEnable, swLed, redLed, greenLed, blueLed, debugPin0, debugPin1, and debugPin2. rstn is bit 0 and the named fields then run from bit 2 to bit 9, because bit 1 is unnamed and unassigned, as are bits 31:10. One setter exists per named field, so you can modify a single field without modifying the others.

FunctionPurpose
void C0HALSetLed(bool state)Drive the green LED.
void C0HALSetConfigRegisterRedLed(bool state)Drive the red LED.
void C0HALSetConfigRegisterGreenLed(bool state)Drive the green LED.
void C0HALSetConfigRegisterBlueLed(bool state)Drive the blue LED.
void C0HALSetConfigRegisterSwLedEnable(bool state)Hand the on-board red LED to software.
void C0HALSetConfigRegisterSwLed(bool state)Drive that software-managed LED.
void C0HALSetConfigRegisterDebugPin0(bool state)Drive debug pin 0.
void C0HALSetConfigRegisterDebugPin1(bool state)Drive debug pin 1.
void C0HALSetConfigRegisterDebugPin2(bool state)Drive debug pin 2.
void C0HALSetConfigRegisterRstn(bool state)Drive the core reset bit directly.
void C0HALStopCore(void)Halt the core, a one-shot workload ending itself.

A device application cannot unlock the bitstream region. The lock is controlled solely by the BITSTREAM_UNLOCK register, and the hardware forces its key to 0x00000000 whenever the core is running, so a key write from the core is discarded. Unlocking is a host-side operation performed with the core stopped. See BITSTREAM_UNLOCK register.

C0HALSetLed() is the variant-agnostic way to drive the status LED, and on the C0-microSD+ it maps to the green LED. Prefer it when you want application code that also builds for other modules.

The data buffers

The module provides one 64 KiB MMIO buffer, and the HAL presents it as two halves. The output half carries data from the device to the host, and the input half carries data from the host to the device. C0HAL.h gives each half a typed pointer per element type, so you index the buffer as an array of the type you use.

Element typeInput half, host to deviceOutput half, device to host
uint8_tkC0HALInputBufferUint8kC0HALOutputBufferUint8
uint32_tkC0HALInputBufferUint32kC0HALOutputBufferUint32
int32_tkC0HALInputBufferInt32kC0HALOutputBufferInt32
floatkC0HALInputBufferFloatkC0HALOutputBufferFloat
doublekC0HALInputBufferDoublekC0HALOutputBufferDouble

Each pointer has a matching length constant, so you can bound a loop without hard-coding a size. The names follow the same pattern, for example kC0HALInputBufferFloatLength and kC0HALOutputBufferDoubleLength. The byte sizes are available as kC0HALInputBufferSizeBytes and kC0HALOutputBufferSizeBytes.

Memory alignment

The RISC-V core of the C0-microSD+ does not support unaligned memory access. Every load and store has to be naturally aligned, meaning that the address of an access is a multiple of the size of the type being accessed. A 4-byte uint32_t or float needs an address that is a multiple of 4, and an 8-byte double needs a multiple of 8.

warning

An unaligned load or store raises a trap, which halts your device application. See Host Interface and Protocol → Trap registers for how to recognize that a trap has occurred and how to find the instruction that caused it.

The typed buffer pointers keep you aligned as long as you index them with their own element type, because each index step advances the address by one whole element. Both halves of the MMIO buffer start on an address that satisfies every element type, so kC0HALInputBufferDouble[3] and kC0HALOutputBufferFloat[7] are both safe.

Alignment breaks when you step outside that pattern. Reading a float out of a byte-oriented layout is the usual way to trip it.

/*
* Unsafe. Byte offset 6 is not a multiple of 4, so this load traps.
*/
float value = *(volatile float *) &kC0HALInputBufferUint8[6];

/*
* Safe. Index the typed pointer instead, and lay the data out so that
* each element starts on a multiple of its own size.
*/
float value = kC0HALInputBufferFloat[1];

A minimal device application

The following skeleton is the shape every device application takes. It waits for a command, publishes progress, reads the input half, writes the output half, and then waits for the host to clear the command before looping.

#include "C0HAL.h"

typedef enum
{
kMyCommandNone = 0,
kMyCommandAddition = 1,
} MyCommand;

int
main(void)
{
uint32_t command;

while (1)
{
C0HALSetStatusRegister(kSignaloidSoCStatusWaitingForCommand);

/*
* Block until the host writes a command.
*/
while ((command = C0HALGetCommandRegister()) == kMyCommandNone) {}

C0HALSetStatusRegister(kSignaloidSoCStatusCalculating);
C0HALSetLed(true);

switch (command)
{
case kMyCommandAddition:
kC0HALOutputBufferFloat[0] =
kC0HALInputBufferFloat[0] + kC0HALInputBufferFloat[1];

C0HALSetStatusRegister(kSignaloidSoCStatusDone);
break;

default:
C0HALSetStatusRegister(kSignaloidSoCStatusInvalidCommand);
break;
}

C0HALSetLed(false);

/*
* Block until the host clears the command.
*/
while (C0HALGetCommandRegister() != kMyCommandNone) {}
}
}

Command numbers are a contract between the two halves of your application. The enum above has to agree with the command numbers your host application sends, and nothing in the library enforces that, so make sure that the two definitions align when you add a new command.

Debug logging

C0Logger.h adds printing to a device application that has no console. Output goes into a 512-byte window that the host reads back over the SD interface. The window size is a build-time constant, and 512 bytes is its default.

Adding src/c/src/C0Logger.c to your sources is the only step, because the header defines ENABLE_DEBUG_LOGGING as 1 unless your build overrides it. In the application template that means uncommenting one line in config.mk.

SOURCES += $(UTILITIES_DIR)/src/c/src/C0Logger.c

The API keeps its footprint small, which matters on a module with 320 KiB of SRAM.

FunctionPurpose
print_lstr("string")Print a literal string with zero overhead.
print_int(int)Print a signed decimal integer.
print_uint(unsigned)Print an unsigned decimal integer.
print_hex(unsigned)Print lowercase hexadecimal.
print_hexdump(p, len)Print a hex and ASCII dump of a region.
tiny_printf(fmt, ...)Print a formatted string, a small subset of printf.

Every one of these becomes a no-op when your build defines ENABLE_DEBUG_LOGGING as 0, and the template's config.mk carries a commented BUILD_FLAGS += -DENABLE_DEBUG_LOGGING=0 line for exactly that, so disable logging in production builds rather than deleting the calls.

warning

The log window sits at the end of the output half of the MMIO buffer. Keep your results clear of the last 512 bytes of that half in any build with logging enabled, because the logger and your output data otherwise overwrite each other.

Read the records back on the host with the C0_debug_logger.py script from the Signaloid-Compute-Module-Utilities repository, passing --variant C0-microSD+.

Next steps