Sound Byte Libs uses a declarative hardware configuration system that separates MCU drivers from board definitions, enabling portable firmware across different hardware targets.
Overview
┌─────────────────────────────────────────────────────────────┐
│ Your Application │
│ └── sbl.json (declares hardware target) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ sloth (Sound Byte Labs Open Tool for Hardware) │
│ ├── Fetches sources (local or GitHub) │
│ ├── Walks attaches_to chain │
│ ├── Resolves pins to MCU port/pin values │
│ └── Generates sbl/hw/config/*.hpp + sbl.lock │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Generated Code │
│ └── sbl::hw::gpio::status_led, sbl::hw::adc::knob1, etc. │
└─────────────────────────────────────────────────────────────┘
Quick Start
- Create
sbl.json in your project root:
{
"$schema": "https://soundbytelabs.net/schema/sbl.schema.json",
"schemaVersion": "0.1",
"hardware": {
"sources": [
{
"name": "local",
"path": "./hardware"
}
],
"target": "local:mainboards/raspberry-pi-pico"
}
}
- Include in your CMakeLists.txt:
set(SBL_PATH "/path/to/sound-byte-libs")
include(${SBL_PATH}/tools/cmake/SblHardware.cmake)
sbl_resolve_hardware()
- Use generated handles in your code:
auto led = sbl::hw::gpio::status_led;
Uber-umbrella header for all target-specific code.
Hardware Hierarchy
| Level | Description | Example |
| MCU | Silicon + driver + pin definitions | rp2040, stm32h750 |
| Mainboard | Primary board with MCU, runs SBL application, exposes pins | raspberry-pi-pico, stm32-devboard |
| Module | Attaches to mainboard or another module, claims pins | led-panel, custom eurorack panel |
Configuration Files
sbl.json
Main configuration file declaring hardware sources and target:
{
"schemaVersion": "0.1",
"hardware": {
"sources": [
{ "name": "sbl", "github": "mjrskiles/sbl-hardware", "ref": "main" },
{ "name": "local", "path": "./hardware" }
],
"target": "local:modules/boards/my-panel"
}
}
Sources can be:
- Local:
{ "name": "local", "path": "./hardware" }
- GitHub:
{ "name": "sbl", "github": "owner/repo", "ref": "main" }
Target uses source:path format:
- Mainboards:
sbl:mainboards/raspberry-pi-pico
- Modules:
sbl:modules/boards/led-panel or sbl:modules/ic/mcp4822
MCU Definition (mcu.json)
Defines MCU capabilities with pin alternate functions:
{
"schemaVersion": "0.1",
"mcu": {
"name": "rp2040",
"vendor": "raspberry-pi",
"architecture": "arm-cortex-m0plus",
"driver": "./driver"
},
"pins": {
"GPIO0": {
"functions": {
"gpio": { "port": 0, "pin": 0 },
"spi": { "peripheral": "SPI0", "signal": "miso" },
"uart": { "peripheral": "UART0", "signal": "tx" },
"i2c": { "peripheral": "I2C0", "signal": "sda" },
"pwm": { "peripheral": "PWM0", "channel": 0 }
}
},
"GPIO25": {
"functions": {
"gpio": { "port": 0, "pin": 25 },
"pwm": { "peripheral": "PWM4", "channel": 1 }
}
}
},
"peripherals": {
"adc": {
"ADC": { "resolution_bits": 12, "channels": 5 }
}
}
}
Mainboard Manifest (hardware.json)
Mainboards are primary boards with an SBL-compatible MCU. They expose resources to modules.
Simple Mainboard (like Raspberry Pi Pico):
{
"schemaVersion": "0.1",
"mainboard": {
"name": "raspberry-pi-pico",
"manifestVersion": "1.0.0",
"revision": "1.0",
"mcu": "mcu/arm/rp2040"
},
"exposes": {
"gpio": {
"gp0": { "mcu_pin": "GPIO0", "functions": ["gpio", "spi", "uart", "i2c", "pwm"] },
"gp25": { "mcu_pin": "GPIO25", "functions": ["gpio", "pwm"] }
},
"adc": {
"a0": { "mcu_pin": "GPIO26", "mcu_channel": "ADC0" }
}
},
"claims": {
"pins": [
{ "mcu_pin": "GPIO25", "function": "gpio", "direction": "out", "id": "status_led" }
]
}
}
Host Mainboard (exposes pins for attached modules):
{
"schemaVersion": "0.1",
"mainboard": {
"name": "stm32-devboard",
"manifestVersion": "1.0.0",
"revision": "1.1",
"mcu": "mcu/arm/stm32h750"
},
"exposes": {
"gpio": {
"d0": { "mcu_pin": "PB12", "functions": ["gpio", "spi"] },
"d20": { "mcu_pin": "PC1", "functions": ["gpio", "adc"] }
},
"adc": {
"a0": { "mcu_pin": "PA0", "mcu_channel": "ADC1_IN0" }
},
"buses": {
"spi1": { "type": "spi", "mcu_peripheral": "SPI1", "pins": { "mosi": "PB5", "miso": "PB4", "sck": "PB3" } }
}
},
"claims": {
"pins": [
{ "mcu_pin": "PC7", "function": "gpio", "direction": "out", "id": "status_led" }
]
}
}
Module Manifest (hardware.json)
Modules attach to mainboards or other modules and claim resources.
Expansion Board (attaches to a mainboard):
{
"schemaVersion": "0.1",
"module": {
"name": "led-panel",
"manifestVersion": "1.0.0",
"revision": "1.0",
"category": "boards",
"attaches_to": "mainboards/stm32-devboard"
},
"claims": {
"pins": [
{ "pin": "d20", "function": "gpio", "direction": "out", "id": "led1_red" },
{ "pin": "d27", "function": "gpio", "direction": "in", "pull": "up", "active_low": true, "id": "button1" }
],
"adc": [
{ "pin": "a0", "id": "knob1" }
]
}
}
IC Module (like a DAC chip):
{
"schemaVersion": "0.1",
"module": {
"name": "mcp4822",
"category": "ic",
"manufacturer": "Microchip",
"datasheet": "https://ww1.microchip.com/downloads/en/DeviceDoc/20002249B.pdf"
},
"interface": {
"type": "spi",
"role": "peripheral",
"mode": 0,
"max_speed_hz": 20000000
},
"requires": {
"pins": [
{ "function": "gpio", "direction": "out", "id": "ldac", "optional": true }
]
},
"provides": {
"dac": { "channels": 2, "resolution_bits": 12 }
}
}
Key difference: Modules claim logical pin names from their parent's exposes (e.g., "d20"), not physical MCU pins (e.g., "PC1").
Pin Resolution
The resolver walks the attaches_to chain to resolve logical pin names with function validation:
Module → Mainboard → MCU → Generated code, with validation at each step.
Resolution steps:
- Module claims logical pin
d20 with function gpio
- Mainboard exposes
d20 → validates requested function is available
- MCU defines physical pin
PC1 → checks for conflicts
- Generator outputs type-safe handle with resolved port/pin
Pin conflicts are detected at resolution time. If the same MCU pin is claimed twice (even through different exposed names), the resolver fails with a conflict error.
Generated Code
The resolver generates split config headers in sbl/hw/config/:
build/generated/sbl/hw/config/
├── gpio.hpp # GPIO handles
├── adc.hpp # ADC handles (if any ADC claims)
├── mcu.hpp # MCU metadata
└── system.hpp # System configuration
Example gpio.hpp:
namespace sbl::hw::gpio {
}
GPIO pin handle with port, pin number, and polarity.
Example mcu.hpp:
namespace sbl::hw::mcu {
inline constexpr const char* name = "stm32h750";
inline constexpr const char* family = "stm32h7";
inline constexpr const char* vendor = "st";
inline constexpr const char* arch = "arm-cortex-m7";
}
The <sbl/hw/hw.hpp> umbrella header includes all generated configs plus MCU drivers.
Lock File (sbl.lock)
The resolver generates a lock file containing the fully resolved hardware graph. Commit this file for reproducible builds:
{
"schemaVersion": "0.1",
"generated": "2025-12-10T12:00:00Z",
"config_file": "sbl.json",
"resolution": {
"target": "local:modules/boards/led-panel",
"chain": ["local:modules/boards/led-panel", "local:mainboards/stm32-devboard"],
"mcu": { "name": "stm32h750", "driver_path": "..." }
},
"resolved_claims": {
"gpio": {
"led1_red": { "port": 2, "pin": 1, "direction": "out", "active_low": false }
}
}
}
Multiple Targets
Create alternate config files for different targets:
sbl.json # Production hardware
sbl.sim.json # Native simulator for development
sbl.pico.json # Raspberry Pi Pico variant
Build with:
cmake -B build -DSBL_CONFIG=sbl.sim.json
cmake --build build
Directory Structure
sbl-hardware/
├── mcu/
│ ├── arm/ # ARM Cortex-M MCUs
│ │ ├── rp2040/
│ │ │ ├── mcu.json
│ │ │ └── driver/
│ │ └── stm32h750/
│ │ ├── mcu.json
│ │ └── driver/
│ └── native/ # Native simulator
│ ├── mcu.json
│ └── driver/
├── mainboards/ # Primary boards running SBL
│ ├── raspberry-pi-pico/
│ │ └── hardware.json
│ ├── stm32-devboard/
│ │ └── hardware.json
│ └── sbl-simulator-0/
│ └── hardware.json
└── modules/
├── boards/ # Expansion boards
│ └── led-panel/
│ └── hardware.json
└── ic/ # IC modules (DACs, ADCs, etc.)
└── mcp4822/
└── hardware.json
Schema Reference
| Schema | Purpose |
sbl.schema.json | Main project configuration |
mcu.schema.json | MCU definition with pin functions and peripherals |
mainboard.schema.json | Mainboard manifests (primary boards with MCU) |
module.schema.json | Module manifests (expansion boards, ICs) |
sbl-lock.schema.json | Lock file format |
Schemas are maintained in sound-byte-libs/schema/. For detailed documentation with visual diagrams, see the Hardware Schema Documentation.
CLI Tools (sloth)
sloth (Sound Byte Labs Open Tool for Hardware) is the CLI tool for hardware resolution.
# Run from sound-byte-libs/tools/ directory
cd /path/to/sound-byte-libs/tools
# Resolve and print summary
python -m sloth resolve /path/to/project/sbl.json
# Generate lock file and resolved JSON
python -m sloth resolve sbl.json --lock sbl.lock --output resolved.json
# Generate config headers to directory
python -m sloth generate sbl.json -o build/generated/sbl/hw/config
# Get MCU info (useful for build scripts)
python -m sloth resolve sbl.json --print-mcu-name # prints: rp2040
python -m sloth resolve sbl.json --print-mcu-driver # prints: /path/to/driver
# Show help
python -m sloth --help
python -m sloth resolve --help
Package Structure
tools/
├── sloth/ # Hardware resolution tool
│ ├── __init__.py # Public API exports
│ ├── __main__.py # CLI entry point
│ ├── config.py # Configuration constants
│ ├── errors.py # Exception hierarchy
│ ├── source.py # Local and GitHub source handling
│ ├── validation.py # JSON Schema validation at load time
│ ├── manifest.py # Manifest loading adapters
│ ├── resolver.py # Resolution engine (walks board chain)
│ ├── ir.py # Frozen IR types (ResolutionResult)
│ ├── generator.py # Code generation using template packs
│ └── graph.py # Hardware graph visualization
│
└── sloth-templates/ # Template packs (decoupled from sloth core)
└── sbl/ # SBL C++ template pack
├── manifest.json # Pack metadata and output configuration
└── templates/ # Jinja2 templates (arbitrary nesting)
└── config/
├── gpio.hpp.j2
├── adc.hpp.j2
├── uart.hpp.j2
├── mcu.hpp.j2
├── system.hpp.j2
└── meta.hpp.j2
Template Packs
sloth uses template packs to decouple code generation from the core tool. This allows non-SBL projects to use sloth with their own output formats.
A template pack is a directory containing:
manifest.json - Defines outputs and template paths
- Templates - Jinja2 templates at any path within the pack
Discovery order:
--templates CLI flag (highest priority)
SLOTH_TEMPLATES environment variable
- Default SBL pack (
tools/sloth-templates/sbl/)
Example manifest.json:
{
"name": "sbl",
"version": "1.0.0",
"description": "Sound Byte Labs C++ hardware configuration templates",
"outputs": {
"gpio": {
"template": "templates/config/gpio.hpp.j2",
"output": "gpio.hpp",
"required": true
},
"adc": {
"template": "templates/config/adc.hpp.j2",
"output": "adc.hpp",
"condition": "adc_claims"
}
}
}
Using custom templates:
# Via CLI flag
python -m sloth generate sbl.json --templates /path/to/my-templates
# Via environment variable
export SLOTH_TEMPLATES=/path/to/my-templates
python -m sloth generate sbl.json
Python API
from pathlib import Path
from sloth import HardwareResolver, HardwareGenerator
resolver = HardwareResolver(Path("sbl.json"))
result = resolver.resolve_to_ir()
print(f"MCU: {result.mcu.name}")
print(f"GPIO claims: {len(result.gpio_claims)}")
generator = HardwareGenerator()
generator.generate(result, Path("build/generated/sbl/hw/config"))
resolver.write_lock(Path("sbl.lock"))
Two resolution methods:
resolve_to_ir() - Returns frozen ResolutionResult with flat access (preferred for generation)
resolve() - Returns mutable Resolution with manifest access (for debugging/introspection)
Design Decisions
See ADR-001: Hardware Source System for the full rationale behind this design.
Key principles:
- Declarative over imperative: Hardware is described, not programmed
- Composition over inheritance: Modules attach to boards, boards use MCUs
- Build-time resolution: All pin mappings resolved before compilation
- Reproducible builds: Lock file captures exact resolution state
CMake Integration
SBL provides optional CMake hooks that simplify SDK integration. See the examples for usage patterns.
Hook Lifecycle
Three-phase lifecycle with SDK plugin branching and user extension points.
Usage in CMakeLists.txt:
include($ENV{SBL_PATH}/tools/cmake/SblHooks.cmake)
sbl_pre_project() # 1. Resolve hardware, load SDK plugin
project(my_app ...)
sbl_post_project() # 2. Initialize SDK, create sbl::driver target
# ... add_executable, target_link_libraries ...
sbl_finalize(my_app) # 3. Generate output files (.uf2, .bin, .hex)
SDK Plugin System
The hooks use a plugin system to support different SDKs. Plugins live in tools/cmake/sdk/:
| Plugin | When Used | What It Does |
pico-sdk.cmake | RP2040, RP2350 | Loads Pico SDK, calls pico_sdk_init(), generates UF2 |
none.cmake | STM32, bare-metal | Configures ARM toolchain from JSON, generates BIN/HEX |
The plugin is selected based on SBL_EXT_SDK from the MCU manifest (e.g., "ext_sdk": "pico-sdk").
Extending Hooks
For custom SDK integration or additional build steps, use SBL_EXTEND_SDK_HOOKS:
# In your CMakeLists.txt, before sbl_pre_project()
set(SBL_EXTEND_SDK_HOOKS "${CMAKE_CURRENT_SOURCE_DIR}/my-hooks.cmake")
include($ENV{SBL_PATH}/tools/cmake/SblHooks.cmake)
sbl_pre_project()
# ...
Your extension file can define these functions to hook into the lifecycle:
# my-hooks.cmake
# Called after SDK pre-project setup
function(sbl_user_pre_project)
message(STATUS "Custom pre-project hook")
endfunction()
# Called after SDK post-project setup
function(sbl_user_post_project)
message(STATUS "Custom post-project hook")
endfunction()
# Called after SDK finalize
function(sbl_user_finalize TARGET)
message(STATUS "Custom finalize for ${TARGET}")
# Add custom post-processing here
endfunction()
Key Variables Set by Hooks
After sbl_pre_project(), these CMake variables are available:
| Variable | Example | Description |
SBL_MCU_NAME | rp2350 | MCU identifier |
SBL_MCU_FAMILY | rp2 | MCU family |
SBL_MCU_DRIVER_PATH | /path/to/driver | Path to driver sources |
SBL_EXT_SDK | pico-sdk | External SDK name (or none) |
SBL_DRIVER_LIB | sbl_rp2350_driver | Driver library target name |
SBL_GENERATED_DIR | build/generated | Generated headers directory |
The sbl::driver INTERFACE target is created by sbl_post_project() and links to the platform driver.