Sound Byte Libs 1ee2ca6
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
Hardware Source System

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

  1. 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"
}
}
  1. Include in your CMakeLists.txt:
set(SBL_PATH "/path/to/sound-byte-libs")
include(${SBL_PATH}/tools/cmake/SblHardware.cmake)
sbl_resolve_hardware()
  1. Use generated handles in your code:
#include <sbl/hw/hw.hpp> // Single include for drivers + config
auto led = sbl::hw::gpio::status_led;
// led.port, led.pin, led.active_low are resolved at build time
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:

Pin Resolution Chain

Module → Mainboard → MCU → Generated code, with validation at each step.

Resolution steps:

  1. Module claims logical pin d20 with function gpio
  2. Mainboard exposes d20 → validates requested function is available
  3. MCU defines physical pin PC1 → checks for conflicts
  4. 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:

// sbl/hw/config/gpio.hpp - Generated by sloth
namespace sbl::hw::gpio {
inline constexpr sbl::GpioHandle status_led{2, 7, false};
inline constexpr sbl::GpioHandle led1_red{2, 1, false};
inline constexpr sbl::GpioHandle button1{6, 9, true};
}
GPIO pin handle with port, pin number, and polarity.
Definition handle.hpp:29

Example mcu.hpp:

// sbl/hw/config/mcu.hpp - Generated by sloth
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:

  1. --templates CLI flag (highest priority)
  2. SLOTH_TEMPLATES environment variable
  3. 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
# Resolve hardware to frozen IR (preferred for code generation)
resolver = HardwareResolver(Path("sbl.json"))
result = resolver.resolve_to_ir()
print(f"MCU: {result.mcu.name}")
print(f"GPIO claims: {len(result.gpio_claims)}")
# Generate config headers to directory
generator = HardwareGenerator()
generator.generate(result, Path("build/generated/sbl/hw/config"))
# Write lock file
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

CMake 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.