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.
| Path | Contents |
|---|---|
src/c/include | Public headers. Your device application includes from here. |
src/c/src | Device-side implementations, one HAL.c per module variant, plus the logger. |
src/c/lib | Host-side helpers. A device application never uses these. |
src/c/regmaps | Auto-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 holds register maps for other module variants, and the
C0-microSD build does not use it.
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
C0microSD/HAL.h or C0microSD/Constants.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.
-DBUILD_FOR=SIGNALOID_C0_MICROSD
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/C0microSD/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)
SOURCES += $(UTILITIES_DIR)/src/c/src/C0microSD/HAL.c
# ... branches for the other module variants ...
endif
# Include helper headers from the Signaloid Compute Module Utilities package
INC_DIRS += $(UTILITIES_DIR)/src/c/include
Set DEVICE_TYPE to SIGNALOID_C0_MICROSD in the top-level Makefile and the rest
follows. 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/C0microSD/HAL.c.
| Function | Purpose |
|---|---|
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 and configuration registers of the C0-microSD are write-only from the core, so
the HAL keeps an internal copy of each and mirrors every write to the hardware.
C0HALGetStatusRegister() and C0HALGetConfigRegister() therefore return the last value
your application wrote through the HAL rather than a hardware readback.
C0HALGetCommandRegister() is a true hardware read, because the command register is written
by the host.
Both internal copies start at zero, which matches the value that the hardware registers hold
when the module powers up, so C0HALGetStatusRegister() and C0HALGetConfigRegister()
return a defined value before your application writes anything.
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
On the C0-microSD the configuration register is the SoC control register of the host
interface, and the C0HALConfigRegister union exposes its two fields as swLed and
debugPin0. The host can read the register back over the SD interface, so these two bits
double as a primitive signal toward the host. For the bit assignments, see
Host Interface and Protocol → Communication registers.
| Function | Purpose |
|---|---|
void C0HALSetLed(bool state) | Drive the on-board red LED. |
void C0HALSetConfigRegisterDebugPin0(bool state) | Drive the CONFIG_DONE pin. |
C0HALSetLed() is the variant-agnostic way to drive the status LED, and on the C0-microSD
it maps to the swLed field and the on-board red LED. Prefer it when you want application
code that also builds for other modules. The wider setter families of other Signaloid
compute modules do not exist on this module.
The data buffers
The module provides two separate 4 KiB data buffers. The output buffer, called the MISO
buffer, carries data from the device to the host, and the input buffer, called the MOSI
buffer, carries data from the host to the device. C0HAL.h gives each buffer a typed
pointer per element type, so you index the buffer as an array of the type you use.
| Element type | Input buffer, host to device | Output buffer, device to host |
|---|---|---|
uint8_t | kC0HALInputBufferUint8 | kC0HALOutputBufferUint8 |
uint32_t | kC0HALInputBufferUint32 | kC0HALOutputBufferUint32 |
int32_t | kC0HALInputBufferInt32 | kC0HALOutputBufferInt32 |
float | kC0HALInputBufferFloat | kC0HALOutputBufferFloat |
double | kC0HALInputBufferDouble | kC0HALOutputBufferDouble |
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, and both are 4096 on this
module.
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.
An unaligned load or store halts your device application, and the module reports no error to the host, so the host sees a status register that never changes. Keep every access naturally aligned.
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 buffers 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 buffer, writes the output buffer, 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 whose 128 KiB of RAM is shared by your code and data.
| Function | Purpose |
|---|---|
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.
The log window sits at the end of the output buffer. Keep your results clear of the last 512 bytes of that buffer in any build with logging enabled, because the logger and your output data otherwise overwrite each other. With the default window size, that leaves 3584 bytes of the 4 KiB output buffer for your results.
Read the records back on the host with the C0_debug_logger.py script from the
Signaloid-Compute-Module-Utilities repository. The script defaults to
the C0-microSD, so it needs no variant flag.
Next steps
- Developing UxHw Applications, building and flashing an application that uses this API.
- Using the Python Host Interface, the other half of the application, running on the host.
- Host Interface and Protocol, the registers, buffers, and addresses the HAL wraps.